diff --git a/cockatrice/CMakeLists.txt b/cockatrice/CMakeLists.txt index f34961b0b..b18846079 100644 --- a/cockatrice/CMakeLists.txt +++ b/cockatrice/CMakeLists.txt @@ -15,7 +15,6 @@ set(cockatrice_SOURCES src/client/network/update/client/client_update_checker.cpp src/client/network/update/client/release_channel.cpp src/client/network/update/card_spoiler/spoiler_background_updater.cpp - src/client/network/update/card_spoiler/spoiler_file.cpp src/client/crash/crash_reporter.cpp src/client/latency_graph_widget.cpp src/client/latency_status_widget.cpp @@ -151,7 +150,6 @@ set(cockatrice_SOURCES src/interface/widgets/utility/reversed_completer_model.cpp src/interface/pixel_map_generator.cpp src/interface/theme_config.cpp - src/interface/theme_icon.cpp src/interface/theme_manager.cpp src/interface/palette_editor/color_button.cpp src/interface/palette_editor/palette_generator.cpp @@ -253,8 +251,6 @@ set(cockatrice_SOURCES src/interface/widgets/playmat/playmat_settings_dialog.cpp src/interface/widgets/quick_settings/settings_button_widget.cpp src/interface/widgets/quick_settings/settings_popup_widget.cpp - src/interface/widgets/replay/replay_event_index.cpp - src/interface/widgets/replay/replay_event_sidebar.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 diff --git a/cockatrice/cockatrice.qrc b/cockatrice/cockatrice.qrc index 229cdd125..4ec3d2385 100644 --- a/cockatrice/cockatrice.qrc +++ b/cockatrice/cockatrice.qrc @@ -6,18 +6,19 @@ resources/hand.svg resources/hr.jpg - resources/icons/arrow_down.svg + resources/icons/arrow_bottom_green.svg + resources/icons/arrow_down_green.svg resources/icons/arrow_history.svg - resources/icons/arrow_left.svg + resources/icons/arrow_left_green.svg resources/icons/arrow_redo.svg - resources/icons/arrow_right.svg - resources/icons/arrow_right_sideboard.svg - resources/icons/arrow_to_bottom.svg - resources/icons/arrow_to_top.svg - resources/icons/arrow_up.svg + resources/icons/arrow_right_blue.svg + resources/icons/arrow_right_green.svg + resources/icons/arrow_top_green.svg + resources/icons/arrow_up_green.svg resources/icons/arrow_undo.svg resources/icons/circle_half_stroke.svg resources/icons/clearsearch.svg + resources/icons/cogwheel.svg resources/icons/conceded.svg resources/icons/decrement.svg resources/icons/delete.svg @@ -27,6 +28,7 @@ resources/icons/filter.svg resources/icons/floppy_disk.svg resources/icons/forgot_password.svg + resources/icons/gear.svg resources/icons/increment.svg resources/icons/info.svg resources/icons/lock.svg @@ -39,6 +41,7 @@ resources/icons/reload.svg resources/icons/remove_row.svg resources/icons/rename.svg + resources/icons/scale_balanced.svg resources/icons/scales.svg resources/icons/scroll.svg resources/icons/search.svg @@ -49,6 +52,7 @@ resources/icons/swap.svg resources/icons/sync.svg resources/icons/tab_changed.svg + resources/icons/update.png resources/icons/view.svg resources/icons/mana/B.svg diff --git a/cockatrice/resources/icons/arrow_bottom_green.svg b/cockatrice/resources/icons/arrow_bottom_green.svg new file mode 100644 index 000000000..3a45053ad --- /dev/null +++ b/cockatrice/resources/icons/arrow_bottom_green.svg @@ -0,0 +1,793 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + diff --git a/cockatrice/resources/icons/arrow_down.svg b/cockatrice/resources/icons/arrow_down.svg deleted file mode 100644 index 0ff237f52..000000000 --- a/cockatrice/resources/icons/arrow_down.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/cockatrice/resources/icons/arrow_down_green.svg b/cockatrice/resources/icons/arrow_down_green.svg new file mode 100644 index 000000000..574c12060 --- /dev/null +++ b/cockatrice/resources/icons/arrow_down_green.svg @@ -0,0 +1,793 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + diff --git a/cockatrice/resources/icons/arrow_history.svg b/cockatrice/resources/icons/arrow_history.svg index 29374d23a..d130758e6 100644 --- a/cockatrice/resources/icons/arrow_history.svg +++ b/cockatrice/resources/icons/arrow_history.svg @@ -1,5 +1,8 @@ - - - - - + + + + history + + + + \ No newline at end of file diff --git a/cockatrice/resources/icons/arrow_left.svg b/cockatrice/resources/icons/arrow_left.svg deleted file mode 100644 index 7a88deddb..000000000 --- a/cockatrice/resources/icons/arrow_left.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/cockatrice/resources/icons/arrow_left_green.svg b/cockatrice/resources/icons/arrow_left_green.svg new file mode 100644 index 000000000..1b9eb0684 --- /dev/null +++ b/cockatrice/resources/icons/arrow_left_green.svg @@ -0,0 +1,791 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + diff --git a/cockatrice/resources/icons/arrow_redo.svg b/cockatrice/resources/icons/arrow_redo.svg index 141260269..dd89e3314 100644 --- a/cockatrice/resources/icons/arrow_redo.svg +++ b/cockatrice/resources/icons/arrow_redo.svg @@ -1,4 +1,40 @@ - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/cockatrice/resources/icons/arrow_right.svg b/cockatrice/resources/icons/arrow_right.svg deleted file mode 100644 index f0ac5d5bf..000000000 --- a/cockatrice/resources/icons/arrow_right.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/cockatrice/resources/icons/arrow_right_blue.svg b/cockatrice/resources/icons/arrow_right_blue.svg new file mode 100644 index 000000000..334cd68fd --- /dev/null +++ b/cockatrice/resources/icons/arrow_right_blue.svg @@ -0,0 +1,789 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + diff --git a/cockatrice/resources/icons/arrow_right_green.svg b/cockatrice/resources/icons/arrow_right_green.svg new file mode 100644 index 000000000..db491985d --- /dev/null +++ b/cockatrice/resources/icons/arrow_right_green.svg @@ -0,0 +1,789 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + diff --git a/cockatrice/resources/icons/arrow_right_sideboard.svg b/cockatrice/resources/icons/arrow_right_sideboard.svg deleted file mode 100644 index 79a8cd5d3..000000000 --- a/cockatrice/resources/icons/arrow_right_sideboard.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/cockatrice/resources/icons/arrow_to_bottom.svg b/cockatrice/resources/icons/arrow_to_bottom.svg deleted file mode 100644 index 291a527d2..000000000 --- a/cockatrice/resources/icons/arrow_to_bottom.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/cockatrice/resources/icons/arrow_to_top.svg b/cockatrice/resources/icons/arrow_to_top.svg deleted file mode 100644 index aac38cc33..000000000 --- a/cockatrice/resources/icons/arrow_to_top.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/cockatrice/resources/icons/arrow_top_green.svg b/cockatrice/resources/icons/arrow_top_green.svg new file mode 100644 index 000000000..c515c6a1d --- /dev/null +++ b/cockatrice/resources/icons/arrow_top_green.svg @@ -0,0 +1,793 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + diff --git a/cockatrice/resources/icons/arrow_undo.svg b/cockatrice/resources/icons/arrow_undo.svg index c748b37b8..2e182ffb2 100644 --- a/cockatrice/resources/icons/arrow_undo.svg +++ b/cockatrice/resources/icons/arrow_undo.svg @@ -1,4 +1,40 @@ - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/cockatrice/resources/icons/arrow_up.svg b/cockatrice/resources/icons/arrow_up.svg deleted file mode 100644 index ac32b5c92..000000000 --- a/cockatrice/resources/icons/arrow_up.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/cockatrice/resources/icons/arrow_up_green.svg b/cockatrice/resources/icons/arrow_up_green.svg new file mode 100644 index 000000000..1d0f61112 --- /dev/null +++ b/cockatrice/resources/icons/arrow_up_green.svg @@ -0,0 +1,792 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + diff --git a/cockatrice/resources/icons/circle_half_stroke.svg b/cockatrice/resources/icons/circle_half_stroke.svg index c16f84858..42e4dbca2 100644 --- a/cockatrice/resources/icons/circle_half_stroke.svg +++ b/cockatrice/resources/icons/circle_half_stroke.svg @@ -1,4 +1 @@ - - - - + \ No newline at end of file diff --git a/cockatrice/resources/icons/clearsearch.svg b/cockatrice/resources/icons/clearsearch.svg index 49d883c68..be50d68d6 100644 --- a/cockatrice/resources/icons/clearsearch.svg +++ b/cockatrice/resources/icons/clearsearch.svg @@ -1,4 +1,276 @@ - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + diff --git a/cockatrice/resources/icons/cogwheel.svg b/cockatrice/resources/icons/cogwheel.svg new file mode 100644 index 000000000..8c7aad617 --- /dev/null +++ b/cockatrice/resources/icons/cogwheel.svg @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/cockatrice/resources/icons/conceded.svg b/cockatrice/resources/icons/conceded.svg index b9faf047c..a487f18bb 100644 --- a/cockatrice/resources/icons/conceded.svg +++ b/cockatrice/resources/icons/conceded.svg @@ -1,4 +1,103 @@ - - - + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + diff --git a/cockatrice/resources/icons/decrement.svg b/cockatrice/resources/icons/decrement.svg index 033245f85..0ceddf69f 100644 --- a/cockatrice/resources/icons/decrement.svg +++ b/cockatrice/resources/icons/decrement.svg @@ -1,3 +1,205 @@ - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + diff --git a/cockatrice/resources/icons/delete.svg b/cockatrice/resources/icons/delete.svg index d79312f1e..008d8d429 100644 --- a/cockatrice/resources/icons/delete.svg +++ b/cockatrice/resources/icons/delete.svg @@ -1,5 +1,357 @@ - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + diff --git a/cockatrice/resources/icons/dragon.svg b/cockatrice/resources/icons/dragon.svg index cc48d1f5c..f45af39ef 100644 --- a/cockatrice/resources/icons/dragon.svg +++ b/cockatrice/resources/icons/dragon.svg @@ -1,6 +1 @@ - - - - - - + \ No newline at end of file diff --git a/cockatrice/resources/icons/dropdown_collapsed.svg b/cockatrice/resources/icons/dropdown_collapsed.svg index 429c24974..52e833e65 100644 --- a/cockatrice/resources/icons/dropdown_collapsed.svg +++ b/cockatrice/resources/icons/dropdown_collapsed.svg @@ -1,3 +1,10 @@ - - + + + + + ]> + + diff --git a/cockatrice/resources/icons/dropdown_expanded.svg b/cockatrice/resources/icons/dropdown_expanded.svg index 0b24aeb48..fa3ee205c 100644 --- a/cockatrice/resources/icons/dropdown_expanded.svg +++ b/cockatrice/resources/icons/dropdown_expanded.svg @@ -1,3 +1,10 @@ - - + + + + + ]> + + diff --git a/cockatrice/resources/icons/filter.svg b/cockatrice/resources/icons/filter.svg index d9cdf879c..d53d9ba29 100644 --- a/cockatrice/resources/icons/filter.svg +++ b/cockatrice/resources/icons/filter.svg @@ -1,3 +1,12 @@ - - - + + + + + + + + \ No newline at end of file diff --git a/cockatrice/resources/icons/floppy_disk.svg b/cockatrice/resources/icons/floppy_disk.svg index 10b9e8172..3c5b5054d 100644 --- a/cockatrice/resources/icons/floppy_disk.svg +++ b/cockatrice/resources/icons/floppy_disk.svg @@ -1,5 +1 @@ - - - - - + \ No newline at end of file diff --git a/cockatrice/resources/icons/forgot_password.svg b/cockatrice/resources/icons/forgot_password.svg index 3bfb3bdb1..f938ca3f4 100644 --- a/cockatrice/resources/icons/forgot_password.svg +++ b/cockatrice/resources/icons/forgot_password.svg @@ -1,6 +1,6 @@ - - - - - + + + + + diff --git a/cockatrice/resources/icons/gear.svg b/cockatrice/resources/icons/gear.svg new file mode 100644 index 000000000..fdee3a297 --- /dev/null +++ b/cockatrice/resources/icons/gear.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/cockatrice/resources/icons/increment.svg b/cockatrice/resources/icons/increment.svg index 52dbdb2aa..6d5f7a85a 100644 --- a/cockatrice/resources/icons/increment.svg +++ b/cockatrice/resources/icons/increment.svg @@ -1,4 +1,199 @@ - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + diff --git a/cockatrice/resources/icons/info.svg b/cockatrice/resources/icons/info.svg index 1305f6b3d..00b23cc2c 100644 --- a/cockatrice/resources/icons/info.svg +++ b/cockatrice/resources/icons/info.svg @@ -1,5 +1,43 @@ - - - - + + + + + + + + + + diff --git a/cockatrice/resources/icons/lock.svg b/cockatrice/resources/icons/lock.svg index f150ece49..b7ebf1a53 100644 --- a/cockatrice/resources/icons/lock.svg +++ b/cockatrice/resources/icons/lock.svg @@ -1,4 +1,750 @@ - - - + + + + + + + + + padlock + + + + icon + + lock + security + secure + padlock + signs_and_symbols + + + + + AJ Ashton + + + + + AJ Ashton + + + + + AJ Ashton + + + + image/svg+xml + + + en + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/cockatrice/resources/icons/not_ready_start.svg b/cockatrice/resources/icons/not_ready_start.svg index c20a5cfe6..73afda9db 100644 --- a/cockatrice/resources/icons/not_ready_start.svg +++ b/cockatrice/resources/icons/not_ready_start.svg @@ -1,4 +1,368 @@ - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + diff --git a/cockatrice/resources/icons/pen_to_square.svg b/cockatrice/resources/icons/pen_to_square.svg index 7d3e161ac..60ff1d9c4 100644 --- a/cockatrice/resources/icons/pen_to_square.svg +++ b/cockatrice/resources/icons/pen_to_square.svg @@ -1,4 +1 @@ - - - - + \ No newline at end of file diff --git a/cockatrice/resources/icons/pencil.svg b/cockatrice/resources/icons/pencil.svg index d274e98db..c8b81f698 100644 --- a/cockatrice/resources/icons/pencil.svg +++ b/cockatrice/resources/icons/pencil.svg @@ -1,4 +1,123 @@ - - - + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + diff --git a/cockatrice/resources/icons/pin.svg b/cockatrice/resources/icons/pin.svg index 9c173286b..71d4c9c9c 100644 --- a/cockatrice/resources/icons/pin.svg +++ b/cockatrice/resources/icons/pin.svg @@ -1,4 +1,24 @@ - - - + + + + + + diff --git a/cockatrice/resources/icons/player.svg b/cockatrice/resources/icons/player.svg index 9fbe39a11..e83b24654 100644 --- a/cockatrice/resources/icons/player.svg +++ b/cockatrice/resources/icons/player.svg @@ -1,8 +1,142 @@ - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + diff --git a/cockatrice/resources/icons/ready_start.svg b/cockatrice/resources/icons/ready_start.svg index 4c4ea5d2d..238d2aa46 100644 --- a/cockatrice/resources/icons/ready_start.svg +++ b/cockatrice/resources/icons/ready_start.svg @@ -1,3 +1,81 @@ - - + + + + + + + + + + + + image/svg+xml + + + + + + + + + diff --git a/cockatrice/resources/icons/reload.svg b/cockatrice/resources/icons/reload.svg index e09b1b7ef..102995876 100644 --- a/cockatrice/resources/icons/reload.svg +++ b/cockatrice/resources/icons/reload.svg @@ -1,4 +1,7 @@ - - - - + + + + Svg Vector Icons : http://www.onlinewebfonts.com/icon + + \ No newline at end of file diff --git a/cockatrice/resources/icons/remove_row.svg b/cockatrice/resources/icons/remove_row.svg index 5d00ed57c..6f133cdb6 100644 --- a/cockatrice/resources/icons/remove_row.svg +++ b/cockatrice/resources/icons/remove_row.svg @@ -1,4 +1,356 @@ - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + diff --git a/cockatrice/resources/icons/rename.svg b/cockatrice/resources/icons/rename.svg index bab850808..c68a00246 100644 --- a/cockatrice/resources/icons/rename.svg +++ b/cockatrice/resources/icons/rename.svg @@ -1,7 +1,10 @@ - - - - - - + + + + + + + diff --git a/cockatrice/resources/icons/scale_balanced.svg b/cockatrice/resources/icons/scale_balanced.svg new file mode 100644 index 000000000..fae468d38 --- /dev/null +++ b/cockatrice/resources/icons/scale_balanced.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/cockatrice/resources/icons/scales.svg b/cockatrice/resources/icons/scales.svg index 4b2e16fcc..a410921dd 100644 --- a/cockatrice/resources/icons/scales.svg +++ b/cockatrice/resources/icons/scales.svg @@ -1,7 +1,6 @@ - - - - - - + + + + + diff --git a/cockatrice/resources/icons/scroll.svg b/cockatrice/resources/icons/scroll.svg index edcf29d11..f4828e679 100644 --- a/cockatrice/resources/icons/scroll.svg +++ b/cockatrice/resources/icons/scroll.svg @@ -1,6 +1 @@ - - - - - - + \ No newline at end of file diff --git a/cockatrice/resources/icons/search.svg b/cockatrice/resources/icons/search.svg index be4a87f00..05439a12e 100644 --- a/cockatrice/resources/icons/search.svg +++ b/cockatrice/resources/icons/search.svg @@ -1,4 +1,198 @@ - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + \ No newline at end of file diff --git a/cockatrice/resources/icons/settings.svg b/cockatrice/resources/icons/settings.svg index 3942a0916..77a2ccfa9 100644 --- a/cockatrice/resources/icons/settings.svg +++ b/cockatrice/resources/icons/settings.svg @@ -1,4 +1,280 @@ - - - + + + + + + + + Ruota dentata grigia + + + + unsorted + + + + + Open Clip Art Library + + + + + Architetto Francesco Rollandin + + + + + Architetto Francesco Rollandin + + + + image/svg+xml + + + en + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/cockatrice/resources/icons/share.svg b/cockatrice/resources/icons/share.svg index 9d6ff21a0..a84f55847 100644 --- a/cockatrice/resources/icons/share.svg +++ b/cockatrice/resources/icons/share.svg @@ -1,7 +1,25 @@ - - - - - - + + + + + + + + + diff --git a/cockatrice/resources/icons/sort_arrow_down.svg b/cockatrice/resources/icons/sort_arrow_down.svg index e10b401f2..adf10f9f0 100644 --- a/cockatrice/resources/icons/sort_arrow_down.svg +++ b/cockatrice/resources/icons/sort_arrow_down.svg @@ -1,7 +1 @@ - - - - - - - + \ No newline at end of file diff --git a/cockatrice/resources/icons/spectator.svg b/cockatrice/resources/icons/spectator.svg index 19242f058..c580c448d 100644 --- a/cockatrice/resources/icons/spectator.svg +++ b/cockatrice/resources/icons/spectator.svg @@ -1,4 +1,106 @@ - - - + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + diff --git a/cockatrice/resources/icons/swap.svg b/cockatrice/resources/icons/swap.svg index 4eddb8c08..a18ca2f06 100644 --- a/cockatrice/resources/icons/swap.svg +++ b/cockatrice/resources/icons/swap.svg @@ -1,6 +1,12 @@ - - - - - - + + + + + + + + \ No newline at end of file diff --git a/cockatrice/resources/icons/sync.svg b/cockatrice/resources/icons/sync.svg index fa00b3024..e287b1993 100644 --- a/cockatrice/resources/icons/sync.svg +++ b/cockatrice/resources/icons/sync.svg @@ -1,6 +1,6 @@ - - - - - + + + + + diff --git a/cockatrice/resources/icons/tab_changed.svg b/cockatrice/resources/icons/tab_changed.svg index 1d9b8d09c..ae09ef8fd 100644 --- a/cockatrice/resources/icons/tab_changed.svg +++ b/cockatrice/resources/icons/tab_changed.svg @@ -1,4 +1,116 @@ - - - + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + diff --git a/cockatrice/resources/icons/update.png b/cockatrice/resources/icons/update.png new file mode 100644 index 000000000..825c0aee7 Binary files /dev/null and b/cockatrice/resources/icons/update.png differ diff --git a/cockatrice/resources/icons/view.svg b/cockatrice/resources/icons/view.svg index 19242f058..e5676d825 100644 --- a/cockatrice/resources/icons/view.svg +++ b/cockatrice/resources/icons/view.svg @@ -1,4 +1,29 @@ - - - - + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + \ No newline at end of file diff --git a/cockatrice/src/client/network/update/card_spoiler/spoiler_background_updater.cpp b/cockatrice/src/client/network/update/card_spoiler/spoiler_background_updater.cpp index d74414d4a..62668c4d9 100644 --- a/cockatrice/src/client/network/update/card_spoiler/spoiler_background_updater.cpp +++ b/cockatrice/src/client/network/update/card_spoiler/spoiler_background_updater.cpp @@ -2,305 +2,253 @@ #include "../../../../main.h" #include "../../../settings/cache_settings.h" -#include "spoiler_file.h" -#include +#include +#include +#include #include +#include #include #include #include +#include +#include +#include +#include #include #include #include +#include #include -#include +#include #include #include -#include +#include +#include #include #include #include #include #include +#include +#include #include -namespace -{ -const QUrl SPOILERS_STATUS_URL( - QStringLiteral("https://raw.githubusercontent.com/Cockatrice/Magic-Spoiler/files/SpoilerSeasonEnabled")); -const QUrl SPOILERS_URL(QStringLiteral("https://raw.githubusercontent.com/Cockatrice/Magic-Spoiler/files/spoiler.xml")); -// Request coordination stays on the GUI thread; reload workers never access it. -QPointer currentUpdater; -QPointer> activeReload; -quint64 newestOperation = 0; +#define SPOILERS_STATUS_URL "https://raw.githubusercontent.com/Cockatrice/Magic-Spoiler/files/SpoilerSeasonEnabled" +#define SPOILERS_URL "https://raw.githubusercontent.com/Cockatrice/Magic-Spoiler/files/spoiler.xml" -bool removeSpoilerFile(const QString &path) +SpoilerBackgroundUpdater::SpoilerBackgroundUpdater(QObject *apParent) : QObject(apParent), cardUpdateProcess(nullptr) { - QFile file(path); - if (!file.exists()) { - return false; - } - if (!file.remove()) { - qCWarning(SpoilerBackgroundUpdaterLog) << "Could not remove spoiler database" << path << file.errorString(); - return false; - } - return true; -} -} // namespace - -SpoilerBackgroundUpdater::SpoilerBackgroundUpdater(QObject *parent, bool forceReload) - : QObject(parent), enabledAtStart(SettingsCache::instance().downloads().getDownloadSpoilersStatus()), - forceReload(forceReload), targetPath(SettingsCache::instance().paths().getSpoilerCardDatabasePath()), - operationId(++newestOperation), manager(NetworkManagerFactory::create(this)), notificationIcon(trayIcon) -{ - connect(&SettingsCache::instance().downloads(), &DownloadSettings::downloadSpoilerStatusChanged, this, - &SpoilerBackgroundUpdater::cancelIfSettingsChanged); - connect(&SettingsCache::instance().paths(), &PathsSettings::cardDatabasePathChanged, this, - &SpoilerBackgroundUpdater::cancelIfSettingsChanged); - // Even a disabled/no-op operation completes after its caller can connect signals. - QTimer::singleShot(0, this, &SpoilerBackgroundUpdater::begin); -} - -void SpoilerBackgroundUpdater::begin() -{ - if (completed) { - return; - } - if (cancelled || operationId != newestOperation || - (reloadOnly ? SettingsCache::instance().downloads().getDownloadSpoilersStatus() - : (!enabledAtStart || !SettingsCache::instance().downloads().getDownloadSpoilersStatus())) || - targetPath != SettingsCache::instance().paths().getSpoilerCardDatabasePath()) { - finish(); - return; - } - const auto previous = currentUpdater; - if (activeReload) { - // The existing database API stores its final load status after its parser - // mutex is released. Do not overlap updater-owned calls to that API. - connect(activeReload, &QFutureWatcher::finished, this, &SpoilerBackgroundUpdater::begin, - static_cast(Qt::QueuedConnection | Qt::SingleShotConnection)); - if (previous && previous != this) { - previous->cancel(); - } - return; - } - currentUpdater = this; - QPointer guard(this); - if (previous && previous != this) { - previous->cancel(); - } - if (!guard) { - return; - } - if (!isCurrent()) { - finish(); - } else if (reloadOnly) { - if (QFileInfo::exists(targetPath) && !removeSpoilerFile(targetPath)) { - finish(tr("The spoiler database could not be removed. Check its file permissions and use Reload Card " - "Database to retry.")); - return; - } - reloadDatabase(false); + 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 + startSpoilerDownloadProcess(SPOILERS_STATUS_URL, false); } else { - downloadFromURL(SPOILERS_STATUS_URL, false); + qCInfo(SpoilerBackgroundUpdaterLog) << "Spoilers Disabled"; } } -bool SpoilerBackgroundUpdater::isCurrent() const +void SpoilerBackgroundUpdater::startSpoilerDownloadProcess(QString url, bool saveResults) { - return !completed && !cancelled && currentUpdater == this && operationId == newestOperation && - (reloadOnly != SettingsCache::instance().downloads().getDownloadSpoilersStatus()) && - targetPath == SettingsCache::instance().paths().getSpoilerCardDatabasePath(); + auto spoilerURL = QUrl(url); + downloadFromURL(spoilerURL, saveResults); } -void SpoilerBackgroundUpdater::cancelIfSettingsChanged() +void SpoilerBackgroundUpdater::downloadFromURL(QUrl url, bool saveResults) { - if (reloadOnly == SettingsCache::instance().downloads().getDownloadSpoilersStatus() || - targetPath != SettingsCache::instance().paths().getSpoilerCardDatabasePath()) { - cancel(); - } -} - -void SpoilerBackgroundUpdater::cancel() -{ - if (completed) { - return; - } - cancelled = true; - const auto reply = activeReply; - activeReply.clear(); - QPointer guard(this); - if (reply) { - reply->abort(); - if (reply) { - reply->deleteLater(); - } - } - // A running reload owns no updater state. Allow its completion to retire its - // watcher before a heap-owned updater is released by the caller. - if (guard && !reloading) { - finish(); - } -} - -void SpoilerBackgroundUpdater::downloadFromURL(const QUrl &url, bool saveResults) -{ - QNetworkRequest request(url); + auto *nam = NetworkManagerFactory::create(this); + auto request = QNetworkRequest(url); request.setHeader(QNetworkRequest::UserAgentHeader, QString("Cockatrice %1").arg(VERSION_STRING)); - request.setTransferTimeout(60000); - activeReply = manager->get(request); - if (saveResults) { - connect(activeReply, &QNetworkReply::finished, this, - &SpoilerBackgroundUpdater::actDownloadFinishedSpoilersFile); - } else { - connect(activeReply, &QNetworkReply::finished, this, &SpoilerBackgroundUpdater::actCheckIfSpoilerSeasonEnabled); - } -} + QNetworkReply *reply = nam->get(request); -void SpoilerBackgroundUpdater::actCheckIfSpoilerSeasonEnabled() -{ - auto *reply = qobject_cast(sender()); - if (!reply || reply != activeReply) { - return; - } - activeReply.clear(); - reply->deleteLater(); - if (!isCurrent()) { - finish(); - return; - } - if (reply->error() == QNetworkReply::NoError) { - downloadFromURL(SPOILERS_URL, true); - } else if (reply->error() == QNetworkReply::ContentNotFoundError) { - const bool existed = QFileInfo::exists(targetPath); - seasonEnded = existed; - if (existed && !removeSpoilerFile(targetPath)) { - finish(tr("The spoiler season has ended, but the old spoiler database could not be removed.")); - return; - } - if (existed || forceReload) { - reloadDatabase(false); - } else { - finish(); - } + if (saveResults) { + // This will write out to the file (used for spoiler.xml) + connect(reply, &QNetworkReply::finished, this, &SpoilerBackgroundUpdater::actDownloadFinishedSpoilersFile); } else { - finish(tr("Could not check spoiler availability: %1").arg(reply->errorString())); + // This will check the status (used to see if we're in spoiler season or not) + connect(reply, &QNetworkReply::finished, this, &SpoilerBackgroundUpdater::actCheckIfSpoilerSeasonEnabled); } } void SpoilerBackgroundUpdater::actDownloadFinishedSpoilersFile() { - auto *reply = qobject_cast(sender()); - if (!reply || reply != activeReply) { - return; - } - activeReply.clear(); - reply->deleteLater(); - if (!isCurrent()) { - finish(); - return; - } - if (reply->error() != QNetworkReply::NoError) { - finish(tr("Could not download spoilers: %1").arg(reply->errorString())); - return; - } - spoilerData = reply->readAll(); - QString error; - const auto result = SpoilerFile::save(targetPath, spoilerData, [this] { return isCurrent(); }, error); - if (result == SpoilerFile::SaveResult::Failed) { - finish(isCurrent() ? error : QString()); - } else if (result == SpoilerFile::SaveResult::Written || forceReload) { - reloadDatabase(result == SpoilerFile::SaveResult::Written); + // Check for server reply + auto *reply = dynamic_cast(sender()); + QNetworkReply::NetworkError errorCode = reply->error(); + + if (errorCode == QNetworkReply::NoError) { + spoilerData = reply->readAll(); + + // Save the spoiler.xml file to the disk + saveDownloadedFile(spoilerData); + + reply->deleteLater(); + emit spoilerCheckerDone(); } else { - finish(); - } -} - -void SpoilerBackgroundUpdater::reloadDatabase(bool updated) -{ - reloading = true; - auto *database = CardDatabaseManager::getInstance(); - // The job outlives an individual request, so destroying that request cannot - // let a newer updater overlap the worker that is still completing. - auto *watcher = new QFutureWatcher(QCoreApplication::instance()); - activeReload = watcher; - connect(watcher, &QFutureWatcher::finished, watcher, [watcher] { - if (activeReload == watcher) { - activeReload.clear(); - } - watcher->deleteLater(); - }); - connect(watcher, &QFutureWatcher::finished, this, [this, watcher, database, updated] { - bool loaded = false; - try { - loaded = watcher->result() && database->getLoadStatus() == Ok; - } catch (const std::exception &error) { - qCWarning(SpoilerBackgroundUpdaterLog) << "Spoiler database reload failed:" << error.what(); - } - reloading = false; - if (!isCurrent()) { - finish(); - } else if (!loaded) { - finish(reloadOnly - ? tr("Could not reload card data after disabling spoilers. Use Reload Card Database to retry.") - : tr("Could not reload card data after updating spoilers. Use Update Spoilers to retry the " - "reload.")); - } else { - finish({}, updated); - } - }); - watcher->setFuture(QtConcurrent::run([database] { - database->reloadCardDatabasesAndNotify(); - return true; - })); -} - -void SpoilerBackgroundUpdater::finish(const QString &error, bool updated) -{ - if (completed) { - return; - } - completed = true; - spoilerData = QByteArray(); - if (currentUpdater == this) { - currentUpdater.clear(); - } - QPointer guard(this); - if (!error.isEmpty() && !cancelled) { - qCWarning(SpoilerBackgroundUpdaterLog) << error; - if (notificationIcon) { - notificationIcon->showMessage(tr("Spoiler update failed"), error); - } - if (!guard) { - return; - } - emit spoilerUpdateFailed(error); - } else if (updated && !cancelled) { - if (notificationIcon) { - notificationIcon->showMessage(tr("Spoilers have been updated!"), - tr("The card database now includes the downloaded spoilers.")); - } - } else if (seasonEnded && !cancelled && notificationIcon) { - notificationIcon->showMessage(tr("Spoiler season has ended"), - tr("The spoiler database was removed. Please run Oracle to get released cards.")); - } - if (guard) { + qCWarning(SpoilerBackgroundUpdaterLog) << "Error downloading spoilers file" << errorCode; emit spoilerCheckerDone(); } } -SpoilerBackgroundUpdater *SpoilerBackgroundUpdater::current() +bool SpoilerBackgroundUpdater::deleteSpoilerFile() { - return currentUpdater; + QString fileName = SettingsCache::instance().paths().getSpoilerCardDatabasePath(); + QFileInfo fi(fileName); + QDir fileDir(fi.path()); + QFile file(fileName); + + // Delete the spoiler.xml file + if (file.exists() && file.remove()) { + qCInfo(SpoilerBackgroundUpdaterLog) << "Deleting spoiler.xml"; + return true; + } + + qCInfo(SpoilerBackgroundUpdaterLog) << "Error: Spoiler.xml not found or not deleted"; + return false; } -SpoilerBackgroundUpdater *SpoilerBackgroundUpdater::deleteSpoilerFile() +void SpoilerBackgroundUpdater::actCheckIfSpoilerSeasonEnabled() { - // Deletion and the following reload share the operation lifetime. Callers can - // observe either failure before the queued operation starts, and an explicit - // retry still reloads when the file has already been removed. - auto *updater = new SpoilerBackgroundUpdater(QCoreApplication::instance(), true); - updater->reloadOnly = true; - connect(updater, &SpoilerBackgroundUpdater::spoilerCheckerDone, updater, &QObject::deleteLater); - return updater; + auto *response = dynamic_cast(sender()); + QNetworkReply::NetworkError errorCode = response->error(); + + if (errorCode == QNetworkReply::ContentNotFoundError) { + // Spoiler season is offline at this point, so the spoiler.xml file can be safely deleted + // The user should run Oracle to get the latest card information + if (deleteSpoilerFile() && trayIcon) { + trayIcon->showMessage(tr("Spoilers season has ended"), tr("Deleting spoiler.xml. Please run Oracle")); + } + + qCInfo(SpoilerBackgroundUpdaterLog) << "Spoiler Season Offline"; + emit spoilerCheckerDone(); + } else if (errorCode == QNetworkReply::NoError) { + qCInfo(SpoilerBackgroundUpdaterLog) << "Spoiler Service Online"; + startSpoilerDownloadProcess(SPOILERS_URL, true); + } else if (errorCode == QNetworkReply::HostNotFoundError) { + if (trayIcon) { + trayIcon->showMessage(tr("Spoilers download failed"), tr("No internet connection")); + } + + qCWarning(SpoilerBackgroundUpdaterLog) << "Spoiler download failed due to no internet connection"; + emit spoilerCheckerDone(); + } else { + if (trayIcon) { + trayIcon->showMessage(tr("Spoilers download failed"), tr("Error") + " " + (short)errorCode); + } + + qCWarning(SpoilerBackgroundUpdaterLog) << "Spoiler download failed with reason" << errorCode; + emit spoilerCheckerDone(); + } +} + +bool SpoilerBackgroundUpdater::saveDownloadedFile(QByteArray data) +{ + QString fileName = SettingsCache::instance().paths().getSpoilerCardDatabasePath(); + QFileInfo fi(fileName); + QDir fileDir(fi.path()); + + if (!fileDir.exists() && !fileDir.mkpath(fileDir.absolutePath())) { + return false; + } + + // Check if the data matches. If it does, then spoilers are up to date. + if (getHash(fileName) == getHash(data)) { + if (trayIcon) { + trayIcon->showMessage(tr("Spoilers already up to date"), tr("No new spoilers added")); + } + + qCInfo(SpoilerBackgroundUpdaterLog) << "Spoilers Up to Date"; + return false; + } + + QFile file(fileName); + if (!file.open(QIODevice::WriteOnly)) { + qCWarning(SpoilerBackgroundUpdaterLog) << "Spoiler Service Error: File open (w) failed for" << fileName; + file.close(); + return false; + } + + if (file.write(data) == -1) { + qCWarning(SpoilerBackgroundUpdaterLog) << "Spoiler Service Error: File write (w) failed for" << fileName; + file.close(); + return false; + } + + file.close(); + + // Data written, so reload the card database + qCInfo(SpoilerBackgroundUpdaterLog) << "Spoiler Service Data Written"; + // Spoilers can introduce sets the config has never seen; the loader never + // runs checkUnknownSets() itself, so run it once the finished snapshot has + // been swapped in. + auto *watcher = new QFutureWatcher(this); + connect(watcher, &QFutureWatcher::finished, this, [watcher] { + watcher->deleteLater(); + CardDatabaseManager::getInstance()->checkUnknownSets(); + }); + watcher->setFuture(QtConcurrent::run([] { CardDatabaseManager::getInstance()->loadCardDatabases(); })); + + // If the user has notifications enabled, let them know + // when the database was last updated + if (trayIcon) { + QList lines = data.split('\n'); + + for (const QByteArray &line : lines) { + if (line.contains("Created At:")) { + QString timeStamp = QString(line).replace("Created At:", "").trimmed(); + timeStamp.chop(6); // Remove " (UTC)" + + auto utcTime = QLocale().toDateTime(timeStamp, "ddd, MMM dd yyyy, hh:mm:ss"); +#if QT_VERSION >= QT_VERSION_CHECK(6, 7, 0) + utcTime.setTimeZone(QTimeZone::UTC); +#else + utcTime.setTimeSpec(Qt::UTC); +#endif + + QString localTime = utcTime.toLocalTime().toString("MMM d, hh:mm"); + + trayIcon->showMessage(tr("Spoilers have been updated!"), tr("Last change:") + " " + localTime); + emit spoilersUpdatedSuccessfully(); + return true; + } + } + } + + return true; +} + +QByteArray SpoilerBackgroundUpdater::getHash(const QString fileName) +{ + QFile file(fileName); + + if (file.open(QFile::ReadOnly)) { + // Only read the first 512 bytes (enough to get the "created" tag) + const QByteArray bytes = file.read(512); + + QCryptographicHash hash(QCryptographicHash::Algorithm::Md5); + hash.addData(bytes); + + qCInfo(SpoilerBackgroundUpdaterLog) << "File Hash =" << hash.result(); + + file.close(); + return hash.result(); + } else { + qCWarning(SpoilerBackgroundUpdaterLog) << "getHash ReadOnly failed!"; + file.close(); + return QByteArray(); + } +} + +QByteArray SpoilerBackgroundUpdater::getHash(QByteArray data) +{ + // Only read the first 512 bytes (enough to get the "created" tag) + const QByteArray bytes = data.left(512); + + QCryptographicHash hash(QCryptographicHash::Algorithm::Md5); + hash.addData(bytes); + + qCInfo(SpoilerBackgroundUpdaterLog) << "Data Hash =" << hash.result(); + + return hash.result(); } diff --git a/cockatrice/src/client/network/update/card_spoiler/spoiler_background_updater.h b/cockatrice/src/client/network/update/card_spoiler/spoiler_background_updater.h index fd90eb688..a6c4ed00f 100644 --- a/cockatrice/src/client/network/update/card_spoiler/spoiler_background_updater.h +++ b/cockatrice/src/client/network/update/card_spoiler/spoiler_background_updater.h @@ -2,6 +2,7 @@ * @file spoiler_background_updater.h * @ingroup Client */ +//! \todo Document this file. #ifndef COCKATRICE_SPOILER_DOWNLOADER_H #define COCKATRICE_SPOILER_DOWNLOADER_H @@ -9,13 +10,10 @@ #include #include #include -#include #include #include -class QNetworkAccessManager; -class QNetworkReply; -class QSystemTrayIcon; +class QProcess; class QUrl; inline Q_LOGGING_CATEGORY(SpoilerBackgroundUpdaterLog, "spoiler_background_updater"); @@ -24,48 +22,30 @@ class SpoilerBackgroundUpdater : public QObject { Q_OBJECT public: - explicit SpoilerBackgroundUpdater(QObject *parent = nullptr, bool forceReload = false); + explicit SpoilerBackgroundUpdater(QObject *apParent = nullptr); inline QString getCardUpdaterBinaryName() { return "oracle"; } - // Queues removal and reload; the application owns the returned updater and - // deletes it on completion. Callers can connect its failure/completion signals. - static SpoilerBackgroundUpdater *deleteSpoilerFile(); - // The operation currently running, if any. The updater outlives the page that - // started it, so a reopened page uses this to observe the in-flight update. - static SpoilerBackgroundUpdater *current(); + QByteArray getHash(const QString fileName); + QByteArray getHash(QByteArray data); + static bool deleteSpoilerFile(); private slots: - void begin(); void actDownloadFinishedSpoilersFile(); void actCheckIfSpoilerSeasonEnabled(); - void cancelIfSettingsChanged(); private: - const bool enabledAtStart; - const bool forceReload; - const QString targetPath; - const quint64 operationId; - QNetworkAccessManager *manager; - QPointer activeReply; - QPointer notificationIcon; + bool isSpoilerDownloadEnabled; + QProcess *cardUpdateProcess; QByteArray spoilerData; - bool completed = false; - bool cancelled = false; - bool reloading = false; - bool seasonEnded = false; - bool reloadOnly = false; - - bool isCurrent() const; - void cancel(); - void finish(const QString &error = {}, bool updated = false); - void downloadFromURL(const QUrl &url, bool saveResults); - void reloadDatabase(bool updated); + void startSpoilerDownloadProcess(QString url, bool saveResults); + void downloadFromURL(QUrl url, bool saveResults); + bool saveDownloadedFile(QByteArray data); signals: + void spoilersUpdatedSuccessfully(); void spoilerCheckerDone(); - void spoilerUpdateFailed(const QString &message); }; -#endif // COCKATRICE_SPOILER_DOWNLOADER_H +#endif // COCKATRICE_SPOILER_DOWNLOADER_H \ No newline at end of file diff --git a/cockatrice/src/client/network/update/card_spoiler/spoiler_file.cpp b/cockatrice/src/client/network/update/card_spoiler/spoiler_file.cpp deleted file mode 100644 index 554ea1108..000000000 --- a/cockatrice/src/client/network/update/card_spoiler/spoiler_file.cpp +++ /dev/null @@ -1,109 +0,0 @@ -#include "spoiler_file.h" - -#include -#include -#include -#include -#include -#include -#include - -namespace -{ -QByteArray hashFile(const QString &path) -{ - QFile file(path); - if (!file.open(QIODevice::ReadOnly)) { - return {}; - } - QCryptographicHash hash(QCryptographicHash::Sha256); - return hash.addData(&file) ? hash.result() : QByteArray(); -} -} // namespace - -namespace SpoilerFile -{ -QString validate(const QByteArray &data) -{ - QXmlStreamReader xml(data); - if (!xml.readNextStartElement() || xml.name() != QStringLiteral("cockatrice_carddatabase")) { - return QCoreApplication::translate( - "SpoilerBackgroundUpdater", - "The download is not a Cockatrice card database. The previous spoiler file was preserved."); - } - bool versionOk = false; - const int version = xml.attributes().value(QStringLiteral("version")).toInt(&versionOk); - if (!versionOk || (version != 3 && version != 4)) { - return QCoreApplication::translate( - "SpoilerBackgroundUpdater", - "The spoiler database uses an unsupported format. The previous spoiler file was preserved."); - } - bool hasCards = false; - int depth = 1; - while (!xml.atEnd()) { - const auto token = xml.readNext(); - if (token == QXmlStreamReader::StartElement) { - ++depth; - if (depth == 2 && xml.name() == QStringLiteral("cards")) { - hasCards = true; - } - } else if (token == QXmlStreamReader::EndElement) { - --depth; - } - } - if (xml.hasError() || !hasCards) { - return QCoreApplication::translate( - "SpoilerBackgroundUpdater", - "The spoiler database is incomplete or invalid. The previous spoiler file was preserved."); - } - return {}; -} - -SaveResult -save(const QString &targetPath, const QByteArray &data, const std::function &isCurrent, QString &error) -{ - error = validate(data); - if (!error.isEmpty()) { - return SaveResult::Failed; - } - if (!isCurrent()) { - error = QCoreApplication::translate("SpoilerBackgroundUpdater", - "The spoiler update was cancelled because its settings changed."); - return SaveResult::Failed; - } - if (hashFile(targetPath) == QCryptographicHash::hash(data, QCryptographicHash::Sha256)) { - return SaveResult::Unchanged; - } - const QDir directory = QFileInfo(targetPath).dir(); - if (!directory.exists() && !directory.mkpath(QStringLiteral("."))) { - error = QCoreApplication::translate("SpoilerBackgroundUpdater", - "Could not create the spoiler database directory: %1") - .arg(directory.absolutePath()); - return SaveResult::Failed; - } - QSaveFile file(targetPath); - file.setDirectWriteFallback(false); - if (!file.open(QIODevice::WriteOnly) || file.write(data) != data.size() || !file.flush()) { - error = QCoreApplication::translate( - "SpoilerBackgroundUpdater", - "Could not save the spoiler database to %1: %2. The previous file was preserved.") - .arg(targetPath, file.errorString()); - return SaveResult::Failed; - } - if (!isCurrent()) { - file.cancelWriting(); - error = QCoreApplication::translate("SpoilerBackgroundUpdater", - "The spoiler update was cancelled because its settings changed."); - return SaveResult::Failed; - } - if (!file.commit()) { - error = QCoreApplication::translate( - "SpoilerBackgroundUpdater", - "Could not replace the spoiler database at %1: %2. The previous file was preserved.") - .arg(targetPath, file.errorString()); - return SaveResult::Failed; - } - return SaveResult::Written; -} - -} // namespace SpoilerFile diff --git a/cockatrice/src/client/network/update/card_spoiler/spoiler_file.h b/cockatrice/src/client/network/update/card_spoiler/spoiler_file.h deleted file mode 100644 index 3a138cdea..000000000 --- a/cockatrice/src/client/network/update/card_spoiler/spoiler_file.h +++ /dev/null @@ -1,24 +0,0 @@ -#ifndef COCKATRICE_SPOILER_FILE_H -#define COCKATRICE_SPOILER_FILE_H - -#include -#include -#include - -namespace SpoilerFile -{ -enum class SaveResult -{ - Failed, - Unchanged, - Written -}; - -QString validate(const QByteArray &data); -// Validate before opening the destination, compare complete files, and replace -// atomically. Check operation ownership both before writing and before committing. -SaveResult -save(const QString &targetPath, const QByteArray &data, const std::function &isCurrent, QString &error); -} // namespace SpoilerFile - -#endif // COCKATRICE_SPOILER_FILE_H diff --git a/cockatrice/src/client/settings/cache_settings.cpp b/cockatrice/src/client/settings/cache_settings.cpp index b40e26377..08eccf1e4 100644 --- a/cockatrice/src/client/settings/cache_settings.cpp +++ b/cockatrice/src/client/settings/cache_settings.cpp @@ -6,7 +6,6 @@ #include "shortcuts_settings.h" #include -#include #include #include #include @@ -48,32 +47,7 @@ Q_GLOBAL_STATIC(SettingsCache, settingsCache) SettingsCache &SettingsCache::instance() { - SettingsCache &cache = *settingsCache; // returns a QT managed singleton reference - // The release channels own QNetworkAccessManagers, which must be destroyed - // while the application object is alive. ~SettingsCache() runs during - // static teardown, after QApplication is gone, so hook the application's - // shutdown instead. Retried here because the first access may predate the - // application object. - cache.installShutdownHook(); - return cache; -} - -void SettingsCache::installShutdownHook() -{ - if (shutdownHookInstalled) { - return; - } - QCoreApplication *application = QCoreApplication::instance(); - if (application == nullptr) { - return; - } - connect(application, &QCoreApplication::aboutToQuit, this, [this] { - for (ReleaseChannel *releaseChannel : std::as_const(releaseChannels)) { - delete releaseChannel; - } - releaseChannels.clear(); - }); - shutdownHookInstalled = true; + return *settingsCache; // returns a QT managed singleton reference } QString SettingsCache::getDataPath() diff --git a/cockatrice/src/client/settings/cache_settings.h b/cockatrice/src/client/settings/cache_settings.h index 8ccf6f76b..9aeb88a18 100644 --- a/cockatrice/src/client/settings/cache_settings.h +++ b/cockatrice/src/client/settings/cache_settings.h @@ -90,13 +90,8 @@ private: QString themeName; QList releaseChannels; - bool shutdownHookInstalled = false; bool isPortableBuild; - // Connects the aboutToQuit handler that deletes the release channels while - // the application object is still alive; a no-op before it exists. - void installShutdownHook(); - [[nodiscard]] QString getSafeConfigPath(QString configEntry, QString defaultPath) const; [[nodiscard]] QString getSafeConfigFilePath(QString configEntry, QString defaultPath) const; void loadPaths(); diff --git a/cockatrice/src/filters/filter_builder.cpp b/cockatrice/src/filters/filter_builder.cpp index 9c2a91a5b..22679abb1 100644 --- a/cockatrice/src/filters/filter_builder.cpp +++ b/cockatrice/src/filters/filter_builder.cpp @@ -1,6 +1,6 @@ #include "filter_builder.h" -#include "../interface/theme_icon.h" +#include "../interface/pixel_map_generator.h" #include "../interface/widgets/utility/custom_line_edit.h" #include @@ -30,7 +30,7 @@ FilterBuilder::FilterBuilder(QWidget *parent) : QWidget(parent) typeCombo->addItem(CardFilter::typeName(static_cast(i)), QVariant(i)); } - QPushButton *ok = new QPushButton(loadThemedIcon(QStringLiteral("icons/increment")), QString()); + QPushButton *ok = new QPushButton(themePixmap(QStringLiteral("icons/increment")), QString()); ok->setObjectName("ok"); ok->setMaximumSize(20, 20); diff --git a/cockatrice/src/game_graphics/log/message_log_widget.cpp b/cockatrice/src/game_graphics/log/message_log_widget.cpp index d526766af..8b351e022 100644 --- a/cockatrice/src/game_graphics/log/message_log_widget.cpp +++ b/cockatrice/src/game_graphics/log/message_log_widget.cpp @@ -4,8 +4,6 @@ #include "../../client/settings/card_counter_settings.h" #include "../../client/sound_engine.h" #include "../../interface/card_localization.h" -#include "../../interface/theme_icon.h" -#include "../../interface/theme_manager.h" #include "../../interface/widgets/server/chat_view/chat_view.h" #include "../board/card_item.h" #include "../board/translate_counter_name.h" @@ -14,12 +12,8 @@ #include #include #include -#include -#include #include #include -#include -#include #include #include #include @@ -126,7 +120,6 @@ MessageLogWidget::getFromStr(CardZoneLogic *zone, QString cardName, int position void MessageLogWidget::containerProcessingDone() { currentContext = MessageContext_None; - contextJudgeName.clear(); messageSuffix = messagePrefix = QString(); } @@ -862,27 +855,8 @@ void MessageLogWidget::logUndoDrawFailed(PlayerLogic *player) void MessageLogWidget::setContextJudgeName(QString name) { - contextJudgeName = std::move(name); - refreshJudgeSuffix(); -} - -void MessageLogWidget::refreshJudgeSuffix() -{ - const QPixmap pixmap = loadThemedIcon(QStringLiteral("icons/scales")).pixmap(QSize(24, 24)); - if (!pixmap.isNull()) { - // Rich text cannot resolve `currentColor`; register a palette-rendered - // image so already-appended judge tags repaint on a theme switch. - document()->addResource(QTextDocument::ImageResource, QUrl(QStringLiteral("cockatrice:scales")), pixmap); - document()->markContentsDirty(0, document()->characterCount()); - } - - if (contextJudgeName.isEmpty()) { - return; - } - - messagePrefix = QStringLiteral("").arg(palette().color(QPalette::WindowText).name()); - messageSuffix = - QStringLiteral(" [ %1]").arg(sanitizeHtml(contextJudgeName)); + messagePrefix = QString(""); + messageSuffix = QString(" [ %1]").arg(sanitizeHtml(name)); } void MessageLogWidget::appendHtmlServerMessage(const QString &html, bool optionalIsBold, QString optionalFontColor) @@ -931,6 +905,4 @@ void MessageLogWidget::connectToPlayerEventHandler(PlayerEventHandler *playerEve MessageLogWidget::MessageLogWidget(TabSupervisor *_tabSupervisor, AbstractGame *_game, QWidget *parent) : ChatView(_tabSupervisor, _game, true, parent), currentContext(MessageContext_None) { - connect(themeManager, &ThemeManager::paletteChanged, this, &MessageLogWidget::refreshJudgeSuffix); - connect(themeManager, &ThemeManager::themeChanged, this, &MessageLogWidget::refreshJudgeSuffix); } diff --git a/cockatrice/src/game_graphics/log/message_log_widget.h b/cockatrice/src/game_graphics/log/message_log_widget.h index 3ec5ff93c..5632f5af8 100644 --- a/cockatrice/src/game_graphics/log/message_log_widget.h +++ b/cockatrice/src/game_graphics/log/message_log_widget.h @@ -39,10 +39,8 @@ private: MessageContext currentContext; QString messagePrefix, messageSuffix; - QString contextJudgeName; static QPair getFromStr(CardZoneLogic *zone, QString cardName, int position, bool ownerChange); - void refreshJudgeSuffix(); public: void connectToPlayerEventHandler(PlayerEventHandler *player); diff --git a/cockatrice/src/game_graphics/player/player_list_widget.cpp b/cockatrice/src/game_graphics/player/player_list_widget.cpp index 6895e7db8..7b358010f 100644 --- a/cockatrice/src/game_graphics/player/player_list_widget.cpp +++ b/cockatrice/src/game_graphics/player/player_list_widget.cpp @@ -1,7 +1,6 @@ #include "player_list_widget.h" #include "../../interface/pixel_map_generator.h" -#include "../../interface/theme_icon.h" #include "../../interface/widgets/server/user/user_context_menu.h" #include "libcockatrice/protocol/pb/serverinfo_user.pb.h" #include "user_level.h" @@ -68,13 +67,13 @@ PlayerListWidget::PlayerListWidget(TabSupervisor *_tabSupervisor, QWidget *parent) : QTreeWidget(parent), tabSupervisor(_tabSupervisor), client(_client), game(_game), gameStarted(false) { - readyIcon = loadThemedIcon(QStringLiteral("icons/ready_start")); - notReadyIcon = loadThemedIcon(QStringLiteral("icons/not_ready_start")); - concededIcon = loadThemedIcon(QStringLiteral("icons/conceded")); - playerIcon = loadThemedIcon(QStringLiteral("icons/player")); - judgeIcon = loadThemedIcon(QStringLiteral("icons/scales")); - spectatorIcon = loadThemedIcon(QStringLiteral("icons/spectator")); - lockIcon = loadThemedIcon(QStringLiteral("icons/lock")); + readyIcon = themePixmap(QStringLiteral("icons/ready_start")); + notReadyIcon = themePixmap(QStringLiteral("icons/not_ready_start")); + concededIcon = themePixmap(QStringLiteral("icons/conceded")); + playerIcon = loadColorAdjustedPixmap("theme:icons/player"); + judgeIcon = loadColorAdjustedPixmap("theme:icons/scales"); + spectatorIcon = loadColorAdjustedPixmap("theme:icons/spectator"); + lockIcon = themePixmap(QStringLiteral("icons/lock")); if (tabSupervisor) { itemDelegate = new PlayerListItemDelegate(this); diff --git a/cockatrice/src/game_graphics/zones/view_zone_widget.cpp b/cockatrice/src/game_graphics/zones/view_zone_widget.cpp index 3c6cf9d02..b1ca42ee1 100644 --- a/cockatrice/src/game_graphics/zones/view_zone_widget.cpp +++ b/cockatrice/src/game_graphics/zones/view_zone_widget.cpp @@ -3,7 +3,6 @@ #include "../../client/settings/cache_settings.h" #include "../../filters/syntax_help.h" #include "../../interface/pixel_map_generator.h" -#include "../../interface/theme_icon.h" #include "../card_dimensions.h" #include "../z_values.h" #include "libcockatrice/utility/macros.h" @@ -94,8 +93,8 @@ ZoneViewWidget::ZoneViewWidget(PlayerLogic *_player, searchEdit.setFocusPolicy(Qt::ClickFocus); searchEdit.setPlaceholderText(tr("Search by card name (or search expressions)")); searchEdit.setClearButtonEnabled(true); - searchEdit.addAction(loadThemedIcon(QStringLiteral("icons/search")), QLineEdit::LeadingPosition); - auto help = searchEdit.addAction(loadThemedIcon(QStringLiteral("icons/info")), QLineEdit::TrailingPosition); + searchEdit.addAction(loadColorAdjustedPixmap("theme:icons/search"), QLineEdit::LeadingPosition); + auto help = searchEdit.addAction(themePixmap(QStringLiteral("icons/info")), QLineEdit::TrailingPosition); connect(help, &QAction::triggered, this, [this] { createSearchSyntaxHelpWindow(&searchEdit); }); diff --git a/cockatrice/src/interface/pixel_map_generator.cpp b/cockatrice/src/interface/pixel_map_generator.cpp index 037293af9..2f5c4b6dd 100644 --- a/cockatrice/src/interface/pixel_map_generator.cpp +++ b/cockatrice/src/interface/pixel_map_generator.cpp @@ -1,7 +1,6 @@ #include "pixel_map_generator.h" #include "libcockatrice/network/server/remote/user_level.h" -#include "theme_icon.h" #include "theme_manager.h" #include @@ -16,6 +15,7 @@ #include #include #include +#include #include #include #include @@ -436,19 +436,33 @@ QIcon UserLevelPixmapGenerator::generateIconWithColorOverride(int height, QMap UserLevelPixmapGenerator::iconCache; -QPixmap DropdownIconPixmapGenerator::generatePixmap(int height, bool expanded) +QPixmap LockPixmapGenerator::generatePixmap(int height) { - const QString key = QString::number(expanded) + ":" + QString::number(height); + + int key = height; if (pmCache.contains(key)) { return pmCache.value(key); } - // Render through the themed icon engine: it keeps the glyph's aspect ratio, - // follows the palette, and renders at the device pixel ratio instead of - // handing a DPR-1 bitmap to a label that would upscale and blur it on a 2x - // display. - const QString prefix = QStringLiteral("icons/") + (expanded ? "dropdown_expanded" : "dropdown_collapsed"); - const QPixmap pixmap = loadThemedIcon(prefix).pixmap(QSize(height, height), qApp->devicePixelRatio()); + QPixmap pixmap = tryLoadImage(QStringLiteral("theme:") + themeManager->assetPath(QStringLiteral("icons/lock")), + QSize(height, height), true); + pmCache.insert(key, pixmap); + return pixmap; +} + +QMap LockPixmapGenerator::pmCache; + +QPixmap DropdownIconPixmapGenerator::generatePixmap(int height, bool expanded) +{ + QString key = QString::number(expanded) + ":" + QString::number(height); + if (pmCache.contains(key)) { + return pmCache.value(key); + } + + QString name = expanded ? "dropdown_expanded" : "dropdown_collapsed"; + QPixmap pixmap = tryLoadImage(QStringLiteral("theme:") + themeManager->assetPath(QStringLiteral("icons/") + name), + QSize(height, height), true); + pmCache.insert(key, pixmap); return pixmap; } @@ -506,11 +520,30 @@ QPixmap ManaSymbolPixmapGenerator::generatePixmap(const QString &symbol, const Q QHash ManaSymbolPixmapGenerator::masterCache; QHash ManaSymbolPixmapGenerator::scaledCache; +QPixmap loadColorAdjustedPixmap(const QString &name) +{ + // Prefer an authored scheme-qualified variant when one exists for this asset. + const QString variant = themeManager->schemeVariantPath(QStringView(name).mid(QStringLiteral("theme:").size())); + if (!variant.isEmpty()) { + return QPixmap(QStringLiteral("theme:") + variant); + } + + // Legacy fallback: runtime-invert for dark mode when no authored variant. + if (qApp->palette().windowText().color().lightness() > 200) { + QImage img(name); + img.invertPixels(); + QPixmap result; + result.convertFromImage(img); + return result; + } else { + return QPixmap(name); + } +} + QPixmap themePixmap(QStringView prefix) { const QString resolved = themeManager->assetPath(prefix); - const QPixmap sentinel = loadSentinelSvgPixmap(resolved); - return sentinel.isNull() ? QPixmap(QStringLiteral("theme:") + resolved) : sentinel; + return QPixmap(QStringLiteral("theme:") + resolved); } void clearPixmapGeneratorCaches() @@ -520,6 +553,7 @@ void clearPixmapGeneratorCaches() PingPixmapGenerator::clear(); CountryPixmapGenerator::clear(); UserLevelPixmapGenerator::clear(); + LockPixmapGenerator::clear(); DropdownIconPixmapGenerator::clear(); ManaSymbolPixmapGenerator::clear(); } diff --git a/cockatrice/src/interface/pixel_map_generator.h b/cockatrice/src/interface/pixel_map_generator.h index 951f4231b..7c01bb332 100644 --- a/cockatrice/src/interface/pixel_map_generator.h +++ b/cockatrice/src/interface/pixel_map_generator.h @@ -106,6 +106,19 @@ public: } }; +class LockPixmapGenerator +{ +private: + static QMap pmCache; + +public: + static QPixmap generatePixmap(int height); + static void clear() + { + pmCache.clear(); + } +}; + class DropdownIconPixmapGenerator { private: @@ -147,11 +160,13 @@ public: } }; -// Returns a "theme:" asset (with no file extension in prefix), preferring the +QPixmap loadColorAdjustedPixmap(const QString &name); + +// Loads a "theme:" asset (with no file extension in prefix), preferring the // scheme-qualified variant (prefix-dark / prefix-light, resolved via -// ThemeManager::assetPath) and falling back to the plain asset. Token-driven -// SVGs resolve their `currentColor` sentinel from the palette. Use for -// scheme-sensitive pixmaps like backgrounds, the card back, and the app logo. +// ThemeManager::assetPath) and falling back to the plain asset. Callers load +// the returned path directly. Use for scheme-sensitive pixmaps like +// backgrounds, the card back, and the app logo. QPixmap themePixmap(QStringView prefix); // Clears every PixmapGenerator's static cache so scheme variants are diff --git a/cockatrice/src/interface/theme_icon.cpp b/cockatrice/src/interface/theme_icon.cpp deleted file mode 100644 index 161c436ca..000000000 --- a/cockatrice/src/interface/theme_icon.cpp +++ /dev/null @@ -1,300 +0,0 @@ -#include "theme_icon.h" - -#include "libcockatrice/utility/svg_recolor.h" -#include "theme_manager.h" - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -namespace -{ -/** - * @brief Palette color that replaces `currentColor`, following the icon mode. - */ -QByteArray sentinelColor(QIcon::Mode mode, QPalette::ColorRole role) -{ - const QPalette::ColorGroup group = mode == QIcon::Disabled ? QPalette::Disabled : QPalette::Active; - const QPalette::ColorRole effective = - mode == QIcon::Selected && role == QPalette::WindowText ? QPalette::HighlightedText : role; - return qApp->palette().color(group, effective).name(QColor::HexRgb).toLatin1(); -} - -/** - * @brief Reads an SVG through the `theme:` search path. - */ -QByteArray readSvg(const QString &resolvedPath) -{ - QFile file(QStringLiteral("theme:") + resolvedPath); - if (!file.open(QIODevice::ReadOnly)) { - return {}; - } - return file.readAll(); -} - -/** - * @brief Renders SVG bytes, scaled to @p size when one is given. - * - * An empty size means the asset's intrinsic size; @p devicePixelRatio only - * applies in that case so HiDPI callers still get a crisp glyph. - */ -QPixmap renderSvg(const QByteArray &data, const QSize &size, qreal devicePixelRatio) -{ - QSvgRenderer renderer(data); - if (!renderer.isValid()) { - return {}; - } - - QSize renderSize = size; - if (renderSize.isEmpty()) { - renderSize = renderer.defaultSize(); - renderSize = - QSize(qRound(renderSize.width() * devicePixelRatio), qRound(renderSize.height() * devicePixelRatio)); - } - if (!renderSize.isValid() || renderSize.isEmpty()) { - return {}; - } - - QPixmap pixmap(renderSize); - pixmap.fill(Qt::transparent); - QPainter painter(&pixmap); - renderer.render(&painter); - return pixmap; -} - -bool hasSentinel(const QByteArray &svg) -{ - return !svg.isEmpty() && QString::fromUtf8(svg).contains(QStringLiteral("currentColor"), Qt::CaseInsensitive); -} - -QString cacheKey(const QString &path, const QByteArray &variant, const QSize &size, qreal devicePixelRatio) -{ - return path + QLatin1Char('|') + QString::fromLatin1(variant) + QLatin1Char('|') + QString::number(size.width()) + - QLatin1Char('x') + QString::number(size.height()) + QLatin1Char('@') + QString::number(devicePixelRatio); -} - -/** - * @brief QIcon engine rendering a `theme:` SVG with `currentColor` resolved on demand. - * - * Qt's SVG renderer has no ambient color, so the sentinel is substituted from - * the current palette on every pixmap request. Asset resolution, the file bytes - * and the intrinsic size are cached until the theme changes: a repaint then - * does no file I/O at all. The render cache is keyed by resolved path and pixel - * size plus a variant: the palette color for sentinel SVGs, the icon mode for - * assets that carry their own colors. A palette change therefore produces new - * keys instead of reusing art built under the previous palette, and the - * non-sentinel path (raster overrides, plain SVGs) is cached like any other. - */ -class ThemeSvgIconEngine final : public QObject, public QIconEngine -{ -public: - ThemeSvgIconEngine(QString _prefix, QPalette::ColorRole _role) : prefix(std::move(_prefix)), role(_role) - { - if (themeManager) { - // A theme switch changes the resolved asset (scheme-qualified - // variants, raster overrides) and a palette change invalidates - // every cached colour, so both drop the caches instead of - // re-reading theme.cfg and the assets on every repaint. - connect(themeManager, &ThemeManager::themeChanged, this, &ThemeSvgIconEngine::invalidate); - connect(themeManager, &ThemeManager::paletteChanged, this, &ThemeSvgIconEngine::invalidate); - } - } - - QIconEngine *clone() const override - { - return new ThemeSvgIconEngine(prefix, role); - } - - void paint(QPainter *painter, const QRect &rect, QIcon::Mode mode, QIcon::State) override - { - const qreal devicePixelRatio = painter->device() ? painter->device()->devicePixelRatioF() : 1.0; - const QSize logicalSize = fitToSize(rect.size()); - if (logicalSize.isEmpty()) { - return; - } - const QSize pixelSize(qRound(logicalSize.width() * devicePixelRatio), - qRound(logicalSize.height() * devicePixelRatio)); - const QPixmap pixmap = render(pixelSize, devicePixelRatio, mode); - if (!pixmap.isNull()) { - // QRect::center() would floor and drop the last row/column of a - // 16x16 glyph; the center has to be fractional so glyphs stay - // unshifted and unsclipped in item views. - const QSizeF contentSize = pixmap.deviceIndependentSize(); - const QRectF targetRect( - QRectF(rect).center() - QPointF(contentSize.width() / 2.0, contentSize.height() / 2.0), contentSize); - painter->drawPixmap(targetRect, pixmap, QRectF(pixmap.rect())); - } - } - - QPixmap pixmap(const QSize &size, QIcon::Mode mode, QIcon::State) override - { - return render(fitToSize(size), 1.0, mode); - } - - QPixmap scaledPixmap(const QSize &size, QIcon::Mode mode, QIcon::State, qreal scale) override - { - const QSize logicalSize = fitToSize(size); -#if QT_VERSION >= QT_VERSION_CHECK(6, 8, 0) - const QSize pixelSize(qRound(logicalSize.width() * scale), qRound(logicalSize.height() * scale)); -#else - // Qt < 6.8 passes the device-dependent size to the engine. - const QSize pixelSize = logicalSize; -#endif - return render(pixelSize, scale, mode); - } - - QSize actualSize(const QSize &size, QIcon::Mode, QIcon::State) override - { - return fitToSize(size); - } - -private: - void invalidate() - { - hasResolvedAsset = false; - hasIntrinsicSize = false; - cache.clear(); - } - - void ensureResolved() const - { - if (hasResolvedAsset && (!themeManager || themeManager->getCurrentThemePath() == resolvedThemePath)) { - return; - } - resolvedThemePath = themeManager ? themeManager->getCurrentThemePath() : QString(); - resolvedPath = themeManager ? themeManager->assetPath(prefix) : QString(); - resolvedAssetIsSvg = resolvedPath.endsWith(QStringLiteral(".svg"), Qt::CaseInsensitive); - resolvedSvg = resolvedAssetIsSvg ? readSvg(resolvedPath) : QByteArray(); - resolvedHasSentinel = hasSentinel(resolvedSvg); - hasResolvedAsset = true; - hasIntrinsicSize = false; - } - - QSize intrinsicSize() const - { - ensureResolved(); - if (!hasIntrinsicSize) { - if (resolvedAssetIsSvg) { - QSvgRenderer renderer(resolvedSvg); - intrinsicSizeCache = renderer.isValid() ? renderer.defaultSize() : QSize(); - } else { - // A theme may override a glyph with a raster; take its real - // aspect ratio instead of assuming the 24x24 SVG default, so - // actualSize() and the rendered pixmap agree. - QImageReader reader(QStringLiteral("theme:") + resolvedPath); - intrinsicSizeCache = reader.size(); - } - if (intrinsicSizeCache.isEmpty()) { - intrinsicSizeCache = QSize(24, 24); - } - hasIntrinsicSize = true; - } - return intrinsicSizeCache; - } - - QSize fitToSize(const QSize &size) const - { - if (size.isEmpty()) { - return size; - } - return intrinsicSize().scaled(size, Qt::KeepAspectRatio); - } - - QPixmap fromAsset(const QSize &pixelSize, QIcon::Mode mode) const - { - QPixmap pixmap(QStringLiteral("theme:") + resolvedPath); - if (!pixelSize.isEmpty() && pixmap.size() != pixelSize) { - pixmap = pixmap.scaled(pixelSize, Qt::KeepAspectRatio, Qt::SmoothTransformation); - } - if (mode == QIcon::Disabled && !pixmap.isNull()) { - pixmap = QApplication::style()->generatedIconPixmap(QIcon::Disabled, pixmap, nullptr); - } - return pixmap; - } - - QPixmap render(const QSize &pixelSize, qreal devicePixelRatio, QIcon::Mode mode) const - { - ensureResolved(); - - // Same cache for both paths: sentinel art varies by palette color, art - // with its own colors varies by mode (Disabled is generated). A color - // variant is never a bare digit, so the two key spaces cannot collide. - const QByteArray variant = - resolvedHasSentinel ? sentinelColor(mode, role) : QByteArray::number(static_cast(mode)); - const QString key = cacheKey(resolvedPath, variant, pixelSize, devicePixelRatio); - const auto cached = cache.constFind(key); - if (cached != cache.constEnd()) { - return cached.value(); - } - - QPixmap pixmap; - if (resolvedHasSentinel) { - pixmap = renderSvg(resolveSvgColorSentinel(resolvedSvg, variant), pixelSize, devicePixelRatio); - } else { - // Authored art carries its own colors: use Qt's normal loader so - // raster overrides and non-SVG assets keep working. - pixmap = fromAsset(pixelSize, mode); - } - if (!pixmap.isNull()) { - // Set the ratio before caching: assigning it to a cached pixmap - // afterwards detaches (deep-copies) it on every paint. - pixmap.setDevicePixelRatio(devicePixelRatio); - } - if (cache.size() >= 64) { - cache.clear(); - } - cache.insert(key, pixmap); - return pixmap; - } - - QString prefix; - QPalette::ColorRole role; - mutable QHash cache; - mutable QString resolvedPath; - mutable QString resolvedThemePath; - mutable QByteArray resolvedSvg; - mutable QSize intrinsicSizeCache; - mutable bool resolvedAssetIsSvg = false; - mutable bool resolvedHasSentinel = false; - mutable bool hasResolvedAsset = false; - mutable bool hasIntrinsicSize = false; -}; -} // namespace - -QIcon loadThemedIcon(QStringView prefix, QPalette::ColorRole role) -{ - return QIcon(new ThemeSvgIconEngine(prefix.toString(), role)); -} - -QPixmap loadSentinelSvgPixmap(const QString &resolvedPath, QPalette::ColorRole role, const QSize &size) -{ - if (!resolvedPath.endsWith(QStringLiteral(".svg"), Qt::CaseInsensitive)) { - return {}; - } - - const QByteArray svg = readSvg(resolvedPath); - if (!hasSentinel(svg)) { - return {}; - } - - const QByteArray color = qApp->palette().color(role).name(QColor::HexRgb).toLatin1(); - return renderSvg(resolveSvgColorSentinel(svg, color), size, 1.0); -} diff --git a/cockatrice/src/interface/theme_icon.h b/cockatrice/src/interface/theme_icon.h deleted file mode 100644 index e26015ea1..000000000 --- a/cockatrice/src/interface/theme_icon.h +++ /dev/null @@ -1,50 +0,0 @@ -/** - * @file theme_icon.h - * @ingroup Interface - */ - -#ifndef THEME_ICON_H -#define THEME_ICON_H - -#include -#include -#include -#include -#include -#include - -/** - * @brief Builds a palette-aware icon from a `theme:` asset. - * - * SVG glyphs may use the `currentColor` sentinel. A plain QPixmap snapshot - * cannot follow palette changes, so this returns a QIcon whose engine resolves - * the sentinel from the current palette every time it is asked for a pixmap. - * Icons therefore stay legible when the user switches between light and dark - * themes without recreating any actions or buttons. - * - * @param prefix Asset prefix relative to the `theme:` search path, with no file - * extension (for example `icons/search`), matching themePixmap(). - * @param role Palette role whose color replaces the sentinel. Selected glyphs - * use HighlightedText unless a role other than WindowText is given. - * @return An icon that follows the application palette. - */ -QIcon loadThemedIcon(QStringView prefix, QPalette::ColorRole role = QPalette::WindowText); - -/** - * @brief Renders a resolved theme SVG when it carries the `currentColor` sentinel. - * - * Non-SVG assets and SVGs without the sentinel return a null pixmap so callers - * can fall back to their usual loading path (including legacy inversion). - * - * @param resolvedPath Asset path relative to the `theme:` search path, as - * returned by ThemeManager::assetPath (includes extension). - * @param role Palette role whose color replaces the sentinel. - * @param size Pixel size to render at; an empty size uses the SVG's intrinsic size. - * @return The recolored pixmap, or a null pixmap when the asset is not a - * sentinel-carrying SVG. - */ -QPixmap loadSentinelSvgPixmap(const QString &resolvedPath, - QPalette::ColorRole role = QPalette::WindowText, - const QSize &size = {}); - -#endif // THEME_ICON_H diff --git a/cockatrice/src/interface/widgets/deck_analytics/abstract_analytics_panel_widget.cpp b/cockatrice/src/interface/widgets/deck_analytics/abstract_analytics_panel_widget.cpp index c8571ab08..730043d95 100644 --- a/cockatrice/src/interface/widgets/deck_analytics/abstract_analytics_panel_widget.cpp +++ b/cockatrice/src/interface/widgets/deck_analytics/abstract_analytics_panel_widget.cpp @@ -1,6 +1,6 @@ #include "abstract_analytics_panel_widget.h" -#include "../../theme_icon.h" +#include "../../pixel_map_generator.h" #include "../general/display/banner_widget.h" #include "deck_list_statistics_analyzer.h" @@ -28,7 +28,7 @@ AbstractAnalyticsPanelWidget::AbstractAnalyticsPanelWidget(QWidget *parent, Deck // config button configureButton = new QPushButton(this); - configureButton->setIcon(loadThemedIcon(QStringLiteral("icons/settings"))); + configureButton->setIcon(themePixmap(QStringLiteral("icons/cogwheel"))); configureButton->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Minimum); connect(configureButton, &QPushButton::clicked, this, &AbstractAnalyticsPanelWidget::applyConfigFromDialog); bannerAndSettingsLayout->addWidget(configureButton, 0); diff --git a/cockatrice/src/interface/widgets/deck_editor/deck_editor_database_display_widget.cpp b/cockatrice/src/interface/widgets/deck_editor/deck_editor_database_display_widget.cpp index f25e6c0ff..7ed398535 100644 --- a/cockatrice/src/interface/widgets/deck_editor/deck_editor_database_display_widget.cpp +++ b/cockatrice/src/interface/widgets/deck_editor/deck_editor_database_display_widget.cpp @@ -3,7 +3,7 @@ #include "../../../client/settings/cache_settings.h" #include "../../../filters/syntax_help.h" #include "../../key_signals.h" -#include "../../theme_icon.h" +#include "../../pixel_map_generator.h" #include "../utility/custom_line_edit.h" #include "card_database_display_model.h" #include "card_database_model.h" @@ -40,8 +40,8 @@ DeckEditorDatabaseDisplayWidget::DeckEditorDatabaseDisplayWidget(QWidget *parent searchEdit->setObjectName("searchEdit"); searchEdit->setPlaceholderText(tr("Search by card name (or search expressions)")); searchEdit->setClearButtonEnabled(true); - searchEdit->addAction(loadThemedIcon(QStringLiteral("icons/search")), QLineEdit::LeadingPosition); - auto help = searchEdit->addAction(loadThemedIcon(QStringLiteral("icons/info")), QLineEdit::TrailingPosition); + searchEdit->addAction(loadColorAdjustedPixmap("theme:icons/search"), QLineEdit::LeadingPosition); + auto help = searchEdit->addAction(themePixmap(QStringLiteral("icons/info")), QLineEdit::TrailingPosition); setFocusProxy(searchEdit); setFocusPolicy(Qt::ClickFocus); @@ -83,13 +83,13 @@ DeckEditorDatabaseDisplayWidget::DeckEditorDatabaseDisplayWidget(QWidget *parent &DeckEditorDatabaseDisplayWidget::onRelatedCardClicked); aAddCard = new QAction(QString(), this); - aAddCard->setIcon(loadThemedIcon(QStringLiteral("icons/arrow_right"))); + aAddCard->setIcon(themePixmap(QStringLiteral("icons/arrow_right_green"))); connect(aAddCard, &QAction::triggered, this, &DeckEditorDatabaseDisplayWidget::actAddCardToMainDeck); auto *tbAddCard = new QToolButton(this); tbAddCard->setDefaultAction(aAddCard); aAddCardToSideboard = new QAction(QString(), this); - aAddCardToSideboard->setIcon(loadThemedIcon(QStringLiteral("icons/arrow_right_sideboard"))); + aAddCardToSideboard->setIcon(themePixmap(QStringLiteral("icons/arrow_right_blue"))); connect(aAddCardToSideboard, &QAction::triggered, this, &DeckEditorDatabaseDisplayWidget::actAddCardToSideboard); auto *tbAddCardToSideboard = new QToolButton(this); tbAddCardToSideboard->setDefaultAction(aAddCardToSideboard); diff --git a/cockatrice/src/interface/widgets/deck_editor/deck_editor_deck_dock_widget.cpp b/cockatrice/src/interface/widgets/deck_editor/deck_editor_deck_dock_widget.cpp index 47d2a1aae..30fde1595 100644 --- a/cockatrice/src/interface/widgets/deck_editor/deck_editor_deck_dock_widget.cpp +++ b/cockatrice/src/interface/widgets/deck_editor/deck_editor_deck_dock_widget.cpp @@ -3,7 +3,7 @@ #include "../../../client/settings/cache_settings.h" #include "../../../client/settings/shortcuts_settings.h" #include "../../key_signals.h" -#include "../../theme_icon.h" +#include "../../pixel_map_generator.h" #include "../playmat/playmat_settings_dialog.h" #include "../quick_settings/settings_button_widget.h" #include "../tabs/abstract_tab_deck_editor.h" @@ -248,25 +248,25 @@ void DeckEditorDeckDockWidget::createDeckDock() &DeckEditorDeckDockWidget::applyActiveGroupCriteria); aIncrement = new QAction(QString(), this); - aIncrement->setIcon(loadThemedIcon(QStringLiteral("icons/increment"))); + aIncrement->setIcon(themePixmap(QStringLiteral("icons/increment"))); connect(aIncrement, &QAction::triggered, this, &DeckEditorDeckDockWidget::actIncrementSelection); auto *tbIncrement = new QToolButton(this); tbIncrement->setDefaultAction(aIncrement); aDecrement = new QAction(QString(), this); - aDecrement->setIcon(loadThemedIcon(QStringLiteral("icons/decrement"))); + aDecrement->setIcon(themePixmap(QStringLiteral("icons/decrement"))); connect(aDecrement, &QAction::triggered, this, &DeckEditorDeckDockWidget::actDecrementSelection); auto *tbDecrement = new QToolButton(this); tbDecrement->setDefaultAction(aDecrement); aRemoveCard = new QAction(QString(), this); - aRemoveCard->setIcon(loadThemedIcon(QStringLiteral("icons/remove_row"))); + aRemoveCard->setIcon(themePixmap(QStringLiteral("icons/remove_row"))); connect(aRemoveCard, &QAction::triggered, this, &DeckEditorDeckDockWidget::actRemoveCard); auto *tbRemoveCard = new QToolButton(this); tbRemoveCard->setDefaultAction(aRemoveCard); aSwapCard = new QAction(QString(), this); - aSwapCard->setIcon(loadThemedIcon(QStringLiteral("icons/swap"))); + aSwapCard->setIcon(themePixmap(QStringLiteral("icons/swap"))); connect(aSwapCard, &QAction::triggered, this, &DeckEditorDeckDockWidget::actSwapSelection); auto *tbSwapCard = new QToolButton(this); tbSwapCard->setDefaultAction(aSwapCard); diff --git a/cockatrice/src/interface/widgets/deck_editor/deck_editor_filter_dock_widget.cpp b/cockatrice/src/interface/widgets/deck_editor/deck_editor_filter_dock_widget.cpp index 8122d1847..adeb5c3e1 100644 --- a/cockatrice/src/interface/widgets/deck_editor/deck_editor_filter_dock_widget.cpp +++ b/cockatrice/src/interface/widgets/deck_editor/deck_editor_filter_dock_widget.cpp @@ -5,7 +5,7 @@ #include "../../../filters/filter_builder.h" #include "../../../filters/filter_tree_model.h" #include "../../key_signals.h" -#include "../../theme_icon.h" +#include "../../pixel_map_generator.h" #include "../tabs/abstract_tab_deck_editor.h" #include "libcockatrice/filters/filter_card.h" @@ -64,11 +64,11 @@ void DeckEditorFilterDockWidget::createFiltersDock() connect(filterBuilder, &FilterBuilder::add, filterModel, &FilterTreeModel::addFilter); aClearFilterOne = new QAction(QString(), this); - aClearFilterOne->setIcon(loadThemedIcon(QStringLiteral("icons/decrement"))); + aClearFilterOne->setIcon(themePixmap(QStringLiteral("icons/decrement"))); connect(aClearFilterOne, &QAction::triggered, this, &DeckEditorFilterDockWidget::actClearFilterOne); aClearFilterAll = new QAction(QString(), this); - aClearFilterAll->setIcon(loadThemedIcon(QStringLiteral("icons/clearsearch"))); + aClearFilterAll->setIcon(themePixmap(QStringLiteral("icons/clearsearch"))); connect(aClearFilterAll, &QAction::triggered, this, &DeckEditorFilterDockWidget::actClearFilterAll); auto *filterDelOne = new QToolButton(); diff --git a/cockatrice/src/interface/widgets/deck_editor/deck_list_history_manager_widget.cpp b/cockatrice/src/interface/widgets/deck_editor/deck_list_history_manager_widget.cpp index a1914e1b5..209dc7273 100644 --- a/cockatrice/src/interface/widgets/deck_editor/deck_list_history_manager_widget.cpp +++ b/cockatrice/src/interface/widgets/deck_editor/deck_list_history_manager_widget.cpp @@ -1,6 +1,6 @@ #include "deck_list_history_manager_widget.h" -#include "../../theme_icon.h" +#include "../../pixel_map_generator.h" #include "../quick_settings/settings_button_widget.h" #include "deck_state_manager.h" #include "libcockatrice/deck_list/deck_list_history_manager.h" @@ -32,7 +32,7 @@ DeckListHistoryManagerWidget::DeckListHistoryManagerWidget(DeckStateManager *_de layout = new QHBoxLayout(this); aUndo = new QAction(QString(), this); - aUndo->setIcon(loadThemedIcon(QStringLiteral("icons/arrow_undo"))); + aUndo->setIcon(themePixmap(QStringLiteral("icons/arrow_undo"))); aUndo->setShortcut(QKeySequence::Undo); aUndo->setShortcutContext(Qt::ApplicationShortcut); connect(aUndo, &QAction::triggered, this, &DeckListHistoryManagerWidget::doUndo); @@ -41,7 +41,7 @@ DeckListHistoryManagerWidget::DeckListHistoryManagerWidget(DeckStateManager *_de undoButton->setDefaultAction(aUndo); aRedo = new QAction(QString(), this); - aRedo->setIcon(loadThemedIcon(QStringLiteral("icons/arrow_redo"))); + aRedo->setIcon(themePixmap(QStringLiteral("icons/arrow_redo"))); aRedo->setShortcut(QKeySequence::Redo); aRedo->setShortcutContext(Qt::ApplicationShortcut); connect(aRedo, &QAction::triggered, this, &DeckListHistoryManagerWidget::doRedo); @@ -53,7 +53,7 @@ DeckListHistoryManagerWidget::DeckListHistoryManagerWidget(DeckStateManager *_de layout->addWidget(redoButton); historyButton = new SettingsButtonWidget(this); - historyButton->setButtonIcon(loadThemedIcon(QStringLiteral("icons/arrow_history"))); + historyButton->setButtonIcon(themePixmap(QStringLiteral("icons/arrow_history"))); historyLabel = new QLabel(this); diff --git a/cockatrice/src/interface/widgets/dialogs/dlg_connect.cpp b/cockatrice/src/interface/widgets/dialogs/dlg_connect.cpp index daee5f1f9..853a00828 100644 --- a/cockatrice/src/interface/widgets/dialogs/dlg_connect.cpp +++ b/cockatrice/src/interface/widgets/dialogs/dlg_connect.cpp @@ -1,7 +1,7 @@ #include "dlg_connect.h" #include "../../../client/settings/cache_settings.h" -#include "../../theme_icon.h" +#include "../../pixel_map_generator.h" #include "../server/handle_public_servers.h" #include "../server/user/user_info_connection.h" #include "libcockatrice/utility/macros.h" @@ -38,7 +38,7 @@ DlgConnect::DlgConnect(QWidget *parent) : QDialog(parent) previousHosts = new QComboBox(this); btnDeleteServer = new QPushButton(this); - btnDeleteServer->setIcon(loadThemedIcon(QStringLiteral("icons/remove_row"))); + btnDeleteServer->setIcon(themePixmap(QStringLiteral("icons/remove_row"))); btnDeleteServer->setToolTip(tr("Delete the currently selected saved server")); btnDeleteServer->setFixedWidth(30); @@ -46,7 +46,7 @@ DlgConnect::DlgConnect(QWidget *parent) : QDialog(parent) hps = new HandlePublicServers(this); btnRefreshServers = new QPushButton(this); - btnRefreshServers->setIcon(loadThemedIcon(QStringLiteral("icons/sync"))); + btnRefreshServers->setIcon(themePixmap(QStringLiteral("icons/sync"))); btnRefreshServers->setToolTip(tr("Refresh the server list with known public servers")); btnRefreshServers->setFixedWidth(30); @@ -121,7 +121,7 @@ DlgConnect::DlgConnect(QWidget *parent) : QDialog(parent) updateDisplayInfo(previousHosts->currentText()); btnForgotPassword = new QPushButton(this); - btnForgotPassword->setIcon(loadThemedIcon(QStringLiteral("icons/forgot_password"))); + btnForgotPassword->setIcon(themePixmap(QStringLiteral("icons/forgot_password"))); btnForgotPassword->setToolTip(tr("Reset Password")); btnForgotPassword->setFixedWidth(30); connect(btnForgotPassword, &QPushButton::released, this, &DlgConnect::actForgotPassword); diff --git a/cockatrice/src/interface/widgets/dialogs/dlg_edit_tokens.cpp b/cockatrice/src/interface/widgets/dialogs/dlg_edit_tokens.cpp index b06481c4b..3845029f7 100644 --- a/cockatrice/src/interface/widgets/dialogs/dlg_edit_tokens.cpp +++ b/cockatrice/src/interface/widgets/dialogs/dlg_edit_tokens.cpp @@ -1,6 +1,6 @@ #include "dlg_edit_tokens.h" -#include "../../theme_icon.h" +#include "../../pixel_map_generator.h" #include "../interface/widgets/utility/get_text_with_max.h" #include "card_database_display_model.h" #include "libcockatrice/card/card_info.h" @@ -108,10 +108,10 @@ DlgEditTokens::DlgEditTokens(QWidget *parent) : QDialog(parent), currentCard(nul &DlgEditTokens::tokenSelectionChanged); QAction *aAddToken = new QAction(tr("Add token"), this); - aAddToken->setIcon(loadThemedIcon(QStringLiteral("icons/increment"))); + aAddToken->setIcon(themePixmap(QStringLiteral("icons/increment"))); connect(aAddToken, &QAction::triggered, this, &DlgEditTokens::actAddToken); QAction *aRemoveToken = new QAction(tr("Remove token"), this); - aRemoveToken->setIcon(loadThemedIcon(QStringLiteral("icons/decrement"))); + aRemoveToken->setIcon(themePixmap(QStringLiteral("icons/decrement"))); connect(aRemoveToken, &QAction::triggered, this, &DlgEditTokens::actRemoveToken); auto *databaseToolBar = new QToolBar; diff --git a/cockatrice/src/interface/widgets/dialogs/dlg_manage_sets.cpp b/cockatrice/src/interface/widgets/dialogs/dlg_manage_sets.cpp index c6e640a9f..caca5da92 100644 --- a/cockatrice/src/interface/widgets/dialogs/dlg_manage_sets.cpp +++ b/cockatrice/src/interface/widgets/dialogs/dlg_manage_sets.cpp @@ -1,7 +1,7 @@ #include "dlg_manage_sets.h" #include "../../../client/settings/cache_settings.h" -#include "../../theme_icon.h" +#include "../../pixel_map_generator.h" #include "../interface/card_picture_loader/card_picture_loader.h" #include "../interface/widgets/utility/custom_line_edit.h" #include "libcockatrice/card/database/card_database.h" @@ -58,28 +58,28 @@ WndSets::WndSets(QWidget *parent) : QMainWindow(parent) setsEditToolBar->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Minimum); aTop = new QAction(QString(), this); - aTop->setIcon(loadThemedIcon(QStringLiteral("icons/arrow_to_top"))); + aTop->setIcon(themePixmap(QStringLiteral("icons/arrow_top_green"))); aTop->setToolTip(tr("Move selected set to the top")); aTop->setEnabled(false); connect(aTop, &QAction::triggered, this, &WndSets::actTop); setsEditToolBar->addAction(aTop); aUp = new QAction(QString(), this); - aUp->setIcon(loadThemedIcon(QStringLiteral("icons/arrow_up"))); + aUp->setIcon(themePixmap(QStringLiteral("icons/arrow_up_green"))); aUp->setToolTip(tr("Move selected set up")); aUp->setEnabled(false); connect(aUp, &QAction::triggered, this, &WndSets::actUp); setsEditToolBar->addAction(aUp); aDown = new QAction(QString(), this); - aDown->setIcon(loadThemedIcon(QStringLiteral("icons/arrow_down"))); + aDown->setIcon(themePixmap(QStringLiteral("icons/arrow_down_green"))); aDown->setToolTip(tr("Move selected set down")); aDown->setEnabled(false); connect(aDown, &QAction::triggered, this, &WndSets::actDown); setsEditToolBar->addAction(aDown); aBottom = new QAction(QString(), this); - aBottom->setIcon(loadThemedIcon(QStringLiteral("icons/arrow_to_bottom"))); + aBottom->setIcon(themePixmap(QStringLiteral("icons/arrow_bottom_green"))); aBottom->setToolTip(tr("Move selected set to the bottom")); aBottom->setEnabled(false); connect(aBottom, &QAction::triggered, this, &WndSets::actBottom); @@ -89,7 +89,7 @@ WndSets::WndSets(QWidget *parent) : QMainWindow(parent) searchField = new LineEditUnfocusable; searchField->setObjectName("searchEdit"); searchField->setPlaceholderText(tr("Search by set name, code, type, or release date")); - searchField->addAction(loadThemedIcon(QStringLiteral("icons/search")), LineEditUnfocusable::LeadingPosition); + searchField->addAction(themePixmap(QStringLiteral("icons/search")), LineEditUnfocusable::LeadingPosition); searchField->setClearButtonEnabled(true); setFocusProxy(searchField); diff --git a/cockatrice/src/interface/widgets/dialogs/dlg_register.cpp b/cockatrice/src/interface/widgets/dialogs/dlg_register.cpp index 09afee32d..9e32e87d1 100644 --- a/cockatrice/src/interface/widgets/dialogs/dlg_register.cpp +++ b/cockatrice/src/interface/widgets/dialogs/dlg_register.cpp @@ -1,7 +1,7 @@ #include "dlg_register.h" #include "../../../client/settings/cache_settings.h" -#include "../../theme_icon.h" +#include "../../pixel_map_generator.h" #include "../server/handle_public_servers.h" #include "../server/user/user_info_connection.h" @@ -34,7 +34,7 @@ DlgRegister::DlgRegister(QWidget *parent) : QDialog(parent) previousHosts = new QComboBox(this); btnDeleteServer = new QPushButton(this); - btnDeleteServer->setIcon(loadThemedIcon(QStringLiteral("icons/remove_row"))); + btnDeleteServer->setIcon(themePixmap(QStringLiteral("icons/remove_row"))); btnDeleteServer->setToolTip(tr("Delete the currently selected saved server")); btnDeleteServer->setFixedWidth(30); @@ -42,7 +42,7 @@ DlgRegister::DlgRegister(QWidget *parent) : QDialog(parent) hps = new HandlePublicServers(this); btnRefreshServers = new QPushButton(this); - btnRefreshServers->setIcon(loadThemedIcon(QStringLiteral("icons/sync"))); + btnRefreshServers->setIcon(themePixmap(QStringLiteral("icons/sync"))); btnRefreshServers->setToolTip(tr("Refresh the server list with known public servers")); btnRefreshServers->setFixedWidth(30); diff --git a/cockatrice/src/interface/widgets/dialogs/settings_view.cpp b/cockatrice/src/interface/widgets/dialogs/settings_view.cpp index b1a80c86b..005824995 100644 --- a/cockatrice/src/interface/widgets/dialogs/settings_view.cpp +++ b/cockatrice/src/interface/widgets/dialogs/settings_view.cpp @@ -6,7 +6,7 @@ #include "settings_view.h" #include "../../../client/settings/cache_settings.h" -#include "../../theme_icon.h" +#include "../../pixel_map_generator.h" #include "../interface/theme_manager.h" #include "../settings_page/abstract_settings_page.h" #include "../settings_page/appearance_settings_page.h" @@ -135,7 +135,7 @@ void SettingsView::setupUi() // Search bar searchEdit = new QLineEdit; searchEdit->setClearButtonEnabled(true); - searchEdit->addAction(loadThemedIcon(QStringLiteral("icons/search")), QLineEdit::LeadingPosition); + searchEdit->addAction(themePixmap(QStringLiteral("icons/search")), QLineEdit::LeadingPosition); searchEdit->installEventFilter(this); connect(searchEdit, &QLineEdit::textChanged, this, &SettingsView::onSearchTextChanged); searchEdit->setObjectName("settingsSearchEdit"); diff --git a/cockatrice/src/interface/widgets/printing_selector/printing_selector_card_overlay_widget.cpp b/cockatrice/src/interface/widgets/printing_selector/printing_selector_card_overlay_widget.cpp index a5177a6bc..94acdbdce 100644 --- a/cockatrice/src/interface/widgets/printing_selector/printing_selector_card_overlay_widget.cpp +++ b/cockatrice/src/interface/widgets/printing_selector/printing_selector_card_overlay_widget.cpp @@ -2,8 +2,6 @@ #include "../../../client/settings/cache_settings.h" #include "../../card_picture_loader/card_picture_loader.h" -#include "../../theme_icon.h" -#include "../../theme_manager.h" #include "../cards/card_info_picture_widget.h" #include "../tabs/abstract_tab_deck_editor.h" #include "all_zones_card_amount_widget.h" @@ -14,19 +12,16 @@ #include #include -#include #include #include #include #include +#include #include #include #include #include #include -#include -#include -#include #include #include #include @@ -41,6 +36,7 @@ #include #include #include +#include #include #include #include @@ -124,12 +120,6 @@ PrintingSelectorCardOverlayWidget::PrintingSelectorCardOverlayWidget(QWidget *pa mainLayout->addWidget(cardInfoPicture); initializePinBadge(); - auto refreshPinBadge = [this] { - initializePinBadge(); - updatePinBadgeVisibility(); - }; - connect(themeManager, &ThemeManager::paletteChanged, this, refreshPinBadge); - connect(themeManager, &ThemeManager::themeChanged, this, refreshPinBadge); // Parent the preview to this overlay so it is destroyed with it (Qt::ToolTip keeps // it a frameless, non-activating top-level window despite the parent). @@ -411,55 +401,10 @@ void PrintingSelectorCardOverlayWidget::customMenu(QPoint point) } /** - * @brief Composes the pin badge: the glyph over a rounded palette backdrop. + * @brief Initializes the pin badge overlay and loads its icon with DPI-aware rasterization. * - * The badge floats on card art rather than on a palette surface, so a bare glyph - * colored WindowText is not enough: a thin dark stroke disappears on dark art in a - * light palette, and a thin light stroke disappears on light art in a dark palette. - * A Window-filled rounded rect with a WindowText outline gives the glyph a solid - * backing, and the outline guarantees the badge edge contrasts with the art even when - * the fill matches it. Both roles follow the palette, like the glyph. - * - * @param logicalSize The badge's size in logical pixels. - * @param devicePixelRatio The label's device pixel ratio, applied to the pixmap so it - * stays crisp on HiDPI displays. - * @param palette The palette the glyph's WindowText resolves against. - */ -static QPixmap composePinBadgePixmap(const QSize &logicalSize, qreal devicePixelRatio, const QPalette &palette) -{ - // Inset the glyph so the backdrop reads as a badge around it, not as a border. - const int inset = qRound(logicalSize.width() * 0.16); - const QSize glyphSize = logicalSize - QSize(inset * 2, inset * 2); - const QPixmap glyph = loadThemedIcon(QStringLiteral("icons/pin")).pixmap(glyphSize, devicePixelRatio); - if (glyph.isNull()) { - // Let the caller fall back to the textual badge instead of drawing an empty panel. - return QPixmap(); - } - - QPixmap badge(logicalSize * devicePixelRatio); - badge.setDevicePixelRatio(devicePixelRatio); - badge.fill(Qt::transparent); - - QPainter painter(&badge); - painter.setRenderHint(QPainter::Antialiasing); - - // Half-pixel inset keeps the 1.5px outline inside the pixmap instead of clipping it. - const QRectF box(QPointF(0.5, 0.5), QSizeF(logicalSize) - QSizeF(1, 1)); - painter.setPen(QPen(palette.color(QPalette::WindowText), 1.5)); - painter.setBrush(palette.color(QPalette::Window)); - painter.drawRoundedRect(box, 8, 8); - painter.drawPixmap(QPointF(inset, inset), glyph); - - return badge; -} - -/** - * @brief Initializes the pin badge overlay and loads a DPI-aware icon pixmap. - * - * The glyph is drawn onto a rounded palette backdrop so the badge stays readable on - * card art, and it is rasterized at the label's device pixel ratio so it stays crisp on - * HiDPI displays. It is re-rendered when the palette or theme changes so the badge - * follows the surrounding UI instead of freezing at its first colors. + * The icon is rasterized once using the label's device pixel ratio so it stays crisp on HiDPI + * displays. The resulting pixmap is cached on the QLabel and simply shown/hidden when needed. * If the SVG cannot be read, a textual fallback badge is created instead. */ void PrintingSelectorCardOverlayWidget::initializePinBadge() @@ -475,13 +420,24 @@ void PrintingSelectorCardOverlayWidget::initializePinBadge() } bool pinLoaded = false; - const QSize targetSize(64, 64); - const QPixmap pinPix = composePinBadgePixmap(targetSize, pinBadge->devicePixelRatioF(), pinBadge->palette()); - if (!pinPix.isNull()) { - pinBadge->setPixmap(pinPix); - pinBadge->setFixedSize(targetSize); - pinBadge->setStyleSheet(QStringLiteral("background: transparent;")); - pinLoaded = true; + QImageReader pinReader(QStringLiteral("theme:icons/pin")); + + if (pinReader.canRead()) { + // Rasterize a 64×64 logical icon so it has a consistent size regardless of card scaling. + const QSize targetSize(64, 64); + const qreal dpr = pinBadge->devicePixelRatioF(); + const QSize rasterSize(qMax(1, qCeil(targetSize.width() * dpr)), qMax(1, qCeil(targetSize.height() * dpr))); + pinReader.setScaledSize(rasterSize); + const QImage pinImage = pinReader.read(); + if (!pinImage.isNull()) { + // Tag the pixmap with the same DPR so Qt renders it at the correct physical size. + QPixmap pinPix = QPixmap::fromImage(pinImage); + pinPix.setDevicePixelRatio(dpr); + pinBadge->setPixmap(pinPix); + pinBadge->setFixedSize(targetSize); + pinBadge->setStyleSheet(QStringLiteral("background: transparent;")); + pinLoaded = true; + } } if (!pinLoaded) { diff --git a/cockatrice/src/interface/widgets/quick_settings/settings_button_widget.cpp b/cockatrice/src/interface/widgets/quick_settings/settings_button_widget.cpp index c6a6d989f..ea0d2db5a 100644 --- a/cockatrice/src/interface/widgets/quick_settings/settings_button_widget.cpp +++ b/cockatrice/src/interface/widgets/quick_settings/settings_button_widget.cpp @@ -1,12 +1,12 @@ #include "settings_button_widget.h" -#include "../../theme_icon.h" +#include "../../pixel_map_generator.h" #include "settings_popup_widget.h" #include #include -#include #include +#include #include #include #include @@ -21,7 +21,7 @@ SettingsButtonWidget::SettingsButtonWidget(QWidget *parent) : QWidget(parent), button(new QToolButton(this)), popup(new SettingsPopupWidget(nullptr)) { - button->setIcon(loadThemedIcon(QStringLiteral("icons/settings"))); + button->setIcon(themePixmap(QStringLiteral("icons/cogwheel"))); button->setCheckable(true); button->setFixedSize(32, 32); connect(button, &QToolButton::clicked, this, &SettingsButtonWidget::togglePopup); @@ -49,9 +49,9 @@ void SettingsButtonWidget::removeSettingsWidget(QWidget *toRemove) const popup->removeSettingsWidget(toRemove); } -void SettingsButtonWidget::setButtonIcon(const QIcon &icon) +void SettingsButtonWidget::setButtonIcon(QPixmap iconMap) { - button->setIcon(icon); + button->setIcon(iconMap); } void SettingsButtonWidget::setButtonText(const QString &text) diff --git a/cockatrice/src/interface/widgets/quick_settings/settings_button_widget.h b/cockatrice/src/interface/widgets/quick_settings/settings_button_widget.h index 56d542d37..015300bf8 100644 --- a/cockatrice/src/interface/widgets/quick_settings/settings_button_widget.h +++ b/cockatrice/src/interface/widgets/quick_settings/settings_button_widget.h @@ -13,7 +13,7 @@ #include class QHBoxLayout; -class QIcon; +class QPixmap; class QToolButton; class SettingsPopupWidget; @@ -28,7 +28,7 @@ public: void addSettingsWidget(QWidget *toAdd) const; void removeSettingsWidget(QWidget *toRemove) const; - void setButtonIcon(const QIcon &icon); + void setButtonIcon(QPixmap iconMap); void setButtonText(const QString &buttonText); void setCompact(bool compact); bool isCompact() const diff --git a/cockatrice/src/interface/widgets/replay/replay_event_index.cpp b/cockatrice/src/interface/widgets/replay/replay_event_index.cpp deleted file mode 100644 index d21671b5a..000000000 --- a/cockatrice/src/interface/widgets/replay/replay_event_index.cpp +++ /dev/null @@ -1,876 +0,0 @@ -#include "replay_event_index.h" - -#include "libcockatrice/protocol/get_pb_extension.h" -#include "libcockatrice/protocol/pb/card_attributes.pb.h" -#include "libcockatrice/protocol/pb/context_concede.pb.h" -#include "libcockatrice/protocol/pb/context_connection_state_changed.pb.h" -#include "libcockatrice/protocol/pb/context_deck_select.pb.h" -#include "libcockatrice/protocol/pb/context_mulligan.pb.h" -#include "libcockatrice/protocol/pb/context_ready_start.pb.h" -#include "libcockatrice/protocol/pb/context_set_sideboard_lock.pb.h" -#include "libcockatrice/protocol/pb/context_undo_draw.pb.h" -#include "libcockatrice/protocol/pb/event_attach_card.pb.h" -#include "libcockatrice/protocol/pb/event_change_zone_properties.pb.h" -#include "libcockatrice/protocol/pb/event_create_counter.pb.h" -#include "libcockatrice/protocol/pb/event_create_token.pb.h" -#include "libcockatrice/protocol/pb/event_del_counter.pb.h" -#include "libcockatrice/protocol/pb/event_destroy_card.pb.h" -#include "libcockatrice/protocol/pb/event_draw_cards.pb.h" -#include "libcockatrice/protocol/pb/event_dump_zone.pb.h" -#include "libcockatrice/protocol/pb/event_flip_card.pb.h" -#include "libcockatrice/protocol/pb/event_game_closed.pb.h" -#include "libcockatrice/protocol/pb/event_game_host_changed.pb.h" -#include "libcockatrice/protocol/pb/event_game_log_notice.pb.h" -#include "libcockatrice/protocol/pb/event_game_say.pb.h" -#include "libcockatrice/protocol/pb/event_game_state_changed.pb.h" -#include "libcockatrice/protocol/pb/event_join.pb.h" -#include "libcockatrice/protocol/pb/event_kicked.pb.h" -#include "libcockatrice/protocol/pb/event_leave.pb.h" -#include "libcockatrice/protocol/pb/event_move_card.pb.h" -#include "libcockatrice/protocol/pb/event_player_properties_changed.pb.h" -#include "libcockatrice/protocol/pb/event_reveal_cards.pb.h" -#include "libcockatrice/protocol/pb/event_reverse_turn.pb.h" -#include "libcockatrice/protocol/pb/event_roll_die.pb.h" -#include "libcockatrice/protocol/pb/event_set_active_phase.pb.h" -#include "libcockatrice/protocol/pb/event_set_active_player.pb.h" -#include "libcockatrice/protocol/pb/event_set_card_attr.pb.h" -#include "libcockatrice/protocol/pb/event_set_card_counter.pb.h" -#include "libcockatrice/protocol/pb/event_set_counter.pb.h" -#include "libcockatrice/protocol/pb/event_shuffle.pb.h" -#include "libcockatrice/protocol/pb/game_event.pb.h" -#include "libcockatrice/protocol/pb/game_event_container.pb.h" -#include "libcockatrice/protocol/pb/game_event_context.pb.h" -#include "libcockatrice/protocol/pb/game_replay.pb.h" -#include "libcockatrice/protocol/pb/serverinfo_card.pb.h" -#include "libcockatrice/protocol/pb/serverinfo_counter.pb.h" -#include "libcockatrice/protocol/pb/serverinfo_player.pb.h" -#include "libcockatrice/protocol/pb/serverinfo_playerproperties.pb.h" -#include "libcockatrice/protocol/pb/serverinfo_zone.pb.h" -#include "libcockatrice/utility/zone_names.h" - -#include -#include -#include -#include -#include -#include - -namespace -{ -struct PendingDetail -{ - QString text; - QString badge; - ReplayEventKind kind = ReplayEventKind::Action; - QString actor; -}; - -QString formatTime(int timeMs) -{ - const int totalSeconds = qMax(0, timeMs) / 1000; - const int hours = totalSeconds / 3600; - const int minutes = (totalSeconds / 60) % 60; - const int seconds = totalSeconds % 60; - - if (hours > 0) { - return QStringLiteral("%1:%2:%3") - .arg(hours) - .arg(minutes, 2, 10, QLatin1Char('0')) - .arg(seconds, 2, 10, QLatin1Char('0')); - } - - return QStringLiteral("%1:%2").arg(minutes, 2, 10, QLatin1Char('0')).arg(seconds, 2, 10, QLatin1Char('0')); -} - -class ReplayEventIndexBuilder -{ - Q_DECLARE_TR_FUNCTIONS(ReplayEventIndex) - -public: - ReplayEventIndexBuilder(const GameReplay *replay, const QList &timeline) : replay(replay), timeline(timeline) - { - } - - QList build() - { - QList entries; - - for (int i = 0; i < replay->event_list_size(); ++i) { - const GameEventContainer &container = replay->event_list(i); - const QList details = describeContainer(container); - if (details.isEmpty()) { - continue; - } - - ReplayEventEntry entry; - entry.eventIndex = i; - entry.timeMs = timeline.value(i, static_cast(container.seconds_elapsed()) * 1000); - entry.timeLabel = formatTime(entry.timeMs); - entry.kind = details.first().kind; - entry.badge = details.first().badge; - if (details.size() > 1) { - entry.badge += QStringLiteral(" +%1").arg(details.size() - 1); - } - - const QString sharedActor = details.first().actor; - const bool mixedActors = std::any_of(details.cbegin(), details.cend(), [&sharedActor](const auto &detail) { - return detail.actor != sharedActor; - }); - - QStringList detailTexts; - for (const PendingDetail &detail : details) { - if (mixedActors && !detail.actor.isEmpty()) { - detailTexts.append(tr("%1: %2").arg(detail.actor, detail.text)); - } else { - detailTexts.append(detail.text); - } - } - entry.actor = mixedActors ? QString() : sharedActor; - entry.detail = detailTexts.join(QStringLiteral(" • ")); - - entries.append(entry); - } - - return entries; - } - -private: - const GameReplay *replay; - const QList &timeline; - - QHash playerNames; - QHash spectators; - QHash cardNames; - QHash counterNames; - - int activePlayerId = -1; - bool gameStarted = false; - bool sawGameState = false; - - static QString cardKey(int playerId, const QString &zone, int cardId) - { - return QStringLiteral("%1/%2/%3").arg(playerId).arg(zone).arg(cardId); - } - - static QString counterKey(int playerId, int counterId) - { - return QStringLiteral("%1/%2").arg(playerId).arg(counterId); - } - - QString playerName(int playerId) const - { - if (playerId < 0) { - return tr("Game"); - } - return playerNames.value(playerId, tr("Player %1").arg(playerId)); - } - - QString actorName(int playerId) const - { - if (playerId < 0) { - return QString(); - } - return playerNames.value(playerId, tr("Player %1").arg(playerId)); - } - - bool isSpectator(int playerId) const - { - return spectators.value(playerId, false); - } - - void rememberPlayer(const ServerInfo_PlayerProperties &properties) - { - const int playerId = properties.player_id(); - if (playerId < 0) { - return; - } - - const QString name = QString::fromStdString(properties.user_info().name()); - if (!name.isEmpty()) { - playerNames.insert(playerId, name); - } - spectators.insert(playerId, properties.spectator()); - } - - void rememberCard(int playerId, const QString &zone, int cardId, const QString &name) - { - if (cardId >= 0 && !name.isEmpty()) { - cardNames.insert(cardKey(playerId, zone, cardId), name); - } - } - - void forgetCard(int playerId, const QString &zone, int cardId) - { - if (cardId >= 0) { - cardNames.remove(cardKey(playerId, zone, cardId)); - } - } - - QString cardName(int playerId, const QString &zone, int cardId) const - { - if (cardId < 0) { - return QString(); - } - return cardNames.value(cardKey(playerId, zone, cardId)); - } - - void rememberCounter(int playerId, const ServerInfo_Counter &counter) - { - const QString name = QString::fromStdString(counter.name()); - if (!name.isEmpty()) { - counterNames.insert(counterKey(playerId, counter.id()), name); - } - } - - QString counterName(int playerId, int counterId) const - { - return counterNames.value(counterKey(playerId, counterId)); - } - - QString zoneName(const QString &zone) const - { - if (zone == ZoneNames::TABLE) { - return tr("battlefield"); - } - if (zone == ZoneNames::GRAVE) { - return tr("graveyard"); - } - if (zone == ZoneNames::EXILE) { - return tr("exile"); - } - if (zone == ZoneNames::HAND) { - return tr("hand"); - } - if (zone == ZoneNames::DECK) { - return tr("library"); - } - if (zone == ZoneNames::SIDEBOARD) { - return tr("sideboard"); - } - if (zone == ZoneNames::STACK) { - return tr("stack"); - } - return zone; - } - - QString zonePhrase(int playerId, const QString &zone, int actorId) const - { - if (zone == ZoneNames::TABLE) { - return tr("the battlefield"); - } - if (zone == ZoneNames::EXILE) { - return tr("exile"); - } - if (zone == ZoneNames::STACK) { - return tr("the stack"); - } - - const QString name = zoneName(zone); - if (playerId < 0 || playerId == actorId) { - return tr("their %1").arg(name); - } - return tr("%1's %2").arg(playerName(playerId), name); - } - - QString formatCardList(const QStringList &names, int total) const - { - // Keep repeats: each entry is a drawn/revealed card, so the list length must agree with the count. - QStringList listedNames; - for (const QString &name : names) { - if (!name.isEmpty()) { - listedNames.append(name); - } - } - - if (listedNames.isEmpty()) { - if (total <= 0) { - return tr("no cards"); - } - return total == 1 ? tr("1 card") : tr("%1 cards").arg(total); - } - - if (total < listedNames.size()) { - total = listedNames.size(); - } - - if (listedNames.size() > 3) { - return tr("%1 and %2 more").arg(listedNames.mid(0, 3).join(QStringLiteral(", "))).arg(total - 3); - } - if (total > listedNames.size()) { - return tr("%1 and %2 more").arg(listedNames.join(QStringLiteral(", "))).arg(total - listedNames.size()); - } - - return listedNames.join(QStringLiteral(", ")); - } - - void updateGameState(const Event_GameStateChanged &event) - { - for (int i = 0; i < event.player_list_size(); ++i) { - const ServerInfo_Player &player = event.player_list(i); - const int playerId = player.properties().player_id(); - rememberPlayer(player.properties()); - - for (int j = 0; j < player.zone_list_size(); ++j) { - const ServerInfo_Zone &zone = player.zone_list(j); - for (int k = 0; k < zone.card_list_size(); ++k) { - const ServerInfo_Card &card = zone.card_list(k); - rememberCard(playerId, QString::fromStdString(zone.name()), card.id(), - QString::fromStdString(card.name())); - } - } - - for (int j = 0; j < player.counter_list_size(); ++j) { - rememberCounter(playerId, player.counter_list(j)); - } - } - - if (event.has_active_player_id()) { - activePlayerId = event.active_player_id(); - } - if (event.has_game_started()) { - gameStarted = event.game_started(); - } - } - - QString describeMoveCard(const Event_MoveCard &event, int actorId, const GameEventContext &context) - { - const int startPlayerId = event.start_player_id() >= 0 ? event.start_player_id() : actorId; - const int targetPlayerId = event.target_player_id() >= 0 ? event.target_player_id() : startPlayerId; - const QString startZone = QString::fromStdString(event.start_zone()); - const QString targetZone = event.has_target_zone() ? QString::fromStdString(event.target_zone()) : startZone; - - QString name; - if (event.has_card_name()) { - name = QString::fromStdString(event.card_name()); - } - if (name.isEmpty()) { - name = cardName(startPlayerId, startZone, event.card_id()); - } - - const bool knownName = !name.isEmpty(); - if (name.isEmpty()) { - name = event.face_down() ? tr("a face-down card") : tr("a card"); - } - - const int newCardId = event.has_new_card_id() ? event.new_card_id() : event.card_id(); - forgetCard(startPlayerId, startZone, event.card_id()); - if (knownName) { - rememberCard(targetPlayerId, targetZone, newCardId, name); - } - - if (startZone == targetZone) { - return tr("moved %1 within %2.").arg(name, zonePhrase(targetPlayerId, targetZone, actorId)); - } - if (context.HasExtension(Context_UndoDraw::ext)) { - return tr("returned %1 to %2.").arg(name, zonePhrase(targetPlayerId, targetZone, actorId)); - } - - return tr("moved %1 from %2 to %3.") - .arg(name, zonePhrase(startPlayerId, startZone, actorId), zonePhrase(targetPlayerId, targetZone, actorId)); - } - - QString describeSetCardAttribute(const Event_SetCardAttr &event, int actorId) - { - const QString zone = QString::fromStdString(event.zone_name()); - QString name; - if (event.has_card_id()) { - name = cardName(actorId, zone, event.card_id()); - } - if (name.isEmpty()) { - name = event.has_card_id() ? tr("a card") : tr("all cards"); - } - - const QString value = QString::fromStdString(event.attr_value()); - switch (event.attribute()) { - case AttrTapped: - return value == QLatin1String("1") ? tr("tapped %1.").arg(name) : tr("untapped %1.").arg(name); - case AttrAttacking: - return value == QLatin1String("1") ? tr("%1 is now attacking.").arg(name) - : tr("%1 is no longer attacking.").arg(name); - case AttrFaceDown: - return value == QLatin1String("1") ? tr("turned %1 face down.").arg(name) - : tr("turned %1 face up.").arg(name); - case AttrColor: - return tr("changed %1's color to %2.").arg(name, value); - case AttrPT: - return tr("set %1's power/toughness to %2.").arg(name, value); - case AttrAnnotation: - return value.isEmpty() ? tr("cleared %1's annotation.").arg(name) - : tr("annotated %1: %2").arg(name, value); - case AttrDoesntUntap: - return value == QLatin1String("1") ? tr("%1 will no longer untap.").arg(name) - : tr("%1 will untap normally again.").arg(name); - } - - return tr("changed %1.").arg(name); - } - - QString badgeForCardAttribute(CardAttribute attribute) const - { - switch (attribute) { - case AttrTapped: - return tr("TAP"); - case AttrAttacking: - return tr("ATTACK"); - case AttrFaceDown: - return tr("FLIP"); - case AttrColor: - return tr("COLOR"); - case AttrPT: - return tr("PT"); - case AttrAnnotation: - return tr("NOTE"); - case AttrDoesntUntap: - return tr("TAP"); - } - return tr("CARD"); - } - - QString describeLeaveReason(Event_Leave::LeaveReason reason) const - { - switch (reason) { - case Event_Leave::USER_KICKED: - return tr("kicked by game host or moderator"); - case Event_Leave::USER_LEFT: - return tr("left the game"); - case Event_Leave::USER_DISCONNECTED: - return tr("disconnected"); - case Event_Leave::OTHER: - default: - return tr("reason unknown"); - } - } - - void addDetail(QList &details, - const QString &text, - const QString &badge, - ReplayEventKind kind, - int actorId, - bool forcedByJudge) const - { - if (text.isEmpty()) { - return; - } - - const QString finalText = forcedByJudge ? tr("%1 (judge)").arg(text) : text; - details.append(PendingDetail{.text = finalText, .badge = badge, .kind = kind, .actor = actorName(actorId)}); - } - - QList describeContainer(const GameEventContainer &container) - { - QList details; - const GameEventContext &context = container.context(); - const bool forcedByJudge = container.has_forced_by_judge(); - const auto contextType = static_cast(getPbExtension(context)); - - for (int i = 0; i < container.event_list_size(); ++i) { - const GameEvent &event = container.event_list(i); - const int eventPlayerId = event.player_id(); - const auto eventType = static_cast(getPbExtension(event)); - - switch (eventType) { - case GameEvent::GAME_STATE_CHANGED: { - const Event_GameStateChanged &state = event.GetExtension(Event_GameStateChanged::ext); - const bool wasStarted = gameStarted; - updateGameState(state); - if (state.game_started() && !wasStarted) { - addDetail(details, tr("Game started."), tr("GAME"), ReplayEventKind::System, -1, forcedByJudge); - } else if (!sawGameState) { - addDetail(details, tr("Game state loaded."), tr("GAME"), ReplayEventKind::System, -1, - forcedByJudge); - } - sawGameState = true; - break; - } - case GameEvent::PLAYER_PROPERTIES_CHANGED: { - const Event_PlayerPropertiesChanged &propertiesEvent = - event.GetExtension(Event_PlayerPropertiesChanged::ext); - const bool hasProperties = propertiesEvent.has_player_properties(); - const ServerInfo_PlayerProperties &properties = propertiesEvent.player_properties(); - // The server only fills in the field that changed, so player_id is absent from these events - // unless they are about a specific player; fall back to the event's own player id. - const bool describesPlayer = hasProperties && properties.has_player_id(); - const int playerId = describesPlayer ? properties.player_id() : eventPlayerId; - if (describesPlayer) { - rememberPlayer(properties); - } - - switch (contextType) { - case GameEventContext::READY_START: - addDetail(details, - properties.ready_start() ? tr("is ready to start.") : tr("is no longer ready."), - tr("READY"), ReplayEventKind::System, playerId, forcedByJudge); - break; - case GameEventContext::CONCEDE: - addDetail(details, tr("conceded the game."), tr("CONCEDE"), ReplayEventKind::System, - playerId, forcedByJudge); - break; - case GameEventContext::UNCONCEDE: - addDetail(details, tr("unconceded the game."), tr("CONCEDE"), ReplayEventKind::System, - playerId, forcedByJudge); - break; - case GameEventContext::DECK_SELECT: { - const Context_DeckSelect &deckSelect = context.GetExtension(Context_DeckSelect::ext); - const int sideboardSize = deckSelect.sideboard_size(); - addDetail(details, - sideboardSize >= 0 - ? tr("selected a deck with %1 sideboard cards.").arg(sideboardSize) - : tr("selected a deck."), - tr("DECK"), ReplayEventKind::System, playerId, forcedByJudge); - break; - } - case GameEventContext::MULLIGAN: { - const Context_Mulligan &mulligan = context.GetExtension(Context_Mulligan::ext); - addDetail(details, - mulligan.number() > 0 ? tr("took a mulligan (%1).").arg(mulligan.number()) - : tr("took a mulligan."), - tr("MULLIGAN"), ReplayEventKind::System, playerId, forcedByJudge); - break; - } - case GameEventContext::SET_SIDEBOARD_LOCK: - addDetail(details, - properties.sideboard_locked() ? tr("locked their sideboard.") - : tr("unlocked their sideboard."), - tr("SIDEBOARD"), ReplayEventKind::System, playerId, forcedByJudge); - break; - case GameEventContext::CONNECTION_STATE_CHANGED: - addDetail(details, - properties.ping_seconds() != -1 ? tr("reconnected to the game.") - : tr("lost connection to the game."), - tr("CONNECTION"), ReplayEventKind::System, playerId, forcedByJudge); - break; - default: - break; - } - break; - } - case GameEvent::JOIN: { - const Event_Join &join = event.GetExtension(Event_Join::ext); - if (join.has_player_properties()) { - rememberPlayer(join.player_properties()); - } - const int playerId = join.player_properties().player_id() >= 0 - ? join.player_properties().player_id() - : eventPlayerId; - addDetail(details, isSpectator(playerId) ? tr("joined as a spectator.") : tr("joined the game."), - tr("JOIN"), ReplayEventKind::System, playerId, forcedByJudge); - break; - } - case GameEvent::LEAVE: { - const Event_Leave &leave = event.GetExtension(Event_Leave::ext); - const QString reason = describeLeaveReason(leave.reason()); - if (eventPlayerId < 0) { - addDetail(details, tr("A player left the game (%1).").arg(reason), tr("LEAVE"), - ReplayEventKind::System, -1, forcedByJudge); - } else { - addDetail(details, - isSpectator(eventPlayerId) ? tr("stopped spectating (%1).").arg(reason) - : tr("left the game (%1).").arg(reason), - tr("LEAVE"), ReplayEventKind::System, eventPlayerId, forcedByJudge); - } - break; - } - case GameEvent::KICKED: - addDetail(details, - eventPlayerId >= 0 ? tr("was kicked from the game.") - : tr("A player was kicked from the game."), - tr("KICK"), ReplayEventKind::System, eventPlayerId, forcedByJudge); - break; - case GameEvent::GAME_CLOSED: - addDetail(details, tr("Game closed."), tr("GAME"), ReplayEventKind::System, -1, forcedByJudge); - break; - case GameEvent::GAME_HOST_CHANGED: - addDetail(details, eventPlayerId >= 0 ? tr("is now the game host.") : tr("The game host changed."), - tr("HOST"), ReplayEventKind::System, eventPlayerId, forcedByJudge); - break; - case GameEvent::GAME_SAY: { - const Event_GameSay &say = event.GetExtension(Event_GameSay::ext); - addDetail(details, QString::fromStdString(say.message()), tr("CHAT"), ReplayEventKind::Chat, - eventPlayerId, forcedByJudge); - break; - } - case GameEvent::SET_ACTIVE_PLAYER: { - const Event_SetActivePlayer &setActivePlayer = event.GetExtension(Event_SetActivePlayer::ext); - activePlayerId = setActivePlayer.active_player_id(); - if (activePlayerId >= 0) { - addDetail(details, tr("begins their turn."), tr("TURN"), ReplayEventKind::Turn, activePlayerId, - forcedByJudge); - } - break; - } - case GameEvent::SET_ACTIVE_PHASE: { - const Event_SetActivePhase &setActivePhase = event.GetExtension(Event_SetActivePhase::ext); - if (setActivePhase.phase() >= 0) { - const QString phaseName = Phases::getPhase(setActivePhase.phase()).getName(); - addDetail(details, tr("entered the %1 phase.").arg(phaseName), tr("PHASE"), - ReplayEventKind::Phase, activePlayerId >= 0 ? activePlayerId : eventPlayerId, - forcedByJudge); - } - break; - } - case GameEvent::REVERSE_TURN: { - const Event_ReverseTurn &reverseTurn = event.GetExtension(Event_ReverseTurn::ext); - addDetail(details, - reverseTurn.reversed() ? tr("reversed the turn order.") : tr("restored the turn order."), - tr("TURN"), ReplayEventKind::Turn, eventPlayerId, forcedByJudge); - break; - } - case GameEvent::MOVE_CARD: { - const Event_MoveCard &moveCard = event.GetExtension(Event_MoveCard::ext); - const QString text = describeMoveCard(moveCard, eventPlayerId, context); - const QString startZone = QString::fromStdString(moveCard.start_zone()); - const QString targetZone = QString::fromStdString(moveCard.target_zone()); - QString badge = tr("MOVE"); - if (context.HasExtension(Context_UndoDraw::ext)) { - badge = tr("UNDO"); - } else if (startZone == ZoneNames::STACK) { - badge = tr("RESOLVE"); - } else if (startZone == ZoneNames::HAND && targetZone == ZoneNames::TABLE) { - badge = tr("PLAY"); - } - addDetail(details, text, badge, ReplayEventKind::Action, eventPlayerId, forcedByJudge); - break; - } - case GameEvent::DRAW_CARDS: { - const Event_DrawCards &drawCards = event.GetExtension(Event_DrawCards::ext); - QStringList names; - for (int j = 0; j < drawCards.cards_size(); ++j) { - const ServerInfo_Card &card = drawCards.cards(j); - const QString name = QString::fromStdString(card.name()); - names.append(name); - rememberCard(eventPlayerId, ZoneNames::HAND, card.id(), name); - } - if (drawCards.number() > 0) { - addDetail(details, tr("drew %1.").arg(formatCardList(names, drawCards.number())), tr("DRAW"), - ReplayEventKind::Action, eventPlayerId, forcedByJudge); - } - break; - } - case GameEvent::REVEAL_CARDS: { - const Event_RevealCards &revealCards = event.GetExtension(Event_RevealCards::ext); - const QString zone = QString::fromStdString(revealCards.zone_name()); - QStringList names; - for (int j = 0; j < revealCards.cards_size(); ++j) { - const ServerInfo_Card &card = revealCards.cards(j); - const QString name = QString::fromStdString(card.name()); - names.append(name); - rememberCard(eventPlayerId, zone, card.id(), name); - } - const int total = - revealCards.number_of_cards() > 0 ? revealCards.number_of_cards() : revealCards.cards_size(); - QString text = - tr("revealed %1 from %2.") - .arg(formatCardList(names, total), zonePhrase(eventPlayerId, zone, eventPlayerId)); - if (revealCards.other_player_id() >= 0 && revealCards.other_player_id() != eventPlayerId) { - text = tr("revealed %1 from %2 to %3.") - .arg(formatCardList(names, total), zonePhrase(eventPlayerId, zone, eventPlayerId), - playerName(revealCards.other_player_id())); - } - addDetail(details, text, tr("REVEAL"), ReplayEventKind::Action, eventPlayerId, forcedByJudge); - break; - } - case GameEvent::SHUFFLE: { - const Event_Shuffle &shuffle = event.GetExtension(Event_Shuffle::ext); - addDetail( - details, - tr("shuffled %1.") - .arg(zonePhrase(eventPlayerId, QString::fromStdString(shuffle.zone_name()), eventPlayerId)), - tr("SHUFFLE"), ReplayEventKind::Action, eventPlayerId, forcedByJudge); - break; - } - case GameEvent::ROLL_DIE: { - const Event_RollDie &rollDie = event.GetExtension(Event_RollDie::ext); - QStringList rolls; - for (int j = 0; j < rollDie.values_size(); ++j) { - rolls.append(QString::number(rollDie.values(j))); - } - if (rolls.isEmpty() && rollDie.value() > 0) { - rolls.append(QString::number(rollDie.value())); - } - const QString sides = rollDie.sides() > 0 ? QString::number(rollDie.sides()) : QStringLiteral("?"); - addDetail(details, tr("rolled a d%1: %2.").arg(sides, rolls.join(QStringLiteral(", "))), tr("DICE"), - ReplayEventKind::Action, eventPlayerId, forcedByJudge); - break; - } - case GameEvent::FLIP_CARD: { - const Event_FlipCard &flipCard = event.GetExtension(Event_FlipCard::ext); - const QString zone = QString::fromStdString(flipCard.zone_name()); - if (flipCard.has_card_name()) { - rememberCard(eventPlayerId, zone, flipCard.card_id(), - QString::fromStdString(flipCard.card_name())); - } - QString name = flipCard.has_card_name() ? QString::fromStdString(flipCard.card_name()) - : cardName(eventPlayerId, zone, flipCard.card_id()); - if (name.isEmpty()) { - name = tr("a card"); - } - addDetail(details, - flipCard.face_down() ? tr("flipped %1 face down.").arg(name) - : tr("flipped %1 face up.").arg(name), - tr("FLIP"), ReplayEventKind::Action, eventPlayerId, forcedByJudge); - break; - } - case GameEvent::DESTROY_CARD: { - const Event_DestroyCard &destroyCard = event.GetExtension(Event_DestroyCard::ext); - const QString zone = QString::fromStdString(destroyCard.zone_name()); - const QString name = cardName(eventPlayerId, zone, destroyCard.card_id()); - forgetCard(eventPlayerId, zone, destroyCard.card_id()); - addDetail(details, tr("destroyed %1.").arg(name.isEmpty() ? tr("a card") : name), tr("DESTROY"), - ReplayEventKind::Action, eventPlayerId, forcedByJudge); - break; - } - case GameEvent::ATTACH_CARD: { - const Event_AttachCard &attachCard = event.GetExtension(Event_AttachCard::ext); - const QString startZone = QString::fromStdString(attachCard.start_zone()); - const QString source = cardName(eventPlayerId, startZone, attachCard.card_id()); - const QString sourceText = source.isEmpty() ? tr("a card") : source; - // An unattach event carries no target at all, so the target fields default to player 0 / no zone - // / card 0; only a present target_player_id means the card was attached to something. - if (!attachCard.has_target_player_id()) { - addDetail(details, tr("unattached %1.").arg(sourceText), tr("ATTACH"), ReplayEventKind::Action, - eventPlayerId, forcedByJudge); - break; - } - const QString targetZone = QString::fromStdString(attachCard.target_zone()); - const int targetPlayerId = - attachCard.target_player_id() >= 0 ? attachCard.target_player_id() : eventPlayerId; - const QString target = cardName(targetPlayerId, targetZone, attachCard.target_card_id()); - addDetail(details, - tr("attached %1 to %2.").arg(sourceText, target.isEmpty() ? tr("a card") : target), - tr("ATTACH"), ReplayEventKind::Action, eventPlayerId, forcedByJudge); - break; - } - case GameEvent::CREATE_TOKEN: { - const Event_CreateToken &createToken = event.GetExtension(Event_CreateToken::ext); - const QString name = QString::fromStdString(createToken.card_name()); - rememberCard(eventPlayerId, QString::fromStdString(createToken.zone_name()), createToken.card_id(), - name); - const QString pt = QString::fromStdString(createToken.pt()); - addDetail( - details, - tr("created a token: %1%2.") - .arg(name.isEmpty() ? tr("token") : name, pt.isEmpty() ? QString() : tr(" (%1)").arg(pt)), - tr("TOKEN"), ReplayEventKind::Action, eventPlayerId, forcedByJudge); - break; - } - case GameEvent::SET_CARD_ATTR: { - const Event_SetCardAttr &setCardAttr = event.GetExtension(Event_SetCardAttr::ext); - addDetail(details, describeSetCardAttribute(setCardAttr, eventPlayerId), - badgeForCardAttribute(setCardAttr.attribute()), ReplayEventKind::Action, eventPlayerId, - forcedByJudge); - break; - } - case GameEvent::SET_CARD_COUNTER: { - const Event_SetCardCounter &setCardCounter = event.GetExtension(Event_SetCardCounter::ext); - const QString name = cardName(eventPlayerId, QString::fromStdString(setCardCounter.zone_name()), - setCardCounter.card_id()); - addDetail( - details, - tr("set a counter on %1 to %2.") - .arg(name.isEmpty() ? tr("a card") : name, QString::number(setCardCounter.counter_value())), - tr("COUNTER"), ReplayEventKind::Action, eventPlayerId, forcedByJudge); - break; - } - case GameEvent::CREATE_COUNTER: { - const Event_CreateCounter &createCounter = event.GetExtension(Event_CreateCounter::ext); - const ServerInfo_Counter &counter = createCounter.counter_info(); - rememberCounter(eventPlayerId, counter); - const QString name = QString::fromStdString(counter.name()); - addDetail(details, - tr("created the %1 counter (%2).") - .arg(name.isEmpty() ? tr("new") : name, QString::number(counter.count())), - tr("COUNTER"), ReplayEventKind::Action, eventPlayerId, forcedByJudge); - break; - } - case GameEvent::SET_COUNTER: { - const Event_SetCounter &setCounter = event.GetExtension(Event_SetCounter::ext); - const QString name = counterName(eventPlayerId, setCounter.counter_id()); - addDetail(details, - tr("set %1 to %2.") - .arg(name.isEmpty() ? tr("a counter") : tr("the %1 counter").arg(name), - QString::number(setCounter.value())), - tr("COUNTER"), ReplayEventKind::Action, eventPlayerId, forcedByJudge); - break; - } - case GameEvent::DEL_COUNTER: { - const Event_DelCounter &delCounter = event.GetExtension(Event_DelCounter::ext); - const QString name = counterName(eventPlayerId, delCounter.counter_id()); - counterNames.remove(counterKey(eventPlayerId, delCounter.counter_id())); - addDetail(details, - tr("removed %1.").arg(name.isEmpty() ? tr("a counter") : tr("the %1 counter").arg(name)), - tr("COUNTER"), ReplayEventKind::Action, eventPlayerId, forcedByJudge); - break; - } - case GameEvent::DUMP_ZONE: { - const Event_DumpZone &dumpZone = event.GetExtension(Event_DumpZone::ext); - const QString zoneOwnerPhrase = zonePhrase( - dumpZone.zone_owner_id(), QString::fromStdString(dumpZone.zone_name()), eventPlayerId); - if (dumpZone.number_cards() < 0) { - addDetail(details, tr("looked at %1.").arg(zoneOwnerPhrase), tr("LOOK"), - ReplayEventKind::Action, eventPlayerId, forcedByJudge); - } else { - addDetail(details, - tr("looked at %1 from %2.") - .arg(formatCardList(QStringList(), dumpZone.number_cards()), zoneOwnerPhrase), - tr("LOOK"), ReplayEventKind::Action, eventPlayerId, forcedByJudge); - } - break; - } - case GameEvent::CHANGE_ZONE_PROPERTIES: { - const Event_ChangeZoneProperties &changeZone = event.GetExtension(Event_ChangeZoneProperties::ext); - const QString zone = QString::fromStdString(changeZone.zone_name()); - QStringList changes; - if (changeZone.always_reveal_top_card()) { - changes.append(tr("top card revealed to all")); - } - if (changeZone.always_look_at_top_card()) { - changes.append(tr("top card visible to its owner")); - } - addDetail( - details, - changes.isEmpty() - ? tr("changed the properties of %1.").arg(zonePhrase(eventPlayerId, zone, eventPlayerId)) - : tr("changed %1: %2.") - .arg(zonePhrase(eventPlayerId, zone, eventPlayerId), - changes.join(QStringLiteral(", "))), - tr("ZONE"), ReplayEventKind::Action, eventPlayerId, forcedByJudge); - break; - } - case GameEvent::GAME_LOG_NOTICE: { - const Event_GameLogNotice ¬ice = event.GetExtension(Event_GameLogNotice::ext); - switch (notice.notice_type()) { - case Event_GameLogNotice::UNDO_DRAW_FAILED: - addDetail(details, tr("failed to undo a draw."), tr("NOTICE"), ReplayEventKind::System, - eventPlayerId, forcedByJudge); - break; - default: - addDetail(details, tr("A game notice was logged."), tr("NOTICE"), ReplayEventKind::System, - -1, forcedByJudge); - break; - } - break; - } - case GameEvent::CREATE_ARROW: - case GameEvent::DELETE_ARROW: - // Arrows are transient visual annotations and would make the history very noisy. - break; - default: - addDetail(details, tr("Unrecognized game event."), tr("EVENT"), ReplayEventKind::Action, - eventPlayerId, forcedByJudge); - break; - } - } - - return details; - } -}; -} // namespace - -QList buildReplayEventEntries(const GameReplay *replay, const QList &timeline) -{ - if (!replay) { - return {}; - } - - return ReplayEventIndexBuilder(replay, timeline).build(); -} diff --git a/cockatrice/src/interface/widgets/replay/replay_event_index.h b/cockatrice/src/interface/widgets/replay/replay_event_index.h deleted file mode 100644 index 40f72a965..000000000 --- a/cockatrice/src/interface/widgets/replay/replay_event_index.h +++ /dev/null @@ -1,43 +0,0 @@ -#ifndef COCKATRICE_REPLAY_EVENT_INDEX_H -#define COCKATRICE_REPLAY_EVENT_INDEX_H - -#include -#include - -class GameReplay; - -enum class ReplayEventKind -{ - Action, - Turn, - Phase, - Chat, - System -}; - -/** - * @brief One user-visible entry in the replay history sidebar. - * - * The event index refers to the corresponding GameEventContainer in the replay. It is used to seek the replay to that - * container. - */ -struct ReplayEventEntry -{ - int eventIndex = -1; - int timeMs = 0; - QString timeLabel; - QString actor; - QString badge; - QString detail; - ReplayEventKind kind = ReplayEventKind::Action; -}; - -/** - * @brief Builds a compact, human-readable index of the events in a replay. - * - * The returned entries copy all the data they need, so the caller does not need to keep the replay object alive after - * this function returns. - */ -QList buildReplayEventEntries(const GameReplay *replay, const QList &timeline); - -#endif // COCKATRICE_REPLAY_EVENT_INDEX_H diff --git a/cockatrice/src/interface/widgets/replay/replay_event_sidebar.cpp b/cockatrice/src/interface/widgets/replay/replay_event_sidebar.cpp deleted file mode 100644 index b5fab2794..000000000 --- a/cockatrice/src/interface/widgets/replay/replay_event_sidebar.cpp +++ /dev/null @@ -1,284 +0,0 @@ -#include "replay_event_sidebar.h" - -#include "replay_event_index.h" - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -namespace -{ -enum ItemDataRole -{ - EventIndexRole = Qt::UserRole + 1, - TimeLabelRole, - KindRole, - ActorRole, - BadgeRole, - DetailRole -}; - -QColor accentForKind(ReplayEventKind kind, const QPalette &palette) -{ - switch (kind) { - case ReplayEventKind::Turn: - return QColor(0x4c, 0xaf, 0x50); - case ReplayEventKind::Phase: - return QColor(0x42, 0xa5, 0xf5); - case ReplayEventKind::Chat: - return QColor(0xff, 0xb3, 0x00); - case ReplayEventKind::System: - return palette.mid().color(); - case ReplayEventKind::Action: - default: - return palette.highlight().color(); - } -} - -class ReplayEventDelegate : public QStyledItemDelegate -{ -public: - explicit ReplayEventDelegate(QObject *parent) : QStyledItemDelegate(parent) - { - } - - void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const override - { - painter->save(); - painter->setRenderHint(QPainter::Antialiasing, true); - - const QPalette &palette = option.palette; - const bool selected = option.state.testFlag(QStyle::State_Selected); - const bool hovered = option.state.testFlag(QStyle::State_MouseOver); - const auto kind = static_cast(index.data(KindRole).toInt()); - const QColor accent = accentForKind(kind, palette); - - const QRect card = option.rect.adjusted(4, 2, -4, -2); - QColor cardBackground = selected ? palette.highlight().color() : palette.base().color(); - if (!selected && hovered) { - cardBackground = palette.alternateBase().color(); - } - - painter->setPen(Qt::NoPen); - painter->setBrush(cardBackground); - painter->drawRoundedRect(card, 6, 6); - - QColor borderColor = selected ? palette.highlightedText().color() : palette.mid().color(); - borderColor.setAlpha(selected ? 100 : 130); - painter->setBrush(Qt::NoBrush); - painter->setPen(QPen(borderColor, 1)); - painter->drawRoundedRect(card.adjusted(0, 0, -1, -1), 6, 6); - - QColor accentColor = accent; - if (selected) { - accentColor = palette.highlightedText().color(); - accentColor.setAlpha(210); - } - painter->setPen(Qt::NoPen); - painter->setBrush(accentColor); - painter->drawRoundedRect(QRect(card.left() + 7, card.top() + 7, 3, card.height() - 14), 1.5, 1.5); - - const QColor textColor = selected ? palette.highlightedText().color() : palette.text().color(); - QColor mutedColor = textColor; - mutedColor.setAlpha(170); - - QFont timeFont = option.font; - timeFont.setBold(true); - timeFont.setPointSizeF(qMax(7.0, timeFont.pointSizeF() * 0.9)); - - QFont actorFont = option.font; - actorFont.setBold(true); - - QFont badgeFont = option.font; - badgeFont.setBold(true); - badgeFont.setPointSizeF(qMax(6.5, badgeFont.pointSizeF() * 0.72)); - - QFont detailFont = option.font; - detailFont.setPointSizeF(qMax(7.0, detailFont.pointSizeF() * 0.95)); - - const QFontMetrics timeMetrics(timeFont); - const QFontMetrics actorMetrics(actorFont); - const QFontMetrics badgeMetrics(badgeFont); - const QFontMetrics detailMetrics(detailFont); - - const int contentLeft = card.left() + 15; - const int contentRight = card.right() - 10; - const int topRowTop = card.top() + 6; - const int topRowHeight = qMax(timeMetrics.height(), badgeMetrics.height()) + 2; - - const QString timeText = index.data(TimeLabelRole).toString(); - const QString actorText = index.data(ActorRole).toString(); - const QString badgeText = index.data(BadgeRole).toString(); - const QString detailText = index.data(DetailRole).toString(); - - const int timeWidth = timeMetrics.horizontalAdvance(timeText); - painter->setFont(timeFont); - painter->setPen(mutedColor); - painter->drawText(QRect(contentLeft, topRowTop, timeWidth + 2, topRowHeight), Qt::AlignLeft | Qt::AlignVCenter, - timeText); - - int actorRight = contentRight; - if (!badgeText.isEmpty()) { - const int badgeWidth = badgeMetrics.horizontalAdvance(badgeText) + 10; - const int badgeHeight = badgeMetrics.height() + 2; - const QRect badgeRect(contentRight - badgeWidth, topRowTop + (topRowHeight - badgeHeight) / 2, badgeWidth, - badgeHeight); - - QColor badgeBackground = accentColor; - badgeBackground.setAlpha(selected ? 45 : 32); - painter->setPen(Qt::NoPen); - painter->setBrush(badgeBackground); - painter->drawRoundedRect(badgeRect, 3, 3); - - painter->setFont(badgeFont); - painter->setPen(accentColor); - painter->drawText(badgeRect, Qt::AlignCenter, badgeText); - - actorRight = badgeRect.left() - 8; - } - - const int actorLeft = contentLeft + timeWidth + 10; - if (actorRight > actorLeft) { - const QString elidedActor = - actorMetrics.elidedText(actorText, Qt::ElideRight, qMax(0, actorRight - actorLeft)); - painter->setFont(actorFont); - painter->setPen(textColor); - painter->drawText(QRect(actorLeft, topRowTop, actorRight - actorLeft, topRowHeight), - Qt::AlignLeft | Qt::AlignVCenter, elidedActor); - } - - const int detailTop = topRowTop + topRowHeight + 2; - const int detailHeight = qMax(detailMetrics.height(), card.bottom() - detailTop); - const QString elidedDetail = - detailMetrics.elidedText(detailText, Qt::ElideRight, qMax(0, contentRight - contentLeft)); - painter->setFont(detailFont); - painter->setPen(textColor); - painter->drawText(QRect(contentLeft, detailTop, contentRight - contentLeft, detailHeight), - Qt::AlignLeft | Qt::AlignVCenter, elidedDetail); - - painter->restore(); - } - - QSize sizeHint(const QStyleOptionViewItem &option, const QModelIndex & /*index*/) const override - { - const QFontMetrics metrics(option.font); - return {0, qMax(58, metrics.height() * 3 + 10)}; - } -}; -} // namespace - -ReplayEventSidebar::ReplayEventSidebar(QWidget *parent, const GameReplay *replay, const QList &timeline) - : QWidget(parent), eventList(new QListWidget(this)), emptyLabel(new QLabel(this)) -{ - setObjectName("replayEventSidebar"); - - eventList->setObjectName("replayEventList"); - eventList->setItemDelegate(new ReplayEventDelegate(eventList)); - eventList->setFrameShape(QFrame::NoFrame); - eventList->setFocusPolicy(Qt::NoFocus); - eventList->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); - eventList->setSelectionMode(QAbstractItemView::SingleSelection); - eventList->setMouseTracking(true); - eventList->setUniformItemSizes(true); - eventList->setVerticalScrollMode(QAbstractItemView::ScrollPerPixel); - - emptyLabel->setAlignment(Qt::AlignCenter); - emptyLabel->setWordWrap(true); - emptyLabel->setMargin(16); - emptyLabel->setForegroundRole(QPalette::PlaceholderText); - - auto *layout = QtUtils::makeLayout(this); - layout->addWidget(eventList); - layout->addWidget(emptyLabel); - - const QList entries = buildReplayEventEntries(replay, timeline); - eventIndices.reserve(entries.size()); - - for (const ReplayEventEntry &entry : entries) { - auto *item = new QListWidgetItem(eventList); - item->setData(EventIndexRole, entry.eventIndex); - item->setData(TimeLabelRole, entry.timeLabel); - item->setData(KindRole, static_cast(entry.kind)); - item->setData(ActorRole, entry.actor); - item->setData(BadgeRole, entry.badge); - item->setData(DetailRole, entry.detail); - const QString tooltip = entry.actor.isEmpty() - ? tr("%1\n%2").arg(entry.timeLabel, entry.detail) - : tr("%1 · %2\n%3").arg(entry.timeLabel, entry.actor, entry.detail); - item->setToolTip(tooltip); - item->setData(Qt::AccessibleTextRole, entry.actor.isEmpty() - ? tr("%1: %2").arg(entry.timeLabel, entry.detail) - : tr("%1, %2: %3").arg(entry.timeLabel, entry.actor, entry.detail)); - eventIndices.append(entry.eventIndex); - } - - const bool hasEvents = !entries.isEmpty(); - eventList->setVisible(hasEvents); - emptyLabel->setVisible(!hasEvents); - emptyLabel->setText(tr("No game events in this replay.")); - - connect(eventList, &QListWidget::itemClicked, this, [this](QListWidgetItem *item) { - if (!item) { - return; - } - - setCurrentRow(eventList->row(item)); - emit eventSelected(item->data(EventIndexRole).toInt()); - }); - - if (hasEvents) { - setCurrentRow(-1); - } -} - -void ReplayEventSidebar::setCurrentEvent(int eventIndex) -{ - if (eventIndices.isEmpty()) { - setCurrentRow(-1); - return; - } - - // The rows hold a subset of the replay's events, in playback order, so the last row at or before eventIndex is the - // one the game state has reached. An eventIndex of -1 (nothing replayed yet) selects no row. - const auto it = std::upper_bound(eventIndices.cbegin(), eventIndices.cend(), eventIndex); - const int row = static_cast(it - eventIndices.cbegin()) - 1; - setCurrentRow(row); -} - -void ReplayEventSidebar::setCurrentRow(int row) -{ - if (row < 0 || row >= eventList->count()) { - row = -1; - } - - if (row >= 0) { - // The view's current index also moves on mouse press and can be deselected by a ctrl-click, - // so the cached row alone cannot tell whether the view is already in the desired state. - const QListWidgetItem *item = eventList->item(row); - if (row == eventList->currentRow() && item && item->isSelected()) { - return; - } - } else if (row == currentRow) { - return; - } - - currentRow = row; - eventList->setCurrentRow(currentRow); - - if (currentRow >= 0) { - QListWidgetItem *item = eventList->item(currentRow); - if (!eventList->viewport()->rect().contains(eventList->visualItemRect(item))) { - eventList->scrollToItem(item, QAbstractItemView::PositionAtCenter); - } - } -} diff --git a/cockatrice/src/interface/widgets/replay/replay_event_sidebar.h b/cockatrice/src/interface/widgets/replay/replay_event_sidebar.h deleted file mode 100644 index ee163356e..000000000 --- a/cockatrice/src/interface/widgets/replay/replay_event_sidebar.h +++ /dev/null @@ -1,50 +0,0 @@ -#ifndef COCKATRICE_REPLAY_EVENT_SIDEBAR_H -#define COCKATRICE_REPLAY_EVENT_SIDEBAR_H - -#include -#include -#include - -class GameReplay; -class QLabel; -class QListWidget; - -/** - * @brief A scrollable list of replay events that can be used to seek the replay. - * - * The sidebar copies the event data it needs at construction time. It does not keep a reference to the replay or to - * the replay manager. - */ -class ReplayEventSidebar : public QWidget -{ - Q_OBJECT - -public: - /** - * @param parent The parent widget - * @param replay The replay to build the event list from - * @param timeline Event index to time in milliseconds, as used by ReplayManager - */ - ReplayEventSidebar(QWidget *parent, const GameReplay *replay, const QList &timeline); - -public slots: - /** - * @brief Highlights the row of the event the game state has reached. - * - * @param eventIndex Index into the replay's event list, or -1 when nothing has been replayed yet - */ - void setCurrentEvent(int eventIndex); - -signals: - void eventSelected(int eventIndex); - -private: - void setCurrentRow(int row); - - QListWidget *eventList; - QLabel *emptyLabel; - QList eventIndices; ///< Replay event index of each row, ascending - int currentRow = -1; -}; - -#endif // COCKATRICE_REPLAY_EVENT_SIDEBAR_H diff --git a/cockatrice/src/interface/widgets/replay/replay_manager.h b/cockatrice/src/interface/widgets/replay/replay_manager.h index e3745778e..34bae5f9a 100644 --- a/cockatrice/src/interface/widgets/replay/replay_manager.h +++ b/cockatrice/src/interface/widgets/replay/replay_manager.h @@ -66,16 +66,6 @@ public: return replayTimeline; } - /** - * @brief Index of the last event the game state has processed. - * - * @return The event index, or -1 while no event has been replayed yet - */ - [[nodiscard]] int getLastProcessedEventIndex() const - { - return currentEvent - 1; - } - void setTimeScaleFactor(qreal _timeScaleFactor); void setSkipEmptySections(bool value); diff --git a/cockatrice/src/interface/widgets/replay/replay_widget.cpp b/cockatrice/src/interface/widgets/replay/replay_widget.cpp index e911bc0f9..3f0bf35d9 100644 --- a/cockatrice/src/interface/widgets/replay/replay_widget.cpp +++ b/cockatrice/src/interface/widgets/replay/replay_widget.cpp @@ -19,11 +19,6 @@ class GameReplay; -int ReplayWidget::getLastProcessedEventIndex() const -{ - return replayManager->getLastProcessedEventIndex(); -} - ReplayWidget::ReplayWidget(QWidget *parent, GameReplay *replay) : QWidget(parent), replayPlayButton(nullptr), replayFastForwardButton(nullptr), aReplaySkipForward(nullptr), aReplaySkipBackward(nullptr), aReplaySkipForwardBig(nullptr), aReplaySkipBackwardBig(nullptr) diff --git a/cockatrice/src/interface/widgets/replay/replay_widget.h b/cockatrice/src/interface/widgets/replay/replay_widget.h index cbb676288..5d23b358d 100644 --- a/cockatrice/src/interface/widgets/replay/replay_widget.h +++ b/cockatrice/src/interface/widgets/replay/replay_widget.h @@ -38,11 +38,6 @@ public: [[nodiscard]] const QList &getReplayTimeline() const; - /** - * @brief Index of the last replay event the game state has processed, or -1 before the first event. - */ - [[nodiscard]] int getLastProcessedEventIndex() const; - public slots: void seekToEvent(int eventIndex); diff --git a/cockatrice/src/interface/widgets/server/game_selector.cpp b/cockatrice/src/interface/widgets/server/game_selector.cpp index 2c0050afe..2702a527d 100644 --- a/cockatrice/src/interface/widgets/server/game_selector.cpp +++ b/cockatrice/src/interface/widgets/server/game_selector.cpp @@ -1,7 +1,7 @@ #include "game_selector.h" #include "../../../client/settings/cache_settings.h" -#include "../../theme_icon.h" +#include "../../pixel_map_generator.h" #include "../interface/widgets/dialogs/dlg_create_game.h" #include "../interface/widgets/dialogs/dlg_filter_games.h" #include "../interface/widgets/tabs/tab_room.h" @@ -130,10 +130,10 @@ GameSelector::GameSelector(AbstractClient *_client, } filterButton = new QPushButton; - filterButton->setIcon(loadThemedIcon(QStringLiteral("icons/search"))); + filterButton->setIcon(themePixmap(QStringLiteral("icons/search"))); connect(filterButton, &QPushButton::clicked, this, &GameSelector::actSetFilter); clearFilterButton = new QPushButton; - clearFilterButton->setIcon(loadThemedIcon(QStringLiteral("icons/clearsearch"))); + clearFilterButton->setIcon(themePixmap(QStringLiteral("icons/clearsearch"))); bool filtersSetToDefault = showFilters && gameListProxyModel->areFilterParametersSetToDefaults(); clearFilterButton->setEnabled(!filtersSetToDefault); connect(clearFilterButton, &QPushButton::clicked, this, &GameSelector::actClearFilter); diff --git a/cockatrice/src/interface/widgets/server/games_model.cpp b/cockatrice/src/interface/widgets/server/games_model.cpp index d33526bd1..8fed95833 100644 --- a/cockatrice/src/interface/widgets/server/games_model.cpp +++ b/cockatrice/src/interface/widgets/server/games_model.cpp @@ -1,7 +1,6 @@ #include "games_model.h" #include "../../../client/settings/cache_settings.h" -#include "../../theme_icon.h" #include "../interface/pixel_map_generator.h" #include "libcockatrice/protocol/pb/serverinfo_user.pb.h" #include "user/user_list_proxy.h" @@ -86,8 +85,7 @@ const QString GamesModel::getGameCreatedString(const int secs) } GamesModel::GamesModel(const QMap &_rooms, const QMap &_gameTypes, QObject *parent) - : QAbstractTableModel(parent), rooms(_rooms), gameTypes(_gameTypes), - lockIcon(loadThemedIcon(QStringLiteral("icons/lock"))) + : QAbstractTableModel(parent), rooms(_rooms), gameTypes(_gameTypes) { } @@ -193,9 +191,7 @@ QVariant GamesModel::data(const QModelIndex &index, int role) const return result.join(", "); } case Qt::DecorationRole: { - // One engine per model: it is palette- and theme-aware and - // invalidates itself, so a repaint must not build a new one. - return gameentry.with_password() ? lockIcon : QVariant(); + return gameentry.with_password() ? QIcon(LockPixmapGenerator::generatePixmap(13)) : QVariant(); case Qt::TextAlignmentRole: return Qt::AlignLeft; default: diff --git a/cockatrice/src/interface/widgets/server/games_model.h b/cockatrice/src/interface/widgets/server/games_model.h index 62862417d..9bf54b83a 100644 --- a/cockatrice/src/interface/widgets/server/games_model.h +++ b/cockatrice/src/interface/widgets/server/games_model.h @@ -5,7 +5,6 @@ #include "game_type_map.h" #include -#include #include #include #include @@ -33,7 +32,6 @@ private: QList gameList; /**< List of games currently displayed. */ QMap rooms; /**< Map of room IDs to room names. */ QMap gameTypes; /**< Map of room IDs to available game types. */ - QIcon lockIcon; /**< Themed lock for password-protected games. */ static const int NUM_COLS = 8; /**< Number of columns in the table. */ diff --git a/cockatrice/src/interface/widgets/server/remote/remote_replay_list_tree_widget.cpp b/cockatrice/src/interface/widgets/server/remote/remote_replay_list_tree_widget.cpp index c1db6138d..c131c3cbf 100644 --- a/cockatrice/src/interface/widgets/server/remote/remote_replay_list_tree_widget.cpp +++ b/cockatrice/src/interface/widgets/server/remote/remote_replay_list_tree_widget.cpp @@ -1,6 +1,6 @@ #include "remote_replay_list_tree_widget.h" -#include "../../../theme_icon.h" +#include "../../../pixel_map_generator.h" #include "libcockatrice/protocol/pb/response.pb.h" #include "libcockatrice/protocol/pb/serverinfo_replay_match.pb.h" @@ -50,7 +50,7 @@ RemoteReplayList_TreeModel::RemoteReplayList_TreeModel(AbstractClient *_client, QFileIconProvider fip; dirIcon = fip.icon(QFileIconProvider::Folder); fileIcon = fip.icon(QFileIconProvider::File); - lockIcon = loadThemedIcon(QStringLiteral("icons/lock")); + lockIcon = themePixmap(QStringLiteral("icons/lock")); } RemoteReplayList_TreeModel::~RemoteReplayList_TreeModel() diff --git a/cockatrice/src/interface/widgets/settings_page/deck_editor_settings_page.cpp b/cockatrice/src/interface/widgets/settings_page/deck_editor_settings_page.cpp index e90603858..31178ba4c 100644 --- a/cockatrice/src/interface/widgets/settings_page/deck_editor_settings_page.cpp +++ b/cockatrice/src/interface/widgets/settings_page/deck_editor_settings_page.cpp @@ -1,14 +1,13 @@ #include "deck_editor_settings_page.h" #include "../../../client/settings/cache_settings.h" -#include "../../theme_icon.h" +#include "../../pixel_map_generator.h" #include "../dialogs/path_safety_warning.h" #include "update/card_spoiler/spoiler_background_updater.h" #include #include #include -#include #include #include #include @@ -62,7 +61,7 @@ DeckEditorSettingsPage::DeckEditorSettingsPage() connect(mpSpoilerPathButton, &QPushButton::clicked, this, &DeckEditorSettingsPage::spoilerPathButtonClicked); updateNowButton = new QPushButton; - updateNowButton->setMinimumWidth(150); + updateNowButton->setFixedWidth(150); connect(updateNowButton, &QPushButton::clicked, this, &DeckEditorSettingsPage::updateSpoilers); // Update the GUI depending on if the box is ticked or not @@ -80,19 +79,19 @@ DeckEditorSettingsPage::DeckEditorSettingsPage() } aAdd = new QAction(this); - aAdd->setIcon(loadThemedIcon(QStringLiteral("icons/increment"))); + aAdd->setIcon(themePixmap(QStringLiteral("icons/increment"))); connect(aAdd, &QAction::triggered, this, &DeckEditorSettingsPage::actAddURL); aEdit = new QAction(this); - aEdit->setIcon(loadThemedIcon(QStringLiteral("icons/pencil"))); + aEdit->setIcon(themePixmap(QStringLiteral("icons/pencil"))); connect(aEdit, &QAction::triggered, this, &DeckEditorSettingsPage::actEditURL); aRemove = new QAction(this); - aRemove->setIcon(loadThemedIcon(QStringLiteral("icons/decrement"))); + aRemove->setIcon(themePixmap(QStringLiteral("icons/decrement"))); connect(aRemove, &QAction::triggered, this, &DeckEditorSettingsPage::actRemoveURL); aRateLimit = new QAction(this); - aRateLimit->setIcon(loadThemedIcon(QStringLiteral("icons/settings"))); + aRateLimit->setIcon(themePixmap(QStringLiteral("icons/cogwheel"))); connect(aRateLimit, &QAction::triggered, this, &DeckEditorSettingsPage::actAdjustRateLimit); auto *urlToolBar = new QToolBar; @@ -121,19 +120,12 @@ DeckEditorSettingsPage::DeckEditorSettingsPage() lpSpoilerGrid->addWidget(&lastUpdatedLabel, 2, 0); lpSpoilerGrid->addWidget(updateNowButton, 2, 1); lpSpoilerGrid->addWidget(&infoOnSpoilersLabel, 3, 0, 1, 3, Qt::AlignTop); - infoOnSpoilersLabel.setWordWrap(true); // On a change to the checkbox, hide/un-hide the other fields connect(&mcDownloadSpoilersCheckBox, &QCheckBox::toggled, &SettingsCache::instance().downloads(), &DownloadSettings::setDownloadSpoilerStatus); connect(&mcDownloadSpoilersCheckBox, &QCheckBox::toggled, this, &DeckEditorSettingsPage::setSpoilersEnabled); - // The updater is application-owned and outlives this page, so a Settings page - // reopened mid-update must observe the operation that is still running. - if (auto *runningUpdater = SpoilerBackgroundUpdater::current()) { - observeSpoilerUpdater(runningUpdater); - } - mpGeneralGroupBox = new QGroupBox; mpGeneralGroupBox->setLayout(lpGeneralGrid); @@ -336,60 +328,20 @@ void DeckEditorSettingsPage::urlListChanged(const QModelIndex &, int, int, const void DeckEditorSettingsPage::updateSpoilers() { - if (spoilerUpdater) { - return; - } - // Application ownership lets either operation finish after settings closes. - // When disabled, the same button retries file removal and the local reload. - SpoilerBackgroundUpdater *sbu; - if (SettingsCache::instance().downloads().getDownloadSpoilersStatus()) { - sbu = new SpoilerBackgroundUpdater(QCoreApplication::instance(), true); - // Only an updater this page creates is retired by deleteLater: observation - // may attach to the stack-allocated launch updater, which must not be deleted. - connect(sbu, &SpoilerBackgroundUpdater::spoilerCheckerDone, sbu, &QObject::deleteLater); - } else { - // deleteSpoilerFile() wires its own deleteLater. - sbu = SpoilerBackgroundUpdater::deleteSpoilerFile(); - } - observeSpoilerUpdater(sbu); -} + // Disable the button so the user can only press it once at a time + updateNowButton->setDisabled(true); + updateNowButton->setText(tr("Updating...")); -void DeckEditorSettingsPage::observeSpoilerUpdater(SpoilerBackgroundUpdater *sbu) -{ - spoilerUpdater = sbu; - unlockSettings(); - connect(sbu, &SpoilerBackgroundUpdater::spoilerCheckerDone, this, [this, sbu] { - if (spoilerUpdater == sbu) { - spoilerUpdater.clear(); - unlockSettings(); - } - }); - connect(sbu, &SpoilerBackgroundUpdater::spoilerUpdateFailed, this, [this, sbu](const QString &message) { - if (spoilerUpdater != sbu) { - return; - } - auto *notice = new QMessageBox(QMessageBox::Warning, tr("Spoiler update"), message, QMessageBox::Ok, this); - notice->setTextFormat(Qt::PlainText); - notice->setAttribute(Qt::WA_DeleteOnClose); - notice->open(); - }); + // Create a new SBU that will act as if the client was just reloaded + auto *sbu = new SpoilerBackgroundUpdater(); + connect(sbu, &SpoilerBackgroundUpdater::spoilerCheckerDone, this, &DeckEditorSettingsPage::unlockSettings); + connect(sbu, &SpoilerBackgroundUpdater::spoilersUpdatedSuccessfully, this, &DeckEditorSettingsPage::unlockSettings); } void DeckEditorSettingsPage::unlockSettings() { - updateNowButton->setEnabled(!spoilerUpdater); - updateNowButton->setText(spoilerUpdater ? tr("Updating...") - : SettingsCache::instance().downloads().getDownloadSpoilersStatus() - ? tr("Update Spoilers") - : tr("Reload Card Database")); - lastUpdatedLabel.setText(tr("Last Change") + ": " + getLastUpdateTime()); - infoOnSpoilersLabel.setText( - SettingsCache::instance().downloads().getDownloadSpoilersStatus() - ? tr("Spoilers download automatically on launch") + "\n" + - tr("Press the button to manually update without relaunching") + "\n\n" + - tr("Updates continue if you close settings") - : tr("Spoilers are off. Reload Card Database removes any downloaded spoiler file and reloads your " - "card data.")); + updateNowButton->setDisabled(false); + updateNowButton->setText(tr("Update Spoilers")); } QString DeckEditorSettingsPage::getLastUpdateTime() @@ -427,17 +379,12 @@ void DeckEditorSettingsPage::setSpoilersEnabled(bool anInput) mpSpoilerSavePathLineEdit->setEnabled(anInput); mpSpoilerPathButton->setEnabled(anInput); lastUpdatedLabel.setEnabled(anInput); - // The hint explains the button in both states, especially the disabled-state - // Reload Card Database action, so keep it readable while spoilers are off. - infoOnSpoilersLabel.setEnabled(true); + updateNowButton->setEnabled(anInput); + infoOnSpoilersLabel.setEnabled(anInput); - // Only react to the user disabling the setting. The constructor calls this to - // initialize the controls, and a leftover spoiler file is recovered through the - // Reload Card Database button rather than an unrequested reload on page open. - if (!anInput && sender() == &mcDownloadSpoilersCheckBox) { - observeSpoilerUpdater(SpoilerBackgroundUpdater::deleteSpoilerFile()); + if (!anInput) { + SpoilerBackgroundUpdater::deleteSpoilerFile(); } - unlockSettings(); } void DeckEditorSettingsPage::retranslateUi() @@ -447,10 +394,13 @@ void DeckEditorSettingsPage::retranslateUi() mcDownloadSpoilersCheckBox.setText(tr("Download Spoilers Automatically")); mcSpoilerSaveLabel.setText(tr("Spoiler Location:")); lastUpdatedLabel.setText(tr("Last Change") + ": " + getLastUpdateTime()); + infoOnSpoilersLabel.setText(tr("Spoilers download automatically on launch") + "\n" + + tr("Press the button to manually update without relaunching") + "\n\n" + + tr("Do not close settings until manual update is complete")); picDownloadCheckBox.setText(tr("Download card pictures on the fly")); urlLinkLabel.setText(QString("%2").arg(WIKI_CUSTOM_PIC_URL).arg(tr("How to add a custom URL"))); resetDownloadURLs.setText(tr("Reset Download URLs")); - unlockSettings(); + updateNowButton->setText(tr("Update Spoilers")); aAdd->setText(tr("Add New URL")); aEdit->setText(tr("Edit URL")); aRemove->setText(tr("Remove URL")); diff --git a/cockatrice/src/interface/widgets/settings_page/deck_editor_settings_page.h b/cockatrice/src/interface/widgets/settings_page/deck_editor_settings_page.h index 8e33e697d..47f840191 100644 --- a/cockatrice/src/interface/widgets/settings_page/deck_editor_settings_page.h +++ b/cockatrice/src/interface/widgets/settings_page/deck_editor_settings_page.h @@ -5,7 +5,6 @@ #include #include -#include #include #include #include @@ -16,7 +15,6 @@ class QLineEdit; class QListWidget; class QListWidgetItem; class QModelIndex; -class SpoilerBackgroundUpdater; class DeckEditorSettingsPage : public AbstractSettingsPage { @@ -56,7 +54,6 @@ private: QLabel infoOnSpoilersLabel; QPushButton *mpSpoilerPathButton; QPushButton *updateNowButton; - QPointer spoilerUpdater; /** @brief Adds a list item for the given URL, storing the raw URL alongside its displayed label. */ QListWidgetItem *addUrlItem(const QString &url); @@ -69,8 +66,6 @@ private: /** @brief Refreshes the displayed label of every URL item after limits or settings change. */ void refreshUrlItems(); - - void observeSpoilerUpdater(SpoilerBackgroundUpdater *updater); }; #endif // COCKATRICE_DECK_EDITOR_SETTINGS_PAGE_H diff --git a/cockatrice/src/interface/widgets/settings_page/messages_settings_page.cpp b/cockatrice/src/interface/widgets/settings_page/messages_settings_page.cpp index 09dd9e9b0..6128a7830 100644 --- a/cockatrice/src/interface/widgets/settings_page/messages_settings_page.cpp +++ b/cockatrice/src/interface/widgets/settings_page/messages_settings_page.cpp @@ -1,7 +1,7 @@ #include "messages_settings_page.h" #include "../../../client/settings/cache_settings.h" -#include "../../theme_icon.h" +#include "../../pixel_map_generator.h" #include "../interface/widgets/utility/get_text_with_max.h" #include "libcockatrice/utility/macros.h" @@ -146,15 +146,15 @@ MessagesSettingsPage::MessagesSettingsPage() } aAdd = new QAction(this); - aAdd->setIcon(loadThemedIcon(QStringLiteral("icons/increment"))); + aAdd->setIcon(themePixmap(QStringLiteral("icons/increment"))); connect(aAdd, &QAction::triggered, this, &MessagesSettingsPage::actAdd); aEdit = new QAction(this); - aEdit->setIcon(loadThemedIcon(QStringLiteral("icons/pencil"))); + aEdit->setIcon(themePixmap(QStringLiteral("icons/pencil"))); connect(aEdit, &QAction::triggered, this, &MessagesSettingsPage::actEdit); aRemove = new QAction(this); - aRemove->setIcon(loadThemedIcon(QStringLiteral("icons/decrement"))); + aRemove->setIcon(themePixmap(QStringLiteral("icons/decrement"))); connect(aRemove, &QAction::triggered, this, &MessagesSettingsPage::actRemove); auto *messageToolBar = new QToolBar; diff --git a/cockatrice/src/interface/widgets/settings_page/shortcut_settings_page.cpp b/cockatrice/src/interface/widgets/settings_page/shortcut_settings_page.cpp index 83c2ec585..a77c0c2c3 100644 --- a/cockatrice/src/interface/widgets/settings_page/shortcut_settings_page.cpp +++ b/cockatrice/src/interface/widgets/settings_page/shortcut_settings_page.cpp @@ -3,7 +3,7 @@ #include "../../../client/settings/cache_settings.h" #include "../../../client/settings/shortcut_treeview.h" #include "../../../client/settings/shortcuts_settings.h" -#include "../../theme_icon.h" +#include "../../pixel_map_generator.h" #include "../interface/widgets/utility/custom_line_edit.h" #include "../interface/widgets/utility/sequence_edit.h" @@ -58,8 +58,8 @@ ShortcutSettingsPage::ShortcutSettingsPage() btnResetAll = new QPushButton(this); btnClearAll = new QPushButton(this); - btnResetAll->setIcon(loadThemedIcon(QStringLiteral("icons/reload"))); - btnClearAll->setIcon(loadThemedIcon(QStringLiteral("icons/clearsearch"))); + btnResetAll->setIcon(themePixmap(QStringLiteral("icons/update"))); + btnClearAll->setIcon(themePixmap(QStringLiteral("icons/clearsearch"))); // layout auto *_editLayout = new QGridLayout; diff --git a/cockatrice/src/interface/widgets/tabs/api/archidekt/tab_archidekt.cpp b/cockatrice/src/interface/widgets/tabs/api/archidekt/tab_archidekt.cpp index ae3a9baef..9f7369435 100644 --- a/cockatrice/src/interface/widgets/tabs/api/archidekt/tab_archidekt.cpp +++ b/cockatrice/src/interface/widgets/tabs/api/archidekt/tab_archidekt.cpp @@ -1,7 +1,7 @@ #include "tab_archidekt.h" #include "../../../../../client/settings/cache_settings.h" -#include "../../../../theme_icon.h" +#include "../../../../pixel_map_generator.h" #include "../../../cards/additional_info/mana_symbol_widget.h" #include "../../../cards/card_size_widget.h" #include "../../../quick_settings/settings_button_widget.h" @@ -282,7 +282,7 @@ void TabArchidekt::setupFilterWidgets() // Format filter (collapsible) formatButton = new SettingsButtonWidget(secondaryToolbar); formatButton->setButtonText(tr("Formats")); - formatButton->setButtonIcon(loadThemedIcon(QStringLiteral("icons/scales"))); + formatButton->setButtonIcon(themePixmap(QStringLiteral("icons/scale_balanced"))); QWidget *formatContainer = new QWidget(secondaryToolbar); QGridLayout *formatLayout = new QGridLayout(formatContainer); diff --git a/cockatrice/src/interface/widgets/tabs/api/commander_spellbook/commander_bracket_widget.cpp b/cockatrice/src/interface/widgets/tabs/api/commander_spellbook/commander_bracket_widget.cpp index a0e3f2491..9adda008e 100644 --- a/cockatrice/src/interface/widgets/tabs/api/commander_spellbook/commander_bracket_widget.cpp +++ b/cockatrice/src/interface/widgets/tabs/api/commander_spellbook/commander_bracket_widget.cpp @@ -1,7 +1,7 @@ #include "commander_bracket_widget.h" #include "../../../../../client/settings/cache_settings.h" -#include "../../../../theme_icon.h" +#include "../../../../pixel_map_generator.h" #include "commander_bracket_service.h" #include "libcockatrice/deck_list/deck_list.h" @@ -49,7 +49,7 @@ CommanderBracketWidget::CommanderBracketWidget(QWidget *parent) : QWidget(parent bracketInfoButton->setEnabled(false); bracketRefreshButton = new QToolButton(this); - bracketRefreshButton->setIcon(loadThemedIcon(QStringLiteral("icons/reload"))); + bracketRefreshButton->setIcon(themePixmap(QStringLiteral("icons/reload"))); bracketRefreshButton->setAutoRaise(true); connect(bracketRefreshButton, &QToolButton::clicked, this, &CommanderBracketWidget::requestBracketEstimate); diff --git a/cockatrice/src/interface/widgets/tabs/tab_deck_storage.cpp b/cockatrice/src/interface/widgets/tabs/tab_deck_storage.cpp index 8af9d5e6e..3dfdc956b 100644 --- a/cockatrice/src/interface/widgets/tabs/tab_deck_storage.cpp +++ b/cockatrice/src/interface/widgets/tabs/tab_deck_storage.cpp @@ -2,7 +2,7 @@ #include "../../../client/settings/cache_settings.h" #include "../../deck_loader/deck_loader.h" -#include "../../theme_icon.h" +#include "../../pixel_map_generator.h" #include "../cards/additional_info/deck_color_identity.h" #include "../deck_share/deck_share_utils.h" #include "../deck_share/share_bar_widget.h" @@ -187,19 +187,19 @@ TabDeckStorage::TabDeckStorage(TabSupervisor *_tabSupervisor, // Left side actions aOpenLocalDeck = new QAction(this); - aOpenLocalDeck->setIcon(loadThemedIcon(QStringLiteral("icons/pencil"))); + aOpenLocalDeck->setIcon(themePixmap(QStringLiteral("icons/pencil"))); connect(aOpenLocalDeck, &QAction::triggered, this, &TabDeckStorage::actOpenLocalDeck); aRenameLocal = new QAction(this); - aRenameLocal->setIcon(loadThemedIcon(QStringLiteral("icons/rename"))); + aRenameLocal->setIcon(themePixmap(QStringLiteral("icons/rename"))); connect(aRenameLocal, &QAction::triggered, this, &TabDeckStorage::actRenameLocal); aUpload = new QAction(this); - aUpload->setIcon(loadThemedIcon(QStringLiteral("icons/arrow_right"))); + aUpload->setIcon(themePixmap(QStringLiteral("icons/arrow_right_green"))); connect(aUpload, &QAction::triggered, this, &TabDeckStorage::actUpload); aNewLocalFolder = new QAction(this); aNewLocalFolder->setIcon(qApp->style()->standardIcon(QStyle::SP_FileDialogNewFolder)); connect(aNewLocalFolder, &QAction::triggered, this, &TabDeckStorage::actNewLocalFolder); aDeleteLocalDeck = new QAction(this); - aDeleteLocalDeck->setIcon(loadThemedIcon(QStringLiteral("icons/remove_row"))); + aDeleteLocalDeck->setIcon(themePixmap(QStringLiteral("icons/remove_row"))); connect(aDeleteLocalDeck, &QAction::triggered, this, &TabDeckStorage::actDeleteLocalDeck); aOpenDecksFolder = new QAction(this); @@ -208,24 +208,24 @@ TabDeckStorage::TabDeckStorage(TabSupervisor *_tabSupervisor, // Right side actions aOpenRemoteDeck = new QAction(this); - aOpenRemoteDeck->setIcon(loadThemedIcon(QStringLiteral("icons/pencil"))); + aOpenRemoteDeck->setIcon(themePixmap(QStringLiteral("icons/pencil"))); connect(aOpenRemoteDeck, &QAction::triggered, this, &TabDeckStorage::actOpenRemoteDeck); aDownload = new QAction(this); - aDownload->setIcon(loadThemedIcon(QStringLiteral("icons/arrow_left"))); + aDownload->setIcon(themePixmap(QStringLiteral("icons/arrow_left_green"))); connect(aDownload, &QAction::triggered, this, &TabDeckStorage::actDownload); aNewFolder = new QAction(this); aNewFolder->setIcon(qApp->style()->standardIcon(QStyle::SP_FileDialogNewFolder)); connect(aNewFolder, &QAction::triggered, this, &TabDeckStorage::actNewFolder); aDeleteRemoteDeck = new QAction(this); - aDeleteRemoteDeck->setIcon(loadThemedIcon(QStringLiteral("icons/remove_row"))); + aDeleteRemoteDeck->setIcon(themePixmap(QStringLiteral("icons/remove_row"))); connect(aDeleteRemoteDeck, &QAction::triggered, this, &TabDeckStorage::actDeleteRemoteDeck); aShareDecks = new QAction(this); - aShareDecks->setIcon(loadThemedIcon(QStringLiteral("icons/share"))); + aShareDecks->setIcon(themePixmap(QStringLiteral("icons/share"))); connect(aShareDecks, &QAction::triggered, this, &TabDeckStorage::actShareDecks); aPublishDeck = new QAction(this); - aPublishDeck->setIcon(loadThemedIcon(QStringLiteral("icons/lock"))); + aPublishDeck->setIcon(QPixmap("theme:icons/lock")); connect(aPublishDeck, &QAction::triggered, this, &TabDeckStorage::actPublishDeck); // Add actions to toolbars diff --git a/cockatrice/src/interface/widgets/tabs/tab_game.cpp b/cockatrice/src/interface/widgets/tabs/tab_game.cpp index ac9edd1ba..34d6f8a51 100644 --- a/cockatrice/src/interface/widgets/tabs/tab_game.cpp +++ b/cockatrice/src/interface/widgets/tabs/tab_game.cpp @@ -27,7 +27,6 @@ #include "../interface/widgets/utility/line_edit_completer.h" #include "../main.h" #include "../menus/tearoff_menu.h" -#include "../replay/replay_event_sidebar.h" #include "../replay/replay_widget.h" #include "../utility/visibility_change_listener.h" #include "card/card_completer_proxy_model.h" @@ -99,7 +98,7 @@ class Message; } // namespace google TabGame::TabGame(TabSupervisor *_tabSupervisor, GameReplay *_replay) - : Tab(_tabSupervisor), sayLabel(nullptr), sayEdit(nullptr), replayDock(nullptr), historyDock(nullptr) + : Tab(_tabSupervisor), sayLabel(nullptr), sayEdit(nullptr) { setObjectName("TabGame"); // THIS CTOR IS USED ON REPLAY @@ -116,7 +115,6 @@ TabGame::TabGame(TabSupervisor *_tabSupervisor, GameReplay *_replay) addDockWidget(Qt::RightDockWidgetArea, cardInfoDock); addDockWidget(Qt::RightDockWidgetArea, playerListDock); addDockWidget(Qt::RightDockWidgetArea, messageLayoutDock); - addDockWidget(Qt::LeftDockWidgetArea, historyDock); addDockWidget(Qt::BottomDockWidgetArea, replayDock); mainWidget = new QStackedWidget(this); @@ -160,7 +158,6 @@ TabGame::TabGame(TabSupervisor *_tabSupervisor, createPlayAreaWidget(); createDeckViewContainerWidget(); replayDock = nullptr; - historyDock = nullptr; addDockWidget(Qt::RightDockWidgetArea, cardInfoDock); addDockWidget(Qt::RightDockWidgetArea, playerListDock); @@ -356,9 +353,6 @@ void TabGame::retranslateUi() } cardInfoDock->setWindowTitle(tr("Card Info") + (cardInfoDock->isWindow() ? tabText : QString())); messageLayoutDock->setWindowTitle(tr("Messages") + (messageLayoutDock->isWindow() ? tabText : QString())); - if (historyDock) { - historyDock->setWindowTitle(tr("Game History") + (historyDock->isWindow() ? tabText : QString())); - } if (replayDock) { replayDock->setWindowTitle(tr("Replay Timeline") + (replayDock->isWindow() ? tabText : QString())); } @@ -435,9 +429,6 @@ void TabGame::retranslateUi() dockToActions[messageLayoutDock].menu->setTitle(tr("Messages")); dockToActions[playerListDock].menu->setTitle(tr("Player List")); - if (historyDock) { - dockToActions[historyDock].menu->setTitle(tr("Game History")); - } if (replayDock) { dockToActions[replayDock].menu->setTitle(tr("Replay Timeline")); } @@ -1300,10 +1291,6 @@ void TabGame::createViewMenuItems() registerDockWidget(viewMenu, messageLayoutDock, {250, 200}); registerDockWidget(viewMenu, playerListDock, {250, 50}); - if (historyDock) { - registerDockWidget(viewMenu, historyDock, {280, 400}); - } - if (replayDock) { registerDockWidget(viewMenu, replayDock, {900, 100}); } @@ -1373,12 +1360,6 @@ void TabGame::actResetLayout() addDockWidget(Qt::RightDockWidgetArea, playerListDock); addDockWidget(Qt::RightDockWidgetArea, messageLayoutDock); - if (historyDock) { - historyDock->setVisible(true); - historyDock->setFloating(false); - addDockWidget(Qt::LeftDockWidgetArea, historyDock); - } - if (replayDock) { replayDock->setVisible(true); replayDock->setFloating(false); @@ -1387,9 +1368,6 @@ void TabGame::actResetLayout() cardInfoDock->resize(250, 360); messageLayoutDock->resize(250, 200); playerListDock->resize(250, 50); - if (historyDock) { - historyDock->resize(280, 400); - } replayDock->resize(900, 100); } else { cardInfoDock->resize(250, 360); @@ -1444,22 +1422,6 @@ void TabGame::createReplayDock(GameReplay *replay) [this](const auto &event, auto options) { game->getGameEventHandler()->processGameEventContainer(event, nullptr, options); }); - - auto *historySidebar = new ReplayEventSidebar(nullptr, replay, replayWidget->getReplayTimeline()); - historyDock = new QDockWidget(this); - historyDock->setObjectName("historyDock"); - historyDock->setFeatures(QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetFloatable | - QDockWidget::DockWidgetMovable); - historyDock->setWidget(historySidebar); - historyDock->setFloating(false); - - connect(historySidebar, &ReplayEventSidebar::eventSelected, replayWidget, &ReplayWidget::seekToEvent); - // The sidebar highlights events rather than times, so ask the manager which event the game state has reached. - connect(replayWidget, &ReplayWidget::timeChanged, historySidebar, - [this, historySidebar] { historySidebar->setCurrentEvent(replayWidget->getLastProcessedEventIndex()); }); - - // No event has been replayed yet when the sidebar is connected, so apply the initial state explicitly. - historySidebar->setCurrentEvent(replayWidget->getLastProcessedEventIndex()); } void TabGame::createDeckViewContainerWidget(bool bReplay) diff --git a/cockatrice/src/interface/widgets/tabs/tab_game.h b/cockatrice/src/interface/widgets/tabs/tab_game.h index 142c02999..412529dc2 100644 --- a/cockatrice/src/interface/widgets/tabs/tab_game.h +++ b/cockatrice/src/interface/widgets/tabs/tab_game.h @@ -50,7 +50,6 @@ class AbstractClient; class GameView; class GameScene; class ReplayWidget; -class ReplayEventSidebar; class CardInfoFrameWidget; class QLabel; class QPushButton; @@ -90,7 +89,7 @@ private: QMap deckViewContainers; QVBoxLayout *deckViewContainerLayout; QWidget *gamePlayAreaWidget, *deckViewContainerWidget; - QDockWidget *cardInfoDock, *messageLayoutDock, *playerListDock, *replayDock, *historyDock; + QDockWidget *cardInfoDock, *messageLayoutDock, *playerListDock, *replayDock; QAction *playersSeparator; QMenu *gameMenu, *viewMenu; TearOffMenu *phasesMenu; diff --git a/cockatrice/src/interface/widgets/tabs/tab_public_decks.cpp b/cockatrice/src/interface/widgets/tabs/tab_public_decks.cpp index dfde08f8e..dc14b6e4a 100644 --- a/cockatrice/src/interface/widgets/tabs/tab_public_decks.cpp +++ b/cockatrice/src/interface/widgets/tabs/tab_public_decks.cpp @@ -2,7 +2,6 @@ #include "../../../client/settings/cache_settings.h" #include "../../deck_loader/deck_loader.h" -#include "../../theme_icon.h" #include "../cards/card_size_widget.h" #include "../general/layout_containers/flow_widget.h" #include "../interface/theme_manager.h" @@ -64,8 +63,8 @@ TabPublicDecks::TabPublicDecks(TabSupervisor *_tabSupervisor, AbstractClient *_c headerLayout->addStretch(1); // Filter/toolbar row, matching the Visual Deck Storage: color identity filter - // first, the search bar stretching in the middle, and the quick-settings button - // at the end. The card size slider lives inside its popup. + // first, the search bar stretching in the middle, and the quick settings + // cogwheel at the end. The card size slider lives inside the cogwheel popup. emptyLabel = new QLabel(tr("This user has not published any decks."), this); emptyLabel->setAlignment(Qt::AlignCenter); emptyLabel->setVisible(false); @@ -80,7 +79,7 @@ TabPublicDecks::TabPublicDecks(TabSupervisor *_tabSupervisor, AbstractClient *_c colorIdentityFilter = new DeckPreviewColorIdentityFilterWidget(this); searchWidget = new VisualDeckStorageSearchWidget(this); refreshButton = new QToolButton(this); - refreshButton->setIcon(loadThemedIcon(QStringLiteral("icons/reload"))); + refreshButton->setIcon(QPixmap("theme:icons/reload")); refreshButton->setFixedSize(32, 32); quickSettingsWidget = new VisualDeckStorageQuickSettingsWidget(this); quickSettingsWidget->setPublicDecksMode(true); diff --git a/cockatrice/src/interface/widgets/tabs/tab_replays.cpp b/cockatrice/src/interface/widgets/tabs/tab_replays.cpp index bd2a69017..d37bff3d8 100644 --- a/cockatrice/src/interface/widgets/tabs/tab_replays.cpp +++ b/cockatrice/src/interface/widgets/tabs/tab_replays.cpp @@ -1,7 +1,7 @@ #include "tab_replays.h" #include "../../../client/settings/cache_settings.h" -#include "../../theme_icon.h" +#include "../../pixel_map_generator.h" #include "../interface/widgets/server/remote/remote_replay_list_tree_widget.h" #include "libcockatrice/protocol/pb/commands.pb.h" #include "libcockatrice/protocol/pb/serverinfo_replay.pb.h" @@ -145,17 +145,17 @@ QGroupBox *TabReplays::createLeftLayout() // Left side actions aOpenLocalReplay = new QAction(this); - aOpenLocalReplay->setIcon(loadThemedIcon(QStringLiteral("icons/view"))); + aOpenLocalReplay->setIcon(themePixmap(QStringLiteral("icons/view"))); connect(aOpenLocalReplay, &QAction::triggered, this, &TabReplays::actOpenLocalReplay); connect(localDirView, &QTreeView::doubleClicked, this, &TabReplays::actOpenLocalReplay); aRenameLocal = new QAction(this); - aRenameLocal->setIcon(loadThemedIcon(QStringLiteral("icons/rename"))); + aRenameLocal->setIcon(themePixmap(QStringLiteral("icons/rename"))); connect(aRenameLocal, &QAction::triggered, this, &TabReplays::actRenameLocal); aNewLocalFolder = new QAction(this); aNewLocalFolder->setIcon(qApp->style()->standardIcon(QStyle::SP_FileDialogNewFolder)); connect(aNewLocalFolder, &QAction::triggered, this, &TabReplays::actNewLocalFolder); aDeleteLocalReplay = new QAction(this); - aDeleteLocalReplay->setIcon(loadThemedIcon(QStringLiteral("icons/remove_row"))); + aDeleteLocalReplay->setIcon(themePixmap(QStringLiteral("icons/remove_row"))); connect(aDeleteLocalReplay, &QAction::triggered, this, &TabReplays::actDeleteLocalReplay); aOpenReplaysFolder = new QAction(this); @@ -207,24 +207,24 @@ QGroupBox *TabReplays::createRightLayout() // Right side actions aOpenRemoteReplay = new QAction(this); - aOpenRemoteReplay->setIcon(loadThemedIcon(QStringLiteral("icons/view"))); + aOpenRemoteReplay->setIcon(themePixmap(QStringLiteral("icons/view"))); connect(aOpenRemoteReplay, &QAction::triggered, this, &TabReplays::actOpenRemoteReplay); connect(serverDirView, &QTreeView::doubleClicked, this, &TabReplays::actOpenRemoteReplay); aDownload = new QAction(this); - aDownload->setIcon(loadThemedIcon(QStringLiteral("icons/arrow_left"))); + aDownload->setIcon(themePixmap(QStringLiteral("icons/arrow_left_green"))); connect(aDownload, &QAction::triggered, this, &TabReplays::actDownload); aKeep = new QAction(this); - aKeep->setIcon(loadThemedIcon(QStringLiteral("icons/lock"))); + aKeep->setIcon(themePixmap(QStringLiteral("icons/lock"))); connect(aKeep, &QAction::triggered, this, &TabReplays::actKeepRemoteReplay); aDeleteRemoteReplay = new QAction(this); - aDeleteRemoteReplay->setIcon(loadThemedIcon(QStringLiteral("icons/remove_row"))); + aDeleteRemoteReplay->setIcon(themePixmap(QStringLiteral("icons/remove_row"))); connect(aDeleteRemoteReplay, &QAction::triggered, this, &TabReplays::actDeleteRemoteReplay); aGetReplayCode = new QAction(this); - aGetReplayCode->setIcon(loadThemedIcon(QStringLiteral("icons/share"))); + aGetReplayCode->setIcon(themePixmap(QStringLiteral("icons/share"))); connect(aGetReplayCode, &QAction::triggered, this, &TabReplays::actGetReplayCode); aSubmitReplayCode = new QAction(this); - aSubmitReplayCode->setIcon(loadThemedIcon(QStringLiteral("icons/search"))); + aSubmitReplayCode->setIcon(themePixmap(QStringLiteral("icons/search"))); connect(aSubmitReplayCode, &QAction::triggered, this, &TabReplays::actSubmitReplayCode); // Add actions to toolbars diff --git a/cockatrice/src/interface/widgets/tabs/tab_room.cpp b/cockatrice/src/interface/widgets/tabs/tab_room.cpp index 329732c64..f63b77445 100644 --- a/cockatrice/src/interface/widgets/tabs/tab_room.cpp +++ b/cockatrice/src/interface/widgets/tabs/tab_room.cpp @@ -2,7 +2,7 @@ #include "../../../client/settings/cache_settings.h" #include "../../../client/settings/shortcuts_settings.h" -#include "../../theme_icon.h" +#include "../../pixel_map_generator.h" #include "../interface/widgets/dialogs/dlg_settings.h" #include "../interface/widgets/server/chat_view/chat_view.h" #include "../interface/widgets/server/game_link.h" @@ -198,7 +198,7 @@ TabRoom::TabRoom(TabSupervisor *_tabSupervisor, connect(aOpenChatSettings, &QAction::triggered, this, &TabRoom::actOpenChatSettings); auto *chatSettingsButton = new QToolButton; - chatSettingsButton->setIcon(loadThemedIcon(QStringLiteral("icons/settings"))); + chatSettingsButton->setIcon(themePixmap(QStringLiteral("icons/settings"))); chatSettingsButton->setMenu(chatSettingsMenu); chatSettingsButton->setPopupMode(QToolButton::InstantPopup); diff --git a/cockatrice/src/interface/widgets/tabs/tab_supervisor.cpp b/cockatrice/src/interface/widgets/tabs/tab_supervisor.cpp index 45c320852..cf71dd7ed 100644 --- a/cockatrice/src/interface/widgets/tabs/tab_supervisor.cpp +++ b/cockatrice/src/interface/widgets/tabs/tab_supervisor.cpp @@ -7,7 +7,6 @@ #include "../../intents/contexts/context_join_room.h" #include "../../intents/intent.h" #include "../../intents/intent_join_server_game.h" -#include "../../theme_icon.h" #include "../interface/pixel_map_generator.h" #include "../interface/widgets/server/game_link.h" #include "../interface/widgets/server/user/user_list_manager.h" @@ -1361,7 +1360,7 @@ void TabSupervisor::tabUserEvent(bool globalEvent) auto *tab = static_cast(sender()); if (tab != currentWidget()) { tab->setContentsChanged(true); - setTabIcon(indexOf(tab), loadThemedIcon(QStringLiteral("icons/tab_changed"))); + setTabIcon(indexOf(tab), themePixmap(QStringLiteral("icons/tab_changed"))); } if (globalEvent && SettingsCache::instance().userInterface().getNotificationsEnabled()) { QApplication::alert(this); diff --git a/cockatrice/src/interface/widgets/utility/sequence_edit.cpp b/cockatrice/src/interface/widgets/utility/sequence_edit.cpp index b09e6da1e..44e6581e9 100644 --- a/cockatrice/src/interface/widgets/utility/sequence_edit.cpp +++ b/cockatrice/src/interface/widgets/utility/sequence_edit.cpp @@ -2,7 +2,7 @@ #include "../../../client/settings/cache_settings.h" #include "../../../client/settings/shortcuts_settings.h" -#include "../../theme_icon.h" +#include "../../pixel_map_generator.h" #include #include @@ -24,8 +24,8 @@ SequenceEdit::SequenceEdit(const QString &_shortcutName, QWidget *parent) : QWid defaultButton = new QPushButton("", this); lineEdit->setMinimumWidth(70); - clearButton->setIcon(loadThemedIcon(QStringLiteral("icons/clearsearch"))); - defaultButton->setIcon(loadThemedIcon(QStringLiteral("icons/reload"))); + clearButton->setIcon(themePixmap(QStringLiteral("icons/clearsearch"))); + defaultButton->setIcon(themePixmap(QStringLiteral("icons/update"))); auto *layout = QtUtils::makeLayout(this, 1); layout->addWidget(lineEdit); diff --git a/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_filter_toolbar_widget.cpp b/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_filter_toolbar_widget.cpp index ecb949b67..5063cce14 100644 --- a/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_filter_toolbar_widget.cpp +++ b/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_filter_toolbar_widget.cpp @@ -1,6 +1,6 @@ #include "visual_database_display_filter_toolbar_widget.h" -#include "../../theme_icon.h" +#include "../../pixel_map_generator.h" #include "../deck_editor/card_database_view.h" #include "../quick_settings/settings_button_widget.h" #include "card_database_display_model.h" @@ -45,13 +45,13 @@ VisualDatabaseDisplayFilterToolbarWidget::VisualDatabaseDisplayFilterToolbarWidg sortGroupBox = new QGroupBox(this); filterGroupBox = new QGroupBox(this); - auto scalePixmap = [](const QString &prefix) { return loadThemedIcon(prefix).pixmap({20, 20}); }; + auto scalePixmap = [](const QString &fileName) { return QIcon(QPixmap(fileName)).pixmap({20, 20}); }; sortByLabel = new QLabel(this); - sortByLabel->setPixmap(scalePixmap("icons/sort_arrow_down")); + sortByLabel->setPixmap(scalePixmap("theme:icons/sort_arrow_down")); filterByLabel = new QLabel(this); - filterByLabel->setPixmap(scalePixmap("icons/filter")); + filterByLabel->setPixmap(scalePixmap("theme:icons/filter")); sortColumnCombo = new QComboBox(this); sortColumnCombo->setSizeAdjustPolicy(QComboBox::SizeAdjustPolicy::AdjustToContents); @@ -89,22 +89,22 @@ VisualDatabaseDisplayFilterToolbarWidget::VisualDatabaseDisplayFilterToolbarWidg }); quickFilterSaveLoadWidget = new SettingsButtonWidget(this); - quickFilterSaveLoadWidget->setButtonIcon(loadThemedIcon(QStringLiteral("icons/floppy_disk"))); + quickFilterSaveLoadWidget->setButtonIcon(themePixmap(QStringLiteral("icons/floppy_disk"))); quickFilterNameWidget = new SettingsButtonWidget(this); - quickFilterNameWidget->setButtonIcon(loadThemedIcon(QStringLiteral("icons/pen_to_square"))); + quickFilterNameWidget->setButtonIcon(themePixmap(QStringLiteral("icons/pen_to_square"))); quickFilterMainTypeWidget = new SettingsButtonWidget(this); - quickFilterMainTypeWidget->setButtonIcon(loadThemedIcon(QStringLiteral("icons/circle_half_stroke"))); + quickFilterMainTypeWidget->setButtonIcon(themePixmap(QStringLiteral("icons/circle_half_stroke"))); quickFilterSubTypeWidget = new SettingsButtonWidget(this); - quickFilterSubTypeWidget->setButtonIcon(loadThemedIcon(QStringLiteral("icons/dragon"))); + quickFilterSubTypeWidget->setButtonIcon(themePixmap(QStringLiteral("icons/dragon"))); quickFilterSetWidget = new SettingsButtonWidget(this); - quickFilterSetWidget->setButtonIcon(loadThemedIcon(QStringLiteral("icons/scroll"))); + quickFilterSetWidget->setButtonIcon(themePixmap(QStringLiteral("icons/scroll"))); quickFilterFormatLegalityWidget = new SettingsButtonWidget(this); - quickFilterFormatLegalityWidget->setButtonIcon(loadThemedIcon(QStringLiteral("icons/scales"))); + quickFilterFormatLegalityWidget->setButtonIcon(themePixmap(QStringLiteral("icons/scale_balanced"))); retranslateUi(); } diff --git a/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_widget.cpp b/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_widget.cpp index af5c74969..6037418ad 100644 --- a/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_widget.cpp +++ b/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_widget.cpp @@ -4,9 +4,7 @@ #include "../../../filters/filter_tree_model.h" #include "../../../filters/syntax_help.h" #include "../../key_signals.h" -#include "../../theme_icon.h" -#include "../cards/card_info_picture_widget.h" -#include "../cards/card_info_picture_with_text_overlay_widget.h" +#include "../../pixel_map_generator.h" #include "../cards/card_size_widget.h" #include "../deck_editor/card_database_view.h" #include "../general/layout_containers/flow_widget.h" @@ -81,8 +79,8 @@ VisualDatabaseDisplayWidget::VisualDatabaseDisplayWidget(QWidget *parent, searchEdit->setObjectName("searchEdit"); searchEdit->setPlaceholderText(tr("Search by card name (or search expressions)")); searchEdit->setClearButtonEnabled(true); - searchEdit->addAction(loadThemedIcon(QStringLiteral("icons/search")), QLineEdit::LeadingPosition); - auto help = searchEdit->addAction(loadThemedIcon(QStringLiteral("icons/info")), QLineEdit::TrailingPosition); + searchEdit->addAction(loadColorAdjustedPixmap("theme:icons/search"), QLineEdit::LeadingPosition); + auto help = searchEdit->addAction(themePixmap(QStringLiteral("icons/info")), QLineEdit::TrailingPosition); connect(help, &QAction::triggered, this, [this] { createSearchSyntaxHelpWindow(searchEdit); }); setFocusProxy(searchEdit); @@ -156,7 +154,7 @@ VisualDatabaseDisplayWidget::VisualDatabaseDisplayWidget(QWidget *parent, clearFilterWidget = new QToolButton(); clearFilterWidget->setFixedSize(32, 32); - clearFilterWidget->setIcon(loadThemedIcon(QStringLiteral("icons/delete"))); + clearFilterWidget->setIcon(themePixmap(QStringLiteral("icons/delete"))); connect(clearFilterWidget, &QToolButton::clicked, this, [this] { filterModel->blockSignals(true); filterModel->filterTree()->blockSignals(true); diff --git a/cockatrice/src/interface/widgets/visual_deck_editor/visual_deck_display_options_widget.cpp b/cockatrice/src/interface/widgets/visual_deck_editor/visual_deck_display_options_widget.cpp index f0c054c3a..63ef07835 100644 --- a/cockatrice/src/interface/widgets/visual_deck_editor/visual_deck_display_options_widget.cpp +++ b/cockatrice/src/interface/widgets/visual_deck_editor/visual_deck_display_options_widget.cpp @@ -1,6 +1,6 @@ #include "visual_deck_display_options_widget.h" -#include "../../theme_icon.h" +#include "../../pixel_map_generator.h" #include "../deck_editor/deck_editor_deck_dock_widget.h" #include "../quick_settings/settings_button_widget.h" #include "../tabs/visual_deck_editor/tab_deck_editor_visual.h" @@ -65,7 +65,7 @@ VisualDeckDisplayOptionsWidget::VisualDeckDisplayOptionsWidget(QWidget *parent) sortByLabel = new QLabel(this); sortCriteriaButton = new SettingsButtonWidget(this); - sortCriteriaButton->setButtonIcon(loadThemedIcon(QStringLiteral("icons/sort_arrow_down"))); + sortCriteriaButton->setButtonIcon(themePixmap(QStringLiteral("icons/sort_arrow_down"))); sortLabel = new QLabel(sortCriteriaButton); sortLabel->setWordWrap(true); @@ -119,11 +119,11 @@ void VisualDeckDisplayOptionsWidget::updateDisplayTypeButton() switch (currentDisplayType) { case DisplayType::Flat: displayTypeButton->setButtonText(tr("Toggle Layout: Flat")); - displayTypeButton->setButtonIcon(loadThemedIcon(QStringLiteral("icons/scroll"))); + displayTypeButton->setButtonIcon(themePixmap(QStringLiteral("icons/scroll"))); break; case DisplayType::Overlap: displayTypeButton->setButtonText(tr("Toggle Layout: Overlap")); - displayTypeButton->setButtonIcon(loadThemedIcon(QStringLiteral("icons/scales"))); + displayTypeButton->setButtonIcon(themePixmap(QStringLiteral("icons/scales"))); break; } } diff --git a/cockatrice/src/interface/widgets/visual_deck_editor/visual_deck_editor_widget.cpp b/cockatrice/src/interface/widgets/visual_deck_editor/visual_deck_editor_widget.cpp index 0e0b5e4b4..506ffa2b4 100644 --- a/cockatrice/src/interface/widgets/visual_deck_editor/visual_deck_editor_widget.cpp +++ b/cockatrice/src/interface/widgets/visual_deck_editor/visual_deck_editor_widget.cpp @@ -1,7 +1,7 @@ #include "visual_deck_editor_widget.h" #include "../../../client/settings/cache_settings.h" -#include "../../theme_icon.h" +#include "../../pixel_map_generator.h" #include "../cards/card_size_widget.h" #include "../cards/deck_card_zone_display_widget.h" #include "../general/layout_containers/flow_widget.h" @@ -144,7 +144,7 @@ void VisualDeckEditorWidget::initializeSearchBarAndCompleter() // Search button functionality searchPushButton = new CompactPushButton(searchContainer); - searchPushButton->setButtonIcon(loadThemedIcon(QStringLiteral("icons/search"))); + searchPushButton->setButtonIcon(themePixmap(QStringLiteral("icons/search"))); connect(searchPushButton, &QPushButton::clicked, this, [this] { addCardFromSearch(); }); searchLayout->addWidget(searchPushButton); diff --git a/cockatrice/src/interface/widgets/visual_deck_storage/visual_deck_storage_search_widget.cpp b/cockatrice/src/interface/widgets/visual_deck_storage/visual_deck_storage_search_widget.cpp index 10a3bf33a..f2669f55d 100644 --- a/cockatrice/src/interface/widgets/visual_deck_storage/visual_deck_storage_search_widget.cpp +++ b/cockatrice/src/interface/widgets/visual_deck_storage/visual_deck_storage_search_widget.cpp @@ -1,7 +1,7 @@ #include "visual_deck_storage_search_widget.h" #include "../../../filters/syntax_help.h" -#include "../../theme_icon.h" +#include "../../pixel_map_generator.h" #include #include @@ -27,9 +27,9 @@ VisualDeckStorageSearchWidget::VisualDeckStorageSearchWidget(QWidget *parent) : searchBar = new QLineEdit(this); searchBar->setPlaceholderText(tr("Search by filename (or search expression)")); searchBar->setClearButtonEnabled(true); - searchBar->addAction(loadThemedIcon(QStringLiteral("icons/search")), QLineEdit::LeadingPosition); + searchBar->addAction(loadColorAdjustedPixmap("theme:icons/search"), QLineEdit::LeadingPosition); - auto help = searchBar->addAction(loadThemedIcon(QStringLiteral("icons/info")), QLineEdit::TrailingPosition); + auto help = searchBar->addAction(themePixmap(QStringLiteral("icons/info")), QLineEdit::TrailingPosition); connect(help, &QAction::triggered, this, [this] { createDeckSearchSyntaxHelpWindow(searchBar); }); layout->addWidget(searchBar); diff --git a/cockatrice/src/interface/widgets/visual_deck_storage/visual_deck_storage_widget.cpp b/cockatrice/src/interface/widgets/visual_deck_storage/visual_deck_storage_widget.cpp index bbf68adb9..4e1d6293f 100644 --- a/cockatrice/src/interface/widgets/visual_deck_storage/visual_deck_storage_widget.cpp +++ b/cockatrice/src/interface/widgets/visual_deck_storage/visual_deck_storage_widget.cpp @@ -1,7 +1,7 @@ #include "visual_deck_storage_widget.h" #include "../../../client/settings/cache_settings.h" -#include "../../theme_icon.h" +#include "../../pixel_map_generator.h" #include "deck_preview/deck_preview_color_identity_filter_widget.h" #include "deck_preview/deck_preview_widget.h" #include "libcockatrice/card/database/card_database.h" @@ -62,12 +62,12 @@ VisualDeckStorageWidget::VisualDeckStorageWidget(QWidget *parent) : QWidget(pare searchWidget = new VisualDeckStorageSearchWidget(this); refreshButton = new QToolButton(this); - refreshButton->setIcon(loadThemedIcon(QStringLiteral("icons/reload"))); + refreshButton->setIcon(themePixmap(QStringLiteral("icons/reload"))); refreshButton->setFixedSize(32, 32); connect(refreshButton, &QPushButton::clicked, this, &VisualDeckStorageWidget::refreshIfPossible); shareButton = new QToolButton(this); - shareButton->setIcon(loadThemedIcon(QStringLiteral("icons/share"))); + shareButton->setIcon(themePixmap(QStringLiteral("icons/share"))); shareButton->setFixedSize(32, 32); shareButton->setVisible(false); connect(shareButton, &QPushButton::clicked, this, &VisualDeckStorageWidget::shareRequested); diff --git a/cockatrice/src/interface/window_main.cpp b/cockatrice/src/interface/window_main.cpp index 715505926..79a802234 100644 --- a/cockatrice/src/interface/window_main.cpp +++ b/cockatrice/src/interface/window_main.cpp @@ -50,7 +50,6 @@ #include "latency_tracker.h" #include "logger.h" #include "pixel_map_generator.h" -#include "theme_icon.h" #include "version_string.h" #include "widgets/dialogs/dlg_connect.h" #include "widgets/server/handle_public_servers.h" @@ -388,10 +387,10 @@ void MainWindow::retranslateUi() aRegister->setText(tr("&Register to server...")); aForgotPassword->setText(tr("&Restore password...")); aSettings->setText(tr("&Settings...")); - aSettings->setIcon(loadThemedIcon(QStringLiteral("icons/settings"))); + aSettings->setIcon(themePixmap(QStringLiteral("icons/settings"))); settingsButton->setToolTip(tr("Settings")); settingsButton->setAccessibleName(tr("Settings")); - settingsButton->setIcon(loadThemedIcon(QStringLiteral("icons/settings"))); + settingsButton->setIcon(themePixmap(QStringLiteral("icons/cogwheel"))); aExit->setText(tr("&Exit")); #if defined(__APPLE__) /* For OSX */ diff --git a/libcockatrice_utility/CMakeLists.txt b/libcockatrice_utility/CMakeLists.txt index 98ad40e69..8d2cb5b92 100644 --- a/libcockatrice_utility/CMakeLists.txt +++ b/libcockatrice_utility/CMakeLists.txt @@ -8,7 +8,6 @@ set(UTILITY_SOURCES libcockatrice/utility/passwordhasher.cpp libcockatrice/utility/report_categories.cpp libcockatrice/utility/server_rate_limiter.cpp - libcockatrice/utility/svg_recolor.cpp libcockatrice/utility/translation_loader.cpp libcockatrice/utility/user_data_backup.cpp libcockatrice/utility/warning_categories.cpp @@ -35,7 +34,6 @@ set(UTILITY_HEADERS libcockatrice/utility/zone_names.h libcockatrice/utility/days_years_between.h libcockatrice/utility/server_rate_limiter.h - libcockatrice/utility/svg_recolor.h libcockatrice/utility/translation_loader.h libcockatrice/utility/user_data_backup.h libcockatrice/utility/warning_categories.h diff --git a/libcockatrice_utility/libcockatrice/utility/svg_recolor.cpp b/libcockatrice_utility/libcockatrice/utility/svg_recolor.cpp deleted file mode 100644 index 041e289fe..000000000 --- a/libcockatrice_utility/libcockatrice/utility/svg_recolor.cpp +++ /dev/null @@ -1,44 +0,0 @@ -#include "svg_recolor.h" - -#include - -namespace -{ -constexpr QByteArrayView SENTINEL = "currentColor"; - -/// Case-insensitive byte search for the sentinel, starting at @p from. -/// QByteArray::indexOf() only matches case-sensitively for QByteArrayView, and -/// its char overload cannot match a word. -qsizetype findSentinel(const QByteArray &bytes, qsizetype from) -{ - // QByteArrayView::sliced() is a view, so the scan does not allocate a copy - // at every byte position (QByteArray::sliced() would). - const QByteArrayView view(bytes); - for (qsizetype i = from; i + SENTINEL.size() <= view.size(); ++i) { - if (view.sliced(i, SENTINEL.size()).compare(SENTINEL, Qt::CaseInsensitive) == 0) { - return i; - } - } - return -1; -} -} // namespace - -QByteArray resolveSvgColorSentinel(const QByteArray &svg, const QByteArray &color) -{ - // The sentinel and the replacement are both ASCII, so stay in bytes: a - // QString round trip would decode the whole document (and mangle bytes that - // are not valid UTF-8) even on the no-op path. - qsizetype pos = findSentinel(svg, 0); - if (pos < 0) { - return svg; - } - - QByteArray resolved = svg; - while (pos >= 0) { - resolved.replace(pos, SENTINEL.size(), color); - // Skip past the inserted color: a caller's color never contains the - // sentinel, and this keeps the scan linear. - pos = findSentinel(resolved, pos + color.size()); - } - return resolved; -} diff --git a/libcockatrice_utility/libcockatrice/utility/svg_recolor.h b/libcockatrice_utility/libcockatrice/utility/svg_recolor.h deleted file mode 100644 index 56d609b87..000000000 --- a/libcockatrice_utility/libcockatrice/utility/svg_recolor.h +++ /dev/null @@ -1,31 +0,0 @@ -/** - * @file svg_recolor.h - * @ingroup Core - */ - -#ifndef SVG_RECOLOR_H -#define SVG_RECOLOR_H - -#include - -/** - * @brief Resolves the `currentColor` sentinel in authored SVG data. - * - * Qt's SVG renderer has no CSS cascade or ambient color context, so the - * sentinel is not resolved by Qt. Callers pass the concrete color they want - * (for example a QPalette role serialized to #RRGGBB) and receive SVG bytes - * that are safe to hand to QSvgRenderer. - * - * Matching is a plain, case-insensitive substring search, not a CSS parse: an - * identifier that merely starts with the sentinel (for example - * `id="currentColorStop"`) is rewritten too. That is acceptable for the assets - * this project authors, and keeps the resolver dependency-free. - * - * @param svg SVG source bytes, expected to be UTF-8. Invalid UTF-8 is preserved - * byte-for-byte, since the resolver never decodes the document. - * @param color Concrete SVG color (for example "#ff00ff"), as ASCII bytes. - * @return The recolored SVG source; the input is left untouched. - */ -QByteArray resolveSvgColorSentinel(const QByteArray &svg, const QByteArray &color); - -#endif // SVG_RECOLOR_H diff --git a/oracle/CMakeLists.txt b/oracle/CMakeLists.txt index 4ca2acd07..f68f51f23 100644 --- a/oracle/CMakeLists.txt +++ b/oracle/CMakeLists.txt @@ -31,7 +31,6 @@ set(oracle_SOURCES ../cockatrice/src/client/network/update/client/release_channel.cpp ../cockatrice/src/interface/pixel_map_generator.cpp ../cockatrice/src/interface/theme_config.cpp - ../cockatrice/src/interface/theme_icon.cpp ../cockatrice/src/interface/theme_manager.cpp ${VERSION_STRING_CPP} ) diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 84b9f02bf..e25390ba0 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -177,7 +177,6 @@ cockatrice_add_test(NAME test_age_formatting SOURCES test_age_formatting.cpp LIB cockatrice_add_test(NAME user_data_backup_test SOURCES user_data_backup_test.cpp LIBS libcockatrice_utility) cockatrice_add_test(NAME password_hash_test SOURCES password_hash_test.cpp LIBS libcockatrice_utility) cockatrice_add_test(NAME report_categories_test SOURCES report_categories_test.cpp LIBS libcockatrice_utility) -cockatrice_add_test(NAME svg_recolor_test SOURCES svg_recolor_test.cpp LIBS libcockatrice_utility) cockatrice_add_test(NAME warning_categories_test SOURCES warning_categories_test.cpp LIBS libcockatrice_utility) cockatrice_add_test( NAME @@ -191,15 +190,6 @@ cockatrice_add_test( ${CMAKE_SOURCE_DIR}/cockatrice/src ${CMAKE_SOURCE_DIR}/libcockatrice_network ) -cockatrice_add_test( - NAME - spoiler_file_test - SOURCES - ${CMAKE_SOURCE_DIR}/cockatrice/src/client/network/update/card_spoiler/spoiler_file.cpp - spoiler_file_test.cpp - INCLUDE_DIRS - ${CMAKE_SOURCE_DIR}/cockatrice/src -) # The installer's holder probe is Windows-only, but the wording it produces and the # question which programs are the payload's own are plain C++ and are what a stuck user # has to act on, so they are tested everywhere. @@ -566,7 +556,6 @@ add_subdirectory(card_zone_algorithms) add_subdirectory(carddatabase) add_subdirectory(deck_list_model) add_subdirectory(deck_list_zones) -add_subdirectory(icon_assets) add_subdirectory(loading_from_clipboard) add_subdirectory(model_contract) add_subdirectory(movecard_tests) diff --git a/tests/icon_assets/CMakeLists.txt b/tests/icon_assets/CMakeLists.txt deleted file mode 100644 index da9776f8e..000000000 --- a/tests/icon_assets/CMakeLists.txt +++ /dev/null @@ -1,3 +0,0 @@ -cockatrice_add_test( - NAME icon_assets_test SOURCES icon_assets_test.cpp DEFINITIONS COCKATRICE_SOURCE_DIR="${CMAKE_SOURCE_DIR}" -) diff --git a/tests/icon_assets/icon_assets_test.cpp b/tests/icon_assets/icon_assets_test.cpp deleted file mode 100644 index f0b06d5cc..000000000 --- a/tests/icon_assets/icon_assets_test.cpp +++ /dev/null @@ -1,241 +0,0 @@ -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -namespace -{ -const QString ICONS_RELATIVE_PATH = QStringLiteral("cockatrice/resources/icons"); - -QString iconsDir() -{ - return QDir(QStringLiteral(COCKATRICE_SOURCE_DIR)).absoluteFilePath(ICONS_RELATIVE_PATH); -} - -/** @brief True for content art that is allowed to carry full color. */ -bool isContentArt(const QString &relativePath) -{ - return relativePath.startsWith(QStringLiteral("mana/")); -} - -QString violation(const QString &rule, const QString &relativePath) -{ - return rule + QLatin1Char(' ') + relativePath; -} - -/** - * @brief Reads one SVG and returns true when the root carries a usable viewBox. - * - * Also reports malformed XML and editor-only metadata through the out params. - */ -bool inspectSvg(const QString &absolutePath, - bool &validXml, - bool &hasViewBox, - bool &hasEditorMetadata, - bool &hasPhysicalSize) -{ - validXml = false; - hasViewBox = false; - hasEditorMetadata = false; - hasPhysicalSize = false; - - QFile file(absolutePath); - if (!file.open(QIODevice::ReadOnly)) { - return false; - } - - QXmlStreamReader reader(&file); - bool sawRoot = false; - - while (!reader.atEnd()) { - reader.readNext(); - - if (reader.hasError()) { - return false; - } - - if (!reader.isStartElement()) { - continue; - } - - if (!sawRoot) { - sawRoot = true; - if (reader.name() != QStringLiteral("svg")) { - return false; - } - const QString viewBox = reader.attributes().value(QStringLiteral("viewBox")).toString().simplified(); - hasViewBox = viewBox == QStringLiteral("0 0 24 24"); - hasPhysicalSize = !reader.attributes().value(QStringLiteral("width")).isEmpty() || - !reader.attributes().value(QStringLiteral("height")).isEmpty(); - } - - if (reader.name() == QStringLiteral("metadata") || reader.name() == QStringLiteral("namedview")) { - hasEditorMetadata = true; - } - - for (const QXmlStreamAttribute &attribute : reader.attributes()) { - const QString prefix = attribute.prefix().toString(); - if (prefix == QStringLiteral("inkscape") || prefix == QStringLiteral("sodipodi") || - prefix == QStringLiteral("dc") || prefix == QStringLiteral("cc") || prefix == QStringLiteral("rdf")) { - hasEditorMetadata = true; - } - } - } - - validXml = sawRoot && !reader.hasError(); - return validXml; -} - -bool isAllowedPaintValue(const QString &value) -{ - const QString normalized = value.trimmed().toLower(); - return normalized.isEmpty() || normalized == QStringLiteral("none") || - normalized == QStringLiteral("currentcolor") || normalized == QStringLiteral("inherit") || - normalized == QStringLiteral("context-fill") || normalized == QStringLiteral("context-stroke") || - normalized.startsWith(QStringLiteral("url(")); -} - -QRegularExpression paintValuePattern() -{ - static const QRegularExpression pattern( - QStringLiteral(R"re((?:fill|stroke|stop-color|color)\s*[:=]\s*["']?\s*([^"'\s;>]+))re"), - QRegularExpression::CaseInsensitiveOption); - return pattern; -} - -bool hasHardcodedColor(const QString &absolutePath) -{ - QFile file(absolutePath); - if (!file.open(QIODevice::ReadOnly)) { - return false; - } - - const QString content = QString::fromUtf8(file.readAll()); - auto matches = paintValuePattern().globalMatch(content); - while (matches.hasNext()) { - const QString value = matches.next().captured(1).trimmed(); - if (isAllowedPaintValue(value)) { - continue; - } - const QString normalized = value.toLower(); - if (normalized.startsWith(QLatin1Char('#')) || normalized.contains(QStringLiteral("rgb(")) || - normalized.contains(QStringLiteral("hsl(")) || QColor(value).isValid()) { - return true; - } - } - return false; -} - -bool hasCurrentColorToken(const QString &absolutePath) -{ - QFile file(absolutePath); - if (!file.open(QIODevice::ReadOnly)) { - return false; - } - - const QString content = QString::fromUtf8(file.readAll()); - auto matches = paintValuePattern().globalMatch(content); - while (matches.hasNext()) { - if (matches.next().captured(1).trimmed().compare(QStringLiteral("currentColor"), Qt::CaseInsensitive) == 0) { - return true; - } - } - return false; -} - -bool isSchemeVariant(const QString &fileName) -{ - return fileName.contains(QStringLiteral("-dark."), Qt::CaseInsensitive) || - fileName.contains(QStringLiteral("-light."), Qt::CaseInsensitive); -} - -bool hasColorInFilename(const QString &fileName) -{ - static const QRegularExpression colorSuffix( - QStringLiteral(R"(_(green|blue|red|yellow|black|white|gray|grey)\.svg$)"), - QRegularExpression::CaseInsensitiveOption); - return colorSuffix.match(fileName).hasMatch(); -} - -/** @brief Collects every current violation as "rule relative/path.svg". */ -QSet collectViolations() -{ - QSet violations; - - QDirIterator it(iconsDir(), QDir::Files, QDirIterator::Subdirectories); - while (it.hasNext()) { - it.next(); - const QFileInfo info = it.fileInfo(); - const QString relativePath = QDir(iconsDir()).relativeFilePath(info.absoluteFilePath()); - if (isContentArt(relativePath)) { - continue; - } - if (info.suffix().compare(QStringLiteral("svg"), Qt::CaseInsensitive) != 0) { - violations.insert(violation(QStringLiteral("raster"), relativePath)); - continue; - } - - bool validXml = false; - bool hasViewBox = false; - bool hasEditorMetadata = false; - bool hasPhysicalSize = false; - inspectSvg(info.absoluteFilePath(), validXml, hasViewBox, hasEditorMetadata, hasPhysicalSize); - - if (!validXml) { - violations.insert(violation(QStringLiteral("xml"), relativePath)); - } - if (validXml && !hasViewBox) { - violations.insert(violation(QStringLiteral("viewbox"), relativePath)); - } - if (hasPhysicalSize) { - violations.insert(violation(QStringLiteral("physical-size"), relativePath)); - } - if (hasEditorMetadata) { - violations.insert(violation(QStringLiteral("editor-metadata"), relativePath)); - } - if (hasHardcodedColor(info.absoluteFilePath()) && !isSchemeVariant(info.fileName())) { - violations.insert(violation(QStringLiteral("hardcoded-color"), relativePath)); - } - if (!hasCurrentColorToken(info.absoluteFilePath()) && !isSchemeVariant(info.fileName())) { - violations.insert(violation(QStringLiteral("no-token"), relativePath)); - } - if (hasColorInFilename(info.fileName())) { - violations.insert(violation(QStringLiteral("color-filename"), relativePath)); - } - } - - return violations; -} - -QString formatSet(const QSet &entries) -{ - QStringList lines(entries.begin(), entries.end()); - lines.sort(); - return lines.join(QLatin1Char('\n')); -} - -} // namespace - -/** - * @brief Every UI glyph follows the palette-token rules. - * - * The legacy set was paid down icon by icon, so no baseline grandfathers - * violations: a new violation fails right here. Scheme variants - * (`*-dark.svg`/`*-light.svg`) and mana art are exempt inside collectViolations(). - */ -TEST(IconAssets, AssetsFollowTheRules) -{ - ASSERT_TRUE(QDir::isAbsolutePath(iconsDir())); - ASSERT_TRUE(QDir(iconsDir()).exists()) << "Missing icon directory: " << iconsDir().toStdString(); - - const QSet violations = collectViolations(); - EXPECT_TRUE(violations.isEmpty()) << "Icon rule violations. Fix the icon; do not add exceptions:\n" - << formatSet(violations).toStdString(); -} diff --git a/tests/spoiler_file_test.cpp b/tests/spoiler_file_test.cpp deleted file mode 100644 index 9944f95bd..000000000 --- a/tests/spoiler_file_test.cpp +++ /dev/null @@ -1,149 +0,0 @@ -#include "client/network/update/card_spoiler/spoiler_file.h" - -#include -#include -#include -#include -#include - -#ifdef Q_OS_UNIX -#include -#include -#include -#endif - -namespace -{ -class SpoilerFileTest : public ::testing::Test -{ -protected: - QTemporaryDir directory; - QString path; - QString error; - const QByteArray original = ""; - - void SetUp() override - { - ASSERT_TRUE(directory.isValid()); - path = directory.filePath("spoiler.xml"); - QFile file(path); - ASSERT_TRUE(file.open(QIODevice::WriteOnly)); - ASSERT_EQ(file.write(original), original.size()); - } - - QByteArray read() - { - QFile file(path); - EXPECT_TRUE(file.open(QIODevice::ReadOnly)); - return file.readAll(); - } - - SpoilerFile::SaveResult save(const QByteArray &data) - { - return SpoilerFile::save(path, data, [] { return true; }, error); - } -}; - -TEST_F(SpoilerFileTest, InvalidDocumentsPreserveExistingFile) -{ - for (const QByteArray &invalid : - {QByteArray(), QByteArray(""), - QByteArray(""), - QByteArray(""), - QByteArray(""), - QByteArray(""), - QByteArray("")}) { - EXPECT_EQ(save(invalid), SpoilerFile::SaveResult::Failed); - EXPECT_FALSE(error.isEmpty()); - EXPECT_EQ(read(), original); - } -} - -TEST_F(SpoilerFileTest, SupportedVersionsAreAccepted) -{ - EXPECT_TRUE(SpoilerFile::validate(original).isEmpty()); - EXPECT_TRUE( - SpoilerFile::validate("").isEmpty()); -} - -TEST_F(SpoilerFileTest, IdenticalFileIsUnchanged) -{ - EXPECT_EQ(save(original), SpoilerFile::SaveResult::Unchanged); - EXPECT_TRUE(error.isEmpty()); - EXPECT_EQ(read(), original); -} - -TEST_F(SpoilerFileTest, CompleteFileHashDetectsChangesAfterLargeCommonPrefix) -{ - QByteArray changed = ""; - ASSERT_EQ(save(changed), SpoilerFile::SaveResult::Written); - changed.replace("", ""); - EXPECT_EQ(save(changed), SpoilerFile::SaveResult::Written); - EXPECT_EQ(read(), changed); - EXPECT_TRUE(error.isEmpty()); -} - -TEST_F(SpoilerFileTest, CancellationBeforeWritePreservesExistingFile) -{ - EXPECT_EQ(SpoilerFile::save(path, original, [] { return false; }, error), SpoilerFile::SaveResult::Failed); - EXPECT_FALSE(error.isEmpty()); - EXPECT_EQ(read(), original); -} - -TEST_F(SpoilerFileTest, CancellationBeforeCommitPreservesExistingFile) -{ - int checks = 0; - EXPECT_EQ(SpoilerFile::save( - path, original + "\n", [&checks] { return ++checks == 1; }, error), - SpoilerFile::SaveResult::Failed); - EXPECT_EQ(checks, 2); - EXPECT_FALSE(error.isEmpty()); - EXPECT_EQ(read(), original); - EXPECT_EQ(QDir(directory.path()).entryList(QDir::Files), QStringList{"spoiler.xml"}); -} - -TEST_F(SpoilerFileTest, MissingParentIsCreated) -{ - path = directory.filePath("nested/spoiler.xml"); - EXPECT_EQ(save(original), SpoilerFile::SaveResult::Written); - EXPECT_EQ(read(), original); -} - -TEST_F(SpoilerFileTest, FileInParentPathReportsFailureWithoutModifyingIt) -{ - EXPECT_EQ(SpoilerFile::save( - QDir(path).filePath("spoiler.xml"), original, [] { return true; }, error), - SpoilerFile::SaveResult::Failed); - EXPECT_FALSE(error.isEmpty()); - EXPECT_EQ(read(), original); -} - -#ifdef Q_OS_UNIX -TEST_F(SpoilerFileTest, ShortWriteLeavesExistingFileIntact) -{ - // Limit only the child process: a real short write exercises QSaveFile's - // buffered flush failure without depending on disk capacity or file ownership. - const QByteArray changed = ""; - EXPECT_EXIT( - { - struct rlimit limit; - if (getrlimit(RLIMIT_FSIZE, &limit) != 0) { - _exit(2); - } - limit.rlim_cur = 128; - if (setrlimit(RLIMIT_FSIZE, &limit) != 0) { - _exit(3); - } - std::signal(SIGXFSZ, SIG_IGN); - const auto result = save(changed); - _exit(result == SpoilerFile::SaveResult::Failed && !error.isEmpty() ? 0 : 1); - }, - ::testing::ExitedWithCode(0), ""); - EXPECT_EQ(read(), original); - EXPECT_EQ(QDir(directory.path()).entryList(QDir::Files), QStringList{"spoiler.xml"}); -} -#endif -} // namespace diff --git a/tests/svg_recolor_test.cpp b/tests/svg_recolor_test.cpp deleted file mode 100644 index abe1e8d1a..000000000 --- a/tests/svg_recolor_test.cpp +++ /dev/null @@ -1,66 +0,0 @@ -#include "libcockatrice/utility/svg_recolor.h" - -#include -#include - -namespace -{ - -TEST(SvgRecolor, ReplacesEverySentinel) -{ - const QByteArray svg = R"()"; - const QByteArray result = resolveSvgColorSentinel(svg, "#ff8800"); - - EXPECT_EQ(result.count("#ff8800"), 2); - EXPECT_FALSE(result.contains("currentColor")); -} - -TEST(SvgRecolor, ReplacesSentinelRegardlessOfCase) -{ - const QByteArray svg = R"()"; - - EXPECT_EQ(resolveSvgColorSentinel(svg, "#ff8800").count("#ff8800"), 2); -} - -TEST(SvgRecolor, LeavesTheRestOfTheDocumentIntact) -{ - const QByteArray svg = - R"()"; - const QByteArray result = resolveSvgColorSentinel(svg, "#123456"); - - EXPECT_TRUE(result.contains(R"(viewBox="0 0 24 24")")); - EXPECT_TRUE(result.contains(R"(d="M0 0h24v24")")); - EXPECT_TRUE(result.contains("an ordinary comment")); -} - -TEST(SvgRecolor, DoesNotModifyTheInput) -{ - const QByteArray svg = R"()"; - const QByteArray original = svg; - - resolveSvgColorSentinel(svg, "#000000"); - - EXPECT_EQ(svg, original); -} - -TEST(SvgRecolor, PreservesBytesThatAreNotValidUtf8) -{ - QByteArray svg = ""; - svg.append("\xff\xfe"); - svg.append(R"()"); - - const QByteArray result = resolveSvgColorSentinel(svg, "#ff8800"); - - EXPECT_TRUE(result.contains("\xff\xfe")); - EXPECT_FALSE(result.contains("currentColor")); - EXPECT_EQ(result.count("#ff8800"), 1); -} - -TEST(SvgRecolor, NoSentinelIsANoOp) -{ - const QByteArray svg = R"()"; - - EXPECT_EQ(resolveSvgColorSentinel(svg, "#ffffff"), svg); -} - -} // namespace diff --git a/tests/theme_migration/CMakeLists.txt b/tests/theme_migration/CMakeLists.txt index 818c1bb16..93493ee5f 100644 --- a/tests/theme_migration/CMakeLists.txt +++ b/tests/theme_migration/CMakeLists.txt @@ -5,14 +5,12 @@ cockatrice_add_test( ${CMAKE_SOURCE_DIR}/cockatrice/src/interface/theme_manager.cpp ${CMAKE_SOURCE_DIR}/cockatrice/src/interface/theme_config.cpp ${CMAKE_SOURCE_DIR}/cockatrice/src/interface/pixel_map_generator.cpp - ${CMAKE_SOURCE_DIR}/cockatrice/src/interface/theme_icon.cpp ${CMAKE_SOURCE_DIR}/cockatrice/src/client/settings/cache_settings.cpp ${CMAKE_SOURCE_DIR}/cockatrice/src/client/settings/card_counter_settings.cpp ${CMAKE_SOURCE_DIR}/cockatrice/src/client/settings/shortcuts_settings.cpp ${CMAKE_SOURCE_DIR}/cockatrice/src/client/network/update/client/release_channel.cpp ${VERSION_STRING_CPP} theme_migration_test.cpp - theme_icon_render_test.cpp LIBS libcockatrice_settings libcockatrice_network @@ -20,6 +18,4 @@ cockatrice_add_test( libcockatrice_protocol INCLUDE_DIRS ${CMAKE_SOURCE_DIR}/cockatrice/src - DEFINITIONS - COCKATRICE_SOURCE_DIR="${CMAKE_SOURCE_DIR}" ) diff --git a/tests/theme_migration/theme_icon_render_test.cpp b/tests/theme_migration/theme_icon_render_test.cpp deleted file mode 100644 index f12db5b16..000000000 --- a/tests/theme_migration/theme_icon_render_test.cpp +++ /dev/null @@ -1,189 +0,0 @@ -#include "interface/theme_icon.h" -#include "interface/theme_manager.h" - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -namespace -{ -/** - * @brief Creates the GUI application QPixmap needs, using the offscreen plugin. - * - * The theme-migration binary is headless; creating this lazily keeps its other - * tests free of a display dependency. - */ -void ensureGuiApplication() -{ - static int argc = 1; - static char name[] = "theme_icon_test"; - static char *argv[] = {name, nullptr}; - // Leaked on purpose: a function-local static QApplication is destroyed before Qt's - // own library statics at exit, and the network module's destructors then run - // against a torn-down QObject layer (SEGV under AddressSanitizer). - static QApplication *app = new QApplication(argc, argv); - Q_UNUSED(app); -} - -QString resourcesDir() -{ - return QDir(QStringLiteral(COCKATRICE_SOURCE_DIR)).absoluteFilePath(QStringLiteral("cockatrice/resources")); -} - -int countOpaquePixels(const QPixmap &pixmap, const QColor &expected) -{ - const QImage image = pixmap.toImage(); - int count = 0; - for (int y = 0; y < image.height(); ++y) { - for (int x = 0; x < image.width(); ++x) { - const QColor pixel = image.pixelColor(x, y); - if (pixel.alpha() > 200 && pixel.red() == expected.red() && pixel.green() == expected.green() && - pixel.blue() == expected.blue()) { - ++count; - } - } - } - return count; -} - -QByteArray svgWith(const QByteArray &colorToken) -{ - return QByteArrayLiteral(R"()"); -} - -/** - * @brief Restores the global theme search paths and palette after each test. - * - * The icon engine reads both process-wide values, so a failing assertion must - * not leak a temporary theme directory or palette into the next test. - */ -class ThemeIconRenderTest : public ::testing::Test -{ -protected: - void SetUp() override - { - qputenv("QT_QPA_PLATFORM", "offscreen"); - ensureGuiApplication(); - savedSearchPaths = QDir::searchPaths(QStringLiteral("theme")); - savedPalette = qApp->palette(); - themeManager = new ThemeManager; - QDir::setSearchPaths(QStringLiteral("theme"), {resourcesDir()}); - } - - void TearDown() override - { - delete themeManager; - themeManager = nullptr; - QDir::setSearchPaths(QStringLiteral("theme"), savedSearchPaths); - qApp->setPalette(savedPalette); - } - - QStringList savedSearchPaths; ///< Search paths captured before the test. - QPalette savedPalette; ///< Application palette captured before the test. -}; - -/** @brief A non-square request must letterbox, not squash, a 24×24 glyph. */ -TEST_F(ThemeIconRenderTest, NonSquareRequestPreservesAspectRatio) -{ - const QIcon icon = loadThemedIcon(QStringLiteral("icons/player")); - ASSERT_FALSE(icon.isNull()); - - EXPECT_EQ(icon.actualSize(QSize(20, 15)), QSize(15, 15)); - - const QPixmap pixmap = icon.pixmap(QSize(20, 15)); - ASSERT_FALSE(pixmap.isNull()); - EXPECT_EQ(pixmap.deviceIndependentSize(), QSizeF(15, 15)); -} - -/** @brief The engine must follow palette flips, icon modes, and DPR. */ -TEST_F(ThemeIconRenderTest, ThemedIconFollowsPaletteModeAndDpr) -{ - QPalette palette = qApp->palette(); - palette.setColor(QPalette::WindowText, QColor(QStringLiteral("#123456"))); - qApp->setPalette(palette); - - const QIcon icon = loadThemedIcon(QStringLiteral("icons/search")); - ASSERT_FALSE(icon.isNull()); - - const QPixmap normal = icon.pixmap(QSize(24, 24), 1.0); - ASSERT_FALSE(normal.isNull()); - EXPECT_EQ(normal.size(), QSize(24, 24)); - EXPECT_DOUBLE_EQ(normal.devicePixelRatio(), 1.0); - EXPECT_GT(countOpaquePixels(normal, QColor(QStringLiteral("#123456"))), 0); - - const QPixmap hidpi = icon.pixmap(QSize(24, 24), 2.0); - ASSERT_FALSE(hidpi.isNull()); - EXPECT_DOUBLE_EQ(hidpi.devicePixelRatio(), 2.0); - EXPECT_EQ(hidpi.deviceIndependentSize(), QSizeF(24, 24)); - - palette.setColor(QPalette::WindowText, QColor(QStringLiteral("#abcdef"))); - qApp->setPalette(palette); - const QPixmap flipped = icon.pixmap(QSize(24, 24), 1.0); - EXPECT_GT(countOpaquePixels(flipped, QColor(QStringLiteral("#abcdef"))), 0); - - QPalette selectedPalette = qApp->palette(); - selectedPalette.setColor(QPalette::HighlightedText, QColor(QStringLiteral("#00ff00"))); - qApp->setPalette(selectedPalette); - const QPixmap selected = icon.pixmap(QSize(24, 24), 1.0, QIcon::Selected, QIcon::Off); - EXPECT_GT(countOpaquePixels(selected, QColor(QStringLiteral("#00ff00"))), 0); -} -} // namespace - -TEST_F(ThemeIconRenderTest, SentinelUsesPaletteColor) -{ - QPalette palette; - palette.setColor(QPalette::WindowText, QColor(Qt::white)); - qApp->setPalette(palette); - - const QPixmap pixmap = loadSentinelSvgPixmap(QStringLiteral("icons/search.svg")); - - ASSERT_FALSE(pixmap.isNull()); - EXPECT_EQ(pixmap.size(), QSize(24, 24)); - EXPECT_GT(countOpaquePixels(pixmap, Qt::white), 0); -} - -TEST_F(ThemeIconRenderTest, NonSentinelSvgReturnsNull) -{ - QTemporaryDir themeDir; - ASSERT_TRUE(themeDir.isValid()); - QFile file(QDir(themeDir.path()).absoluteFilePath(QStringLiteral("plain.svg"))); - ASSERT_TRUE(file.open(QIODevice::WriteOnly)); - file.write(svgWith("#000000")); - file.close(); - QDir::addSearchPath(QStringLiteral("theme"), themeDir.path()); - - EXPECT_TRUE(loadSentinelSvgPixmap(QStringLiteral("plain.svg")).isNull()); - EXPECT_TRUE(loadSentinelSvgPixmap(QStringLiteral("icons/search.png")).isNull()); -} - -TEST_F(ThemeIconRenderTest, LowercaseSentinelIsResolved) -{ - QTemporaryDir themeDir; - ASSERT_TRUE(themeDir.isValid()); - QFile file(QDir(themeDir.path()).absoluteFilePath(QStringLiteral("lower.svg"))); - ASSERT_TRUE(file.open(QIODevice::WriteOnly)); - file.write(svgWith("currentcolor")); - file.close(); - - QDir::addSearchPath(QStringLiteral("theme"), themeDir.path()); - - QPalette palette; - palette.setColor(QPalette::WindowText, QColor(QStringLiteral("#123456"))); - qApp->setPalette(palette); - - const QPixmap pixmap = loadSentinelSvgPixmap(QStringLiteral("lower.svg")); - - ASSERT_FALSE(pixmap.isNull()); - EXPECT_GT(countOpaquePixels(pixmap, QColor(QStringLiteral("#123456"))), 0); -}