mirror of
https://github.com/Cockatrice/Cockatrice.git
synced 2026-09-21 09:05:10 -07:00
Merge branch 'master' into tooomm-qt5
This commit is contained in:
commit
bbf2412157
219 changed files with 7310 additions and 1824 deletions
|
|
@ -159,6 +159,7 @@ if [[ $PACKAGE_TYPE ]]; then
|
|||
fi
|
||||
if [[ $USE_VCPKG ]]; then
|
||||
flags+=("-DUSE_VCPKG=1")
|
||||
flags+=("-DVCPKG_INSTALL_OPTIONS=--x-abi-tools-use-exact-versions")
|
||||
fi
|
||||
|
||||
# Add CMake --build flags
|
||||
|
|
|
|||
2
.github/CONTRIBUTING.md
vendored
2
.github/CONTRIBUTING.md
vendored
|
|
@ -334,7 +334,7 @@ the tr() call, also you can add an extra string as a hint for translators:
|
|||
QString message = tr("Everyone draws %n cards", "english hint for translators", amount);
|
||||
```
|
||||
See [Qt's wiki on translations](
|
||||
https://doc.qt.io/qt-5/i18n-source-translation.html#handling-plurals)
|
||||
https://doc.qt.io/qt-6/i18n-source-translation.html#handling-plurals)
|
||||
|
||||
If you're about to propose a change that adds or modifies any translatable
|
||||
string in the code, you don't need to take care of adding the new strings to
|
||||
|
|
|
|||
|
|
@ -61,10 +61,11 @@ if(WIN32 OR USE_VCPKG) # Windows (including 64bit) or USE_VCPKG set
|
|||
)
|
||||
set(QTDIR
|
||||
""
|
||||
CACHE PATH "Path to Qt (e.g. C:/Qt/5.7/msvc2015_64)"
|
||||
CACHE PATH "Path to Qt (e.g. C:/Qt/6.4.2/msvc2019_64)"
|
||||
)
|
||||
message(
|
||||
WARNING "QTDIR variable is missing. Please set this variable to specify path to Qt (e.g. C:/Qt/5.7/msvc2015_64)"
|
||||
WARNING
|
||||
"QTDIR variable is missing. Please set this variable to specify path to Qt (e.g. C:/Qt/6.4.2/msvc2019_64)"
|
||||
)
|
||||
endif()
|
||||
endif()
|
||||
|
|
@ -99,8 +100,7 @@ include(createversionfile)
|
|||
|
||||
# Define a proper install path
|
||||
if(UNIX)
|
||||
if(APPLE)
|
||||
# macOS
|
||||
if(APPLE) # macOS
|
||||
# Due to the special bundle structure ignore
|
||||
# the prefix eventually set by the user.
|
||||
set(CMAKE_INSTALL_PREFIX ${CMAKE_BINARY_DIR}/release)
|
||||
|
|
@ -120,8 +120,7 @@ if(UNIX)
|
|||
set(CMAKE_XCODE_ATTRIBUTE_LD "${CMAKE_BINARY_DIR}/launch-c")
|
||||
set(CMAKE_XCODE_ATTRIBUTE_LDPLUSPLUS "${CMAKE_BINARY_DIR}/launch-cxx")
|
||||
endif()
|
||||
else()
|
||||
# Linux / BSD
|
||||
else() # Linux / BSD
|
||||
if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT)
|
||||
#fix package build
|
||||
if(PREFIX)
|
||||
|
|
@ -131,7 +130,7 @@ if(UNIX)
|
|||
endif()
|
||||
endif()
|
||||
endif()
|
||||
elseif(WIN32)
|
||||
elseif(WIN32) # Windows, including 64 bit
|
||||
set(CMAKE_INSTALL_PREFIX ${CMAKE_BINARY_DIR}/rundir/${CMAKE_BUILD_TYPE})
|
||||
endif()
|
||||
|
||||
|
|
@ -254,7 +253,7 @@ set(CPACK_PACKAGE_VERSION_PATCH "${PROJECT_VERSION_PATCH}")
|
|||
set(CPACK_PACKAGE_FILE_NAME "${PROJECT_VERSION_FILENAME}")
|
||||
|
||||
if(UNIX)
|
||||
if(APPLE)
|
||||
if(APPLE) #macOS
|
||||
set(CPACK_GENERATOR DragNDrop ${CPACK_GENERATOR})
|
||||
set(CPACK_GENERATOR "DragNDrop")
|
||||
set(CPACK_DMG_FORMAT "UDBZ")
|
||||
|
|
@ -264,14 +263,11 @@ if(UNIX)
|
|||
set(CPACK_DMG_DS_STORE_SETUP_SCRIPT "${CMAKE_CURRENT_SOURCE_DIR}/cmake/CMakeDMGSetup.script")
|
||||
set(CPACK_DMG_BACKGROUND_IMAGE "${CMAKE_CURRENT_SOURCE_DIR}/cmake/dmgBackground.tif")
|
||||
set(CPACK_PRE_BUILD_SCRIPTS "${CMAKE_CURRENT_SOURCE_DIR}/cmake/SignMacApplications.cmake")
|
||||
else()
|
||||
# linux
|
||||
else() # Linux
|
||||
if(CPACK_GENERATOR STREQUAL "RPM")
|
||||
set(CPACK_RPM_PACKAGE_LICENSE "GPLv2")
|
||||
set(CPACK_RPM_MAIN_COMPONENT "cockatrice")
|
||||
if(Qt6_FOUND)
|
||||
set(CPACK_RPM_PACKAGE_REQUIRES "protobuf, qt6-qttools, qt6-qtsvg, qt6-qtmultimedia, qt6-qtimageformats")
|
||||
endif()
|
||||
set(CPACK_RPM_PACKAGE_REQUIRES "protobuf, qt6-qttools, qt6-qtsvg, qt6-qtmultimedia, qt6-qtimageformats")
|
||||
set(CPACK_RPM_PACKAGE_GROUP "Amusements/Games")
|
||||
set(CPACK_RPM_PACKAGE_URL "http://github.com/Cockatrice/Cockatrice")
|
||||
# stop directories from making package conflicts
|
||||
|
|
@ -289,13 +285,11 @@ if(UNIX)
|
|||
set(CPACK_DEBIAN_PACKAGE_SHLIBDEPS ON)
|
||||
set(CPACK_DEBIAN_PACKAGE_SECTION "games")
|
||||
set(CPACK_DEBIAN_PACKAGE_HOMEPAGE "http://github.com/Cockatrice/Cockatrice")
|
||||
if(Qt6_FOUND)
|
||||
set(CPACK_DEBIAN_PACKAGE_DEPENDS "libqt6multimedia6, libqt6svg6, qt6-qpa-plugins, qt6-image-formats-plugins")
|
||||
set(CPACK_DEBIAN_PACKAGE_RECOMMENDS "libqt6sql6-mysql") # for connecting servatrice to a mysql db
|
||||
endif()
|
||||
set(CPACK_DEBIAN_PACKAGE_DEPENDS "libqt6multimedia6, libqt6svg6, qt6-qpa-plugins, qt6-image-formats-plugins")
|
||||
set(CPACK_DEBIAN_PACKAGE_RECOMMENDS "libqt6sql6-mysql") # for connecting servatrice to a mysql db
|
||||
endif()
|
||||
endif()
|
||||
elseif(WIN32)
|
||||
elseif(WIN32) # Windows, including 64 bit
|
||||
set(CPACK_GENERATOR NSIS ${CPACK_GENERATOR})
|
||||
if("${CMAKE_GENERATOR_PLATFORM}" MATCHES "(x64)")
|
||||
set(TRICE_IS_64_BIT 1)
|
||||
|
|
@ -352,6 +346,4 @@ if(TEST)
|
|||
add_subdirectory(tests)
|
||||
endif()
|
||||
|
||||
if(Qt6_FOUND)
|
||||
qt6_finalize_project()
|
||||
endif()
|
||||
qt6_finalize_project()
|
||||
|
|
|
|||
|
|
@ -38,44 +38,38 @@ set(REQUIRED_QT_COMPONENTS ${REQUIRED_QT_COMPONENTS} ${_SERVATRICE_NEEDED} ${_CO
|
|||
)
|
||||
list(REMOVE_DUPLICATES REQUIRED_QT_COMPONENTS)
|
||||
|
||||
# Find Qt and all required components, as well as Linguist
|
||||
# Find Qt and all required components including Linguist
|
||||
find_package(
|
||||
Qt6
|
||||
COMPONENTS ${REQUIRED_QT_COMPONENTS} Linguist
|
||||
QUIET HINTS ${Qt6_DIR}
|
||||
)
|
||||
|
||||
if(Qt6_FOUND)
|
||||
set(COCKATRICE_QT_VERSION_NAME Qt6)
|
||||
set(CMAKE_POSITION_INDEPENDENT_CODE ON)
|
||||
|
||||
if(Qt6LinguistTools_FOUND)
|
||||
list(FIND Qt6LinguistTools_TARGETS Qt6::lrelease QT6_LRELEASE_INDEX)
|
||||
if(QT6_LRELEASE_INDEX EQUAL -1)
|
||||
message(WARNING "Qt6 lrelease not found.")
|
||||
endif()
|
||||
|
||||
list(FIND Qt6LinguistTools_TARGETS Qt6::lupdate QT6_LUPDATE_INDEX)
|
||||
if(QT6_LUPDATE_INDEX EQUAL -1)
|
||||
message(WARNING "Qt6 lupdate not found.")
|
||||
endif()
|
||||
else()
|
||||
message(WARNING "Linguist Tools not found, cannot handle translations")
|
||||
endif()
|
||||
else()
|
||||
message(FATAL_ERROR "Qt6 not found")
|
||||
if(NOT Qt6_FOUND)
|
||||
message(FATAL_ERROR "No suitable version of Qt was found")
|
||||
endif()
|
||||
set(COCKATRICE_QT_VERSION_NAME Qt6)
|
||||
|
||||
list(FIND Qt6LinguistTools_TARGETS Qt6::lrelease QT6_LRELEASE_INDEX)
|
||||
if(QT6_LRELEASE_INDEX EQUAL -1)
|
||||
message(WARNING "Qt6 lrelease not found.")
|
||||
endif()
|
||||
|
||||
list(FIND Qt6LinguistTools_TARGETS Qt6::lupdate QT6_LUPDATE_INDEX)
|
||||
if(QT6_LUPDATE_INDEX EQUAL -1)
|
||||
message(WARNING "Qt6 lupdate not found.")
|
||||
endif()
|
||||
|
||||
set(CMAKE_POSITION_INDEPENDENT_CODE ON)
|
||||
|
||||
# Establish Qt Plugins directory & Library directories
|
||||
get_target_property(QT_LIBRARY_DIR ${COCKATRICE_QT_VERSION_NAME}::Core LOCATION)
|
||||
get_filename_component(QT_LIBRARY_DIR ${QT_LIBRARY_DIR} DIRECTORY)
|
||||
if(Qt6_FOUND)
|
||||
get_filename_component(QT_PLUGINS_DIR "${Qt6Core_DIR}/../../../${QT6_INSTALL_PLUGINS}" ABSOLUTE)
|
||||
get_filename_component(QT_LIBRARY_DIR "${QT_LIBRARY_DIR}/../../.." ABSOLUTE)
|
||||
if(UNIX AND APPLE)
|
||||
# Mac needs a bit more help finding all necessary components
|
||||
list(APPEND QT_LIBRARY_DIR "/usr/local/lib")
|
||||
endif()
|
||||
get_filename_component(QT_PLUGINS_DIR "${Qt6Core_DIR}/../../../${QT6_INSTALL_PLUGINS}" ABSOLUTE)
|
||||
get_filename_component(QT_LIBRARY_DIR "${QT_LIBRARY_DIR}/../../.." ABSOLUTE)
|
||||
if(UNIX AND APPLE)
|
||||
# Mac needs a bit more help finding all necessary components
|
||||
list(APPEND QT_LIBRARY_DIR "/usr/local/lib")
|
||||
endif()
|
||||
message(DEBUG "QT_PLUGINS_DIR = ${QT_PLUGINS_DIR}")
|
||||
message(DEBUG "QT_LIBRARY_DIR = ${QT_LIBRARY_DIR}")
|
||||
|
|
|
|||
|
|
@ -1,38 +1,118 @@
|
|||
<?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">
|
||||
<dict>
|
||||
|
||||
<!-- ========================= -->
|
||||
<!-- CORE BUNDLE METADATA -->
|
||||
<!-- ========================= -->
|
||||
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>English</string>
|
||||
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>${MACOSX_BUNDLE_EXECUTABLE_NAME}</string>
|
||||
|
||||
<key>CFBundleGetInfoString</key>
|
||||
<string>${MACOSX_BUNDLE_INFO_STRING}</string>
|
||||
|
||||
<key>CFBundleIconFile</key>
|
||||
<string>${MACOSX_BUNDLE_ICON_FILE}</string>
|
||||
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>${MACOSX_BUNDLE_GUI_IDENTIFIER}</string>
|
||||
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
|
||||
<key>CFBundleLongVersionString</key>
|
||||
<string>${MACOSX_BUNDLE_LONG_VERSION_STRING}</string>
|
||||
|
||||
<key>CFBundleName</key>
|
||||
<string>${MACOSX_BUNDLE_BUNDLE_NAME}</string>
|
||||
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>${MACOSX_BUNDLE_SHORT_VERSION_STRING}</string>
|
||||
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
|
||||
<key>CFBundleVersion</key>
|
||||
<string>${MACOSX_BUNDLE_BUNDLE_VERSION}</string>
|
||||
<key>CSResourcesFileMapped</key>
|
||||
<true/>
|
||||
<key>LSRequiresCarbon</key>
|
||||
<true/>
|
||||
|
||||
<key>NSHumanReadableCopyright</key>
|
||||
<string>${MACOSX_BUNDLE_COPYRIGHT}</string>
|
||||
|
||||
<key>NSHighResolutionCapable</key>
|
||||
<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>
|
||||
</plist>
|
||||
|
|
|
|||
|
|
@ -99,6 +99,7 @@ set(cockatrice_SOURCES
|
|||
src/game_graphics/player/menu/sideboard_menu.cpp
|
||||
src/game_graphics/player/menu/tally_menu.cpp
|
||||
src/game_graphics/player/menu/utility_menu.cpp
|
||||
src/game_graphics/tally/stats_tally.cpp
|
||||
src/game_graphics/tally/subtype_tally.cpp
|
||||
src/game_graphics/tally/tally.cpp
|
||||
src/game/player/player_actions.cpp
|
||||
|
|
@ -134,6 +135,12 @@ set(cockatrice_SOURCES
|
|||
src/interface/card_picture_loader/card_picture_loader_worker.cpp
|
||||
src/interface/card_picture_loader/card_picture_loader_worker_work.cpp
|
||||
src/interface/card_picture_loader/card_picture_to_load.cpp
|
||||
src/interface/intents/intent.cpp
|
||||
src/interface/intents/intent.h
|
||||
src/interface/intents/intent_open_local_deck.cpp
|
||||
src/interface/intents/intent_open_local_deck.h
|
||||
src/interface/intents/intent_wait_for_database_load.cpp
|
||||
src/interface/intents/intent_wait_for_database_load.h
|
||||
src/interface/layouts/flow_layout.cpp
|
||||
src/interface/layouts/overlap_layout.cpp
|
||||
src/interface/widgets/utility/line_edit_completer.cpp
|
||||
|
|
@ -229,6 +236,7 @@ set(cockatrice_SOURCES
|
|||
src/interface/widgets/printing_selector/set_name_and_collectors_number_display_widget.cpp
|
||||
src/interface/widgets/quick_settings/settings_button_widget.cpp
|
||||
src/interface/widgets/quick_settings/settings_popup_widget.cpp
|
||||
src/interface/widgets/replay/replay_manager.cpp
|
||||
src/interface/widgets/replay/replay_quick_settings_widget.cpp
|
||||
src/interface/widgets/replay/replay_timeline_widget.cpp
|
||||
src/interface/widgets/replay/replay_widget.cpp
|
||||
|
|
@ -249,10 +257,13 @@ set(cockatrice_SOURCES
|
|||
src/interface/widgets/server/user/user_list_manager.cpp
|
||||
src/interface/widgets/server/user/user_list_painter.cpp
|
||||
src/interface/widgets/server/user/user_list_widget.cpp
|
||||
src/interface/widgets/settings_page/abstract_settings_page.cpp
|
||||
src/interface/widgets/settings_page/appearance_settings_page.cpp
|
||||
src/interface/widgets/settings_page/deck_editor_settings_page.cpp
|
||||
src/interface/widgets/settings_page/general_settings_page.cpp
|
||||
src/interface/widgets/settings_page/messages_settings_page.cpp
|
||||
src/interface/widgets/settings_page/settings_search_delegate.cpp
|
||||
src/interface/widgets/settings_page/settings_search_model.cpp
|
||||
src/interface/widgets/settings_page/shortcut_settings_page.cpp
|
||||
src/interface/widgets/settings_page/sound_settings_page.cpp
|
||||
src/interface/widgets/settings_page/storage_settings_page.cpp
|
||||
|
|
@ -291,6 +302,7 @@ set(cockatrice_SOURCES
|
|||
src/interface/widgets/visual_deck_storage/visual_deck_storage_widget.cpp
|
||||
src/interface/window_main.cpp
|
||||
src/main.cpp
|
||||
src/single_instance_manager.cpp
|
||||
src/interface/widgets/tabs/abstract_tab_deck_editor.cpp
|
||||
src/interface/widgets/tabs/api/archidekt/tab_archidekt.cpp
|
||||
src/interface/widgets/tabs/api/archidekt/api_response/archidekt_deck_listing_api_response.cpp
|
||||
|
|
@ -306,6 +318,13 @@ set(cockatrice_SOURCES
|
|||
src/interface/widgets/tabs/api/archidekt/display/archidekt_api_response_deck_entry_display_widget.cpp
|
||||
src/interface/widgets/tabs/api/archidekt/display/archidekt_api_response_deck_listings_display_widget.cpp
|
||||
src/interface/widgets/tabs/api/archidekt/display/archidekt_deck_preview_image_display_widget.cpp
|
||||
src/interface/widgets/tabs/api/commander_spellbook/api_response/card_in_deck_request.cpp
|
||||
src/interface/widgets/tabs/api/commander_spellbook/api_response/commander_spellbook_deck_request.cpp
|
||||
src/interface/widgets/tabs/api/commander_spellbook/api_response/commander_spellbook_card_result.cpp
|
||||
src/interface/widgets/tabs/api/commander_spellbook/api_response/commander_spellbook_variant_result.cpp
|
||||
src/interface/widgets/tabs/api/commander_spellbook/api_response/commander_spellbook_estimate_bracket_result.cpp
|
||||
src/interface/widgets/tabs/api/commander_spellbook/commander_spellbook_bracket_explainer.cpp
|
||||
src/interface/widgets/tabs/api/commander_spellbook/commander_spellbook_api_accessor.cpp
|
||||
src/interface/widgets/tabs/api/edhrec/api_response/archidekt_links/edhrec_api_response_archidekt_links.cpp
|
||||
src/interface/widgets/tabs/api/edhrec/api_response/average_deck/edhrec_average_deck_api_response.cpp
|
||||
src/interface/widgets/tabs/api/edhrec/api_response/average_deck/edhrec_deck_api_response.cpp
|
||||
|
|
@ -352,12 +371,29 @@ set(cockatrice_SOURCES
|
|||
src/interface/widgets/tabs/visual_deck_storage/tab_deck_storage_visual.cpp
|
||||
src/interface/key_signals.cpp
|
||||
src/interface/logger.cpp
|
||||
src/interface/widgets/tabs/api/commander_spellbook/commander_bracket_service.cpp
|
||||
src/interface/widgets/tabs/api/commander_spellbook/commander_bracket_widget.cpp
|
||||
src/interface/widgets/tabs/api/commander_spellbook/handle_commander_brackets.cpp
|
||||
src/interface/widgets/tabs/api/edhrec/display/commander/edhrec_commander_api_response_bracket_navigation_widget.cpp
|
||||
src/interface/widgets/tabs/api/edhrec/display/commander/edhrec_commander_api_response_bracket_navigation_widget.h
|
||||
src/interface/widgets/tabs/api/edhrec/display/commander/edhrec_commander_api_response_budget_navigation_widget.cpp
|
||||
src/interface/widgets/tabs/api/edhrec/display/commander/edhrec_commander_api_response_budget_navigation_widget.h
|
||||
src/interface/widgets/utility/compact_push_button.cpp
|
||||
src/interface/widgets/utility/compact_push_button.h
|
||||
src/single_instance_manager.h
|
||||
src/client/url_scheme_event_filter.h
|
||||
src/interface/intents/intent_connect_to_server.cpp
|
||||
src/interface/intents/intent_connect_to_server.h
|
||||
src/interface/intents/intent_disconnect_from_server.cpp
|
||||
src/interface/intents/intent_disconnect_from_server.h
|
||||
src/interface/intents/intent_join_server_game.cpp
|
||||
src/interface/intents/intent_join_server_game.h
|
||||
src/interface/intents/intent_join_server_room.cpp
|
||||
src/interface/intents/intent_join_server_room.h
|
||||
src/interface/intents/intent_login.cpp
|
||||
src/interface/intents/intent_login.h
|
||||
src/interface/intents/url_parser.cpp
|
||||
src/interface/intents/url_parser.h
|
||||
src/interface/widgets/server/user/user_info_popup.cpp
|
||||
src/interface/widgets/server/user/user_info_popup.h
|
||||
)
|
||||
|
|
@ -401,9 +437,7 @@ if(APPLE)
|
|||
set(cockatrice_SOURCES ${cockatrice_SOURCES} ${CMAKE_CURRENT_SOURCE_DIR}/resources/appicon.icns)
|
||||
endif(APPLE)
|
||||
|
||||
if(Qt6_FOUND)
|
||||
qt6_add_resources(cockatrice_RESOURCES_RCC ${cockatrice_RESOURCES})
|
||||
endif()
|
||||
qt6_add_resources(cockatrice_RESOURCES_RCC ${cockatrice_RESOURCES})
|
||||
|
||||
# Declare path variables
|
||||
set(ICONDIR
|
||||
|
|
@ -415,34 +449,37 @@ set(DESKTOPDIR
|
|||
CACHE STRING "desktop file destination"
|
||||
)
|
||||
|
||||
set(MIMEDIR
|
||||
share/mime/packages
|
||||
CACHE STRING "mime file destination"
|
||||
)
|
||||
|
||||
set(COCKATRICE_MAC_QM_INSTALL_DIR "cockatrice.app/Contents/Resources/translations")
|
||||
set(COCKATRICE_UNIX_QM_INSTALL_DIR "share/cockatrice/translations")
|
||||
set(COCKATRICE_WIN32_QM_INSTALL_DIR "translations")
|
||||
|
||||
if(Qt6_FOUND)
|
||||
qt6_add_executable(
|
||||
cockatrice
|
||||
WIN32
|
||||
MACOSX_BUNDLE
|
||||
${cockatrice_SOURCES}
|
||||
${cockatrice_RESOURCES_RCC}
|
||||
${cockatrice_MOC_SRCS}
|
||||
MANUAL_FINALIZATION
|
||||
)
|
||||
qt6_add_executable(
|
||||
cockatrice
|
||||
WIN32
|
||||
MACOSX_BUNDLE
|
||||
${cockatrice_SOURCES}
|
||||
${cockatrice_RESOURCES_RCC}
|
||||
${cockatrice_MOC_SRCS}
|
||||
MANUAL_FINALIZATION
|
||||
)
|
||||
|
||||
target_link_libraries(
|
||||
cockatrice
|
||||
PUBLIC libcockatrice_card
|
||||
libcockatrice_deck_list
|
||||
libcockatrice_filters
|
||||
libcockatrice_utility
|
||||
libcockatrice_network
|
||||
libcockatrice_models
|
||||
libcockatrice_rng
|
||||
libcockatrice_settings
|
||||
${COCKATRICE_QT_MODULES}
|
||||
)
|
||||
endif()
|
||||
target_link_libraries(
|
||||
cockatrice
|
||||
PUBLIC libcockatrice_card
|
||||
libcockatrice_deck_list
|
||||
libcockatrice_filters
|
||||
libcockatrice_utility
|
||||
libcockatrice_network
|
||||
libcockatrice_models
|
||||
libcockatrice_rng
|
||||
libcockatrice_settings
|
||||
${COCKATRICE_QT_MODULES}
|
||||
)
|
||||
|
||||
if(UNIX)
|
||||
if(APPLE)
|
||||
|
|
@ -462,6 +499,23 @@ if(UNIX)
|
|||
install(FILES ${CMAKE_CURRENT_SOURCE_DIR}/resources/cockatrice.png DESTINATION ${ICONDIR}/hicolor/48x48/apps)
|
||||
install(FILES ${CMAKE_CURRENT_SOURCE_DIR}/resources/cockatrice.svg DESTINATION ${ICONDIR}/hicolor/scalable/apps)
|
||||
install(FILES ${CMAKE_CURRENT_SOURCE_DIR}/cockatrice.desktop DESTINATION ${DESKTOPDIR})
|
||||
install(FILES ${CMAKE_CURRENT_SOURCE_DIR}/cockatrice-cod.xml DESTINATION ${MIMEDIR})
|
||||
|
||||
# Refresh the freedesktop databases so the file associations and scheme
|
||||
# handler register without requiring the user to run them manually. The
|
||||
# tools may be missing on minimal systems; that is fine, packaging systems
|
||||
# usually refresh these databases through their own triggers.
|
||||
find_program(UPDATE_MIME_DATABASE update-mime-database)
|
||||
if(UPDATE_MIME_DATABASE)
|
||||
install(CODE "execute_process(COMMAND \"${UPDATE_MIME_DATABASE}\" \"${CMAKE_INSTALL_PREFIX}/share/mime\")")
|
||||
endif()
|
||||
|
||||
find_program(UPDATE_DESKTOP_DATABASE update-desktop-database)
|
||||
if(UPDATE_DESKTOP_DATABASE)
|
||||
install(
|
||||
CODE "execute_process(COMMAND \"${UPDATE_DESKTOP_DATABASE}\" \"${CMAKE_INSTALL_PREFIX}/share/applications\")"
|
||||
)
|
||||
endif()
|
||||
endif()
|
||||
elseif(WIN32)
|
||||
install(TARGETS cockatrice RUNTIME DESTINATION ./)
|
||||
|
|
@ -590,7 +644,7 @@ Data = Resources\")
|
|||
endif()
|
||||
endif()
|
||||
|
||||
if(Qt6_FOUND AND Qt6LinguistTools_FOUND)
|
||||
if(Qt6LinguistTools_FOUND)
|
||||
#Qt6 Translations happen after the executable is built up
|
||||
if(UPDATE_TRANSLATIONS)
|
||||
qt6_add_translations(
|
||||
|
|
@ -617,6 +671,4 @@ if(Qt6_FOUND AND Qt6LinguistTools_FOUND)
|
|||
endif()
|
||||
endif()
|
||||
|
||||
if(Qt6_FOUND)
|
||||
qt6_finalize_target(cockatrice)
|
||||
endif()
|
||||
qt6_finalize_target(cockatrice)
|
||||
|
|
|
|||
7
cockatrice/cockatrice-cod.xml
Normal file
7
cockatrice/cockatrice-cod.xml
Normal 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>
|
||||
|
|
@ -3,6 +3,8 @@
|
|||
Version=1.0
|
||||
Type=Application
|
||||
Name=Cockatrice
|
||||
Exec=cockatrice
|
||||
Exec=cockatrice %U
|
||||
Icon=cockatrice
|
||||
Categories=Game;CardGame;
|
||||
MimeType=application/x-cockatrice;
|
||||
X-Scheme-Handler/cockatrice=true
|
||||
|
|
|
|||
|
|
@ -14,8 +14,8 @@
|
|||
#include <QtConcurrent>
|
||||
#include <libcockatrice/card/database/card_database.h>
|
||||
#include <libcockatrice/card/database/card_database_manager.h>
|
||||
#include <libcockatrice/settings/download_settings.h>
|
||||
#include <libcockatrice/settings/paths_settings.h>
|
||||
#include <libcockatrice/settings/personal_settings.h>
|
||||
#include <version_string.h>
|
||||
|
||||
#define SPOILERS_STATUS_URL "https://raw.githubusercontent.com/Cockatrice/Magic-Spoiler/files/SpoilerSeasonEnabled"
|
||||
|
|
@ -23,7 +23,7 @@
|
|||
|
||||
SpoilerBackgroundUpdater::SpoilerBackgroundUpdater(QObject *apParent) : QObject(apParent), cardUpdateProcess(nullptr)
|
||||
{
|
||||
isSpoilerDownloadEnabled = SettingsCache::instance().personal().getDownloadSpoilersStatus();
|
||||
isSpoilerDownloadEnabled = SettingsCache::instance().downloads().getDownloadSpoilersStatus();
|
||||
if (isSpoilerDownloadEnabled) {
|
||||
// Start the process of checking if we're in spoiler season
|
||||
// File exists means we're in spoiler season
|
||||
|
|
|
|||
|
|
@ -11,18 +11,22 @@
|
|||
#include <QGlobalStatic>
|
||||
#include <QSettings>
|
||||
#include <QStandardPaths>
|
||||
#include <libcockatrice/settings/appearance_settings.h>
|
||||
#include <libcockatrice/settings/cache_storage_settings.h>
|
||||
#include <libcockatrice/settings/card_database_settings.h>
|
||||
#include <libcockatrice/settings/card_override_settings.h>
|
||||
#include <libcockatrice/settings/cards_display_settings.h>
|
||||
#include <libcockatrice/settings/chat_settings.h>
|
||||
#include <libcockatrice/settings/commander_bracket_settings.h>
|
||||
#include <libcockatrice/settings/debug_settings.h>
|
||||
#include <libcockatrice/settings/deck_editor_settings.h>
|
||||
#include <libcockatrice/settings/download_settings.h>
|
||||
#include <libcockatrice/settings/game_filters_settings.h>
|
||||
#include <libcockatrice/settings/game_settings.h>
|
||||
#include <libcockatrice/settings/interface_settings.h>
|
||||
#include <libcockatrice/settings/layouts_settings.h>
|
||||
#include <libcockatrice/settings/message_settings.h>
|
||||
#include <libcockatrice/settings/network_settings.h>
|
||||
#include <libcockatrice/settings/paths_settings.h>
|
||||
#include <libcockatrice/settings/personal_settings.h>
|
||||
#include <libcockatrice/settings/recents_settings.h>
|
||||
|
|
@ -135,8 +139,12 @@ SettingsCache::SettingsCache()
|
|||
personalSettings = new PersonalSettings(settingsPath, this);
|
||||
cardsDisplaySettings = new CardsDisplaySettings(settingsPath, this);
|
||||
interfaceSettings = new InterfaceSettings(settingsPath, this);
|
||||
deckEditorSettings = new DeckEditorSettings(settingsPath, this);
|
||||
pathsSettings = new PathsSettings(settingsPath, this);
|
||||
visualDeckStorageSettings = new VisualDeckStorageSettings(settingsPath, this);
|
||||
appearanceSettings = new AppearanceSettings(settingsPath, this);
|
||||
networkSettings = new NetworkSettings(settingsPath, this);
|
||||
commanderBracketSettings = new CommanderBracketSettings(settingsPath, this);
|
||||
|
||||
// Forward ICardDatabasePathProvider signal from PathsSettings
|
||||
connect(pathsSettings, &PathsSettings::cardDatabasePathChanged, this,
|
||||
|
|
@ -147,7 +155,13 @@ SettingsCache::SettingsCache()
|
|||
releaseChannels << new StableReleaseChannel();
|
||||
releaseChannels << new BetaReleaseChannel();
|
||||
|
||||
themeName = personalSettings->getThemeName();
|
||||
themeName = appearanceSettings->getThemeName();
|
||||
|
||||
auto definitions = commanderBracketSettings->loadDefinitions();
|
||||
if (definitions.isEmpty()) {
|
||||
definitions = CommanderBracketSettings::defaultDefinitions();
|
||||
}
|
||||
commanderBracketSettings->reloadDefinitions(definitions);
|
||||
|
||||
loadPaths();
|
||||
}
|
||||
|
|
@ -155,7 +169,7 @@ SettingsCache::SettingsCache()
|
|||
void SettingsCache::setThemeName(const QString &_themeName)
|
||||
{
|
||||
themeName = _themeName;
|
||||
personalSettings->setThemeName(themeName);
|
||||
appearanceSettings->setThemeName(themeName);
|
||||
emit themeChanged();
|
||||
}
|
||||
|
||||
|
|
@ -216,15 +230,15 @@ void SettingsCache::loadPaths()
|
|||
// customPicsPath derived from picsPath
|
||||
QString picsPath = pathsIni.value("paths/pics").toString();
|
||||
if (picsPath.endsWith("/")) {
|
||||
computePath("custompics", picsPath + "CUSTOM/");
|
||||
computePath("customPics", picsPath + "CUSTOM/");
|
||||
} else {
|
||||
computePath("custompics", picsPath + "/CUSTOM/");
|
||||
computePath("customPics", picsPath + "/CUSTOM/");
|
||||
}
|
||||
|
||||
computePath("customsets", dataPath + "/customsets/");
|
||||
computeFilePath("carddatabase", dataPath + "/cards.xml");
|
||||
computeFilePath("tokendatabase", dataPath + "/tokens.xml");
|
||||
computeFilePath("spoilerdatabase", dataPath + "/spoiler.xml");
|
||||
computePath("customSets", dataPath + "/customsets/");
|
||||
computeFilePath("cardDatabase", dataPath + "/cards.xml");
|
||||
computeFilePath("tokenDatabase", dataPath + "/tokens.xml");
|
||||
computeFilePath("spoilerDatabase", dataPath + "/spoiler.xml");
|
||||
}
|
||||
|
||||
void SettingsCache::resetPaths()
|
||||
|
|
@ -272,12 +286,12 @@ QString SettingsCache::getTokenDatabasePath() const
|
|||
// INetworkSettingsProvider - delegate to sub-objects
|
||||
int SettingsCache::getKeepAlive() const
|
||||
{
|
||||
return personalSettings->getKeepAlive();
|
||||
return networkSettings->getKeepAlive();
|
||||
}
|
||||
|
||||
int SettingsCache::getTimeOut() const
|
||||
{
|
||||
return personalSettings->getTimeOut();
|
||||
return networkSettings->getTimeOut();
|
||||
}
|
||||
|
||||
bool SettingsCache::getNotifyAboutUpdates() const
|
||||
|
|
@ -287,17 +301,17 @@ bool SettingsCache::getNotifyAboutUpdates() const
|
|||
|
||||
void SettingsCache::setKnownMissingFeatures(const QString &_knownMissingFeatures)
|
||||
{
|
||||
interfaceSettings->setKnownMissingFeatures(_knownMissingFeatures);
|
||||
networkSettings->setKnownMissingFeatures(_knownMissingFeatures);
|
||||
}
|
||||
|
||||
QString SettingsCache::getKnownMissingFeatures()
|
||||
{
|
||||
return interfaceSettings->getKnownMissingFeatures();
|
||||
return networkSettings->getKnownMissingFeatures();
|
||||
}
|
||||
|
||||
QString SettingsCache::getClientID()
|
||||
{
|
||||
return personalSettings->getClientID();
|
||||
return networkSettings->getClientID();
|
||||
}
|
||||
|
||||
// Release channels
|
||||
|
|
@ -412,7 +426,7 @@ CardsDisplaySettings &SettingsCache::cardsDisplay() const
|
|||
return *cardsDisplaySettings;
|
||||
}
|
||||
|
||||
InterfaceSettings &SettingsCache::interface() const
|
||||
InterfaceSettings &SettingsCache::userInterface() const
|
||||
{
|
||||
return *interfaceSettings;
|
||||
}
|
||||
|
|
@ -422,7 +436,22 @@ PathsSettings &SettingsCache::paths() const
|
|||
return *pathsSettings;
|
||||
}
|
||||
|
||||
DeckEditorSettings &SettingsCache::deckEditor() const
|
||||
{
|
||||
return *deckEditorSettings;
|
||||
}
|
||||
|
||||
VisualDeckStorageSettings &SettingsCache::visualDeckStorage() const
|
||||
{
|
||||
return *visualDeckStorageSettings;
|
||||
}
|
||||
|
||||
AppearanceSettings &SettingsCache::appearance() const
|
||||
{
|
||||
return *appearanceSettings;
|
||||
}
|
||||
|
||||
NetworkSettings &SettingsCache::network() const
|
||||
{
|
||||
return *networkSettings;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -28,7 +28,9 @@ class CardDatabaseSettings;
|
|||
class CardOverrideSettings;
|
||||
class CardsDisplaySettings;
|
||||
class ChatSettings;
|
||||
class CommanderBracketSettings;
|
||||
class DebugSettings;
|
||||
class DeckEditorSettings;
|
||||
class DownloadSettings;
|
||||
class GameFiltersSettings;
|
||||
class GameSettings;
|
||||
|
|
@ -44,6 +46,8 @@ class SoundSettings;
|
|||
class TabsSettings;
|
||||
class UpdatesSettings;
|
||||
class VisualDeckStorageSettings;
|
||||
class AppearanceSettings;
|
||||
class NetworkSettings;
|
||||
class QSettings;
|
||||
|
||||
class SettingsCache : public ICardDatabasePathProvider, public INetworkSettingsProvider
|
||||
|
|
@ -75,8 +79,12 @@ private:
|
|||
PersonalSettings *personalSettings;
|
||||
CardsDisplaySettings *cardsDisplaySettings;
|
||||
InterfaceSettings *interfaceSettings;
|
||||
DeckEditorSettings *deckEditorSettings;
|
||||
PathsSettings *pathsSettings;
|
||||
VisualDeckStorageSettings *visualDeckStorageSettings;
|
||||
AppearanceSettings *appearanceSettings;
|
||||
NetworkSettings *networkSettings;
|
||||
CommanderBracketSettings *commanderBracketSettings;
|
||||
|
||||
QString themeName;
|
||||
|
||||
|
|
@ -138,9 +146,16 @@ public:
|
|||
[[nodiscard]] UpdatesSettings &updates() const;
|
||||
[[nodiscard]] PersonalSettings &personal() const;
|
||||
[[nodiscard]] CardsDisplaySettings &cardsDisplay() const;
|
||||
[[nodiscard]] InterfaceSettings &interface() const;
|
||||
[[nodiscard]] InterfaceSettings &userInterface() const;
|
||||
[[nodiscard]] DeckEditorSettings &deckEditor() const;
|
||||
[[nodiscard]] PathsSettings &paths() const;
|
||||
[[nodiscard]] VisualDeckStorageSettings &visualDeckStorage() const;
|
||||
[[nodiscard]] AppearanceSettings &appearance() const;
|
||||
[[nodiscard]] NetworkSettings &network() const;
|
||||
[[nodiscard]] CommanderBracketSettings &commanderBrackets() const
|
||||
{
|
||||
return *commanderBracketSettings;
|
||||
}
|
||||
|
||||
[[nodiscard]] bool getIsPortableBuild() const
|
||||
{
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
#include <QtMath>
|
||||
|
||||
CardCounterSettings::CardCounterSettings(const QString &settingsPath, QObject *parent)
|
||||
: SettingsManager(settingsPath + "global.ini", "cards", "counters", parent)
|
||||
: SettingsManager(settingsPath + "card_counters.ini", "cards", "counters", parent)
|
||||
{
|
||||
}
|
||||
|
||||
|
|
|
|||
69
cockatrice/src/client/url_scheme_event_filter.h
Normal file
69
cockatrice/src/client/url_scheme_event_filter.h
Normal 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
|
||||
|
|
@ -31,7 +31,7 @@ AbstractClient *AbstractGame::getClientForPlayer(int playerId) const
|
|||
}
|
||||
}
|
||||
|
||||
void AbstractGame::loadReplay(GameReplay *replay)
|
||||
void AbstractGame::loadReplay(const GameReplay *replay)
|
||||
{
|
||||
gameMetaInfo->setFromProto(replay->game_info());
|
||||
gameMetaInfo->setSpectatorsOmniscient(true);
|
||||
|
|
|
|||
|
|
@ -53,7 +53,7 @@ public:
|
|||
|
||||
AbstractClient *getClientForPlayer(int playerId) const;
|
||||
|
||||
void loadReplay(GameReplay *replay);
|
||||
void loadReplay(const GameReplay *replay);
|
||||
|
||||
CardItem *getCard(int playerId, const QString &zoneName, int cardId) const;
|
||||
|
||||
|
|
|
|||
|
|
@ -69,7 +69,7 @@ void PlayerActions::playCard(CardItem *card, bool faceDown)
|
|||
const CardInfo &info = exactCard.getInfo();
|
||||
|
||||
int tableRow = info.getUiAttributes().tableRow;
|
||||
bool playToStack = SettingsCache::instance().interface().getPlayToStack();
|
||||
bool playToStack = SettingsCache::instance().userInterface().getPlayToStack();
|
||||
QString currentZone = card->getZone()->getName();
|
||||
if (!faceDown && currentZone == ZoneNames::STACK && tableRow == 3) {
|
||||
cmd.set_target_zone(ZoneNames::GRAVE);
|
||||
|
|
@ -312,7 +312,7 @@ void PlayerActions::actDrawCard()
|
|||
|
||||
void PlayerActions::actRequestMulliganDialog()
|
||||
{
|
||||
int startSize = SettingsCache::instance().interface().getStartingHandSize();
|
||||
int startSize = SettingsCache::instance().userInterface().getStartingHandSize();
|
||||
int handSize = player->getHandZone()->getCards().size();
|
||||
int deckSize = player->getDeckZone()->getCards().size() + handSize;
|
||||
|
||||
|
|
@ -328,7 +328,7 @@ void PlayerActions::actMulligan(int number)
|
|||
}
|
||||
|
||||
doMulligan(number);
|
||||
SettingsCache::instance().interface().setStartingHandSize(number);
|
||||
SettingsCache::instance().userInterface().setStartingHandSize(number);
|
||||
}
|
||||
|
||||
void PlayerActions::actMulliganSameSize()
|
||||
|
|
@ -932,13 +932,13 @@ void PlayerActions::setLastTokenInfo(CardInfoPtr cardInfo)
|
|||
return;
|
||||
}
|
||||
|
||||
lastTokenInfo = {.name = cardInfo->getName(),
|
||||
.color = cardInfo->getColors().isEmpty() ? QString() : cardInfo->getColors().left(1).toLower(),
|
||||
.pt = cardInfo->getPowTough(),
|
||||
.annotation = SettingsCache::instance().interface().getAnnotateTokens() ? cardInfo->getText() : "",
|
||||
.destroy = true,
|
||||
.providerId =
|
||||
SettingsCache::instance().cardOverrides().getCardPreferenceOverride(cardInfo->getName())};
|
||||
lastTokenInfo = {
|
||||
.name = cardInfo->getName(),
|
||||
.color = cardInfo->getColors().isEmpty() ? QString() : cardInfo->getColors().left(1).toLower(),
|
||||
.pt = cardInfo->getPowTough(),
|
||||
.annotation = SettingsCache::instance().userInterface().getAnnotateTokens() ? cardInfo->getText() : "",
|
||||
.destroy = true,
|
||||
.providerId = SettingsCache::instance().cardOverrides().getCardPreferenceOverride(cardInfo->getName())};
|
||||
|
||||
lastTokenTableRow = TableZone::tableRowToGridY(cardInfo->getUiAttributes().tableRow);
|
||||
|
||||
|
|
@ -1171,7 +1171,7 @@ void PlayerActions::createCard(const CardItem *sourceCard,
|
|||
}
|
||||
|
||||
cmd.set_pt(cardInfo->getPowTough().toStdString());
|
||||
if (SettingsCache::instance().interface().getAnnotateTokens()) {
|
||||
if (SettingsCache::instance().userInterface().getAnnotateTokens()) {
|
||||
cmd.set_annotation(cardInfo->getText().toStdString());
|
||||
} else {
|
||||
cmd.set_annotation("");
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
#include "../interface/widgets/tabs/tab_game.h"
|
||||
|
||||
Replay::Replay(QObject *_parent, GameReplay *_replay, bool isLocalGame) : AbstractGame(_parent)
|
||||
Replay::Replay(QObject *_parent, const GameReplay *_replay, bool isLocalGame) : AbstractGame(_parent)
|
||||
{
|
||||
gameState = new GameState(this, 0, -1, isLocalGame, {}, false, false, -1, false);
|
||||
connect(gameMetaInfo, &GameMetaInfo::startedChanged, gameState, &GameState::onStartedChanged);
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ class Replay : public AbstractGame
|
|||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit Replay(QObject *_parent, GameReplay *_replay, bool isLocalGame);
|
||||
explicit Replay(QObject *_parent, const GameReplay *_replay, bool isLocalGame);
|
||||
};
|
||||
|
||||
#endif // COCKATRICE_REPLAY_H
|
||||
|
|
|
|||
|
|
@ -58,7 +58,7 @@ bool ZoneViewZoneLogic::prepareAddCard(int x)
|
|||
|
||||
// autoclose check is done both here and in removeCard
|
||||
|
||||
if (cards.isEmpty() && !doInsert && SettingsCache::instance().interface().getCloseEmptyCardView()) {
|
||||
if (cards.isEmpty() && !doInsert && SettingsCache::instance().userInterface().getCloseEmptyCardView()) {
|
||||
emit closeView();
|
||||
}
|
||||
|
||||
|
|
@ -145,7 +145,7 @@ void ZoneViewZoneLogic::removeCard(int position, bool toNewZone)
|
|||
// card gets dragged within the view.
|
||||
// Another autoclose check is done in prepareAddCard so that the view autocloses if the last card was moved to an
|
||||
// unrevealed portion of the same zone.
|
||||
if (cards.isEmpty() && SettingsCache::instance().interface().getCloseEmptyCardView() && toNewZone) {
|
||||
if (cards.isEmpty() && SettingsCache::instance().userInterface().getCloseEmptyCardView() && toNewZone) {
|
||||
emit closeView();
|
||||
return;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,9 +12,9 @@
|
|||
#include <algorithm>
|
||||
#include <libcockatrice/card/database/card_database.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/debug_settings.h>
|
||||
#include <libcockatrice/settings/personal_settings.h>
|
||||
|
||||
AbstractCardItem::AbstractCardItem(QGraphicsItem *parent, const CardRef &cardRef, PlayerLogic *_owner, int _id)
|
||||
: ArrowTarget(_owner, parent), id(_id), cardRef(cardRef), tapped(false), facedown(false), tapAngle(0),
|
||||
|
|
@ -107,7 +107,7 @@ QSizeF AbstractCardItem::getTranslatedSize(QPainter *painter) const
|
|||
|
||||
void AbstractCardItem::transformPainter(QPainter *painter, const QSizeF &translatedSize, int angle)
|
||||
{
|
||||
const int MAX_FONT_SIZE = SettingsCache::instance().personal().getMaxFontSize();
|
||||
const int MAX_FONT_SIZE = SettingsCache::instance().appearance().getMaxFontSize();
|
||||
const int fontSize = std::max(9, MAX_FONT_SIZE);
|
||||
|
||||
QRectF totalBoundingRect = painter->combinedTransform().mapRect(boundingRect());
|
||||
|
|
|
|||
|
|
@ -262,7 +262,7 @@ void ArrowDragItem::mouseReleaseEvent(QGraphicsSceneMouseEvent *event)
|
|||
if (startZone->getName() == ZoneNames::HAND) {
|
||||
startCard->playCard(false);
|
||||
CardInfoPtr ci = startCard->getCard().getCardPtr();
|
||||
bool playToStack = SettingsCache::instance().interface().getPlayToStack();
|
||||
bool playToStack = SettingsCache::instance().userInterface().getPlayToStack();
|
||||
if (ci && ((!playToStack && ci->getUiAttributes().tableRow == 3) ||
|
||||
(playToStack && ci->getUiAttributes().tableRow != 0 &&
|
||||
startCard->getZone()->getName() != ZoneNames::STACK))) {
|
||||
|
|
|
|||
|
|
@ -281,7 +281,7 @@ void CardItem::drawArrow(const QColor &arrowColor)
|
|||
auto *game = owner->getGame();
|
||||
PlayerLogic *arrowOwner = game->getPlayerManager()->getActiveLocalPlayer(game->getGameState()->getActivePlayer());
|
||||
int phase = 0; // 0 means to not set the phase
|
||||
if (SettingsCache::instance().interface().getDoNotDeleteArrowsInSubPhases()) {
|
||||
if (SettingsCache::instance().userInterface().getDoNotDeleteArrowsInSubPhases()) {
|
||||
int currentPhase = game->getGameState()->getCurrentPhase();
|
||||
phase = Phases::getLastSubphase(currentPhase) + 1;
|
||||
}
|
||||
|
|
@ -400,7 +400,7 @@ void CardItem::playCard(bool faceDown)
|
|||
if (tz) {
|
||||
emit tz->toggleTapped();
|
||||
} else {
|
||||
if (SettingsCache::instance().interface().getClickPlaysAllSelected()) {
|
||||
if (SettingsCache::instance().userInterface().getClickPlaysAllSelected()) {
|
||||
if (faceDown) {
|
||||
emit playSelectedFaceDown(this);
|
||||
} else {
|
||||
|
|
@ -464,7 +464,7 @@ static bool isUnwritableRevealZone(CardZoneLogic *zone)
|
|||
void CardItem::handleClickedToPlay(bool shiftHeld)
|
||||
{
|
||||
if (isUnwritableRevealZone(state->getZone())) {
|
||||
if (SettingsCache::instance().interface().getClickPlaysAllSelected()) {
|
||||
if (SettingsCache::instance().userInterface().getClickPlaysAllSelected()) {
|
||||
emit hideSelected(this);
|
||||
} else {
|
||||
state->getZone()->removeCard(this);
|
||||
|
|
@ -481,7 +481,7 @@ void CardItem::mouseReleaseEvent(QGraphicsSceneMouseEvent *event)
|
|||
return;
|
||||
}
|
||||
if ((event->modifiers() != Qt::AltModifier) && (event->button() == Qt::LeftButton) &&
|
||||
(!SettingsCache::instance().interface().getDoubleClickToPlay())) {
|
||||
(!SettingsCache::instance().userInterface().getDoubleClickToPlay())) {
|
||||
handleClickedToPlay(event->modifiers().testFlag(Qt::ShiftModifier));
|
||||
}
|
||||
if (owner != nullptr) {
|
||||
|
|
@ -493,7 +493,7 @@ void CardItem::mouseReleaseEvent(QGraphicsSceneMouseEvent *event)
|
|||
void CardItem::mouseDoubleClickEvent(QGraphicsSceneMouseEvent *event)
|
||||
{
|
||||
if ((event->modifiers() != Qt::AltModifier) && (event->buttons() == Qt::LeftButton) &&
|
||||
(SettingsCache::instance().interface().getDoubleClickToPlay())) {
|
||||
(SettingsCache::instance().userInterface().getDoubleClickToPlay())) {
|
||||
handleClickedToPlay(event->modifiers().testFlag(Qt::ShiftModifier));
|
||||
}
|
||||
event->accept();
|
||||
|
|
|
|||
|
|
@ -189,7 +189,7 @@ void DlgCreateToken::tokenSelectionChanged(const QModelIndex ¤t, const QMo
|
|||
const QChar cardColor = cardInfo->getColorChar();
|
||||
colorEdit->setCurrentIndex(colorEdit->findData(cardColor, Qt::UserRole, Qt::MatchFixedString));
|
||||
ptEdit->setText(cardInfo->getPowTough());
|
||||
if (SettingsCache::instance().interface().getAnnotateTokens()) {
|
||||
if (SettingsCache::instance().userInterface().getAnnotateTokens()) {
|
||||
annotationEdit->setText(cardInfo->getText());
|
||||
}
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@ GameScene::GameScene(PhasesToolbar *_phasesToolbar, QObject *parent)
|
|||
{
|
||||
animationTimer = new QBasicTimer;
|
||||
addItem(phasesToolbar);
|
||||
connect(&SettingsCache::instance().interface(), &InterfaceSettings::minPlayersForMultiColumnLayoutChanged, this,
|
||||
connect(&SettingsCache::instance().userInterface(), &InterfaceSettings::minPlayersForMultiColumnLayoutChanged, this,
|
||||
&GameScene::rearrange);
|
||||
|
||||
rearrange();
|
||||
|
|
@ -336,7 +336,7 @@ QList<PlayerLogic *> GameScene::rotatePlayers(const QList<PlayerLogic *> &active
|
|||
|
||||
int GameScene::determineColumnCount(int playerCount)
|
||||
{
|
||||
return playerCount < SettingsCache::instance().interface().getMinPlayersForMultiColumnLayout() ? 1 : 2;
|
||||
return playerCount < SettingsCache::instance().userInterface().getMinPlayersForMultiColumnLayout() ? 1 : 2;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -47,11 +47,11 @@ GameView::GameView(GameScene *scene, QWidget *parent) : QGraphicsView(scene, par
|
|||
connect(scene, &GameScene::sigResizeRubberBand, this, &GameView::resizeRubberBand);
|
||||
connect(scene, &GameScene::sigStopRubberBand, this, &GameView::stopRubberBand);
|
||||
connect(scene, &QGraphicsScene::selectionChanged, this, [this]() { updateTotalSelectionCount(); });
|
||||
connect(&SettingsCache::instance().interface(), &InterfaceSettings::tallyTypeChanged, this,
|
||||
connect(&SettingsCache::instance().userInterface(), &InterfaceSettings::tallyTypeChanged, this,
|
||||
[this] { updateTotalSelectionCount(); });
|
||||
|
||||
setFocusDisabled(SettingsCache::instance().interface().getKeepGameChatFocus());
|
||||
connect(&SettingsCache::instance().interface(), &InterfaceSettings::keepGameChatFocusChanged, this,
|
||||
setFocusDisabled(SettingsCache::instance().userInterface().getKeepGameChatFocus());
|
||||
connect(&SettingsCache::instance().userInterface(), &InterfaceSettings::keepGameChatFocusChanged, this,
|
||||
&GameView::setFocusDisabled);
|
||||
|
||||
aCloseMostRecentZoneView = new QAction(this);
|
||||
|
|
@ -130,7 +130,7 @@ void GameView::resizeRubberBand(const QPointF &cursorPoint, int selectedCount)
|
|||
QRect rect = QRect(mapFromScene(selectionOrigin), cursor).normalized();
|
||||
rubberBand->setGeometry(rect);
|
||||
|
||||
if (!SettingsCache::instance().interface().getShowDragSelectionCount()) {
|
||||
if (!SettingsCache::instance().userInterface().getShowDragSelectionCount()) {
|
||||
dragCountLabel->hide();
|
||||
return;
|
||||
}
|
||||
|
|
@ -239,7 +239,7 @@ void GameView::updateTotalSelectionCount(const QSize &viewSize)
|
|||
|
||||
int count = scene()->selectedItems().count();
|
||||
|
||||
if (!SettingsCache::instance().interface().getShowTotalSelectionCount() || count <= 1) {
|
||||
if (!SettingsCache::instance().userInterface().getShowTotalSelectionCount() || count <= 1) {
|
||||
totalCountLabel->hide();
|
||||
} else {
|
||||
totalCountLabel->setText(QString::number(count));
|
||||
|
|
@ -251,12 +251,12 @@ void GameView::updateTotalSelectionCount(const QSize &viewSize)
|
|||
totalCountLabel->show();
|
||||
}
|
||||
|
||||
TallyType tallyType = Tally::intToType(SettingsCache::instance().interface().getTallyType());
|
||||
TallyType tallyType = Tally::intToType(SettingsCache::instance().userInterface().getTallyType());
|
||||
|
||||
GameScene *gameScene = static_cast<GameScene *>(scene());
|
||||
QList<TallyRow> entries = Tally::compute(gameScene->selectedCards(), tallyType);
|
||||
|
||||
if (entries.isEmpty() || count <= 1) {
|
||||
if (entries.isEmpty()) {
|
||||
tallyContainer->hide();
|
||||
cachedTallyRows.clear();
|
||||
return;
|
||||
|
|
|
|||
|
|
@ -11,24 +11,26 @@ TallyMenu::TallyMenu()
|
|||
|
||||
aTallyNone = createTallyAction(TallyType::None);
|
||||
aTallySubtypes = createTallyAction(TallyType::Subtypes);
|
||||
aTallyTotalPower = createTallyAction(TallyType::TotalPower);
|
||||
|
||||
addAction(aTallyNone);
|
||||
addSeparator();
|
||||
addAction(aTallySubtypes);
|
||||
addAction(aTallyTotalPower);
|
||||
|
||||
retranslateUi();
|
||||
}
|
||||
|
||||
QAction *TallyMenu::createTallyAction(TallyType tallyType)
|
||||
{
|
||||
TallyType currentType = Tally::intToType(SettingsCache::instance().interface().getTallyType());
|
||||
TallyType currentType = Tally::intToType(SettingsCache::instance().userInterface().getTallyType());
|
||||
|
||||
QAction *action = new QAction(this);
|
||||
action->setCheckable(true);
|
||||
action->setChecked(tallyType == currentType);
|
||||
|
||||
connect(action, &QAction::triggered, &SettingsCache::instance().interface(),
|
||||
[tallyType] { SettingsCache::instance().interface().setTallyType(static_cast<int>(tallyType)); });
|
||||
connect(action, &QAction::triggered, &SettingsCache::instance().userInterface(),
|
||||
[tallyType] { SettingsCache::instance().userInterface().setTallyType(static_cast<int>(tallyType)); });
|
||||
|
||||
actionGroup->addAction(action);
|
||||
|
||||
|
|
@ -51,4 +53,5 @@ void TallyMenu::retranslateUi()
|
|||
|
||||
aTallyNone->setText(tr("None"));
|
||||
aTallySubtypes->setText(tr("Subtypes"));
|
||||
aTallyTotalPower->setText(tr("Total Power"));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ private:
|
|||
|
||||
QAction *aTallyNone = nullptr;
|
||||
QAction *aTallySubtypes = nullptr;
|
||||
QAction *aTallyTotalPower = nullptr;
|
||||
|
||||
QAction *createTallyAction(TallyType tallyType);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -17,9 +17,9 @@
|
|||
|
||||
PlayerGraphicsItem::PlayerGraphicsItem(PlayerLogic *_player) : player(_player)
|
||||
{
|
||||
connect(&SettingsCache::instance().interface(), &InterfaceSettings::horizontalHandChanged, this,
|
||||
connect(&SettingsCache::instance().userInterface(), &InterfaceSettings::horizontalHandChanged, this,
|
||||
&PlayerGraphicsItem::rearrangeZones);
|
||||
connect(&SettingsCache::instance().interface(), &InterfaceSettings::handJustificationChanged, this,
|
||||
connect(&SettingsCache::instance().userInterface(), &InterfaceSettings::handJustificationChanged, this,
|
||||
&PlayerGraphicsItem::rearrangeZones);
|
||||
connect(player, &PlayerLogic::rearrangeCounters, this, &PlayerGraphicsItem::rearrangeCounters);
|
||||
connect(player, &PlayerLogic::activeChanged, this, &PlayerGraphicsItem::onPlayerActiveChanged);
|
||||
|
|
@ -149,7 +149,7 @@ qreal PlayerGraphicsItem::getMinimumWidth() const
|
|||
{
|
||||
qreal result = tableZoneGraphicsItem->getMinimumWidth() + CardDimensions::HEIGHT_F + 15 + counterAreaWidth +
|
||||
stackZoneGraphicsItem->boundingRect().width();
|
||||
if (!SettingsCache::instance().interface().getHorizontalHand()) {
|
||||
if (!SettingsCache::instance().userInterface().getHorizontalHand()) {
|
||||
result += handZoneGraphicsItem->boundingRect().width();
|
||||
}
|
||||
return result;
|
||||
|
|
@ -166,7 +166,7 @@ void PlayerGraphicsItem::processSceneSizeChange(int newPlayerWidth)
|
|||
// Extend table (and hand, if horizontal) to accommodate the new player width.
|
||||
qreal tableWidth = newPlayerWidth - CardDimensions::HEIGHT_F - 15 - counterAreaWidth -
|
||||
stackZoneGraphicsItem->boundingRect().width();
|
||||
if (!SettingsCache::instance().interface().getHorizontalHand()) {
|
||||
if (!SettingsCache::instance().userInterface().getHorizontalHand()) {
|
||||
tableWidth -= handZoneGraphicsItem->boundingRect().width();
|
||||
}
|
||||
|
||||
|
|
@ -234,7 +234,7 @@ void PlayerGraphicsItem::rearrangeCounters()
|
|||
void PlayerGraphicsItem::rearrangeZones()
|
||||
{
|
||||
auto base = QPointF(CardDimensions::HEIGHT_F + counterAreaWidth + 15, 0);
|
||||
if (SettingsCache::instance().interface().getHorizontalHand()) {
|
||||
if (SettingsCache::instance().userInterface().getHorizontalHand()) {
|
||||
if (mirrored) {
|
||||
if (player->getHandZone()->contentsKnown()) {
|
||||
handVisible = true;
|
||||
|
|
@ -285,7 +285,7 @@ void PlayerGraphicsItem::updateBoundingRect()
|
|||
{
|
||||
prepareGeometryChange();
|
||||
qreal width = CardDimensions::HEIGHT_F + 15 + counterAreaWidth + stackZoneGraphicsItem->boundingRect().width();
|
||||
if (SettingsCache::instance().interface().getHorizontalHand()) {
|
||||
if (SettingsCache::instance().userInterface().getHorizontalHand()) {
|
||||
qreal handHeight = handVisible ? handZoneGraphicsItem->boundingRect().height() : 0;
|
||||
bRect = QRectF(0, 0, width + tableZoneGraphicsItem->boundingRect().width(),
|
||||
tableZoneGraphicsItem->boundingRect().height() + handHeight);
|
||||
|
|
|
|||
36
cockatrice/src/game_graphics/tally/stats_tally.cpp
Normal file
36
cockatrice/src/game_graphics/tally/stats_tally.cpp
Normal 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)}};
|
||||
}
|
||||
21
cockatrice/src/game_graphics/tally/stats_tally.h
Normal file
21
cockatrice/src/game_graphics/tally/stats_tally.h
Normal 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
|
||||
|
|
@ -66,8 +66,9 @@ QList<TallyRow> countSubtypes(const QList<CardItem *> &cards)
|
|||
});
|
||||
|
||||
// convert entries into TallyRows
|
||||
QList<TallyRow> rows = QList<TallyRow>(entries.size());
|
||||
std::transform(entries.begin(), entries.end(), rows.begin(),
|
||||
QList<TallyRow> rows;
|
||||
rows.reserve(entries.size());
|
||||
std::transform(entries.begin(), entries.end(), std::back_inserter(rows),
|
||||
[](const SubtypeEntry &e) { return TallyRow{e.name, QString::number(e.count)}; });
|
||||
|
||||
return rows;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
#include "tally.h"
|
||||
|
||||
#include "stats_tally.h"
|
||||
#include "subtype_tally.h"
|
||||
|
||||
TallyType Tally::intToType(int value)
|
||||
|
|
@ -18,6 +19,8 @@ QList<TallyRow> Tally::compute(const QList<CardItem *> &cards, const TallyType t
|
|||
return {};
|
||||
case TallyType::Subtypes:
|
||||
return SubtypeTally::countSubtypes(cards);
|
||||
case TallyType::TotalPower:
|
||||
return StatsTally::computeTotalPower(cards);
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,7 +20,8 @@ enum class TallyType
|
|||
{
|
||||
None,
|
||||
Subtypes,
|
||||
MaxValue = Subtypes // sentinel value
|
||||
TotalPower,
|
||||
MaxValue = TotalPower // sentinel value
|
||||
};
|
||||
|
||||
namespace Tally
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@ void HandZone::handleDropEvent(const QList<CardDragItem *> &dragItems,
|
|||
|
||||
QPoint point = dropPoint + scenePos().toPoint();
|
||||
int x = -1;
|
||||
if (SettingsCache::instance().interface().getHorizontalHand()) {
|
||||
if (SettingsCache::instance().userInterface().getHorizontalHand()) {
|
||||
for (x = 0; x < getLogic()->getCards().size(); x++) {
|
||||
if (point.x() < static_cast<CardItem *>(getLogic()->getCards().at(x))->scenePos().x()) {
|
||||
break;
|
||||
|
|
@ -61,7 +61,7 @@ void HandZone::handleDropEvent(const QList<CardDragItem *> &dragItems,
|
|||
|
||||
QRectF HandZone::boundingRect() const
|
||||
{
|
||||
if (SettingsCache::instance().interface().getHorizontalHand()) {
|
||||
if (SettingsCache::instance().userInterface().getHorizontalHand()) {
|
||||
return QRectF(0, 0, width, CardDimensions::HEIGHT_F + 10);
|
||||
} else {
|
||||
return QRectF(0, 0, CardDimensions::WIDTH_F * 1.5, zoneHeight);
|
||||
|
|
@ -78,8 +78,8 @@ void HandZone::reorganizeCards()
|
|||
{
|
||||
if (!getLogic()->getCards().isEmpty()) {
|
||||
const int cardCount = getLogic()->getCards().size();
|
||||
if (SettingsCache::instance().interface().getHorizontalHand()) {
|
||||
bool leftJustified = SettingsCache::instance().interface().getLeftJustified();
|
||||
if (SettingsCache::instance().userInterface().getHorizontalHand()) {
|
||||
bool leftJustified = SettingsCache::instance().userInterface().getLeftJustified();
|
||||
qreal cardWidth = getLogic()->getCards().at(0)->boundingRect().width();
|
||||
const int xPadding = leftJustified ? cardWidth * 1.4 : 5;
|
||||
qreal totalWidth =
|
||||
|
|
@ -127,7 +127,7 @@ void HandZone::sortHand(const QList<CardList::SortOption> &options)
|
|||
|
||||
void HandZone::setWidth(qreal _width)
|
||||
{
|
||||
if (SettingsCache::instance().interface().getHorizontalHand()) {
|
||||
if (SettingsCache::instance().userInterface().getHorizontalHand()) {
|
||||
prepareGeometryChange();
|
||||
width = _width;
|
||||
reorganizeCards();
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ TableZone::TableZone(TableZoneLogic *_logic, bool _mirrored, QGraphicsItem *pare
|
|||
connect(_logic, &TableZoneLogic::contentSizeChanged, this, &TableZone::resizeToContents);
|
||||
connect(_logic, &TableZoneLogic::toggleTapped, this, &TableZone::toggleTapped);
|
||||
connect(themeManager, &ThemeManager::themeChanged, this, &TableZone::updateBg);
|
||||
connect(&SettingsCache::instance().interface(), &InterfaceSettings::invertVerticalCoordinateChanged, this,
|
||||
connect(&SettingsCache::instance().userInterface(), &InterfaceSettings::invertVerticalCoordinateChanged, this,
|
||||
&TableZone::reorganizeCards);
|
||||
|
||||
updateBg();
|
||||
|
|
@ -60,8 +60,8 @@ void TableZone::setMirrored(bool isMirrored)
|
|||
|
||||
bool TableZone::isInverted() const
|
||||
{
|
||||
return ((mirrored && !SettingsCache::instance().interface().getInvertVerticalCoordinate()) ||
|
||||
(!mirrored && SettingsCache::instance().interface().getInvertVerticalCoordinate()));
|
||||
return ((mirrored && !SettingsCache::instance().userInterface().getInvertVerticalCoordinate()) ||
|
||||
(!mirrored && SettingsCache::instance().userInterface().getInvertVerticalCoordinate()));
|
||||
}
|
||||
|
||||
void TableZone::paint(QPainter *painter, const QStyleOptionGraphicsItem * /*option*/, QWidget * /*widget*/)
|
||||
|
|
|
|||
|
|
@ -66,7 +66,7 @@ ZoneViewWidget::ZoneViewWidget(PlayerLogic *_player,
|
|||
|
||||
connect(help, &QAction::triggered, this, [this] { createSearchSyntaxHelpWindow(&searchEdit); });
|
||||
|
||||
if (SettingsCache::instance().interface().getFocusCardViewSearchBar()) {
|
||||
if (SettingsCache::instance().userInterface().getFocusCardViewSearchBar()) {
|
||||
this->setActive(true);
|
||||
searchEdit.setFocus();
|
||||
}
|
||||
|
|
@ -77,9 +77,9 @@ ZoneViewWidget::ZoneViewWidget(PlayerLogic *_player,
|
|||
vbox->addItem(searchEditProxy);
|
||||
|
||||
// hide search bar if chat autofocus setting is enabled, since typing into it will no longer work anyway
|
||||
searchEditProxy->setVisible(!SettingsCache::instance().interface().getKeepGameChatFocus());
|
||||
connect(&SettingsCache::instance().interface(), &InterfaceSettings::keepGameChatFocusChanged, searchEditProxy,
|
||||
[searchEditProxy](bool keepFocus) { searchEditProxy->setVisible(!keepFocus); });
|
||||
searchEditProxy->setVisible(!SettingsCache::instance().userInterface().getKeepGameChatFocus());
|
||||
connect(&SettingsCache::instance().userInterface(), &InterfaceSettings::keepGameChatFocusChanged,
|
||||
searchEditProxy, [searchEditProxy](bool keepFocus) { searchEditProxy->setVisible(!keepFocus); });
|
||||
|
||||
// top row
|
||||
QGraphicsLinearLayout *hTopRow = new QGraphicsLinearLayout(Qt::Horizontal);
|
||||
|
|
@ -159,9 +159,9 @@ ZoneViewWidget::ZoneViewWidget(PlayerLogic *_player,
|
|||
connect(&sortBySelector, static_cast<void (QComboBox::*)(int)>(&QComboBox::currentIndexChanged), this,
|
||||
&ZoneViewWidget::processSortBy);
|
||||
connect(&pileViewCheckBox, &QCheckBox::QT_STATE_CHANGED, this, &ZoneViewWidget::processSetPileView);
|
||||
groupBySelector.setCurrentIndex(SettingsCache::instance().interface().getZoneViewGroupByIndex());
|
||||
sortBySelector.setCurrentIndex(SettingsCache::instance().interface().getZoneViewSortByIndex());
|
||||
pileViewCheckBox.setChecked(SettingsCache::instance().interface().getZoneViewPileView());
|
||||
groupBySelector.setCurrentIndex(SettingsCache::instance().userInterface().getZoneViewGroupByIndex());
|
||||
sortBySelector.setCurrentIndex(SettingsCache::instance().userInterface().getZoneViewSortByIndex());
|
||||
pileViewCheckBox.setChecked(SettingsCache::instance().userInterface().getZoneViewPileView());
|
||||
|
||||
if (CardList::NoSort == static_cast<CardList::SortOption>(groupBySelector.currentData().toInt())) {
|
||||
pileViewCheckBox.setEnabled(false);
|
||||
|
|
@ -191,7 +191,7 @@ ZoneViewWidget::ZoneViewWidget(PlayerLogic *_player,
|
|||
void ZoneViewWidget::processGroupBy(int index)
|
||||
{
|
||||
auto option = static_cast<CardList::SortOption>(groupBySelector.itemData(index).toInt());
|
||||
SettingsCache::instance().interface().setZoneViewGroupByIndex(index);
|
||||
SettingsCache::instance().userInterface().setZoneViewGroupByIndex(index);
|
||||
zone->setGroupBy(option);
|
||||
|
||||
// disable pile view checkbox if we're not grouping by anything
|
||||
|
|
@ -215,13 +215,13 @@ void ZoneViewWidget::processSortBy(int index)
|
|||
return;
|
||||
}
|
||||
|
||||
SettingsCache::instance().interface().setZoneViewSortByIndex(index);
|
||||
SettingsCache::instance().userInterface().setZoneViewSortByIndex(index);
|
||||
zone->setSortBy(option);
|
||||
}
|
||||
|
||||
void ZoneViewWidget::processSetPileView(QT_STATE_CHANGED_T value)
|
||||
{
|
||||
SettingsCache::instance().interface().setZoneViewPileView(value);
|
||||
SettingsCache::instance().userInterface().setZoneViewPileView(value);
|
||||
zone->setPileView(value);
|
||||
}
|
||||
|
||||
|
|
@ -478,7 +478,7 @@ static qreal rowsToHeight(int rows)
|
|||
**/
|
||||
static qreal calcMaxInitialHeight()
|
||||
{
|
||||
return rowsToHeight(SettingsCache::instance().interface().getCardViewInitialRowsMax());
|
||||
return rowsToHeight(SettingsCache::instance().userInterface().getCardViewInitialRowsMax());
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -560,7 +560,7 @@ void ZoneViewWidget::initStyleOption(QStyleOption *option) const
|
|||
void ZoneViewWidget::expandWindow()
|
||||
{
|
||||
qreal maxInitialHeight = calcMaxInitialHeight();
|
||||
qreal maxExpandedHeight = rowsToHeight(SettingsCache::instance().interface().getCardViewExpandedRowsMax());
|
||||
qreal maxExpandedHeight = rowsToHeight(SettingsCache::instance().userInterface().getCardViewExpandedRowsMax());
|
||||
qreal height = rect().height() - extraHeight - 10;
|
||||
qreal maxHeight = maximumHeight() - extraHeight - 10;
|
||||
|
||||
|
|
|
|||
|
|
@ -19,19 +19,22 @@
|
|||
#include <QThread>
|
||||
#include <algorithm>
|
||||
#include <libcockatrice/settings/cache_storage_settings.h>
|
||||
#include <libcockatrice/settings/download_settings.h>
|
||||
#include <libcockatrice/settings/paths_settings.h>
|
||||
#include <libcockatrice/settings/personal_settings.h>
|
||||
#include <utility>
|
||||
|
||||
// never cache more than 300 cards at once for a single deck
|
||||
#define CACHED_CARD_PER_DECK_MAX 300
|
||||
|
||||
// wait at least this long before retrying a card whose picture failed to load
|
||||
static constexpr int RETRY_FAILED_CARDS_SECS = 300;
|
||||
|
||||
CardPictureLoader::CardPictureLoader() : QObject(nullptr)
|
||||
{
|
||||
worker = new CardPictureLoaderWorker;
|
||||
connect(&SettingsCache::instance().paths(), &PathsSettings::picsPathChanged, this,
|
||||
&CardPictureLoader::picsPathChanged);
|
||||
connect(&SettingsCache::instance().personal(), &PersonalSettings::picDownloadChanged, this,
|
||||
connect(&SettingsCache::instance().downloads(), &DownloadSettings::picDownloadChanged, this,
|
||||
&CardPictureLoader::picDownloadChanged);
|
||||
|
||||
qRegisterMetaType<ExactCard>();
|
||||
|
|
@ -135,7 +138,14 @@ void CardPictureLoader::getPixmap(QPixmap &pixmap, const ExactCard &card, QSize
|
|||
QPixmap bigPixmap;
|
||||
if (QPixmapCache::find(key, &bigPixmap)) {
|
||||
if (bigPixmap.isNull()) {
|
||||
qCDebug(CardPictureLoaderLog) << "Cached pixmap for key" << key << "is NULL!";
|
||||
getCardBackLoadingFailedPixmap(pixmap, size);
|
||||
QDateTime failedAtTime = getInstance().failedAt.value(key);
|
||||
if (!failedAtTime.isValid() ||
|
||||
failedAtTime.addSecs(RETRY_FAILED_CARDS_SECS) < QDateTime::currentDateTime()) {
|
||||
getInstance().failedAt.remove(key);
|
||||
QPixmapCache::remove(key);
|
||||
getInstance().worker->enqueueImageLoad(card);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -159,8 +169,10 @@ void CardPictureLoader::imageLoaded(const ExactCard &card, const QImage &image)
|
|||
QPixmap finalPixmap;
|
||||
|
||||
if (image.isNull()) {
|
||||
getInstance().failedAt.insert(card.getPixmapCacheKey(), QDateTime::currentDateTime());
|
||||
qCDebug(CardPictureLoaderLog) << "Caching NULL pixmap for" << card.getName();
|
||||
} else {
|
||||
getInstance().failedAt.remove(card.getPixmapCacheKey());
|
||||
if (card.getInfo().getUiAttributes().upsideDownArt) {
|
||||
#if (QT_VERSION >= QT_VERSION_CHECK(6, 9, 0))
|
||||
QImage mirrorImage = image.flipped(Qt::Horizontal | Qt::Vertical);
|
||||
|
|
@ -184,8 +196,10 @@ void CardPictureLoader::imageLoaded(const ExactCard &card, const QImage &image)
|
|||
// imageLoaded should only be reached if the exactCard isn't already in cache.
|
||||
// (plus there's a deduplication mechanism in CardPictureLoaderWorker)
|
||||
// It should be safe to connect the CardInfo here without worrying about redundant connections.
|
||||
connect(card.getCardPtr().data(), &QObject::destroyed, this,
|
||||
[cacheKey = card.getPixmapCacheKey()] { QPixmapCache::remove(cacheKey); });
|
||||
connect(card.getCardPtr().data(), &QObject::destroyed, this, [cacheKey = card.getPixmapCacheKey()] {
|
||||
QPixmapCache::remove(cacheKey);
|
||||
getInstance().failedAt.remove(cacheKey);
|
||||
});
|
||||
|
||||
card.emitPixmapUpdated();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,8 @@
|
|||
#include "card_picture_loader_status_bar.h"
|
||||
#include "card_picture_loader_worker.h"
|
||||
|
||||
#include <QDateTime>
|
||||
#include <QHash>
|
||||
#include <QLoggingCategory>
|
||||
|
||||
inline Q_LOGGING_CATEGORY(CardPictureLoaderLog, "card_picture_loader");
|
||||
|
|
@ -56,6 +58,7 @@ private:
|
|||
|
||||
CardPictureLoaderWorker *worker; ///< Worker thread for async image loading
|
||||
CardPictureLoaderStatusBar *statusBar; ///< Status bar widget showing load progress
|
||||
QHash<QString, QDateTime> failedAt; ///< Timestamp of the last failed load attempt per pixmap cache key
|
||||
|
||||
public:
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -11,15 +11,17 @@
|
|||
#include <QNetworkReply>
|
||||
#include <QThread>
|
||||
#include <libcockatrice/settings/cache_storage_settings.h>
|
||||
#include <libcockatrice/settings/download_settings.h>
|
||||
#include <libcockatrice/settings/paths_settings.h>
|
||||
#include <libcockatrice/settings/personal_settings.h>
|
||||
#include <utility>
|
||||
#include <version_string.h>
|
||||
|
||||
static constexpr int MAX_REQUESTS_PER_SEC = 10;
|
||||
static constexpr int MIN_HOST_QUOTA = 1; ///< Floor for the per-host request allowance
|
||||
static constexpr qint64 QUOTA_RECOVER_MS = 60000; ///< Idle time before a reduced quota starts recovering
|
||||
|
||||
CardPictureLoaderWorker::CardPictureLoaderWorker()
|
||||
: QObject(nullptr), picDownload(SettingsCache::instance().personal().getPicDownload()),
|
||||
: QObject(nullptr), picDownload(SettingsCache::instance().downloads().getPicDownload()),
|
||||
requestQuota(MAX_REQUESTS_PER_SEC)
|
||||
{
|
||||
networkManager = new QNetworkAccessManager(this);
|
||||
|
|
@ -124,6 +126,19 @@ QNetworkReply *CardPictureLoaderWorker::makeRequest(const QUrl &url, CardPicture
|
|||
void CardPictureLoaderWorker::resetRequestQuota()
|
||||
{
|
||||
requestQuota = MAX_REQUESTS_PER_SEC;
|
||||
|
||||
QDateTime now = QDateTime::currentDateTime();
|
||||
for (auto it = hostRequestQuota.begin(); it != hostRequestQuota.end(); ++it) {
|
||||
if (!hostLast429.contains(it.key()) || now.msecsTo(hostLast429.value(it.key())) < -QUOTA_RECOVER_MS) {
|
||||
it.value() = qMin(MAX_REQUESTS_PER_SEC, it.value() + 1);
|
||||
}
|
||||
}
|
||||
|
||||
for (const auto &request : requestLoadQueue) {
|
||||
const QString host = request.first.host();
|
||||
hostQuotaRemaining.insert(host, hostRequestQuota.value(host, MAX_REQUESTS_PER_SEC));
|
||||
}
|
||||
|
||||
processQueuedRequests();
|
||||
}
|
||||
|
||||
|
|
@ -136,14 +151,26 @@ void CardPictureLoaderWorker::processQueuedRequests()
|
|||
|
||||
bool CardPictureLoaderWorker::processSingleRequest()
|
||||
{
|
||||
if (!requestLoadQueue.isEmpty()) {
|
||||
auto request = requestLoadQueue.takeFirst();
|
||||
makeRequest(request.first, request.second);
|
||||
return true;
|
||||
for (int i = 0; i < requestLoadQueue.size(); ++i) {
|
||||
const auto &request = requestLoadQueue.at(i);
|
||||
QString host = request.first.host();
|
||||
int allowance = hostQuotaRemaining.value(host, MAX_REQUESTS_PER_SEC);
|
||||
if (allowance > 0) {
|
||||
hostQuotaRemaining.insert(host, allowance - 1);
|
||||
makeRequest(request.first, request.second);
|
||||
requestLoadQueue.removeAt(i);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void CardPictureLoaderWorker::onHostRateLimited(const QString &host)
|
||||
{
|
||||
hostRequestQuota.insert(host, qMax(MIN_HOST_QUOTA, hostRequestQuota.value(host, MAX_REQUESTS_PER_SEC) / 2));
|
||||
hostLast429.insert(host, QDateTime::currentDateTime());
|
||||
}
|
||||
|
||||
void CardPictureLoaderWorker::enqueueImageLoad(const ExactCard &card)
|
||||
{
|
||||
// Send call through a connection to ensure the handling is run on the pictureLoader thread
|
||||
|
|
|
|||
|
|
@ -5,6 +5,8 @@
|
|||
#include "card_picture_loader_worker_work.h"
|
||||
#include "card_picture_to_load.h"
|
||||
|
||||
#include <QDateTime>
|
||||
#include <QHash>
|
||||
#include <QLoggingCategory>
|
||||
#include <QMutex>
|
||||
#include <QNetworkAccessManager>
|
||||
|
|
@ -66,6 +68,12 @@ public:
|
|||
*/
|
||||
void queueRequest(const QUrl &url, CardPictureLoaderWorkerWork *worker);
|
||||
|
||||
/**
|
||||
* @brief Handles a server returning HTTP 429 by reducing that host's request quota.
|
||||
* @param host The host that returned 429
|
||||
*/
|
||||
void onHostRateLimited(const QString &host);
|
||||
|
||||
/** @brief Clears the network cache and redirect cache. */
|
||||
void clearNetworkCache();
|
||||
|
||||
|
|
@ -110,8 +118,11 @@ private:
|
|||
bool picDownload; ///< Whether downloading images from network is enabled
|
||||
QQueue<QPair<QUrl, CardPictureLoaderWorkerWork *>> requestLoadQueue; ///< Queue of pending network requests
|
||||
|
||||
int requestQuota; ///< Remaining requests allowed per second
|
||||
QTimer requestTimer; ///< Timer to reset the request quota
|
||||
int requestQuota; ///< Remaining requests allowed per second
|
||||
QTimer requestTimer; ///< Timer to reset the request quota
|
||||
QHash<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
|
||||
QSet<QString> currentlyLoading; ///< Deduplication: contains pixmapCacheKey currently being loaded
|
||||
|
|
|
|||
|
|
@ -8,9 +8,13 @@
|
|||
#include <QLoggingCategory>
|
||||
#include <QMovie>
|
||||
#include <QNetworkReply>
|
||||
#include <QRandomGenerator>
|
||||
#include <QThread>
|
||||
#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
|
||||
static const QStringList MD5_BLACKLIST = {
|
||||
|
|
@ -20,7 +24,7 @@ static const QStringList MD5_BLACKLIST = {
|
|||
|
||||
CardPictureLoaderWorkerWork::CardPictureLoaderWorkerWork(const CardPictureLoaderWorker *worker, const ExactCard &toLoad)
|
||||
: QObject(nullptr), cardToDownload(CardPictureToLoad(toLoad)),
|
||||
picDownload(SettingsCache::instance().personal().getPicDownload())
|
||||
picDownload(SettingsCache::instance().downloads().getPicDownload())
|
||||
{
|
||||
// Hook up signals to the orchestrator
|
||||
connect(this, &CardPictureLoaderWorkerWork::requestImageDownload, worker, &CardPictureLoaderWorker::queueRequest);
|
||||
|
|
@ -30,19 +34,48 @@ CardPictureLoaderWorkerWork::CardPictureLoaderWorkerWork(const CardPictureLoader
|
|||
connect(this, &CardPictureLoaderWorkerWork::imageLoaded, worker, &CardPictureLoaderWorker::handleImageLoaded);
|
||||
connect(this, &CardPictureLoaderWorkerWork::requestSucceeded, worker,
|
||||
&CardPictureLoaderWorker::imageRequestSucceeded);
|
||||
connect(this, &CardPictureLoaderWorkerWork::rateLimited, worker, &CardPictureLoaderWorker::onHostRateLimited);
|
||||
|
||||
// Hook up signals to settings
|
||||
connect(&SettingsCache::instance().personal(), SIGNAL(picDownloadChanged()), this, SLOT(picDownloadChanged()));
|
||||
connect(&SettingsCache::instance().downloads(), SIGNAL(picDownloadChanged()), this, SLOT(picDownloadChanged()));
|
||||
|
||||
startNextPicDownload();
|
||||
}
|
||||
|
||||
void CardPictureLoaderWorkerWork::startNextPicDownload()
|
||||
{
|
||||
QDateTime now = QDateTime::currentDateTime();
|
||||
while (!cardToDownload.getCurrentUrl().isEmpty() &&
|
||||
s_rateLimiter.isRateLimited(QUrl(cardToDownload.getCurrentUrl()).host(), now)) {
|
||||
QString host = QUrl(cardToDownload.getCurrentUrl()).host();
|
||||
if (s_rateLimiter.rounds(host) == 1) {
|
||||
// First 429 round for this server: wait out the backoff and give it
|
||||
// one more chance instead of immediately falling through to a worse
|
||||
// source. A second 429 makes us fall through instead.
|
||||
qCDebug(CardPictureLoaderWorkerWorkLog).nospace()
|
||||
<< "PictureLoader: [card: " << cardToDownload.getCard().getInfo().getCorrectedName()
|
||||
<< " set: " << cardToDownload.getSetName() << "]: Waiting out backoff for " << host << " to retry "
|
||||
<< cardToDownload.getCurrentUrl();
|
||||
scheduleDeferredRetry();
|
||||
return;
|
||||
}
|
||||
|
||||
// The server has already 429'd us at least twice, so further retries are
|
||||
// unlikely to succeed: move on to the other configured sources.
|
||||
qCDebug(CardPictureLoaderWorkerWorkLog).nospace()
|
||||
<< "PictureLoader: [card: " << cardToDownload.getCard().getInfo().getCorrectedName()
|
||||
<< " set: " << cardToDownload.getSetName() << "]: Skipping rate-limited URL "
|
||||
<< cardToDownload.getCurrentUrl() << " (server " << host << " still rate limiting)";
|
||||
if (!cardToDownload.nextUrl() && !cardToDownload.nextSet()) {
|
||||
scheduleDeferredRetry();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
QString picUrl = cardToDownload.getCurrentUrl();
|
||||
|
||||
if (picUrl.isEmpty()) {
|
||||
picDownloadFailed();
|
||||
scheduleDeferredRetry();
|
||||
} else {
|
||||
QUrl url(picUrl);
|
||||
qCDebug(CardPictureLoaderWorkerWorkLog).nospace()
|
||||
|
|
@ -108,7 +141,41 @@ static bool imageIsBlackListed(const QByteArray &picData)
|
|||
void CardPictureLoaderWorkerWork::handleFailedReply(const QNetworkReply *reply)
|
||||
{
|
||||
if (reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() == 429) {
|
||||
qCWarning(CardPictureLoaderWorkerWorkLog) << "Too many requests.";
|
||||
QString host = reply->url().host();
|
||||
QDateTime now = QDateTime::currentDateTime();
|
||||
|
||||
qint64 retryAfterMs = 0;
|
||||
const QByteArray retryAfterHeader = reply->rawHeader("Retry-After");
|
||||
if (!retryAfterHeader.isEmpty()) {
|
||||
bool ok = false;
|
||||
int seconds = retryAfterHeader.toInt(&ok);
|
||||
if (ok && seconds > 0) {
|
||||
retryAfterMs = static_cast<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 {
|
||||
bool isFromCache = reply->attribute(QNetworkRequest::SourceIsFromCacheAttribute).toBool();
|
||||
|
||||
|
|
@ -149,6 +216,9 @@ void CardPictureLoaderWorkerWork::handleSuccessfulReply(QNetworkReply *reply)
|
|||
return;
|
||||
}
|
||||
|
||||
// A non-redirect successful response means the server is not rate limiting us anymore.
|
||||
s_rateLimiter.onSuccess(reply->url().host());
|
||||
|
||||
// peek is used to keep the data in the buffer for use by QImageReader
|
||||
const QByteArray &picData = reply->peek(reply->size());
|
||||
|
||||
|
|
@ -203,6 +273,42 @@ QImage CardPictureLoaderWorkerWork::tryLoadImageFromReply(QNetworkReply *reply)
|
|||
return imgReader.read();
|
||||
}
|
||||
|
||||
void CardPictureLoaderWorkerWork::scheduleDeferredRetry()
|
||||
{
|
||||
QDateTime now = QDateTime::currentDateTime();
|
||||
|
||||
// Prefer waiting on the current URL's server so we retry the same source.
|
||||
QString currentHost = QUrl(cardToDownload.getCurrentUrl()).host();
|
||||
QDateTime backoffUntil = s_rateLimiter.deadline(currentHost);
|
||||
if (!s_rateLimiter.isRateLimited(currentHost, now)) {
|
||||
backoffUntil = s_rateLimiter.earliestDeadline(now);
|
||||
}
|
||||
|
||||
if (!backoffUntil.isValid()) {
|
||||
qCWarning(CardPictureLoaderWorkerWorkLog).nospace()
|
||||
<< "PictureLoader: [card: " << cardToDownload.getCard().getInfo().getCorrectedName()
|
||||
<< " set: " << cardToDownload.getSetName() << "]: All URLs exhausted, no servers in backoff: BAILING OUT";
|
||||
concludeImageLoad(QImage());
|
||||
return;
|
||||
}
|
||||
|
||||
qint64 waitMs = qMax<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)
|
||||
{
|
||||
emit imageLoaded(cardToDownload.getCard(), image);
|
||||
|
|
@ -211,5 +317,5 @@ void CardPictureLoaderWorkerWork::concludeImageLoad(const QImage &image)
|
|||
|
||||
void CardPictureLoaderWorkerWork::picDownloadChanged()
|
||||
{
|
||||
picDownload = SettingsCache::instance().personal().getPicDownload();
|
||||
picDownload = SettingsCache::instance().downloads().getPicDownload();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,12 +4,15 @@
|
|||
#include "card_picture_loader_worker.h"
|
||||
#include "card_picture_to_load.h"
|
||||
|
||||
#include <QDateTime>
|
||||
#include <QLoggingCategory>
|
||||
#include <QMutex>
|
||||
#include <QNetworkAccessManager>
|
||||
#include <QObject>
|
||||
#include <QRandomGenerator>
|
||||
#include <QThread>
|
||||
#include <libcockatrice/card/database/card_database.h>
|
||||
#include <libcockatrice/utility/server_rate_limiter.h>
|
||||
|
||||
inline Q_LOGGING_CATEGORY(CardPictureLoaderWorkerWorkLog, "card_picture_loader.worker");
|
||||
|
||||
|
|
@ -50,6 +53,8 @@ public slots:
|
|||
private:
|
||||
bool picDownload; ///< Whether network downloading is enabled
|
||||
|
||||
static ServerRateLimiter s_rateLimiter; ///< Shared per-server 429 backoff state
|
||||
|
||||
/** @brief Starts downloading the next URL for this card. */
|
||||
void startNextPicDownload();
|
||||
|
||||
|
|
@ -77,6 +82,16 @@ private:
|
|||
*/
|
||||
void concludeImageLoad(const QImage &image);
|
||||
|
||||
/**
|
||||
* @brief Schedules a deferred retry after the relevant server backoff expires.
|
||||
*
|
||||
* Waits on the current URL's server when it is the reason we are blocked,
|
||||
* otherwise on the earliest active backoff. If no servers are in backoff,
|
||||
* concludes with failure. Otherwise resets the CardPictureToLoad indices and
|
||||
* retries after the backoff period.
|
||||
*/
|
||||
void scheduleDeferredRetry();
|
||||
|
||||
private slots:
|
||||
/** @brief Updates the picDownload setting when it changes. */
|
||||
void picDownloadChanged();
|
||||
|
|
@ -100,6 +115,9 @@ signals:
|
|||
/** @brief Emitted when a URL has been redirected. */
|
||||
void urlRedirected(const QUrl &originalUrl, const QUrl &redirectUrl);
|
||||
|
||||
/** @brief Emitted when a server returned HTTP 429. */
|
||||
void rateLimited(const QString &host);
|
||||
|
||||
/** @brief Emitted when a cached URL is invalid and must be removed. */
|
||||
void cachedUrlInvalidated(const QUrl &url);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -17,8 +17,9 @@ CardPictureToLoad::CardPictureToLoad(const ExactCard &_card)
|
|||
{
|
||||
if (card) {
|
||||
sortedSets = extractSetsSorted(card);
|
||||
// The first time called, nextSet will also populate the Urls for the first set.
|
||||
nextSet();
|
||||
currentSetIndex = 0;
|
||||
currentSet = sortedSets.first();
|
||||
populateSetUrls();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -101,15 +102,19 @@ void CardPictureToLoad::populateSetUrls()
|
|||
}
|
||||
}
|
||||
|
||||
/* Call nextUrl to make sure currentUrl is up-to-date
|
||||
but we don't need the result here. */
|
||||
(void)nextUrl();
|
||||
currentUrlIndex = 0;
|
||||
if (!currentSetUrls.isEmpty()) {
|
||||
currentUrl = currentSetUrls.first();
|
||||
} else {
|
||||
currentUrl = QString();
|
||||
}
|
||||
}
|
||||
|
||||
bool CardPictureToLoad::nextSet()
|
||||
{
|
||||
if (!sortedSets.isEmpty()) {
|
||||
currentSet = sortedSets.takeFirst();
|
||||
currentSetIndex++;
|
||||
if (currentSetIndex < sortedSets.size()) {
|
||||
currentSet = sortedSets.at(currentSetIndex);
|
||||
populateSetUrls();
|
||||
return true;
|
||||
}
|
||||
|
|
@ -119,8 +124,9 @@ bool CardPictureToLoad::nextSet()
|
|||
|
||||
bool CardPictureToLoad::nextUrl()
|
||||
{
|
||||
if (!currentSetUrls.isEmpty()) {
|
||||
currentUrl = currentSetUrls.takeFirst();
|
||||
currentUrlIndex++;
|
||||
if (currentUrlIndex < currentSetUrls.size()) {
|
||||
currentUrl = currentSetUrls.at(currentUrlIndex);
|
||||
return true;
|
||||
}
|
||||
currentUrl = QString();
|
||||
|
|
@ -136,6 +142,28 @@ QString CardPictureToLoad::getSetName() const
|
|||
}
|
||||
}
|
||||
|
||||
QString CardPictureToLoad::peekNextUrl() const
|
||||
{
|
||||
int nextIndex = currentUrlIndex + 1;
|
||||
if (nextIndex < currentSetUrls.size()) {
|
||||
return currentSetUrls.at(nextIndex);
|
||||
}
|
||||
return QString();
|
||||
}
|
||||
|
||||
void CardPictureToLoad::resetIndices()
|
||||
{
|
||||
currentSetIndex = 0;
|
||||
if (!sortedSets.isEmpty()) {
|
||||
currentSet = sortedSets.first();
|
||||
populateSetUrls();
|
||||
} else {
|
||||
currentSet = {};
|
||||
currentSetUrls.clear();
|
||||
currentUrl = QString();
|
||||
}
|
||||
}
|
||||
|
||||
static int parse(const QString &urlTemplate,
|
||||
const QString &propType,
|
||||
const QString &cardName,
|
||||
|
|
|
|||
|
|
@ -25,6 +25,8 @@ private:
|
|||
QList<QString> currentSetUrls; ///< URLs for the current set being attempted
|
||||
QString currentUrl; ///< Currently active URL to download
|
||||
CardSetPtr currentSet; ///< Currently active set
|
||||
int currentSetIndex = 0; ///< Current position in sortedSets
|
||||
int currentUrlIndex = 0; ///< Current position in currentSetUrls
|
||||
|
||||
public:
|
||||
/**
|
||||
|
|
@ -56,6 +58,9 @@ public:
|
|||
/** @return The short name of the current set, or empty string if no set. */
|
||||
[[nodiscard]] QString getSetName() const;
|
||||
|
||||
/** @return The next URL in the current set's list without advancing, or empty if at end. */
|
||||
[[nodiscard]] QString peekNextUrl() const;
|
||||
|
||||
/**
|
||||
* @brief Transforms a URL template into a concrete URL for this card/set.
|
||||
* @param urlTemplate The URL template to transform
|
||||
|
|
@ -88,6 +93,14 @@ public:
|
|||
*/
|
||||
void populateSetUrls();
|
||||
|
||||
/**
|
||||
* @brief Resets iteration indices to the beginning.
|
||||
*
|
||||
* Restarts URL/set iteration from the first set and first URL.
|
||||
* Used for deferred retry after server backoff expires.
|
||||
*/
|
||||
void resetIndices();
|
||||
|
||||
/**
|
||||
* @brief Extract all sets from the card and sort them by priority.
|
||||
* @param card The card to extract sets from
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
48
cockatrice/src/interface/intents/intent.cpp
Normal file
48
cockatrice/src/interface/intents/intent.cpp
Normal 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);
|
||||
}
|
||||
}
|
||||
37
cockatrice/src/interface/intents/intent.h
Normal file
37
cockatrice/src/interface/intents/intent.h
Normal 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
|
||||
|
|
@ -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));
|
||||
}
|
||||
29
cockatrice/src/interface/intents/intent_connect_to_server.h
Normal file
29
cockatrice/src/interface/intents/intent_connect_to_server.h
Normal 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
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
76
cockatrice/src/interface/intents/intent_join_server_game.cpp
Normal file
76
cockatrice/src/interface/intents/intent_join_server_game.cpp
Normal 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)); });
|
||||
}
|
||||
37
cockatrice/src/interface/intents/intent_join_server_game.h
Normal file
37
cockatrice/src/interface/intents/intent_join_server_game.h
Normal 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
|
||||
74
cockatrice/src/interface/intents/intent_join_server_room.cpp
Normal file
74
cockatrice/src/interface/intents/intent_join_server_room.cpp
Normal 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));
|
||||
}
|
||||
28
cockatrice/src/interface/intents/intent_join_server_room.h
Normal file
28
cockatrice/src/interface/intents/intent_join_server_room.h
Normal 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
|
||||
33
cockatrice/src/interface/intents/intent_login.cpp
Normal file
33
cockatrice/src/interface/intents/intent_login.cpp
Normal 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"));
|
||||
}
|
||||
23
cockatrice/src/interface/intents/intent_login.h
Normal file
23
cockatrice/src/interface/intents/intent_login.h
Normal 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
|
||||
34
cockatrice/src/interface/intents/intent_open_local_deck.cpp
Normal file
34
cockatrice/src/interface/intents/intent_open_local_deck.cpp
Normal 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);
|
||||
}
|
||||
27
cockatrice/src/interface/intents/intent_open_local_deck.h
Normal file
27
cockatrice/src/interface/intents/intent_open_local_deck.h
Normal 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
|
||||
|
|
@ -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(); });
|
||||
}
|
||||
|
|
@ -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
|
||||
89
cockatrice/src/interface/intents/url_parser.cpp
Normal file
89
cockatrice/src/interface/intents/url_parser.cpp
Normal 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();
|
||||
}
|
||||
20
cockatrice/src/interface/intents/url_parser.h
Normal file
20
cockatrice/src/interface/intents/url_parser.h
Normal 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
|
||||
|
|
@ -2,6 +2,8 @@
|
|||
|
||||
#include "../../../client/settings/cache_settings.h"
|
||||
#include "../../../client/settings/shortcuts_settings.h"
|
||||
#include "../settings_page/user_interface_settings_page.h"
|
||||
#include "../tabs/api/commander_spellbook/commander_bracket_widget.h"
|
||||
#include "deck_list_style_proxy.h"
|
||||
#include "deck_state_manager.h"
|
||||
|
||||
|
|
@ -12,7 +14,7 @@
|
|||
#include <QSplitter>
|
||||
#include <QTextEdit>
|
||||
#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/string_limits.h>
|
||||
|
||||
|
|
@ -111,20 +113,18 @@ void DeckEditorDeckDockWidget::createDeckDock()
|
|||
|
||||
showBannerCardCheckBox = new QCheckBox();
|
||||
showBannerCardCheckBox->setObjectName("showBannerCardCheckBox");
|
||||
showBannerCardCheckBox->setChecked(
|
||||
SettingsCache::instance().cardsDisplay().getDeckEditorBannerCardComboBoxVisible());
|
||||
connect(showBannerCardCheckBox, &QCheckBox::QT_STATE_CHANGED, &SettingsCache::instance().cardsDisplay(),
|
||||
&CardsDisplaySettings::setDeckEditorBannerCardComboBoxVisible);
|
||||
connect(&SettingsCache::instance().cardsDisplay(),
|
||||
&CardsDisplaySettings::deckEditorBannerCardComboBoxVisibleChanged, this,
|
||||
showBannerCardCheckBox->setChecked(SettingsCache::instance().deckEditor().getBannerCardComboBoxVisible());
|
||||
connect(showBannerCardCheckBox, &QCheckBox::QT_STATE_CHANGED, &SettingsCache::instance().deckEditor(),
|
||||
&DeckEditorSettings::setBannerCardComboBoxVisible);
|
||||
connect(&SettingsCache::instance().deckEditor(), &DeckEditorSettings::bannerCardComboBoxVisibleChanged, this,
|
||||
&DeckEditorDeckDockWidget::updateShowBannerCardComboBox);
|
||||
|
||||
showTagsWidgetCheckBox = new QCheckBox();
|
||||
showTagsWidgetCheckBox->setObjectName("showTagsWidgetCheckBox");
|
||||
showTagsWidgetCheckBox->setChecked(SettingsCache::instance().cardsDisplay().getDeckEditorTagsWidgetVisible());
|
||||
connect(showTagsWidgetCheckBox, &QCheckBox::QT_STATE_CHANGED, &SettingsCache::instance().cardsDisplay(),
|
||||
&CardsDisplaySettings::setDeckEditorTagsWidgetVisible);
|
||||
connect(&SettingsCache::instance().cardsDisplay(), &CardsDisplaySettings::deckEditorTagsWidgetVisibleChanged, this,
|
||||
showTagsWidgetCheckBox->setChecked(SettingsCache::instance().deckEditor().getTagsWidgetVisible());
|
||||
connect(showTagsWidgetCheckBox, &QCheckBox::QT_STATE_CHANGED, &SettingsCache::instance().deckEditor(),
|
||||
&DeckEditorSettings::setTagsWidgetVisible);
|
||||
connect(&SettingsCache::instance().deckEditor(), &DeckEditorSettings::tagsWidgetVisibleChanged, this,
|
||||
&DeckEditorDeckDockWidget::updateShowTagsWidget);
|
||||
|
||||
quickSettingsWidget->addSettingsWidget(showBannerCardCheckBox);
|
||||
|
|
@ -136,6 +136,8 @@ void DeckEditorDeckDockWidget::createDeckDock()
|
|||
formatComboBox->addItem(tr("Loading Database..."));
|
||||
formatComboBox->setEnabled(false); // Disable until loaded
|
||||
|
||||
commanderBracketWidget = new CommanderBracketWidget(this);
|
||||
|
||||
commentsLabel = new QLabel();
|
||||
commentsLabel->setObjectName("commentsLabel");
|
||||
commentsEdit = new QTextEdit;
|
||||
|
|
@ -156,7 +158,7 @@ void DeckEditorDeckDockWidget::createDeckDock()
|
|||
bannerCardLabel = new QLabel();
|
||||
bannerCardLabel->setObjectName("bannerCardLabel");
|
||||
bannerCardLabel->setText(tr("Banner Card"));
|
||||
bannerCardLabel->setHidden(!SettingsCache::instance().cardsDisplay().getDeckEditorBannerCardComboBoxVisible());
|
||||
bannerCardLabel->setHidden(!SettingsCache::instance().deckEditor().getBannerCardComboBoxVisible());
|
||||
bannerCardComboBox = new QComboBox(this);
|
||||
connect(getModel(), &DeckListModel::cardNodesChanged, this, [this]() {
|
||||
// Delay the update to avoid race conditions
|
||||
|
|
@ -167,10 +169,10 @@ void DeckEditorDeckDockWidget::createDeckDock()
|
|||
|
||||
connect(bannerCardComboBox, QOverload<int>::of(&QComboBox::currentIndexChanged), this,
|
||||
&DeckEditorDeckDockWidget::writeBannerCard);
|
||||
bannerCardComboBox->setHidden(!SettingsCache::instance().cardsDisplay().getDeckEditorBannerCardComboBoxVisible());
|
||||
bannerCardComboBox->setHidden(!SettingsCache::instance().deckEditor().getBannerCardComboBoxVisible());
|
||||
|
||||
deckTagsDisplayWidget = new DeckPreviewDeckTagsDisplayWidget(this, {});
|
||||
deckTagsDisplayWidget->setHidden(!SettingsCache::instance().cardsDisplay().getDeckEditorTagsWidgetVisible());
|
||||
deckTagsDisplayWidget->setHidden(!SettingsCache::instance().deckEditor().getTagsWidgetVisible());
|
||||
connect(deckTagsDisplayWidget, &DeckPreviewDeckTagsDisplayWidget::tagsChanged, deckStateManager,
|
||||
&DeckStateManager::setTags);
|
||||
|
||||
|
|
@ -221,13 +223,15 @@ void DeckEditorDeckDockWidget::createDeckDock()
|
|||
upperLayout->addWidget(formatLabel, 2, 0);
|
||||
upperLayout->addWidget(formatComboBox, 2, 1);
|
||||
|
||||
upperLayout->addWidget(bannerCardLabel, 3, 0);
|
||||
upperLayout->addWidget(bannerCardComboBox, 3, 1);
|
||||
upperLayout->addWidget(commanderBracketWidget, 3, 0, 1, 2);
|
||||
|
||||
upperLayout->addWidget(deckTagsDisplayWidget, 4, 1);
|
||||
upperLayout->addWidget(bannerCardLabel, 4, 0);
|
||||
upperLayout->addWidget(bannerCardComboBox, 4, 1);
|
||||
|
||||
upperLayout->addWidget(activeGroupCriteriaLabel, 5, 0);
|
||||
upperLayout->addWidget(activeGroupCriteriaComboBox, 5, 1);
|
||||
upperLayout->addWidget(deckTagsDisplayWidget, 5, 1);
|
||||
|
||||
upperLayout->addWidget(activeGroupCriteriaLabel, 6, 0);
|
||||
upperLayout->addWidget(activeGroupCriteriaComboBox, 6, 1);
|
||||
|
||||
hashLabel1 = new QLabel();
|
||||
hashLabel1->setObjectName("hashLabel1");
|
||||
|
|
@ -305,15 +309,19 @@ void DeckEditorDeckDockWidget::initializeFormats()
|
|||
// Ensure no selection is visible initially
|
||||
formatComboBox->setCurrentIndex(-1);
|
||||
}
|
||||
|
||||
connect(formatComboBox, QOverload<int>::of(&QComboBox::currentIndexChanged), this, [this](int index) {
|
||||
QString formatKey;
|
||||
if (index >= 0) {
|
||||
QString formatKey = formatComboBox->itemData(index).toString();
|
||||
formatKey = formatComboBox->itemData(index).toString();
|
||||
deckStateManager->setFormat(formatKey);
|
||||
} else {
|
||||
deckStateManager->setFormat(""); // clear format if deselected
|
||||
}
|
||||
|
||||
commanderBracketWidget->setDeck(deckStateManager->getDeckListShared());
|
||||
});
|
||||
|
||||
commanderBracketWidget->setDeck(deckStateManager->getDeckListShared());
|
||||
}
|
||||
|
||||
ExactCard DeckEditorDeckDockWidget::getCurrentCard()
|
||||
|
|
@ -495,6 +503,8 @@ void DeckEditorDeckDockWidget::syncDisplayWidgetsToModel()
|
|||
formatComboBox->setCurrentIndex(formatComboBox->findData(deckStateManager->getMetadata().gameFormat));
|
||||
formatComboBox->blockSignals(false);
|
||||
|
||||
commanderBracketWidget->setDeck(deckStateManager->getDeckListShared());
|
||||
|
||||
deckTagsDisplayWidget->blockSignals(true);
|
||||
deckTagsDisplayWidget->setTags(deckStateManager->getMetadata().tags);
|
||||
deckTagsDisplayWidget->blockSignals(false);
|
||||
|
|
@ -748,6 +758,7 @@ void DeckEditorDeckDockWidget::retranslateUi()
|
|||
commentsLabel->setText(tr("&Comments:"));
|
||||
activeGroupCriteriaLabel->setText(tr("Group by:"));
|
||||
formatLabel->setText(tr("Format:"));
|
||||
commanderBracketWidget->retranslateUi();
|
||||
|
||||
hashLabel1->setText(tr("Hash:"));
|
||||
|
||||
|
|
@ -755,4 +766,4 @@ void DeckEditorDeckDockWidget::retranslateUi()
|
|||
aDecrement->setText(tr("&Decrement number"));
|
||||
aRemoveCard->setText(tr("&Remove row"));
|
||||
aSwapCard->setText(tr("Swap card to/from sideboard"));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@
|
|||
#include <QTreeView>
|
||||
#include <libcockatrice/card/card_info.h>
|
||||
|
||||
class CommanderBracketWidget;
|
||||
class DeckListModel;
|
||||
class AbstractTabDeckEditor;
|
||||
class DeckEditorDeckDockWidget : public QDockWidget
|
||||
|
|
@ -89,6 +90,8 @@ private:
|
|||
|
||||
QAction *aRemoveCard, *aIncrement, *aDecrement, *aSwapCard;
|
||||
|
||||
CommanderBracketWidget *commanderBracketWidget;
|
||||
|
||||
DeckListModel *getModel() const;
|
||||
[[nodiscard]] QModelIndexList getSelectedCardNodeSourceIndices() const;
|
||||
void offsetCountAtIndex(const QModelIndex &idx, bool isIncrement);
|
||||
|
|
|
|||
|
|
@ -57,6 +57,14 @@ public:
|
|||
*/
|
||||
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.
|
||||
*
|
||||
|
|
|
|||
|
|
@ -271,6 +271,7 @@ void DlgConnect::updateDisplayInfo(const QString &saveName)
|
|||
hostEdit->setText(_data.at(1));
|
||||
portEdit->setText(_data.at(2));
|
||||
playernameEdit->setText(_data.at(3));
|
||||
playernameEdit->setFocus();
|
||||
savePasswordCheckBox->setChecked(savePasswordStatus);
|
||||
|
||||
if (savePasswordStatus) {
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@
|
|||
#include "../../../client/settings/shortcuts_settings.h"
|
||||
#include "../../deck_loader/card_node_function.h"
|
||||
#include "../../deck_loader/deck_loader.h"
|
||||
#include "dlg_settings.h"
|
||||
|
||||
#include <QApplication>
|
||||
#include <QCheckBox>
|
||||
|
|
|
|||
|
|
@ -1,3 +1,8 @@
|
|||
/**
|
||||
* @file dlg_settings.cpp
|
||||
* @brief Implementation of the main settings dialog
|
||||
* @ingroup Dialogs
|
||||
*/
|
||||
#include "dlg_settings.h"
|
||||
|
||||
#include "../../../client/settings/cache_settings.h"
|
||||
|
|
@ -6,6 +11,8 @@
|
|||
#include "../settings_page/deck_editor_settings_page.h"
|
||||
#include "../settings_page/general_settings_page.h"
|
||||
#include "../settings_page/messages_settings_page.h"
|
||||
#include "../settings_page/settings_search_delegate.h"
|
||||
#include "../settings_page/settings_search_model.h"
|
||||
#include "../settings_page/shortcut_settings_page.h"
|
||||
#include "../settings_page/sound_settings_page.h"
|
||||
#include "../settings_page/storage_settings_page.h"
|
||||
|
|
@ -13,20 +20,36 @@
|
|||
#include "libcockatrice/card/database/card_database_loader.h"
|
||||
#include "libcockatrice/card/database/card_database_manager.h"
|
||||
|
||||
#include <QAbstractItemView>
|
||||
#include <QCloseEvent>
|
||||
#include <QDialogButtonBox>
|
||||
#include <QDir>
|
||||
#include <QFrame>
|
||||
#include <QGraphicsOpacityEffect>
|
||||
#include <QGuiApplication>
|
||||
#include <QListWidget>
|
||||
#include <QHBoxLayout>
|
||||
#include <QKeyEvent>
|
||||
#include <QLineEdit>
|
||||
#include <QListView>
|
||||
#include <QMessageBox>
|
||||
#include <QPropertyAnimation>
|
||||
#include <QPushButton>
|
||||
#include <QScreen>
|
||||
#include <QScrollArea>
|
||||
#include <QScrollBar>
|
||||
#include <QSequentialAnimationGroup>
|
||||
#include <QShortcut>
|
||||
#include <QStackedLayout>
|
||||
#include <QStackedWidget>
|
||||
#include <QToolButton>
|
||||
#include <QVBoxLayout>
|
||||
#include <libcockatrice/settings/paths_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)
|
||||
{
|
||||
widget->setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Maximum);
|
||||
|
|
@ -40,112 +63,355 @@ static QScrollArea *makeScrollable(QWidget *widget)
|
|||
return scrollArea;
|
||||
}
|
||||
|
||||
DlgSettings::DlgSettings(QWidget *parent) : QDialog(parent)
|
||||
/**
|
||||
* @brief Returns the theme icon resources for each settings page, indexed by SettingsPage order
|
||||
*/
|
||||
static QStringList pageIconResources()
|
||||
{
|
||||
return {QStringLiteral("theme:config/general"), QStringLiteral("theme:config/appearance"),
|
||||
QStringLiteral("theme:config/interface"), QStringLiteral("theme:config/deckeditor"),
|
||||
QStringLiteral("theme:config/storage"), QStringLiteral("theme:config/messages"),
|
||||
QStringLiteral("theme:config/sound"), QStringLiteral("theme:config/shorcuts")};
|
||||
}
|
||||
|
||||
DlgSettings::DlgSettings(QWidget *parent) : QDialog(parent), currentTabIndex(0), searchActive(false)
|
||||
{
|
||||
auto rec = QGuiApplication::primaryScreen()->availableGeometry();
|
||||
this->setMinimumSize(qMin(700, rec.width()), qMin(700, rec.height()));
|
||||
setMinimumSize(qMin(750, rec.width()), qMin(700, rec.height()));
|
||||
|
||||
connect(&SettingsCache::instance().personal(), &PersonalSettings::langChanged, this, &DlgSettings::updateLanguage);
|
||||
|
||||
contentsWidget = new QListWidget;
|
||||
contentsWidget->setViewMode(QListView::IconMode);
|
||||
contentsWidget->setIconSize(QSize(58, 50));
|
||||
contentsWidget->setMovement(QListView::Static);
|
||||
contentsWidget->setMinimumHeight(85);
|
||||
contentsWidget->setMaximumHeight(85);
|
||||
contentsWidget->setSpacing(5);
|
||||
|
||||
pagesWidget = new QStackedWidget;
|
||||
pagesWidget->addWidget(makeScrollable(new GeneralSettingsPage));
|
||||
pagesWidget->addWidget(makeScrollable(new AppearanceSettingsPage));
|
||||
pagesWidget->addWidget(makeScrollable(new UserInterfaceSettingsPage));
|
||||
pagesWidget->addWidget(new DeckEditorSettingsPage);
|
||||
pagesWidget->addWidget(makeScrollable(new StorageSettingsPage));
|
||||
pagesWidget->addWidget(new MessagesSettingsPage);
|
||||
pagesWidget->addWidget(new SoundSettingsPage);
|
||||
pagesWidget->addWidget(new ShortcutSettingsPage);
|
||||
|
||||
createIcons();
|
||||
contentsWidget->setCurrentRow(0);
|
||||
|
||||
auto *vboxLayout = new QVBoxLayout;
|
||||
vboxLayout->addWidget(contentsWidget);
|
||||
vboxLayout->addWidget(pagesWidget);
|
||||
|
||||
auto *buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok);
|
||||
connect(buttonBox, &QDialogButtonBox::accepted, this, &DlgSettings::close);
|
||||
|
||||
auto *mainLayout = new QVBoxLayout;
|
||||
mainLayout->addLayout(vboxLayout);
|
||||
mainLayout->addSpacing(2);
|
||||
mainLayout->addWidget(buttonBox);
|
||||
setLayout(mainLayout);
|
||||
setupUi();
|
||||
|
||||
connect(&SettingsCache::instance().personal(), &PersonalSettings::langChanged, this, &DlgSettings::retranslateUi);
|
||||
retranslateUi();
|
||||
|
||||
searchEdit->setFocus();
|
||||
|
||||
adjustSize();
|
||||
}
|
||||
|
||||
void DlgSettings::createIcons()
|
||||
void DlgSettings::setupUi()
|
||||
{
|
||||
generalButton = new QListWidgetItem(contentsWidget);
|
||||
generalButton->setTextAlignment(Qt::AlignHCenter);
|
||||
generalButton->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled);
|
||||
generalButton->setIcon(QPixmap("theme:config/general"));
|
||||
// Search bar
|
||||
searchEdit = new QLineEdit;
|
||||
searchEdit->setClearButtonEnabled(true);
|
||||
searchEdit->addAction(QPixmap("theme:icons/search"), QLineEdit::LeadingPosition);
|
||||
searchEdit->installEventFilter(this);
|
||||
connect(searchEdit, &QLineEdit::textChanged, this, &DlgSettings::onSearchTextChanged);
|
||||
|
||||
appearanceButton = new QListWidgetItem(contentsWidget);
|
||||
appearanceButton->setTextAlignment(Qt::AlignHCenter);
|
||||
appearanceButton->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled);
|
||||
appearanceButton->setIcon(QPixmap("theme:config/appearance"));
|
||||
auto *searchLayout = new QHBoxLayout;
|
||||
searchLayout->addWidget(searchEdit);
|
||||
|
||||
userInterfaceButton = new QListWidgetItem(contentsWidget);
|
||||
userInterfaceButton->setTextAlignment(Qt::AlignHCenter);
|
||||
userInterfaceButton->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled);
|
||||
userInterfaceButton->setIcon(QPixmap("theme:config/interface"));
|
||||
// Tab bar (built in setupTabBar)
|
||||
setupTabBar();
|
||||
|
||||
deckEditorButton = new QListWidgetItem(contentsWidget);
|
||||
deckEditorButton->setTextAlignment(Qt::AlignHCenter);
|
||||
deckEditorButton->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled);
|
||||
deckEditorButton->setIcon(QPixmap("theme:config/deckeditor"));
|
||||
// Pages stacked widget
|
||||
pagesWidget = new QStackedWidget;
|
||||
|
||||
storageButton = new QListWidgetItem(contentsWidget);
|
||||
storageButton->setTextAlignment(Qt::AlignHCenter);
|
||||
storageButton->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled);
|
||||
storageButton->setIcon(QPixmap("theme:config/storage"));
|
||||
auto *generalPage = new GeneralSettingsPage;
|
||||
auto *appearancePage = new AppearanceSettingsPage;
|
||||
auto *userInterfacePage = new UserInterfaceSettingsPage;
|
||||
auto *deckEditorPage = new DeckEditorSettingsPage;
|
||||
auto *storagePage = new StorageSettingsPage;
|
||||
auto *messagesPage = new MessagesSettingsPage;
|
||||
auto *soundPage = new SoundSettingsPage;
|
||||
auto *shortcutsPage = new ShortcutSettingsPage;
|
||||
|
||||
messagesButton = new QListWidgetItem(contentsWidget);
|
||||
messagesButton->setTextAlignment(Qt::AlignHCenter);
|
||||
messagesButton->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled);
|
||||
messagesButton->setIcon(QPixmap("theme:config/messages"));
|
||||
pages.append(generalPage);
|
||||
pages.append(appearancePage);
|
||||
pages.append(userInterfacePage);
|
||||
pages.append(deckEditorPage);
|
||||
pages.append(storagePage);
|
||||
pages.append(messagesPage);
|
||||
pages.append(soundPage);
|
||||
pages.append(shortcutsPage);
|
||||
|
||||
soundButton = new QListWidgetItem(contentsWidget);
|
||||
soundButton->setTextAlignment(Qt::AlignHCenter);
|
||||
soundButton->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled);
|
||||
soundButton->setIcon(QPixmap("theme:config/sound"));
|
||||
pagesWidget->addWidget(makeScrollable(generalPage));
|
||||
pagesWidget->addWidget(makeScrollable(appearancePage));
|
||||
pagesWidget->addWidget(makeScrollable(userInterfacePage));
|
||||
pagesWidget->addWidget(makeScrollable(deckEditorPage));
|
||||
pagesWidget->addWidget(makeScrollable(storagePage));
|
||||
pagesWidget->addWidget(messagesPage);
|
||||
pagesWidget->addWidget(soundPage);
|
||||
pagesWidget->addWidget(shortcutsPage);
|
||||
|
||||
shortcutsButton = new QListWidgetItem(contentsWidget);
|
||||
shortcutsButton->setTextAlignment(Qt::AlignHCenter);
|
||||
shortcutsButton->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled);
|
||||
shortcutsButton->setIcon(QPixmap("theme:config/shorcuts"));
|
||||
Q_ASSERT(pages.size() == NumPages);
|
||||
|
||||
connect(contentsWidget, &QListWidget::currentItemChanged, this, &DlgSettings::changePage);
|
||||
// Search results view (hidden by default)
|
||||
searchResultsView = new QListView;
|
||||
searchResultsView->setUniformItemSizes(false);
|
||||
searchResultsView->setSelectionMode(QAbstractItemView::SingleSelection);
|
||||
searchResultsView->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
|
||||
searchResultsView->setVisible(false);
|
||||
searchResultsView->setStyleSheet(
|
||||
"QListView::item:selected { background: palette(highlight); color: palette(highlighted-text); }");
|
||||
|
||||
searchModel = new SettingsSearchModel(this);
|
||||
searchDelegate = new SettingsSearchDelegate(this);
|
||||
searchResultsView->setModel(searchModel);
|
||||
searchResultsView->setItemDelegate(searchDelegate);
|
||||
connect(searchResultsView, &QListView::clicked, this, &DlgSettings::onSearchResultClicked);
|
||||
|
||||
connect(&SettingsCache::instance(), &SettingsCache::themeChanged, this, [this] {
|
||||
const QStringList icons = pageIconResources();
|
||||
for (int i = 0; i < tabButtons.size() && i < icons.size(); ++i) {
|
||||
tabButtons[i]->setIcon(QPixmap(icons[i]));
|
||||
}
|
||||
searchDelegate->setPageIcons(icons);
|
||||
searchResultsView->viewport()->update();
|
||||
});
|
||||
|
||||
// Build search index after pages are created
|
||||
buildSearchIndex();
|
||||
|
||||
// Pages container (stacked widget + search results overlay)
|
||||
pagesContainer = new QWidget;
|
||||
auto *containerLayout = new QStackedLayout;
|
||||
containerLayout->setStackingMode(QStackedLayout::StackAll);
|
||||
containerLayout->addWidget(pagesWidget);
|
||||
containerLayout->addWidget(searchResultsView);
|
||||
pagesContainer->setLayout(containerLayout);
|
||||
|
||||
// Bottom buttons
|
||||
auto *buttonBox = new QHBoxLayout;
|
||||
buttonBox->addStretch();
|
||||
okButton = new QPushButton;
|
||||
okButton->setDefault(true);
|
||||
connect(okButton, &QPushButton::clicked, this, &DlgSettings::close);
|
||||
buttonBox->addWidget(okButton);
|
||||
|
||||
// Main layout
|
||||
auto *mainLayout = new QVBoxLayout;
|
||||
mainLayout->addLayout(searchLayout);
|
||||
mainLayout->addWidget(tabBarWidget);
|
||||
auto *separator = new QFrame;
|
||||
separator->setFrameShape(QFrame::HLine);
|
||||
separator->setFrameShadow(QFrame::Sunken);
|
||||
mainLayout->addWidget(separator);
|
||||
mainLayout->addWidget(pagesContainer);
|
||||
mainLayout->addSpacing(4);
|
||||
mainLayout->addLayout(buttonBox);
|
||||
setLayout(mainLayout);
|
||||
|
||||
// Keyboard shortcuts
|
||||
auto *searchShortcut = new QShortcut(QKeySequence(Qt::CTRL | Qt::Key_F), this);
|
||||
connect(searchShortcut, &QShortcut::activated, searchEdit, qOverload<>(&QLineEdit::setFocus));
|
||||
|
||||
auto *nextTabShortcut = new QShortcut(QKeySequence(Qt::CTRL | Qt::Key_Tab), this);
|
||||
connect(nextTabShortcut, &QShortcut::activated, this, [this] {
|
||||
int next = (currentTabIndex + 1) % tabButtons.size();
|
||||
setActiveTab(next);
|
||||
});
|
||||
|
||||
auto *prevTabShortcut = new QShortcut(QKeySequence(Qt::CTRL | Qt::SHIFT | Qt::Key_Tab), this);
|
||||
connect(prevTabShortcut, &QShortcut::activated, this, [this] {
|
||||
int prev = (currentTabIndex - 1 + tabButtons.size()) % tabButtons.size();
|
||||
setActiveTab(prev);
|
||||
});
|
||||
|
||||
// Initialize to first tab
|
||||
setActiveTab(0);
|
||||
}
|
||||
|
||||
void DlgSettings::changePage(QListWidgetItem *current, QListWidgetItem *previous)
|
||||
void DlgSettings::setupTabBar()
|
||||
{
|
||||
if (!current) {
|
||||
current = previous;
|
||||
tabBarWidget = new QWidget;
|
||||
auto *tabLayout = new QHBoxLayout;
|
||||
tabLayout->setContentsMargins(0, 0, 0, 0);
|
||||
tabLayout->setSpacing(2);
|
||||
|
||||
const QStringList iconResources = pageIconResources();
|
||||
|
||||
for (int i = 0; i < iconResources.size(); ++i) {
|
||||
auto *tabButton = new QToolButton;
|
||||
tabButton->setCheckable(true);
|
||||
tabButton->setIcon(QPixmap(iconResources[i]));
|
||||
tabButton->setIconSize(QSize(48, 48));
|
||||
tabButton->setToolButtonStyle(Qt::ToolButtonTextUnderIcon);
|
||||
tabButton->setAutoExclusive(true);
|
||||
tabButton->setMinimumHeight(85);
|
||||
tabButton->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed);
|
||||
|
||||
connect(tabButton, &QToolButton::clicked, this, [this, idx = i] { onTabClicked(idx); });
|
||||
|
||||
tabButtons.append(tabButton);
|
||||
tabLayout->addWidget(tabButton);
|
||||
}
|
||||
|
||||
pagesWidget->setCurrentIndex(contentsWidget->row(current));
|
||||
tabBarWidget->setLayout(tabLayout);
|
||||
}
|
||||
|
||||
void DlgSettings::buildSearchIndex()
|
||||
{
|
||||
QList<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)
|
||||
{
|
||||
if (index <= contentsWidget->count() - 1 && index >= 0) {
|
||||
changePage(contentsWidget->item(index), contentsWidget->currentItem());
|
||||
contentsWidget->setCurrentRow(index);
|
||||
if (index >= 0 && index < tabButtons.size()) {
|
||||
setActiveTab(index);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -155,6 +421,49 @@ void DlgSettings::updateLanguage()
|
|||
installNewTranslator();
|
||||
}
|
||||
|
||||
bool DlgSettings::eventFilter(QObject *watched, QEvent *event)
|
||||
{
|
||||
if (watched == searchEdit && event->type() == QEvent::KeyPress) {
|
||||
auto *keyEvent = static_cast<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)
|
||||
{
|
||||
bool showLoadError = true;
|
||||
|
|
@ -209,7 +518,6 @@ void DlgSettings::closeEvent(QCloseEvent *event)
|
|||
|
||||
if (!QDir(SettingsCache::instance().paths().getDeckPath()).exists() ||
|
||||
SettingsCache::instance().paths().getDeckPath().isEmpty()) {
|
||||
//! \todo Prompt to create the deck directory.
|
||||
if (QMessageBox::critical(
|
||||
this, tr("Error"),
|
||||
tr("The path to your deck directory is invalid. Would you like to go back and set the correct path?"),
|
||||
|
|
@ -221,7 +529,6 @@ void DlgSettings::closeEvent(QCloseEvent *event)
|
|||
|
||||
if (!QDir(SettingsCache::instance().paths().getPicsPath()).exists() ||
|
||||
SettingsCache::instance().paths().getPicsPath().isEmpty()) {
|
||||
//! \todo Prompt to create the pictures directory.
|
||||
if (QMessageBox::critical(this, tr("Error"),
|
||||
tr("The path to your card pictures directory is invalid. Would you like to go back "
|
||||
"and set the correct path?"),
|
||||
|
|
@ -236,15 +543,26 @@ void DlgSettings::closeEvent(QCloseEvent *event)
|
|||
void DlgSettings::retranslateUi()
|
||||
{
|
||||
setWindowTitle(tr("Settings"));
|
||||
retranslateTabNames();
|
||||
|
||||
generalButton->setText(tr("General"));
|
||||
appearanceButton->setText(tr("Appearance"));
|
||||
userInterfaceButton->setText(tr("User Interface"));
|
||||
storageButton->setText(tr("Storage"));
|
||||
deckEditorButton->setText(tr("Card Sources"));
|
||||
messagesButton->setText(tr("Chat"));
|
||||
soundButton->setText(tr("Sound"));
|
||||
shortcutsButton->setText(tr("Shortcuts"));
|
||||
searchEdit->setPlaceholderText(tr("Search settings..."));
|
||||
okButton->setText(tr("OK"));
|
||||
|
||||
contentsWidget->reset();
|
||||
// Rebuild search index for translated text
|
||||
buildSearchIndex();
|
||||
}
|
||||
|
||||
QStringList DlgSettings::translatedPageNames()
|
||||
{
|
||||
return {tr("General"), tr("Appearance"), tr("User Interface"), tr("Card Sources"),
|
||||
tr("Storage"), tr("Chat"), tr("Sound"), tr("Shortcuts")};
|
||||
}
|
||||
|
||||
void DlgSettings::retranslateTabNames()
|
||||
{
|
||||
const QStringList tabLabels = translatedPageNames();
|
||||
|
||||
for (int i = 0; i < tabButtons.size() && i < tabLabels.size(); ++i) {
|
||||
tabButtons[i]->setText(tabLabels[i]);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,43 +1,99 @@
|
|||
/**
|
||||
* @file dlg_settings.h
|
||||
* @brief Main settings dialog for the Cockatrice client
|
||||
* @ingroup Dialogs
|
||||
*/
|
||||
//! \todo Document this file.
|
||||
|
||||
#ifndef DLG_SETTINGS_H
|
||||
#define DLG_SETTINGS_H
|
||||
|
||||
#include <QComboBox>
|
||||
#include <QDialog>
|
||||
#include <QLoggingCategory>
|
||||
|
||||
class QPushButton;
|
||||
|
||||
inline Q_LOGGING_CATEGORY(DlgSettingsLog, "dlg_settings");
|
||||
|
||||
class QListWidget;
|
||||
class QStackedWidget;
|
||||
class QListWidgetItem;
|
||||
class QToolButton;
|
||||
class QListView;
|
||||
class QLineEdit;
|
||||
|
||||
class AbstractSettingsPage;
|
||||
class SettingsSearchModel;
|
||||
class SettingsSearchDelegate;
|
||||
|
||||
/**
|
||||
* @brief Main application settings dialog with tabbed navigation and search
|
||||
*
|
||||
* Provides a modern settings interface organized into tabbed pages. Users can
|
||||
* either navigate by clicking tabs or search for specific settings using the
|
||||
* built-in search bar. Search results are filtered and ranked by relevance.
|
||||
*/
|
||||
class DlgSettings : public QDialog
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
/**
|
||||
* @brief Page order in the tab bar, matching the order pages are added in setupUi()
|
||||
*
|
||||
* Use these values instead of raw indices so reordering pages never silently
|
||||
* breaks external callers like tab_room.cpp.
|
||||
*/
|
||||
enum SettingsPage
|
||||
{
|
||||
GeneralPage = 0,
|
||||
AppearancePage,
|
||||
UserInterfacePage,
|
||||
DeckEditorPage,
|
||||
StoragePage,
|
||||
MessagesPage,
|
||||
SoundPage,
|
||||
ShortcutsPage,
|
||||
NumPages
|
||||
};
|
||||
|
||||
explicit DlgSettings(QWidget *parent = nullptr);
|
||||
void setTab(int index);
|
||||
|
||||
private slots:
|
||||
void changePage(QListWidgetItem *current, QListWidgetItem *previous);
|
||||
void onTabClicked(int index);
|
||||
void onSearchTextChanged(const QString &text);
|
||||
void onSearchResultClicked(const QModelIndex &index);
|
||||
void updateLanguage();
|
||||
|
||||
private:
|
||||
QListWidget *contentsWidget;
|
||||
QStackedWidget *pagesWidget;
|
||||
QListWidgetItem *generalButton, *appearanceButton, *userInterfaceButton, *deckEditorButton, *storageButton,
|
||||
*messagesButton, *soundButton, *shortcutsButton;
|
||||
void createIcons();
|
||||
// UI elements
|
||||
QLineEdit *searchEdit; ///< Search bar for filtering settings
|
||||
QWidget *tabBarWidget; ///< Container widget for the tab buttons
|
||||
QList<QToolButton *> tabButtons; ///< Navigation tab buttons
|
||||
QStackedWidget *pagesWidget; ///< Stacked widget containing settings pages
|
||||
QListView *searchResultsView; ///< Search results list view
|
||||
QWidget *pagesContainer; ///< Container stacking pages and search results
|
||||
QPushButton *okButton; ///< Button to close the dialog
|
||||
|
||||
// Data
|
||||
QList<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 retranslateTabNames();
|
||||
|
||||
protected:
|
||||
void closeEvent(QCloseEvent *event) override;
|
||||
bool eventFilter(QObject *watched, QEvent *event) override;
|
||||
};
|
||||
|
||||
#endif
|
||||
#endif // DLG_SETTINGS_H
|
||||
|
|
|
|||
|
|
@ -134,7 +134,7 @@ void DlgUpdate::finishedUpdateCheck(bool needToUpdate, bool isCompatible, Releas
|
|||
// If there's no need to update, tell them that. However we still allow them to run the
|
||||
// downloader themselves if there's a compatible build
|
||||
QMessageBox::information(
|
||||
this, tr("No Update Available"),
|
||||
window(), tr("No Update Available"),
|
||||
tr("Cockatrice is up to date!") + "<br><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>" +
|
||||
|
|
@ -147,7 +147,7 @@ void DlgUpdate::finishedUpdateCheck(bool needToUpdate, bool isCompatible, Releas
|
|||
if (isCompatible) {
|
||||
int reply;
|
||||
reply = QMessageBox::question(
|
||||
this, tr("Update Available"),
|
||||
window(), tr("Update Available"),
|
||||
tr("A new version of Cockatrice is available!") + "<br><br>" + "<b>" + tr("New version") +
|
||||
QString(":</b> %1<br>").arg(release->getName()) + "<b>" + tr("Released") +
|
||||
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) {
|
||||
downloadUpdate(release->getName());
|
||||
} else {
|
||||
closeDialog();
|
||||
}
|
||||
} else {
|
||||
QMessageBox::information(
|
||||
this, tr("Update Available"),
|
||||
window(), tr("Update Available"),
|
||||
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 (<a href=\"%2\">").arg(publishDate, release->getDescriptionUrl()) + tr("Changelog") +
|
||||
|
|
|
|||
|
|
@ -82,9 +82,7 @@ void ColorPie::paintEvent(QPaintEvent *)
|
|||
QString label = QString("%1%").arg(int(ratio * 100 + 0.5));
|
||||
|
||||
QFontMetrics fm(p.font());
|
||||
|
||||
int labelWidth = fm.horizontalAdvance(label);
|
||||
|
||||
QRectF textRect(labelPos.x() - labelWidth / 2.0, labelPos.y() - fm.height() / 2.0, labelWidth, fm.height());
|
||||
|
||||
p.setPen(Qt::black);
|
||||
|
|
|
|||
|
|
@ -14,8 +14,8 @@
|
|||
#include <QVBoxLayout>
|
||||
#include <libcockatrice/card/database/card_database_manager.h>
|
||||
#include <libcockatrice/network/client/remote/remote_client.h>
|
||||
#include <libcockatrice/settings/appearance_settings.h>
|
||||
#include <libcockatrice/settings/paths_settings.h>
|
||||
#include <libcockatrice/settings/personal_settings.h>
|
||||
|
||||
HomeWidget::HomeWidget(QWidget *parent, TabSupervisor *_tabSupervisor)
|
||||
: QWidget(parent), tabSupervisor(_tabSupervisor), background("theme:backgrounds/home"), overlay("theme:cockatrice")
|
||||
|
|
@ -43,12 +43,12 @@ HomeWidget::HomeWidget(QWidget *parent, TabSupervisor *_tabSupervisor)
|
|||
updateConnectButton(tabSupervisor->getClient()->getStatus());
|
||||
|
||||
connect(tabSupervisor->getClient(), &RemoteClient::statusChanged, this, &HomeWidget::updateConnectButton);
|
||||
connect(&SettingsCache::instance().personal(), &PersonalSettings::homeTabBackgroundSourceChanged, this,
|
||||
connect(&SettingsCache::instance().appearance(), &AppearanceSettings::homeTabBackgroundSourceChanged, this,
|
||||
&HomeWidget::initializeBackgroundFromSource);
|
||||
connect(&SettingsCache::instance().personal(), &PersonalSettings::homeTabBackgroundShuffleFrequencyChanged, this,
|
||||
&HomeWidget::onBackgroundShuffleFrequencyChanged);
|
||||
connect(&SettingsCache::instance().appearance(), &AppearanceSettings::homeTabBackgroundShuffleFrequencyChanged,
|
||||
this, &HomeWidget::onBackgroundShuffleFrequencyChanged);
|
||||
// Lambda is cleaner to read than overloading this
|
||||
connect(&SettingsCache::instance().personal(), &PersonalSettings::homeTabDisplayCardNameChanged, this,
|
||||
connect(&SettingsCache::instance().appearance(), &AppearanceSettings::homeTabDisplayCardNameChanged, this,
|
||||
[this] { repaint(); });
|
||||
connect(&SettingsCache::instance(), &SettingsCache::themeChanged, this,
|
||||
&HomeWidget::initializeBackgroundFromSource);
|
||||
|
|
@ -65,7 +65,7 @@ void HomeWidget::initializeBackgroundFromSource()
|
|||
}
|
||||
|
||||
auto backgroundSourceType =
|
||||
BackgroundSources::fromId(SettingsCache::instance().personal().getHomeTabBackgroundSource());
|
||||
BackgroundSources::fromId(SettingsCache::instance().appearance().getHomeTabBackgroundSource());
|
||||
|
||||
switch (backgroundSourceType) {
|
||||
case BackgroundSources::Theme:
|
||||
|
|
@ -113,7 +113,7 @@ void HomeWidget::setRandomCard(ExactCard &newCard)
|
|||
void HomeWidget::updateRandomCard()
|
||||
{
|
||||
auto backgroundSourceType =
|
||||
BackgroundSources::fromId(SettingsCache::instance().personal().getHomeTabBackgroundSource());
|
||||
BackgroundSources::fromId(SettingsCache::instance().appearance().getHomeTabBackgroundSource());
|
||||
|
||||
ExactCard newCard;
|
||||
|
||||
|
|
@ -156,8 +156,8 @@ void HomeWidget::updateRandomCard()
|
|||
void HomeWidget::onBackgroundShuffleFrequencyChanged()
|
||||
{
|
||||
cardChangeTimer->stop();
|
||||
if (SettingsCache::instance().personal().getHomeTabBackgroundShuffleFrequency() > 0) {
|
||||
cardChangeTimer->start(SettingsCache::instance().personal().getHomeTabBackgroundShuffleFrequency() * 1000);
|
||||
if (SettingsCache::instance().appearance().getHomeTabBackgroundShuffleFrequency() > 0) {
|
||||
cardChangeTimer->start(SettingsCache::instance().appearance().getHomeTabBackgroundShuffleFrequency() * 1000);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -265,7 +265,7 @@ void HomeWidget::updateConnectButton(const ClientStatus status)
|
|||
|
||||
QPair<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)) {
|
||||
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();
|
||||
font.setPointSize(14);
|
||||
font.setBold(true);
|
||||
|
|
|
|||
|
|
@ -16,16 +16,16 @@ class TearOffMenu : public QMenu
|
|||
public:
|
||||
explicit TearOffMenu(const QString &title, QWidget *parent = nullptr) : QMenu(title, parent)
|
||||
{
|
||||
connect(&SettingsCache::instance().interface(), &InterfaceSettings::useTearOffMenusChanged, this,
|
||||
connect(&SettingsCache::instance().userInterface(), &InterfaceSettings::useTearOffMenusChanged, this,
|
||||
[this](const bool state) { setTearOffEnabled(state); });
|
||||
setTearOffEnabled(SettingsCache::instance().interface().getUseTearOffMenus());
|
||||
setTearOffEnabled(SettingsCache::instance().userInterface().getUseTearOffMenus());
|
||||
}
|
||||
|
||||
explicit TearOffMenu(QWidget *parent = nullptr) : QMenu(parent)
|
||||
{
|
||||
connect(&SettingsCache::instance().interface(), &InterfaceSettings::useTearOffMenusChanged, this,
|
||||
connect(&SettingsCache::instance().userInterface(), &InterfaceSettings::useTearOffMenusChanged, this,
|
||||
[this](const bool state) { setTearOffEnabled(state); });
|
||||
setTearOffEnabled(SettingsCache::instance().interface().getUseTearOffMenus());
|
||||
setTearOffEnabled(SettingsCache::instance().userInterface().getUseTearOffMenus());
|
||||
}
|
||||
|
||||
TearOffMenu *addTearOffMenu(const QString &title)
|
||||
|
|
|
|||
234
cockatrice/src/interface/widgets/replay/replay_manager.cpp
Normal file
234
cockatrice/src/interface/widgets/replay/replay_manager.cpp
Normal 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);
|
||||
}
|
||||
83
cockatrice/src/interface/widgets/replay/replay_manager.h
Normal file
83
cockatrice/src/interface/widgets/replay/replay_manager.h
Normal 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
|
||||
|
|
@ -14,16 +14,21 @@ ReplayQuickSettingsWidget::ReplayQuickSettingsWidget(QWidget *parent) : Settings
|
|||
fastForwardSpeedBox.setMinimum(1);
|
||||
fastForwardSpeedBox.setMaximum(99.9);
|
||||
fastForwardSpeedBox.setDecimals(1);
|
||||
fastForwardSpeedBox.setValue(SettingsCache::instance().interface().getFastForwardSpeed());
|
||||
fastForwardSpeedBox.setValue(SettingsCache::instance().userInterface().getFastForwardSpeed());
|
||||
connect(&fastForwardSpeedBox, qOverload<double>(&QDoubleSpinBox::valueChanged), this,
|
||||
&ReplayQuickSettingsWidget::actUpdateFastForwardSpeed);
|
||||
|
||||
skipEmptyCheckBox.setChecked(SettingsCache::instance().userInterface().getSkipEmptySections());
|
||||
connect(&skipEmptyCheckBox, &QCheckBox::QT_STATE_CHANGED, this,
|
||||
&ReplayQuickSettingsWidget::actUpdateSkipEmptySections);
|
||||
|
||||
// putting it all together
|
||||
auto *widget = new QWidget;
|
||||
auto *grid = new QGridLayout(widget);
|
||||
grid->setContentsMargins(0, 0, 0, 0);
|
||||
grid->addWidget(&fastForwardSpeedLabel, 0, 0, 1, 1);
|
||||
grid->addWidget(&fastForwardSpeedBox, 0, 1, 1, 1);
|
||||
grid->addWidget(&skipEmptyCheckBox, 1, 0, 1, 2);
|
||||
|
||||
this->addSettingsWidget(widget);
|
||||
|
||||
|
|
@ -36,10 +41,18 @@ void ReplayQuickSettingsWidget::retranslateUi()
|
|||
{
|
||||
fastForwardSpeedLabel.setText(tr("Fast forward speed:"));
|
||||
fastForwardSpeedBox.setSuffix("x");
|
||||
|
||||
skipEmptyCheckBox.setText(tr("Skip empty sections"));
|
||||
}
|
||||
|
||||
void ReplayQuickSettingsWidget::actUpdateFastForwardSpeed(qreal value)
|
||||
{
|
||||
SettingsCache::instance().interface().setFastForwardSpeed(value);
|
||||
SettingsCache::instance().userInterface().setFastForwardSpeed(value);
|
||||
emit fastForwardSpeedChanged(value);
|
||||
}
|
||||
|
||||
void ReplayQuickSettingsWidget::actUpdateSkipEmptySections(QT_STATE_CHANGED_T value)
|
||||
{
|
||||
SettingsCache::instance().userInterface().setSkipEmptySections(value);
|
||||
emit skipEmptySectionsChanged(value);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,9 @@
|
|||
|
||||
#include "../../interface/widgets/quick_settings/settings_button_widget.h"
|
||||
|
||||
#include <QCheckBox>
|
||||
#include <QDoubleSpinBox>
|
||||
#include <libcockatrice/utility/macros.h>
|
||||
|
||||
class ReplayQuickSettingsWidget : public SettingsButtonWidget
|
||||
{
|
||||
|
|
@ -16,13 +18,17 @@ public:
|
|||
|
||||
signals:
|
||||
void fastForwardSpeedChanged(qreal speed);
|
||||
void skipEmptySectionsChanged(bool skip);
|
||||
|
||||
private:
|
||||
QLabel fastForwardSpeedLabel;
|
||||
QDoubleSpinBox fastForwardSpeedBox;
|
||||
|
||||
QCheckBox skipEmptyCheckBox;
|
||||
|
||||
private slots:
|
||||
void actUpdateFastForwardSpeed(qreal value);
|
||||
void actUpdateSkipEmptySections(QT_STATE_CHANGED_T value);
|
||||
};
|
||||
|
||||
#endif // COCKATRICE_REPLAY_QUICK_SETTINGS_WIDGET_H
|
||||
|
|
|
|||
|
|
@ -4,26 +4,19 @@
|
|||
|
||||
#include <QPainter>
|
||||
#include <QPainterPath>
|
||||
#include <QTimer>
|
||||
#include <libcockatrice/settings/interface_settings.h>
|
||||
|
||||
ReplayTimelineWidget::ReplayTimelineWidget(QWidget *parent)
|
||||
: QWidget(parent), maxBinValue(1), maxTime(1), timeScaleFactor(1.0), currentVisualTime(0), currentProcessedTime(0),
|
||||
currentEvent(0)
|
||||
static constexpr int BIN_LENGTH = 5000;
|
||||
static constexpr int MIN_RESOLUTION_MS = 1000;
|
||||
|
||||
ReplayTimelineWidget::ReplayTimelineWidget(QWidget *parent) : QWidget(parent)
|
||||
{
|
||||
replayTimer = new QTimer(this);
|
||||
replayTimer->setInterval(TIMER_INTERVAL_MS);
|
||||
connect(replayTimer, &QTimer::timeout, this, &ReplayTimelineWidget::replayTimerTimeout);
|
||||
|
||||
rewindBufferingTimer = new QTimer(this);
|
||||
rewindBufferingTimer->setSingleShot(true);
|
||||
connect(rewindBufferingTimer, &QTimer::timeout, this, &ReplayTimelineWidget::processRewind);
|
||||
}
|
||||
|
||||
void ReplayTimelineWidget::setTimeline(const QList<int> &_replayTimeline)
|
||||
void ReplayTimelineWidget::setTimeline(const QList<int> &replayTimeline)
|
||||
{
|
||||
replayTimeline = _replayTimeline;
|
||||
histogram.clear();
|
||||
currentTime = 0;
|
||||
|
||||
int binEndTime = BIN_LENGTH - 1;
|
||||
int binValue = 0;
|
||||
for (int i : replayTimeline) {
|
||||
|
|
@ -66,70 +59,31 @@ void ReplayTimelineWidget::paintEvent(QPaintEvent * /* event */)
|
|||
painter.fillPath(path, Qt::black);
|
||||
|
||||
const QColor barColor = QColor::fromHsv(120, 255, 255, 100);
|
||||
quint64 w = (quint64)(width() - 1) * (quint64)currentVisualTime / maxTime;
|
||||
quint64 w = (quint64)(width() - 1) * (quint64)currentTime / maxTime;
|
||||
painter.fillRect(0, 0, static_cast<int>(w), height() - 1, barColor);
|
||||
}
|
||||
|
||||
void ReplayTimelineWidget::mousePressEvent(QMouseEvent *event)
|
||||
{
|
||||
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
|
||||
skipToTime(newTime, false);
|
||||
emit timeClicked(newTime);
|
||||
}
|
||||
|
||||
void ReplayTimelineWidget::skipToTime(int newTime, bool doRewindBuffering)
|
||||
void ReplayTimelineWidget::setCurrentTime(int time)
|
||||
{
|
||||
// check boundary conditions
|
||||
if (newTime < 0) {
|
||||
newTime = 0;
|
||||
}
|
||||
if (newTime > maxTime) {
|
||||
newTime = maxTime;
|
||||
int newTime = qBound(0, time, maxTime);
|
||||
|
||||
if (currentTime == newTime) {
|
||||
return;
|
||||
}
|
||||
|
||||
newTime -= newTime % TIMER_INTERVAL_MS; // Time should always be a multiple of the interval
|
||||
bool doUpdate = currentTime / MIN_RESOLUTION_MS != newTime / MIN_RESOLUTION_MS;
|
||||
|
||||
const bool isBackwardsSkip = newTime < currentProcessedTime;
|
||||
currentVisualTime = newTime;
|
||||
currentTime = newTime;
|
||||
|
||||
if (isBackwardsSkip) {
|
||||
handleBackwardsSkip(doRewindBuffering);
|
||||
} else {
|
||||
processNewEvents(FORWARD_SKIP);
|
||||
if (doUpdate) {
|
||||
update();
|
||||
}
|
||||
|
||||
update();
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Handles a backwards skip in the replay timeline.
|
||||
*
|
||||
* @param doRewindBuffering When true, if multiple backward skips are made in quick succession, only a single rewind
|
||||
* is processed at the end. When false, the backwards skip will always cause an immediate rewind.
|
||||
*/
|
||||
void ReplayTimelineWidget::handleBackwardsSkip(bool doRewindBuffering)
|
||||
{
|
||||
if (doRewindBuffering) {
|
||||
// We use a one-shot timer to implement the rewind buffering.
|
||||
// The rewind only happens once the timer runs out.
|
||||
// If another backwards skip happens, the timer will just get reset instead of rewinding.
|
||||
rewindBufferingTimer->stop();
|
||||
rewindBufferingTimer->start(SettingsCache::instance().interface().getRewindBufferingMs());
|
||||
} else {
|
||||
// otherwise, process the rewind immediately
|
||||
processRewind();
|
||||
}
|
||||
}
|
||||
|
||||
void ReplayTimelineWidget::processRewind()
|
||||
{
|
||||
// stop any queued-up rewinds
|
||||
rewindBufferingTimer->stop();
|
||||
|
||||
// process the rewind
|
||||
currentEvent = 0;
|
||||
emit rewound();
|
||||
processNewEvents(BACKWARD_SKIP);
|
||||
}
|
||||
|
||||
QSize ReplayTimelineWidget::sizeHint() const
|
||||
|
|
@ -141,64 +95,3 @@ QSize ReplayTimelineWidget::minimumSizeHint() const
|
|||
{
|
||||
return {400, 50};
|
||||
}
|
||||
|
||||
void ReplayTimelineWidget::replayTimerTimeout()
|
||||
{
|
||||
currentVisualTime += TIMER_INTERVAL_MS;
|
||||
|
||||
processNewEvents(NORMAL_PLAYBACK);
|
||||
|
||||
if (!(currentVisualTime % 1000)) {
|
||||
update();
|
||||
}
|
||||
}
|
||||
|
||||
/** @brief Processes all unprocessed events up to the current time. */
|
||||
void ReplayTimelineWidget::processNewEvents(PlaybackMode playbackMode)
|
||||
{
|
||||
currentProcessedTime = currentVisualTime;
|
||||
|
||||
while ((currentEvent < replayTimeline.size()) && (replayTimeline[currentEvent] < currentProcessedTime)) {
|
||||
EventProcessingOptions options;
|
||||
|
||||
// backwards skip => always skip reveal windows
|
||||
// forwards skip => skip reveal windows that don't happen within a big skip of the target
|
||||
if (playbackMode == BACKWARD_SKIP || currentProcessedTime - replayTimeline[currentEvent] > BIG_SKIP_MS) {
|
||||
options |= SKIP_REVEAL_WINDOW;
|
||||
}
|
||||
|
||||
// backwards skip => always skip tap animation
|
||||
if (playbackMode == BACKWARD_SKIP) {
|
||||
options |= SKIP_TAP_ANIMATION;
|
||||
}
|
||||
|
||||
emit processNextEvent(options);
|
||||
++currentEvent;
|
||||
}
|
||||
if (currentEvent == replayTimeline.size()) {
|
||||
emit replayFinished();
|
||||
replayTimer->stop();
|
||||
}
|
||||
}
|
||||
|
||||
void ReplayTimelineWidget::setTimeScaleFactor(qreal _timeScaleFactor)
|
||||
{
|
||||
timeScaleFactor = _timeScaleFactor;
|
||||
int interval = std::max(1, qRound(TIMER_INTERVAL_MS / timeScaleFactor));
|
||||
replayTimer->setInterval(interval);
|
||||
}
|
||||
|
||||
void ReplayTimelineWidget::startReplay()
|
||||
{
|
||||
replayTimer->start();
|
||||
}
|
||||
|
||||
void ReplayTimelineWidget::stopReplay()
|
||||
{
|
||||
replayTimer->stop();
|
||||
}
|
||||
|
||||
void ReplayTimelineWidget::skipByAmount(int amount)
|
||||
{
|
||||
skipToTime(currentVisualTime + amount, amount < 0);
|
||||
}
|
||||
|
|
@ -18,57 +18,25 @@ class QTimer;
|
|||
class ReplayTimelineWidget : public QWidget
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
signals:
|
||||
void processNextEvent(EventProcessingOptions options);
|
||||
void replayFinished();
|
||||
void rewound();
|
||||
void timeClicked(int newTime);
|
||||
|
||||
private:
|
||||
enum PlaybackMode
|
||||
{
|
||||
NORMAL_PLAYBACK,
|
||||
FORWARD_SKIP,
|
||||
BACKWARD_SKIP
|
||||
};
|
||||
|
||||
static constexpr int TIMER_INTERVAL_MS = 200;
|
||||
static constexpr int BIN_LENGTH = 5000;
|
||||
|
||||
QTimer *replayTimer;
|
||||
QTimer *rewindBufferingTimer;
|
||||
QList<int> replayTimeline;
|
||||
QList<int> histogram;
|
||||
int maxBinValue, maxTime;
|
||||
qreal timeScaleFactor;
|
||||
int currentVisualTime; // time currently displayed by the timeline
|
||||
int currentProcessedTime; // time that events are currently processed up to. Could differ from visual time due to
|
||||
// rewind buffering
|
||||
int currentEvent;
|
||||
int maxBinValue = 1;
|
||||
int maxTime = 1;
|
||||
|
||||
void skipToTime(int newTime, bool doRewindBuffering);
|
||||
void handleBackwardsSkip(bool doRewindBuffering);
|
||||
void processRewind();
|
||||
void processNewEvents(PlaybackMode playbackMode);
|
||||
private slots:
|
||||
void replayTimerTimeout();
|
||||
int currentTime = 0;
|
||||
|
||||
public:
|
||||
static constexpr int SMALL_SKIP_MS = 1000;
|
||||
static constexpr int BIG_SKIP_MS = 10000;
|
||||
|
||||
explicit ReplayTimelineWidget(QWidget *parent = nullptr);
|
||||
void setTimeline(const QList<int> &_replayTimeline);
|
||||
void setTimeline(const QList<int> &replayTimeline);
|
||||
[[nodiscard]] QSize sizeHint() const override;
|
||||
[[nodiscard]] QSize minimumSizeHint() const override;
|
||||
void setTimeScaleFactor(qreal _timeScaleFactor);
|
||||
[[nodiscard]] int getCurrentEvent() const
|
||||
{
|
||||
return currentEvent;
|
||||
}
|
||||
|
||||
public slots:
|
||||
void startReplay();
|
||||
void stopReplay();
|
||||
void skipByAmount(int amount); // use a negative amount to skip backwards
|
||||
void setCurrentTime(int time);
|
||||
|
||||
protected:
|
||||
void paintEvent(QPaintEvent *event) override;
|
||||
|
|
|
|||
|
|
@ -1,70 +1,50 @@
|
|||
#include "replay_widget.h"
|
||||
|
||||
#include "../../../client/settings/cache_settings.h"
|
||||
#include "../../../client/settings/shortcuts_settings.h"
|
||||
#include "../interface/widgets/tabs/tab_game.h"
|
||||
#include "replay_manager.h"
|
||||
#include "replay_quick_settings_widget.h"
|
||||
|
||||
#include <QHBoxLayout>
|
||||
#include <QToolButton>
|
||||
|
||||
ReplayWidget::ReplayWidget(TabGame *parent, GameReplay *_replay)
|
||||
: QWidget(parent), game(parent), replay(_replay), replayPlayButton(nullptr), replayFastForwardButton(nullptr),
|
||||
aReplaySkipForward(nullptr), aReplaySkipBackward(nullptr), aReplaySkipForwardBig(nullptr),
|
||||
aReplaySkipBackwardBig(nullptr)
|
||||
ReplayWidget::ReplayWidget(QWidget *parent, GameReplay *replay)
|
||||
: QWidget(parent), replayPlayButton(nullptr), replayFastForwardButton(nullptr), aReplaySkipForward(nullptr),
|
||||
aReplaySkipBackward(nullptr), aReplaySkipForwardBig(nullptr), aReplaySkipBackwardBig(nullptr)
|
||||
{
|
||||
if (replay) {
|
||||
game->getGame()->loadReplay(replay);
|
||||
|
||||
// Create list: event number -> time [ms]
|
||||
// Distribute simultaneous events evenly across 1 second.
|
||||
unsigned int lastEventTimestamp = 0;
|
||||
const int eventCount = replay->event_list_size();
|
||||
for (int i = 0; i < eventCount; ++i) {
|
||||
int j = i + 1;
|
||||
while ((j < eventCount) && (replay->event_list(j).seconds_elapsed() == lastEventTimestamp)) {
|
||||
++j;
|
||||
}
|
||||
|
||||
const int numberEventsThisSecond = j - i;
|
||||
for (int k = 0; k < numberEventsThisSecond; ++k) {
|
||||
replayTimeline.append(replay->event_list(i + k).seconds_elapsed() * 1000 +
|
||||
(int)((qreal)k / (qreal)numberEventsThisSecond * 1000));
|
||||
}
|
||||
|
||||
if (j < eventCount) {
|
||||
lastEventTimestamp = replay->event_list(j).seconds_elapsed();
|
||||
}
|
||||
i += numberEventsThisSecond - 1;
|
||||
}
|
||||
}
|
||||
// replay manager
|
||||
replayManager = new ReplayManager(this, replay);
|
||||
connect(replayManager, &ReplayManager::eventReplayed, this, &ReplayWidget::eventReplayed);
|
||||
connect(replayManager, &ReplayManager::replayFinished, this, &ReplayWidget::replayFinished);
|
||||
connect(replayManager, &ReplayManager::rewound, this, &ReplayWidget::rewound);
|
||||
|
||||
// timeline widget
|
||||
timelineWidget = new ReplayTimelineWidget;
|
||||
timelineWidget->setTimeline(replayTimeline);
|
||||
connect(timelineWidget, &ReplayTimelineWidget::processNextEvent, this, &ReplayWidget::replayNextEvent);
|
||||
connect(timelineWidget, &ReplayTimelineWidget::replayFinished, this, &ReplayWidget::replayFinished);
|
||||
connect(timelineWidget, &ReplayTimelineWidget::rewound, this, &ReplayWidget::replayRewind);
|
||||
timelineWidget->setTimeline(replayManager->getReplayTimeline());
|
||||
connect(replayManager, &ReplayManager::timeChanged, timelineWidget, &ReplayTimelineWidget::setCurrentTime);
|
||||
connect(timelineWidget, &ReplayTimelineWidget::timeClicked, replayManager, &ReplayManager::setTime);
|
||||
|
||||
// timeline skip shortcuts
|
||||
aReplaySkipForward = new QAction(timelineWidget);
|
||||
timelineWidget->addAction(aReplaySkipForward);
|
||||
connect(aReplaySkipForward, &QAction::triggered, this,
|
||||
[this] { timelineWidget->skipByAmount(ReplayTimelineWidget::SMALL_SKIP_MS); });
|
||||
[this] { replayManager->skipByAmount(ReplayManager::SMALL_SKIP_MS); });
|
||||
|
||||
aReplaySkipBackward = new QAction(timelineWidget);
|
||||
timelineWidget->addAction(aReplaySkipBackward);
|
||||
connect(aReplaySkipBackward, &QAction::triggered, this,
|
||||
[this] { timelineWidget->skipByAmount(-ReplayTimelineWidget::SMALL_SKIP_MS); });
|
||||
[this] { replayManager->skipByAmount(-ReplayManager::SMALL_SKIP_MS); });
|
||||
|
||||
aReplaySkipForwardBig = new QAction(timelineWidget);
|
||||
timelineWidget->addAction(aReplaySkipForwardBig);
|
||||
connect(aReplaySkipForwardBig, &QAction::triggered, this,
|
||||
[this] { timelineWidget->skipByAmount(ReplayTimelineWidget::BIG_SKIP_MS); });
|
||||
[this] { replayManager->skipByAmount(ReplayManager::BIG_SKIP_MS); });
|
||||
|
||||
aReplaySkipBackwardBig = new QAction(timelineWidget);
|
||||
timelineWidget->addAction(aReplaySkipBackwardBig);
|
||||
connect(aReplaySkipBackwardBig, &QAction::triggered, this,
|
||||
[this] { timelineWidget->skipByAmount(-ReplayTimelineWidget::BIG_SKIP_MS); });
|
||||
[this] { replayManager->skipByAmount(-ReplayManager::BIG_SKIP_MS); });
|
||||
|
||||
// buttons
|
||||
replayPlayButton = new QToolButton;
|
||||
|
|
@ -86,6 +66,10 @@ ReplayWidget::ReplayWidget(TabGame *parent, GameReplay *_replay)
|
|||
settingsWidget->setFixedSize(QSize(32, 32));
|
||||
connect(settingsWidget, &ReplayQuickSettingsWidget::fastForwardSpeedChanged, this,
|
||||
[this] { updateTimeScaleFactor(replayFastForwardButton->isChecked()); });
|
||||
connect(settingsWidget, &ReplayQuickSettingsWidget::skipEmptySectionsChanged, replayManager,
|
||||
&ReplayManager::setSkipEmptySections);
|
||||
|
||||
replayManager->setSkipEmptySections(SettingsCache::instance().userInterface().getSkipEmptySections());
|
||||
|
||||
// putting everything together
|
||||
auto replayControlLayout = new QHBoxLayout;
|
||||
|
|
@ -97,18 +81,11 @@ ReplayWidget::ReplayWidget(TabGame *parent, GameReplay *_replay)
|
|||
setObjectName("replayControlWidget");
|
||||
setLayout(replayControlLayout);
|
||||
|
||||
connect(this, &ReplayWidget::requestChatAndPhaseReset, game, &TabGame::resetChatAndPhase);
|
||||
|
||||
connect(&SettingsCache::instance().shortcuts(), &ShortcutsSettings::shortCutChanged, this,
|
||||
&ReplayWidget::refreshShortcuts);
|
||||
refreshShortcuts();
|
||||
}
|
||||
|
||||
void ReplayWidget::replayNextEvent(EventProcessingOptions options)
|
||||
{
|
||||
emit eventReplayed(replay->event_list(timelineWidget->getCurrentEvent()), options);
|
||||
}
|
||||
|
||||
void ReplayWidget::replayFinished()
|
||||
{
|
||||
replayPlayButton->setChecked(false);
|
||||
|
|
@ -117,24 +94,16 @@ void ReplayWidget::replayFinished()
|
|||
void ReplayWidget::replayPlayButtonToggled(bool checked)
|
||||
{
|
||||
if (checked) { // start replay
|
||||
timelineWidget->startReplay();
|
||||
replayManager->startReplay();
|
||||
} else { // pause replay
|
||||
timelineWidget->stopReplay();
|
||||
replayManager->stopReplay();
|
||||
}
|
||||
}
|
||||
|
||||
void ReplayWidget::updateTimeScaleFactor(bool isFastForward)
|
||||
{
|
||||
qreal factor = isFastForward ? SettingsCache::instance().interface().getFastForwardSpeed() : 1.0;
|
||||
timelineWidget->setTimeScaleFactor(factor);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Handles everything that needs to be reset when doing a replay rewind.
|
||||
*/
|
||||
void ReplayWidget::replayRewind()
|
||||
{
|
||||
emit requestChatAndPhaseReset();
|
||||
qreal factor = isFastForward ? SettingsCache::instance().userInterface().getFastForwardSpeed() : 1.0;
|
||||
replayManager->setTimeScaleFactor(factor);
|
||||
}
|
||||
|
||||
void ReplayWidget::refreshShortcuts()
|
||||
|
|
|
|||
|
|
@ -14,11 +14,12 @@
|
|||
#include <QWidget>
|
||||
#include <libcockatrice/protocol/pb/game_replay.pb.h>
|
||||
|
||||
class ReplayManager;
|
||||
class ReplayQuickSettingsWidget;
|
||||
class TabGame;
|
||||
|
||||
/**
|
||||
* @brief The top-level that is put in the replay dock widget.
|
||||
* @brief The top-level widget that is put in the replay dock widget.
|
||||
* Contains the replay timeline as well as the buttons.
|
||||
*/
|
||||
class ReplayWidget : public QWidget
|
||||
|
|
@ -26,29 +27,28 @@ class ReplayWidget : public QWidget
|
|||
Q_OBJECT
|
||||
|
||||
public:
|
||||
ReplayWidget(TabGame *parent, GameReplay *replay);
|
||||
TabGame *game;
|
||||
GameReplay *replay;
|
||||
/**
|
||||
* @param parent The parent widget
|
||||
* @param replay Cannot be null. Takes ownership of the replay.
|
||||
*/
|
||||
ReplayWidget(QWidget *parent, GameReplay *replay);
|
||||
|
||||
signals:
|
||||
void requestChatAndPhaseReset();
|
||||
void rewound();
|
||||
void eventReplayed(const GameEventContainer &cont, EventProcessingOptions options);
|
||||
|
||||
private:
|
||||
// Replay related members
|
||||
int currentReplayStep = 0;
|
||||
QList<int> replayTimeline;
|
||||
ReplayManager *replayManager;
|
||||
|
||||
ReplayTimelineWidget *timelineWidget;
|
||||
QToolButton *replayPlayButton, *replayFastForwardButton;
|
||||
ReplayQuickSettingsWidget *settingsWidget;
|
||||
QAction *aReplaySkipForward, *aReplaySkipBackward, *aReplaySkipForwardBig, *aReplaySkipBackwardBig;
|
||||
|
||||
private slots:
|
||||
void replayNextEvent(EventProcessingOptions options);
|
||||
void replayFinished();
|
||||
void replayPlayButtonToggled(bool checked);
|
||||
void updateTimeScaleFactor(bool checked);
|
||||
void replayRewind();
|
||||
void refreshShortcuts();
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@
|
|||
#include <QDesktopServices>
|
||||
#include <QMouseEvent>
|
||||
#include <QScrollBar>
|
||||
#include <libcockatrice/card/database/card_database_manager.h>
|
||||
#include <libcockatrice/network/server/remote/user_level.h>
|
||||
#include <libcockatrice/settings/chat_settings.h>
|
||||
|
||||
|
|
@ -62,12 +63,14 @@ void ChatView::adjustColorsToPalette()
|
|||
serverMessageColor = QColor(0xFF, 0x73, 0x83);
|
||||
otherUserColor = otherUserColor.lighter(150);
|
||||
linkColor = QColor(71, 158, 252);
|
||||
unresolvedCardTagColor = QColor(0xFF, 0xA5, 0x00);
|
||||
} else {
|
||||
document()->setDefaultStyleSheet(R"(
|
||||
a { text-decoration: none; color: blue; }
|
||||
.blue { color: blue }
|
||||
)");
|
||||
linkColor = palette().link().color();
|
||||
unresolvedCardTagColor = QColor(0xA0, 0x52, 0x2D);
|
||||
}
|
||||
|
||||
QTimer::singleShot(0, this, &ChatView::refreshBlockColors);
|
||||
|
|
@ -173,13 +176,22 @@ void ChatView::appendHtmlServerMessage(const QString &html, bool optionalIsBold,
|
|||
void ChatView::appendCardTag(QTextCursor &cursor, const QString &cardName)
|
||||
{
|
||||
QTextCharFormat oldFormat = cursor.charFormat();
|
||||
QTextCharFormat anchorFormat = oldFormat;
|
||||
anchorFormat.setForeground(linkColor);
|
||||
anchorFormat.setAnchor(true);
|
||||
anchorFormat.setAnchorHref("card://" + cardName);
|
||||
anchorFormat.setFontItalic(true);
|
||||
QTextCharFormat cardFormat = oldFormat;
|
||||
cardFormat.setFontItalic(true);
|
||||
|
||||
cursor.setCharFormat(anchorFormat);
|
||||
if (!CardDatabaseManager::query()->lookupCardByName(cardName)) {
|
||||
cardFormat.setForeground(unresolvedCardTagColor);
|
||||
cursor.setCharFormat(cardFormat);
|
||||
cursor.insertText(cardName);
|
||||
cursor.setCharFormat(oldFormat);
|
||||
return;
|
||||
}
|
||||
|
||||
cardFormat.setForeground(linkColor);
|
||||
cardFormat.setAnchor(true);
|
||||
cardFormat.setAnchorHref("card://" + cardName);
|
||||
|
||||
cursor.setCharFormat(cardFormat);
|
||||
cursor.insertText(cardName);
|
||||
cursor.setCharFormat(oldFormat);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -81,6 +81,7 @@ private:
|
|||
QColor otherUserColor = QColor(0, 65, 255); // dark blue
|
||||
QColor serverMessageColor = QColor(0x85, 0x15, 0x15);
|
||||
QColor linkColor;
|
||||
QColor unresolvedCardTagColor;
|
||||
|
||||
private slots:
|
||||
void openLink(const QUrl &link);
|
||||
|
|
|
|||
|
|
@ -10,18 +10,23 @@
|
|||
#include "games_model.h"
|
||||
#include "user/user_list_manager.h"
|
||||
|
||||
#include <QClipboard>
|
||||
#include <QDebug>
|
||||
#include <QGuiApplication>
|
||||
#include <QHBoxLayout>
|
||||
#include <QHeaderView>
|
||||
#include <QMessageBox>
|
||||
#include <QPushButton>
|
||||
#include <QTreeView>
|
||||
#include <QUrl>
|
||||
#include <QUrlQuery>
|
||||
#include <libcockatrice/network/client/abstract/abstract_client.h>
|
||||
#include <libcockatrice/protocol/pb/response.pb.h>
|
||||
#include <libcockatrice/protocol/pb/room_commands.pb.h>
|
||||
#include <libcockatrice/protocol/pb/serverinfo_game.pb.h>
|
||||
#include <libcockatrice/protocol/pending_command.h>
|
||||
#include <libcockatrice/settings/cards_display_settings.h>
|
||||
#include <libcockatrice/settings/interface_settings.h>
|
||||
|
||||
GameSelector::GameSelector(AbstractClient *_client,
|
||||
TabSupervisor *_tabSupervisor,
|
||||
|
|
@ -79,12 +84,12 @@ GameSelector::GameSelector(AbstractClient *_client,
|
|||
if (showFilters && restoresettings) {
|
||||
quickFilterToolBar = new GameSelectorQuickFilterToolBar(this, tabSupervisor, gameListProxyModel, gameTypeMap);
|
||||
quickFilterToolBar->setVisible(showFilters && restoresettings &&
|
||||
SettingsCache::instance().cardsDisplay().getShowGameSelectorFilterToolbar());
|
||||
SettingsCache::instance().userInterface().getShowGameSelectorFilterToolbar());
|
||||
|
||||
connect(&SettingsCache::instance().cardsDisplay(), &CardsDisplaySettings::showGameSelectorFilterToolbarChanged,
|
||||
connect(&SettingsCache::instance().userInterface(), &InterfaceSettings::showGameSelectorFilterToolbarChanged,
|
||||
this, [this] {
|
||||
quickFilterToolBar->setVisible(
|
||||
SettingsCache::instance().cardsDisplay().getShowGameSelectorFilterToolbar());
|
||||
SettingsCache::instance().userInterface().getShowGameSelectorFilterToolbar());
|
||||
});
|
||||
} else {
|
||||
quickFilterToolBar = nullptr;
|
||||
|
|
@ -315,6 +320,21 @@ void GameSelector::customContextMenu(const QPoint &point)
|
|||
dlg.exec();
|
||||
});
|
||||
|
||||
QAction copyLink(tr("Copy Game Link"));
|
||||
connect(©Link, &QAction::triggered, this, [=, this]() {
|
||||
const ServerInfo_Game &gameInfo = gameListModel->getGame(index.data(Qt::UserRole).toInt());
|
||||
QUrl url;
|
||||
url.setScheme("cockatrice");
|
||||
url.setHost("joingame");
|
||||
QUrlQuery query;
|
||||
query.addQueryItem("hostname", client->serverName());
|
||||
query.addQueryItem("port", QString::number(client->serverPort()));
|
||||
query.addQueryItem("roomid", QString::number(gameInfo.room_id()));
|
||||
query.addQueryItem("gameid", QString::number(gameInfo.game_id()));
|
||||
url.setQuery(query);
|
||||
QGuiApplication::clipboard()->setText(url.toString(QUrl::FullyEncoded));
|
||||
});
|
||||
|
||||
QMenu menu;
|
||||
menu.addAction(&joinGame);
|
||||
|
||||
|
|
@ -332,6 +352,11 @@ void GameSelector::customContextMenu(const QPoint &point)
|
|||
|
||||
menu.addAction(&spectateGame);
|
||||
menu.addAction(&getGameInfo);
|
||||
|
||||
if (!client->serverName().isEmpty()) {
|
||||
menu.addAction(©Link);
|
||||
}
|
||||
|
||||
menu.exec(gameListView->mapToGlobal(point));
|
||||
}
|
||||
|
||||
|
|
@ -379,6 +404,24 @@ void GameSelector::joinGame(const bool asSpectator, const bool asJudge)
|
|||
disableButtons();
|
||||
}
|
||||
|
||||
bool GameSelector::joinGameById(int gameId)
|
||||
{
|
||||
auto *model = gameListView->model();
|
||||
|
||||
for (int row = 0; row < model->rowCount(); ++row) {
|
||||
QModelIndex idx = model->index(row, 0);
|
||||
const ServerInfo_Game &game = gameListModel->getGame(idx.data(Qt::UserRole).toInt());
|
||||
if (game.game_id() == gameId) {
|
||||
gameListView->setCurrentIndex(idx);
|
||||
joinGame();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
qWarning() << "Game" << gameId << "not found";
|
||||
return false;
|
||||
}
|
||||
|
||||
void GameSelector::disableButtons()
|
||||
{
|
||||
if (createButton) {
|
||||
|
|
|
|||
|
|
@ -202,6 +202,7 @@ public:
|
|||
* @param info The ServerInfo_Game object containing information about the game to update.
|
||||
*/
|
||||
void processGameInfo(const ServerInfo_Game &info);
|
||||
bool joinGameById(int gameId);
|
||||
};
|
||||
|
||||
#endif
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@
|
|||
#include <libcockatrice/protocol/pb/response_get_games_of_user.pb.h>
|
||||
#include <libcockatrice/protocol/pb/response_get_user_info.pb.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>
|
||||
|
||||
BanDialog::BanDialog(const ServerInfo_User &info, QWidget *parent) : QDialog(parent)
|
||||
|
|
@ -349,7 +349,7 @@ bool UserListItemDelegate::editorEvent(QEvent *event,
|
|||
|
||||
QSize UserListItemDelegate::sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const
|
||||
{
|
||||
if (!SettingsCache::instance().interface().getStyleUserList()) {
|
||||
if (!SettingsCache::instance().appearance().getStyleUserList()) {
|
||||
return QStyledItemDelegate::sizeHint(option, index);
|
||||
}
|
||||
return UserListPainter::sizeHint();
|
||||
|
|
@ -357,7 +357,7 @@ QSize UserListItemDelegate::sizeHint(const QStyleOptionViewItem &option, const Q
|
|||
|
||||
void UserListItemDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const
|
||||
{
|
||||
if (!SettingsCache::instance().interface().getStyleUserList()) {
|
||||
if (!SettingsCache::instance().appearance().getStyleUserList()) {
|
||||
QStyledItemDelegate::paint(painter, option, index);
|
||||
return;
|
||||
}
|
||||
|
|
@ -521,7 +521,7 @@ UserListWidget::UserListWidget(TabSupervisor *_tabSupervisor,
|
|||
|
||||
// Pin on item click
|
||||
connect(userTree, &QTreeWidget::itemClicked, this, [this](QTreeWidgetItem *item, int) {
|
||||
if (!SettingsCache::instance().interface().getStyleUserList()) {
|
||||
if (!SettingsCache::instance().appearance().getStyleUserList()) {
|
||||
return;
|
||||
}
|
||||
const QString name = static_cast<UserListTWI *>(item)->getUserInfo().name().c_str();
|
||||
|
|
@ -553,7 +553,7 @@ UserListWidget::UserListWidget(TabSupervisor *_tabSupervisor,
|
|||
connect(cardArtProvider, &UserCardArtProvider::cardArtUpdated, this,
|
||||
[this](const QString &) { userTree->viewport()->update(); });
|
||||
|
||||
connect(&SettingsCache::instance().interface(), &InterfaceSettings::styleUserListChanged, this,
|
||||
connect(&SettingsCache::instance().appearance(), &AppearanceSettings::styleUserListChanged, this,
|
||||
&UserListWidget::applyDisplayMode);
|
||||
applyDisplayMode();
|
||||
|
||||
|
|
@ -659,7 +659,7 @@ void UserListWidget::hideEvent(QHideEvent *e)
|
|||
|
||||
void UserListWidget::applyDisplayMode()
|
||||
{
|
||||
const bool styled = SettingsCache::instance().interface().getStyleUserList();
|
||||
const bool styled = SettingsCache::instance().appearance().getStyleUserList();
|
||||
|
||||
if (styled) {
|
||||
userTree->header()->setSectionResizeMode(0, QHeaderView::Stretch);
|
||||
|
|
@ -718,7 +718,7 @@ bool UserListWidget::eventFilter(QObject *obj, QEvent *event)
|
|||
{
|
||||
if (obj == userTree->viewport()) {
|
||||
if (event->type() == QEvent::MouseMove) {
|
||||
if (!SettingsCache::instance().interface().getStyleUserList()) {
|
||||
if (!SettingsCache::instance().appearance().getStyleUserList()) {
|
||||
return QGroupBox::eventFilter(obj, event);
|
||||
}
|
||||
auto *me = static_cast<QMouseEvent *>(event);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,196 @@
|
|||
#include "abstract_settings_page.h"
|
||||
|
||||
#include "settings_search_model.h"
|
||||
|
||||
#include <QCheckBox>
|
||||
#include <QGridLayout>
|
||||
#include <QGroupBox>
|
||||
#include <QLabel>
|
||||
#include <QLayout>
|
||||
#include <QLineEdit>
|
||||
#include <QPair>
|
||||
#include <QSpinBox>
|
||||
|
||||
/**
|
||||
* @brief Recursively collects all widgets within a layout
|
||||
* @param layout The layout to walk
|
||||
* @param widgets Output list of (widget, containing layout) pairs
|
||||
*/
|
||||
static void collectWidgets(QLayout *layout, QList<QPair<QWidget *, QLayout *>> &widgets)
|
||||
{
|
||||
for (int i = 0; i < layout->count(); ++i) {
|
||||
QLayoutItem *item = layout->itemAt(i);
|
||||
if (!item) {
|
||||
continue;
|
||||
}
|
||||
if (QWidget *widget = item->widget()) {
|
||||
widgets.append({widget, layout});
|
||||
} else if (QLayout *subLayout = item->layout()) {
|
||||
collectWidgets(subLayout, widgets);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Rejects QLabels that are not setting names
|
||||
*
|
||||
* HTML link labels, path values, and excessively long labels are filtered out.
|
||||
*/
|
||||
static bool isValidSettingLabel(const QLabel *label)
|
||||
{
|
||||
const QString &text = label->text();
|
||||
if (Qt::mightBeRichText(text)) {
|
||||
return false;
|
||||
}
|
||||
if (text.contains(QLatin1Char('/')) || text.contains(QLatin1Char('\\'))) {
|
||||
return false;
|
||||
}
|
||||
if (text.size() > 60) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Finds the control associated with a setting label
|
||||
*
|
||||
* Uses the explicit buddy if set, otherwise the widget in the cell (or slot)
|
||||
* immediately following the label within the same layout. Returns nullptr when
|
||||
* no obvious control is found.
|
||||
*/
|
||||
static QWidget *controlForLabel(QLabel *label, QLayout *containingLayout)
|
||||
{
|
||||
if (QWidget *buddy = label->buddy()) {
|
||||
return buddy;
|
||||
}
|
||||
|
||||
if (auto *grid = qobject_cast<QGridLayout *>(containingLayout)) {
|
||||
int index = grid->indexOf(label);
|
||||
if (index != -1) {
|
||||
int row = 0;
|
||||
int column = 0;
|
||||
int rowSpan = 1;
|
||||
int columnSpan = 1;
|
||||
grid->getItemPosition(index, &row, &column, &rowSpan, &columnSpan);
|
||||
if (QLayoutItem *next = grid->itemAtPosition(row, column + columnSpan)) {
|
||||
if (QWidget *nextWidget = next->widget()) {
|
||||
return nextWidget;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
int index = containingLayout->indexOf(label);
|
||||
if (index != -1) {
|
||||
for (int i = index + 1; i < containingLayout->count(); ++i) {
|
||||
if (QLayoutItem *next = containingLayout->itemAt(i)) {
|
||||
if (QWidget *nextWidget = next->widget()) {
|
||||
return nextWidget;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Builds the extended search text for an entry
|
||||
*
|
||||
* Combines the group title, label, and any extra searchable text derived from
|
||||
* the associated control (placeholder, prefix/suffix, tooltip). Combo values
|
||||
* and numeric tooltips are excluded since they change at runtime and would
|
||||
* make the search index stale.
|
||||
*/
|
||||
static QString buildFullSearchText(const QString &groupTitle, const QString &cleanLabel, QWidget *control)
|
||||
{
|
||||
QStringList parts = {groupTitle, cleanLabel};
|
||||
if (control) {
|
||||
if (auto *lineEdit = qobject_cast<QLineEdit *>(control)) {
|
||||
parts.append(lineEdit->placeholderText());
|
||||
} else if (auto *spinBox = qobject_cast<QSpinBox *>(control)) {
|
||||
parts.append(spinBox->prefix());
|
||||
parts.append(spinBox->suffix());
|
||||
}
|
||||
if (!control->toolTip().isEmpty()) {
|
||||
bool isNumeric = false;
|
||||
control->toolTip().toInt(&isNumeric);
|
||||
if (!isNumeric) {
|
||||
parts.append(control->toolTip());
|
||||
}
|
||||
}
|
||||
}
|
||||
parts.removeAll(QString());
|
||||
return parts.join(QLatin1Char(' '));
|
||||
}
|
||||
|
||||
QList<SettingsSearchEntry> AbstractSettingsPage::getSearchEntries()
|
||||
{
|
||||
return autoDetectSearchEntries(this, -1);
|
||||
}
|
||||
|
||||
QList<SettingsSearchEntry> AbstractSettingsPage::autoDetectSearchEntries(QWidget *page, int pageIndex)
|
||||
{
|
||||
QList<SettingsSearchEntry> entries;
|
||||
|
||||
const auto children = page->children();
|
||||
for (QObject *child : children) {
|
||||
auto *groupBox = qobject_cast<QGroupBox *>(child);
|
||||
if (!groupBox) {
|
||||
continue;
|
||||
}
|
||||
|
||||
QString groupTitle = groupBox->title();
|
||||
if (groupTitle.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
QLayout *groupLayout = groupBox->layout();
|
||||
if (!groupLayout) {
|
||||
continue;
|
||||
}
|
||||
|
||||
QList<QPair<QWidget *, QLayout *>> widgets;
|
||||
collectWidgets(groupLayout, widgets);
|
||||
|
||||
for (const auto &pair : widgets) {
|
||||
QWidget *widget = pair.first;
|
||||
QString label;
|
||||
|
||||
auto *checkBox = qobject_cast<QCheckBox *>(widget);
|
||||
if (checkBox) {
|
||||
label = checkBox->text();
|
||||
} else {
|
||||
auto *labelWidget = qobject_cast<QLabel *>(widget);
|
||||
if (!labelWidget || labelWidget->text().isEmpty() || !isValidSettingLabel(labelWidget)) {
|
||||
continue;
|
||||
}
|
||||
label = labelWidget->text();
|
||||
}
|
||||
|
||||
if (label.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Strip accelerator markers (&) for search
|
||||
QString cleanLabel = label;
|
||||
cleanLabel.remove(QLatin1Char('&'));
|
||||
|
||||
QWidget *control = widget;
|
||||
if (auto *labelWidget = qobject_cast<QLabel *>(widget)) {
|
||||
control = controlForLabel(labelWidget, pair.second);
|
||||
if (!control) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
entries.append(SettingsSearchEntry{.pageIndex = pageIndex,
|
||||
.groupTitle = groupTitle,
|
||||
.widgetLabel = cleanLabel,
|
||||
.fullSearchText = buildFullSearchText(groupTitle, cleanLabel, control),
|
||||
.widget = control});
|
||||
}
|
||||
}
|
||||
|
||||
return entries;
|
||||
}
|
||||
|
|
@ -1,16 +1,24 @@
|
|||
#ifndef COCKATRICE_ABSTRACT_SETTINGS_PAGE_H
|
||||
#define COCKATRICE_ABSTRACT_SETTINGS_PAGE_H
|
||||
|
||||
#include <QList>
|
||||
#include <QWidget>
|
||||
|
||||
#define WIKI_CUSTOM_PIC_URL "https://github.com/Cockatrice/Cockatrice/wiki/Custom-Picture-Download-URLs"
|
||||
#define WIKI_CUSTOM_SHORTCUTS "https://github.com/Cockatrice/Cockatrice/wiki/Custom-Keyboard-Shortcuts"
|
||||
#define WIKI_TRANSLATION_FAQ "https://github.com/Cockatrice/Cockatrice/wiki/Translation-FAQ"
|
||||
|
||||
struct SettingsSearchEntry;
|
||||
|
||||
class AbstractSettingsPage : public QWidget
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
virtual void retranslateUi() = 0;
|
||||
virtual QList<SettingsSearchEntry> getSearchEntries();
|
||||
|
||||
protected:
|
||||
static QList<SettingsSearchEntry> autoDetectSearchEntries(QWidget *page, int pageIndex);
|
||||
};
|
||||
|
||||
#endif // COCKATRICE_ABSTRACT_SETTINGS_PAGE_H
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@
|
|||
#include <QMessageBox>
|
||||
#include <QStyleFactory>
|
||||
#include <QTimer>
|
||||
#include <libcockatrice/settings/appearance_settings.h>
|
||||
#include <libcockatrice/settings/cards_display_settings.h>
|
||||
#include <libcockatrice/settings/interface_settings.h>
|
||||
#include <libcockatrice/settings/paths_settings.h>
|
||||
|
|
@ -104,7 +105,7 @@ AppearanceSettingsPage::AppearanceSettingsPage()
|
|||
homeTabBackgroundSourceBox.addItem(QObject::tr(entry.trKey), QVariant::fromValue(entry.type));
|
||||
}
|
||||
|
||||
QString homeTabBackgroundSource = SettingsCache::instance().personal().getHomeTabBackgroundSource();
|
||||
QString homeTabBackgroundSource = settings.appearance().getHomeTabBackgroundSource();
|
||||
int homeTabBackgroundSourceId =
|
||||
homeTabBackgroundSourceBox.findData(BackgroundSources::fromId(homeTabBackgroundSource));
|
||||
if (homeTabBackgroundSourceId != -1) {
|
||||
|
|
@ -113,20 +114,19 @@ AppearanceSettingsPage::AppearanceSettingsPage()
|
|||
|
||||
connect(&homeTabBackgroundSourceBox, QOverload<int>::of(&QComboBox::currentIndexChanged), this, [this]() {
|
||||
auto type = homeTabBackgroundSourceBox.currentData().value<BackgroundSources::Type>();
|
||||
SettingsCache::instance().personal().setHomeTabBackgroundSource(BackgroundSources::toId(type));
|
||||
SettingsCache::instance().appearance().setHomeTabBackgroundSource(BackgroundSources::toId(type));
|
||||
updateHomeTabSettingsVisibility();
|
||||
});
|
||||
|
||||
homeTabBackgroundShuffleFrequencySpinBox.setRange(0, 3600);
|
||||
homeTabBackgroundShuffleFrequencySpinBox.setSuffix(tr(" seconds"));
|
||||
homeTabBackgroundShuffleFrequencySpinBox.setValue(
|
||||
SettingsCache::instance().personal().getHomeTabBackgroundShuffleFrequency());
|
||||
connect(&homeTabBackgroundShuffleFrequencySpinBox, qOverload<int>(&QSpinBox::valueChanged), &settings.personal(),
|
||||
&PersonalSettings::setHomeTabBackgroundShuffleFrequency);
|
||||
homeTabBackgroundShuffleFrequencySpinBox.setValue(settings.appearance().getHomeTabBackgroundShuffleFrequency());
|
||||
connect(&homeTabBackgroundShuffleFrequencySpinBox, qOverload<int>(&QSpinBox::valueChanged), &settings.appearance(),
|
||||
&AppearanceSettings::setHomeTabBackgroundShuffleFrequency);
|
||||
|
||||
homeTabDisplayCardNameCheckBox.setChecked(settings.personal().getHomeTabDisplayCardName());
|
||||
connect(&homeTabDisplayCardNameCheckBox, &QCheckBox::QT_STATE_CHANGED, &settings.personal(),
|
||||
&PersonalSettings::setHomeTabDisplayCardName);
|
||||
homeTabDisplayCardNameCheckBox.setChecked(settings.appearance().getHomeTabDisplayCardName());
|
||||
connect(&homeTabDisplayCardNameCheckBox, &QCheckBox::QT_STATE_CHANGED, &settings.appearance(),
|
||||
&AppearanceSettings::setHomeTabDisplayCardName);
|
||||
|
||||
updateHomeTabSettingsVisibility();
|
||||
|
||||
|
|
@ -140,9 +140,9 @@ AppearanceSettingsPage::AppearanceSettingsPage()
|
|||
homeTabGroupBox = new QGroupBox;
|
||||
homeTabGroupBox->setLayout(homeTabGrid);
|
||||
|
||||
styleUserListCheckBox.setChecked(settings.interface().getStyleUserList());
|
||||
connect(&styleUserListCheckBox, &QCheckBox::QT_STATE_CHANGED, &settings.interface(),
|
||||
&InterfaceSettings::setStyleUserList);
|
||||
styleUserListCheckBox.setChecked(settings.appearance().getStyleUserList());
|
||||
connect(&styleUserListCheckBox, &QCheckBox::QT_STATE_CHANGED, &settings.appearance(),
|
||||
&AppearanceSettings::setStyleUserList);
|
||||
|
||||
auto stylingTabGrid = new QGridLayout;
|
||||
stylingTabGrid->addWidget(&styleUserListCheckBox, 0, 0, 1, 2);
|
||||
|
|
@ -151,12 +151,12 @@ AppearanceSettingsPage::AppearanceSettingsPage()
|
|||
stylingGroupBox->setLayout(stylingTabGrid);
|
||||
|
||||
// Menu settings
|
||||
showShortcutsCheckBox.setChecked(settings.cardsDisplay().getShowShortcuts());
|
||||
showShortcutsCheckBox.setChecked(settings.userInterface().getShowShortcuts());
|
||||
connect(&showShortcutsCheckBox, &QCheckBox::QT_STATE_CHANGED, this, &AppearanceSettingsPage::showShortcutsChanged);
|
||||
|
||||
showGameSelectorFilterToolbarCheckBox.setChecked(settings.cardsDisplay().getShowGameSelectorFilterToolbar());
|
||||
connect(&showGameSelectorFilterToolbarCheckBox, &QCheckBox::QT_STATE_CHANGED, &settings.cardsDisplay(),
|
||||
&CardsDisplaySettings::setShowGameSelectorFilterToolbar);
|
||||
showGameSelectorFilterToolbarCheckBox.setChecked(settings.userInterface().getShowGameSelectorFilterToolbar());
|
||||
connect(&showGameSelectorFilterToolbarCheckBox, &QCheckBox::QT_STATE_CHANGED, &settings.userInterface(),
|
||||
&InterfaceSettings::setShowGameSelectorFilterToolbar);
|
||||
|
||||
auto *menuGrid = new QGridLayout;
|
||||
menuGrid->addWidget(&showShortcutsCheckBox, 0, 0);
|
||||
|
|
@ -199,9 +199,9 @@ AppearanceSettingsPage::AppearanceSettingsPage()
|
|||
connect(&roundCardCornersCheckBox, &QAbstractButton::toggled, &settings.cardsDisplay(),
|
||||
&CardsDisplaySettings::setRoundCardCorners);
|
||||
|
||||
connect(&maxFontSizeForCardsEdit, qOverload<int>(&QSpinBox::valueChanged), &settings.personal(),
|
||||
&PersonalSettings::setMaxFontSize);
|
||||
maxFontSizeForCardsEdit.setValue(settings.personal().getMaxFontSize());
|
||||
connect(&maxFontSizeForCardsEdit, qOverload<int>(&QSpinBox::valueChanged), &settings.appearance(),
|
||||
&AppearanceSettings::setMaxFontSize);
|
||||
maxFontSizeForCardsEdit.setValue(settings.appearance().getMaxFontSize());
|
||||
maxFontSizeForCardsLabel.setBuddy(&maxFontSizeForCardsEdit);
|
||||
maxFontSizeForCardsEdit.setMinimum(9);
|
||||
maxFontSizeForCardsEdit.setMaximum(100);
|
||||
|
|
@ -224,12 +224,12 @@ AppearanceSettingsPage::AppearanceSettingsPage()
|
|||
&CardsDisplaySettings::setStackCardOverlapPercent);
|
||||
|
||||
cardViewInitialRowsMaxBox.setRange(1, 999);
|
||||
cardViewInitialRowsMaxBox.setValue(SettingsCache::instance().interface().getCardViewInitialRowsMax());
|
||||
cardViewInitialRowsMaxBox.setValue(SettingsCache::instance().userInterface().getCardViewInitialRowsMax());
|
||||
connect(&cardViewInitialRowsMaxBox, qOverload<int>(&QSpinBox::valueChanged), this,
|
||||
&AppearanceSettingsPage::cardViewInitialRowsMaxChanged);
|
||||
|
||||
cardViewExpandedRowsMaxBox.setRange(1, 999);
|
||||
cardViewExpandedRowsMaxBox.setValue(SettingsCache::instance().interface().getCardViewExpandedRowsMax());
|
||||
cardViewExpandedRowsMaxBox.setValue(SettingsCache::instance().userInterface().getCardViewExpandedRowsMax());
|
||||
connect(&cardViewExpandedRowsMaxBox, qOverload<int>(&QSpinBox::valueChanged), this,
|
||||
&AppearanceSettingsPage::cardViewExpandedRowsMaxChanged);
|
||||
|
||||
|
|
@ -291,12 +291,12 @@ AppearanceSettingsPage::AppearanceSettingsPage()
|
|||
cardCountersGroupBox->setLayout(cardCountersLayout);
|
||||
|
||||
// Hand layout
|
||||
horizontalHandCheckBox.setChecked(settings.interface().getHorizontalHand());
|
||||
connect(&horizontalHandCheckBox, &QCheckBox::QT_STATE_CHANGED, &settings.interface(),
|
||||
horizontalHandCheckBox.setChecked(settings.userInterface().getHorizontalHand());
|
||||
connect(&horizontalHandCheckBox, &QCheckBox::QT_STATE_CHANGED, &settings.userInterface(),
|
||||
&InterfaceSettings::setHorizontalHand);
|
||||
|
||||
leftJustifiedHandCheckBox.setChecked(settings.interface().getLeftJustified());
|
||||
connect(&leftJustifiedHandCheckBox, &QCheckBox::QT_STATE_CHANGED, &settings.interface(),
|
||||
leftJustifiedHandCheckBox.setChecked(settings.userInterface().getLeftJustified());
|
||||
connect(&leftJustifiedHandCheckBox, &QCheckBox::QT_STATE_CHANGED, &settings.userInterface(),
|
||||
&InterfaceSettings::setLeftJustified);
|
||||
|
||||
auto *handGrid = new QGridLayout;
|
||||
|
|
@ -307,13 +307,13 @@ AppearanceSettingsPage::AppearanceSettingsPage()
|
|||
handGroupBox->setLayout(handGrid);
|
||||
|
||||
// table grid layout
|
||||
invertVerticalCoordinateCheckBox.setChecked(settings.interface().getInvertVerticalCoordinate());
|
||||
connect(&invertVerticalCoordinateCheckBox, &QCheckBox::QT_STATE_CHANGED, &settings.interface(),
|
||||
invertVerticalCoordinateCheckBox.setChecked(settings.userInterface().getInvertVerticalCoordinate());
|
||||
connect(&invertVerticalCoordinateCheckBox, &QCheckBox::QT_STATE_CHANGED, &settings.userInterface(),
|
||||
&InterfaceSettings::setInvertVerticalCoordinate);
|
||||
|
||||
minPlayersForMultiColumnLayoutEdit.setMinimum(2);
|
||||
minPlayersForMultiColumnLayoutEdit.setValue(settings.interface().getMinPlayersForMultiColumnLayout());
|
||||
connect(&minPlayersForMultiColumnLayoutEdit, qOverload<int>(&QSpinBox::valueChanged), &settings.interface(),
|
||||
minPlayersForMultiColumnLayoutEdit.setValue(settings.userInterface().getMinPlayersForMultiColumnLayout());
|
||||
connect(&minPlayersForMultiColumnLayoutEdit, qOverload<int>(&QSpinBox::valueChanged), &settings.userInterface(),
|
||||
&InterfaceSettings::setMinPlayersForMultiColumnLayout);
|
||||
minPlayersForMultiColumnLayoutLabel.setBuddy(&minPlayersForMultiColumnLayoutEdit);
|
||||
|
||||
|
|
@ -375,7 +375,7 @@ void AppearanceSettingsPage::editPalette()
|
|||
|
||||
void AppearanceSettingsPage::updateHomeTabSettingsVisibility()
|
||||
{
|
||||
bool visible = SettingsCache::instance().personal().getHomeTabBackgroundSource() !=
|
||||
bool visible = SettingsCache::instance().appearance().getHomeTabBackgroundSource() !=
|
||||
BackgroundSources::toId(BackgroundSources::Theme);
|
||||
|
||||
homeTabBackgroundShuffleFrequencyLabel.setVisible(visible);
|
||||
|
|
@ -385,7 +385,7 @@ void AppearanceSettingsPage::updateHomeTabSettingsVisibility()
|
|||
|
||||
void AppearanceSettingsPage::showShortcutsChanged(QT_STATE_CHANGED_T value)
|
||||
{
|
||||
SettingsCache::instance().cardsDisplay().setShowShortcuts(value);
|
||||
SettingsCache::instance().userInterface().setShowShortcuts(value);
|
||||
qApp->setAttribute(Qt::AA_DontShowShortcutsInContextMenus, value == 0); // 0 = unchecked
|
||||
}
|
||||
|
||||
|
|
@ -412,7 +412,7 @@ void AppearanceSettingsPage::overrideAllCardArtWithPersonalPreferenceToggled(QT_
|
|||
*/
|
||||
void AppearanceSettingsPage::cardViewInitialRowsMaxChanged(int value)
|
||||
{
|
||||
SettingsCache::instance().interface().setCardViewInitialRowsMax(value);
|
||||
SettingsCache::instance().userInterface().setCardViewInitialRowsMax(value);
|
||||
if (cardViewExpandedRowsMaxBox.value() < value) {
|
||||
cardViewExpandedRowsMaxBox.setValue(value);
|
||||
}
|
||||
|
|
@ -425,7 +425,7 @@ void AppearanceSettingsPage::cardViewInitialRowsMaxChanged(int value)
|
|||
*/
|
||||
void AppearanceSettingsPage::cardViewExpandedRowsMaxChanged(int value)
|
||||
{
|
||||
SettingsCache::instance().interface().setCardViewExpandedRowsMax(value);
|
||||
SettingsCache::instance().userInterface().setCardViewExpandedRowsMax(value);
|
||||
if (cardViewInitialRowsMaxBox.value() > value) {
|
||||
cardViewInitialRowsMaxBox.setValue(value);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -40,10 +40,10 @@ private:
|
|||
QSpinBox homeTabBackgroundShuffleFrequencySpinBox;
|
||||
QCheckBox homeTabDisplayCardNameCheckBox;
|
||||
QCheckBox styleUserListCheckBox;
|
||||
QLabel minPlayersForMultiColumnLayoutLabel;
|
||||
QLabel maxFontSizeForCardsLabel;
|
||||
QCheckBox showShortcutsCheckBox;
|
||||
QCheckBox showGameSelectorFilterToolbarCheckBox;
|
||||
QLabel minPlayersForMultiColumnLayoutLabel;
|
||||
QLabel maxFontSizeForCardsLabel;
|
||||
QCheckBox overrideAllCardArtWithPersonalPreferenceCheckBox;
|
||||
QCheckBox bumpSetsWithCardsInDeckToTopCheckBox;
|
||||
QCheckBox displayCardNamesCheckBox;
|
||||
|
|
|
|||
|
|
@ -17,9 +17,9 @@
|
|||
|
||||
DeckEditorSettingsPage::DeckEditorSettingsPage()
|
||||
{
|
||||
picDownloadCheckBox.setChecked(SettingsCache::instance().personal().getPicDownload());
|
||||
connect(&picDownloadCheckBox, &QCheckBox::QT_STATE_CHANGED, &SettingsCache::instance().personal(),
|
||||
&PersonalSettings::setPicDownload);
|
||||
picDownloadCheckBox.setChecked(SettingsCache::instance().downloads().getPicDownload());
|
||||
connect(&picDownloadCheckBox, &QCheckBox::QT_STATE_CHANGED, &SettingsCache::instance().downloads(),
|
||||
&DownloadSettings::setPicDownload);
|
||||
|
||||
urlLinkLabel.setTextInteractionFlags(Qt::LinksAccessibleByMouse);
|
||||
urlLinkLabel.setOpenExternalLinks(true);
|
||||
|
|
@ -29,7 +29,7 @@ DeckEditorSettingsPage::DeckEditorSettingsPage()
|
|||
auto *lpGeneralGrid = new QGridLayout;
|
||||
auto *lpSpoilerGrid = new QGridLayout;
|
||||
|
||||
mcDownloadSpoilersCheckBox.setChecked(SettingsCache::instance().personal().getDownloadSpoilersStatus());
|
||||
mcDownloadSpoilersCheckBox.setChecked(SettingsCache::instance().downloads().getDownloadSpoilersStatus());
|
||||
|
||||
mpSpoilerSavePathLineEdit = new QLineEdit(SettingsCache::instance().getSpoilerCardDatabasePath());
|
||||
mpSpoilerSavePathLineEdit->setReadOnly(true);
|
||||
|
|
@ -91,8 +91,8 @@ DeckEditorSettingsPage::DeckEditorSettingsPage()
|
|||
lpSpoilerGrid->addWidget(&infoOnSpoilersLabel, 3, 0, 1, 3, Qt::AlignTop);
|
||||
|
||||
// On a change to the checkbox, hide/un-hide the other fields
|
||||
connect(&mcDownloadSpoilersCheckBox, &QCheckBox::toggled, &SettingsCache::instance().personal(),
|
||||
&PersonalSettings::setDownloadSpoilerStatus);
|
||||
connect(&mcDownloadSpoilersCheckBox, &QCheckBox::toggled, &SettingsCache::instance().downloads(),
|
||||
&DownloadSettings::setDownloadSpoilerStatus);
|
||||
connect(&mcDownloadSpoilersCheckBox, &QCheckBox::toggled, this, &DeckEditorSettingsPage::setSpoilersEnabled);
|
||||
|
||||
mpGeneralGroupBox = new QGroupBox;
|
||||
|
|
|
|||
|
|
@ -90,6 +90,7 @@ MessagesSettingsPage::MessagesSettingsPage()
|
|||
highlightNotice->addWidget(&hexHighlightLabel, 1, 2);
|
||||
highlightNotice->addWidget(customAlertString, 0, 0);
|
||||
highlightNotice->addWidget(&customAlertStringLabel, 1, 0);
|
||||
customAlertStringLabel.setBuddy(customAlertString);
|
||||
highlightGroupBox = new QGroupBox;
|
||||
highlightGroupBox->setLayout(highlightNotice);
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,125 @@
|
|||
/**
|
||||
* @file settings_search_delegate.cpp
|
||||
* @brief Implementation of the custom settings search result delegate
|
||||
* @ingroup Dialogs
|
||||
*/
|
||||
#include "settings_search_delegate.h"
|
||||
|
||||
#include "settings_search_model.h"
|
||||
|
||||
#include <QPainter>
|
||||
|
||||
SettingsSearchDelegate::SettingsSearchDelegate(QObject *parent) : QStyledItemDelegate(parent)
|
||||
{
|
||||
}
|
||||
|
||||
void SettingsSearchDelegate::setPageNames(const QStringList &names)
|
||||
{
|
||||
pageNames = names;
|
||||
}
|
||||
|
||||
void SettingsSearchDelegate::setPageIcons(const QStringList &iconResources)
|
||||
{
|
||||
pageIcons.clear();
|
||||
for (const QString &resource : iconResources) {
|
||||
pageIcons.append(QPixmap(resource));
|
||||
}
|
||||
}
|
||||
|
||||
void SettingsSearchDelegate::paint(QPainter *painter,
|
||||
const QStyleOptionViewItem &option,
|
||||
const QModelIndex &index) const
|
||||
{
|
||||
painter->save();
|
||||
|
||||
SettingsSearchEntry entry = index.data(SettingsSearchModel::EntryRole).value<SettingsSearchEntry>();
|
||||
|
||||
bool isSelected = option.state & QStyle::State_Selected;
|
||||
bool isHovered = option.state & QStyle::State_MouseOver;
|
||||
|
||||
// Background
|
||||
QColor bgColor = isSelected ? option.palette.color(QPalette::Highlight)
|
||||
: isHovered ? option.palette.color(QPalette::Midlight)
|
||||
: option.palette.color(QPalette::Base);
|
||||
painter->fillRect(option.rect, bgColor);
|
||||
|
||||
if (isSelected) {
|
||||
// Accent bar on the left to make the selection unmistakable
|
||||
painter->fillRect(QRect(option.rect.left(), option.rect.top(), 4, option.rect.height()),
|
||||
option.palette.color(QPalette::Highlight).darker(150));
|
||||
}
|
||||
|
||||
int leftMargin = 12;
|
||||
int topMargin = 8;
|
||||
int rightMargin = 12;
|
||||
int bottomMargin = 4;
|
||||
|
||||
QRect contentRect = option.rect.adjusted(leftMargin, topMargin, -rightMargin, -bottomMargin);
|
||||
int yPos = contentRect.top();
|
||||
|
||||
// Icon of the related settings page
|
||||
const int iconSize = 24;
|
||||
QPixmap pageIcon =
|
||||
(entry.pageIndex >= 0 && entry.pageIndex < pageIcons.size()) ? pageIcons.at(entry.pageIndex) : QPixmap();
|
||||
int iconOffset = pageIcon.isNull() ? 0 : iconSize + 8;
|
||||
if (!pageIcon.isNull()) {
|
||||
QRect iconRect(contentRect.left(), contentRect.top() + (contentRect.height() - iconSize) / 2, iconSize,
|
||||
iconSize);
|
||||
painter->drawPixmap(iconRect, pageIcon);
|
||||
}
|
||||
|
||||
QRect textRect = contentRect.adjusted(iconOffset, 0, 0, 0);
|
||||
|
||||
// Breadcrumb: "Page > Group"
|
||||
QFont breadcrumbFont = option.font;
|
||||
breadcrumbFont.setPointSize(breadcrumbFont.pointSize() - 1);
|
||||
breadcrumbFont.setBold(true);
|
||||
|
||||
QColor breadcrumbColor =
|
||||
isSelected ? option.palette.color(QPalette::HighlightedText) : option.palette.color(QPalette::Text);
|
||||
if (!isSelected) {
|
||||
breadcrumbColor.setAlpha(180);
|
||||
}
|
||||
|
||||
QString pageName;
|
||||
if (entry.pageIndex >= 0 && entry.pageIndex < pageNames.size()) {
|
||||
pageName = pageNames[entry.pageIndex];
|
||||
} else {
|
||||
pageName = QString::number(entry.pageIndex);
|
||||
}
|
||||
|
||||
QString breadcrumbText = QStringLiteral("%1 > %2").arg(pageName, entry.groupTitle);
|
||||
painter->setFont(breadcrumbFont);
|
||||
painter->setPen(breadcrumbColor);
|
||||
painter->drawText(QRect(textRect.left(), yPos, textRect.width(), 20), Qt::AlignLeft | Qt::AlignVCenter,
|
||||
breadcrumbText);
|
||||
yPos += 20;
|
||||
|
||||
// Setting label
|
||||
QFont labelFont = option.font;
|
||||
labelFont.setPointSize(labelFont.pointSize() + 1);
|
||||
labelFont.setBold(isSelected);
|
||||
|
||||
QColor labelColor =
|
||||
isSelected ? option.palette.color(QPalette::HighlightedText) : option.palette.color(QPalette::Text);
|
||||
|
||||
painter->setFont(labelFont);
|
||||
painter->setPen(labelColor);
|
||||
painter->drawText(QRect(textRect.left(), yPos, textRect.width(), 24), Qt::AlignLeft | Qt::AlignVCenter,
|
||||
entry.widgetLabel);
|
||||
yPos += 24;
|
||||
|
||||
// Bottom separator
|
||||
QPen separatorPen(option.palette.color(QPalette::Mid), 1);
|
||||
painter->setPen(separatorPen);
|
||||
painter->drawLine(option.rect.left() + leftMargin, option.rect.bottom(), option.rect.right() - rightMargin,
|
||||
option.rect.bottom());
|
||||
|
||||
painter->restore();
|
||||
}
|
||||
|
||||
QSize SettingsSearchDelegate::sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const
|
||||
{
|
||||
Q_UNUSED(index);
|
||||
return QSize(option.rect.width(), 56);
|
||||
}
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
/**
|
||||
* @file settings_search_delegate.h
|
||||
* @brief Custom delegate for rendering settings search results
|
||||
* @ingroup Dialogs
|
||||
*/
|
||||
#ifndef COCKATRICE_SETTINGS_SEARCH_DELEGATE_H
|
||||
#define COCKATRICE_SETTINGS_SEARCH_DELEGATE_H
|
||||
|
||||
#include <QPixmap>
|
||||
#include <QStyledItemDelegate>
|
||||
|
||||
/**
|
||||
* @brief Custom paint delegate for settings search result items
|
||||
*
|
||||
* Renders each search result with a breadcrumb line ("Page > Group"),
|
||||
* the setting label, and a subtle separator. Supports selected/hovered states.
|
||||
*/
|
||||
class SettingsSearchDelegate : public QStyledItemDelegate
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit SettingsSearchDelegate(QObject *parent = nullptr);
|
||||
|
||||
void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const override;
|
||||
QSize sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const override;
|
||||
|
||||
/** @brief Sets the translated page names for breadcrumb display */
|
||||
void setPageNames(const QStringList &names);
|
||||
|
||||
/** @brief Sets the icons shown in front of results, indexed by page position */
|
||||
void setPageIcons(const QStringList &iconResources);
|
||||
|
||||
private:
|
||||
QStringList pageNames; ///< Translated page names indexed by page position
|
||||
QList<QPixmap> pageIcons; ///< Icons of the related settings pages
|
||||
};
|
||||
|
||||
#endif // COCKATRICE_SETTINGS_SEARCH_DELEGATE_H
|
||||
|
|
@ -0,0 +1,145 @@
|
|||
/**
|
||||
* @file settings_search_model.cpp
|
||||
* @brief Implementation of the settings search list model
|
||||
* @ingroup Dialogs
|
||||
*/
|
||||
#include "settings_search_model.h"
|
||||
|
||||
#include <QPair>
|
||||
#include <algorithm>
|
||||
|
||||
SettingsSearchModel::SettingsSearchModel(QObject *parent) : QAbstractListModel(parent)
|
||||
{
|
||||
}
|
||||
|
||||
void SettingsSearchModel::setSourceEntries(const QList<SettingsSearchEntry> &entries)
|
||||
{
|
||||
beginResetModel();
|
||||
sourceEntries = entries;
|
||||
endResetModel();
|
||||
rebuildFilter();
|
||||
}
|
||||
|
||||
void SettingsSearchModel::setFilterString(const QString &text)
|
||||
{
|
||||
filterActive = !text.trimmed().isEmpty();
|
||||
if (filterActive) {
|
||||
filterQuery = text.trimmed();
|
||||
filterRegex =
|
||||
QRegularExpression(QRegularExpression::escape(filterQuery), QRegularExpression::CaseInsensitiveOption);
|
||||
}
|
||||
rebuildFilter();
|
||||
}
|
||||
|
||||
bool SettingsSearchModel::isFilterActive() const
|
||||
{
|
||||
return filterActive;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Calculates a relevance score for a single entry against the query
|
||||
*
|
||||
* Scoring priorities (highest to lowest):
|
||||
* 1. Label starts with query -> 100
|
||||
* 2. Label contains query -> 80
|
||||
* 3. Group title starts with query -> 60
|
||||
* 4. Group title contains query -> 40
|
||||
* 5. Full text regex match -> 20
|
||||
* 6. No match -> 0 (excluded from results)
|
||||
*/
|
||||
static int relevanceScore(const SettingsSearchEntry &entry, const QString &query, const QRegularExpression ®ex)
|
||||
{
|
||||
QString lowerQuery = query.toLower();
|
||||
|
||||
// Label matches are most relevant
|
||||
QString label = entry.widgetLabel.toLower();
|
||||
if (label.startsWith(lowerQuery)) {
|
||||
return 100;
|
||||
}
|
||||
if (label.contains(lowerQuery)) {
|
||||
return 80;
|
||||
}
|
||||
|
||||
// Group title matches are next
|
||||
QString group = entry.groupTitle.toLower();
|
||||
if (group.startsWith(lowerQuery)) {
|
||||
return 60;
|
||||
}
|
||||
if (group.contains(lowerQuery)) {
|
||||
return 40;
|
||||
}
|
||||
|
||||
// Full text match is least relevant
|
||||
if (entry.fullSearchText.contains(regex)) {
|
||||
return 20;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
void SettingsSearchModel::rebuildFilter()
|
||||
{
|
||||
beginResetModel();
|
||||
filteredIndices.clear();
|
||||
|
||||
if (!filterActive) {
|
||||
for (int i = 0; i < sourceEntries.size(); ++i) {
|
||||
filteredIndices.append(i);
|
||||
}
|
||||
} else {
|
||||
QList<QPair<int, int>> scored; // <score, index>
|
||||
for (int i = 0; i < sourceEntries.size(); ++i) {
|
||||
const SettingsSearchEntry &entry = sourceEntries[i];
|
||||
// Skip conditional settings that are currently disabled or hidden
|
||||
if (entry.widget && (!entry.widget->isEnabled() || entry.widget->isHidden())) {
|
||||
continue;
|
||||
}
|
||||
int score = relevanceScore(entry, filterQuery, filterRegex);
|
||||
if (score > 0) {
|
||||
scored.append({-score, i}); // negative for descending sort
|
||||
}
|
||||
}
|
||||
std::sort(scored.begin(), scored.end());
|
||||
for (const auto &pair : scored) {
|
||||
filteredIndices.append(pair.second);
|
||||
}
|
||||
}
|
||||
endResetModel();
|
||||
}
|
||||
|
||||
int SettingsSearchModel::rowCount(const QModelIndex &parent) const
|
||||
{
|
||||
if (parent.isValid()) {
|
||||
return 0;
|
||||
}
|
||||
return filteredIndices.size();
|
||||
}
|
||||
|
||||
QVariant SettingsSearchModel::data(const QModelIndex &index, int role) const
|
||||
{
|
||||
if (!index.isValid() || index.row() >= filteredIndices.size()) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const SettingsSearchEntry &entry = sourceEntries[filteredIndices[index.row()]];
|
||||
|
||||
switch (role) {
|
||||
case EntryRole:
|
||||
return QVariant::fromValue(entry);
|
||||
case Qt::DisplayRole:
|
||||
return entry.widgetLabel;
|
||||
case Qt::ToolTipRole:
|
||||
return QStringLiteral("%1 > %2 > %3")
|
||||
.arg(QString::number(entry.pageIndex), entry.groupTitle, entry.widgetLabel);
|
||||
default:
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
SettingsSearchEntry SettingsSearchModel::entryForIndex(const QModelIndex &index) const
|
||||
{
|
||||
if (!index.isValid() || index.row() >= filteredIndices.size()) {
|
||||
return {};
|
||||
}
|
||||
return sourceEntries[filteredIndices[index.row()]];
|
||||
}
|
||||
|
|
@ -0,0 +1,76 @@
|
|||
/**
|
||||
* @file settings_search_model.h
|
||||
* @brief Data model for the settings search feature
|
||||
* @ingroup Dialogs
|
||||
*/
|
||||
#ifndef COCKATRICE_SETTINGS_SEARCH_MODEL_H
|
||||
#define COCKATRICE_SETTINGS_SEARCH_MODEL_H
|
||||
|
||||
#include <QAbstractListModel>
|
||||
#include <QList>
|
||||
#include <QRegularExpression>
|
||||
#include <QWidget>
|
||||
|
||||
/**
|
||||
* @brief Represents a single searchable setting entry
|
||||
*
|
||||
* Each settings page provides a list of these entries via getSearchEntries().
|
||||
* The model uses them for filtering, relevance scoring, and display.
|
||||
*/
|
||||
struct SettingsSearchEntry
|
||||
{
|
||||
int pageIndex; ///< Index of the settings page this entry belongs to
|
||||
QString groupTitle; ///< Title of the group/section within the page
|
||||
QString widgetLabel; ///< Display label for the setting widget
|
||||
QString fullSearchText; ///< Extended search text (label, control text, tooltip) for full-text matching
|
||||
QWidget *widget; ///< Pointer to the setting widget for focus/scrolling
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief List model providing filtered, ranked search results
|
||||
*
|
||||
* Manages a list of SettingsSearchEntry items. When a filter string is set,
|
||||
* entries are scored by relevance and sorted so the best matches appear first.
|
||||
* Supports custom roles for accessing entry fields from views and delegates.
|
||||
*/
|
||||
class SettingsSearchModel : public QAbstractListModel
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
/**
|
||||
* @brief Custom data role for accessing the full entry
|
||||
*/
|
||||
enum Roles
|
||||
{
|
||||
EntryRole = Qt::UserRole + 1, ///< Full SettingsSearchEntry object
|
||||
};
|
||||
|
||||
explicit SettingsSearchModel(QObject *parent = nullptr);
|
||||
|
||||
/** @brief Replaces the source entries and rebuilds the filter */
|
||||
void setSourceEntries(const QList<SettingsSearchEntry> &entries);
|
||||
/** @brief Sets the filter string and recalculates the results */
|
||||
void setFilterString(const QString &text);
|
||||
/** @brief Whether a non-empty filter is currently active */
|
||||
bool isFilterActive() const;
|
||||
|
||||
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
|
||||
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
|
||||
|
||||
/** @brief Returns the full entry for a given model index */
|
||||
SettingsSearchEntry entryForIndex(const QModelIndex &index) const;
|
||||
|
||||
private:
|
||||
QList<SettingsSearchEntry> sourceEntries; ///< Complete unfiltered entry list
|
||||
QList<int> filteredIndices; ///< Indices into sourceEntries matching the filter
|
||||
QRegularExpression filterRegex; ///< Compiled regex for the current filter
|
||||
QString filterQuery; ///< Current filter query string
|
||||
bool filterActive = false; ///< Whether filtering is active
|
||||
|
||||
/** @brief Recalculates the filtered index list and ranking */
|
||||
void rebuildFilter();
|
||||
};
|
||||
|
||||
Q_DECLARE_METATYPE(SettingsSearchEntry)
|
||||
|
||||
#endif // COCKATRICE_SETTINGS_SEARCH_MODEL_H
|
||||
|
|
@ -123,6 +123,7 @@ void ShortcutSettingsPage::retranslateUi()
|
|||
currentActionGroupLabel->setText(tr("Section:"));
|
||||
currentActionLabel->setText(tr("Action:"));
|
||||
currentShortcutLabel->setText(tr("Shortcut:"));
|
||||
editShortcutGroupBox->setTitle(tr("Shortcut editor"));
|
||||
editTextBox->retranslateUi();
|
||||
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"));
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue