From 85cb48046f99f2fbbf3633e5937dc930a2c131d1 Mon Sep 17 00:00:00 2001 From: KorewaWatchful Date: Thu, 21 May 2026 15:05:47 -0400 Subject: [PATCH] gui: themes and qss refactor # Conflicts: # .github/workflows/c-cpp.yml # .github/workflows/codeql-analysis.yml # vita3k/gui-qt/src/controls_dialog.cpp --- .ci/setup-linux.sh | 4 +- .github/workflows/c-cpp.yml | 3 + .github/workflows/codeql-analysis.yml | 1 + cmake/qt6.cmake | 3 +- vita3k/CMakeLists.txt | 17 +- vita3k/gui-qt/CMakeLists.txt | 10 +- .../gui-qt/include/gui-qt/controls_dialog.h | 2 + vita3k/gui-qt/include/gui-qt/gui_settings.h | 9 +- vita3k/gui-qt/include/gui-qt/log_widget.h | 10 +- vita3k/gui-qt/include/gui-qt/main_window.h | 34 +- vita3k/gui-qt/include/gui-qt/qt_utils.h | 22 +- .../gui-qt/include/gui-qt/settings_dialog.h | 7 +- .../include/gui-qt/settings_dialog_tooltips.h | 2 + vita3k/gui-qt/include/gui-qt/theme_manager.h | 133 ++ .../gui-qt/{stylesheets.h => theme_surface.h} | 13 +- vita3k/gui-qt/include/gui-qt/vita_theme.h | 93 ++ .../include/gui-qt/vita_themes_dialog.h | 149 +++ vita3k/gui-qt/resources/themes/dark/dark.qss | 285 +++++ .../gui-qt/resources/themes/light/light.qss | 281 ++++ .../resources/themes/vita/generated.qss.in | 664 ++++++++++ .../themes/waves/shell_background.svg | 26 + .../gui-qt/resources/themes/waves/theme.qss | 539 ++++++++ vita3k/gui-qt/src/apps_list.cpp | 7 +- vita3k/gui-qt/src/archive_install_dialog.cpp | 13 +- vita3k/gui-qt/src/controls_dialog.cpp | 90 +- vita3k/gui-qt/src/debug_widget.cpp | 4 +- vita3k/gui-qt/src/gui_settings.cpp | 19 - vita3k/gui-qt/src/log_widget.cpp | 250 ++-- vita3k/gui-qt/src/main_window.cpp | 340 +++-- vita3k/gui-qt/src/main_window.ui | 6 + vita3k/gui-qt/src/qt_utils.cpp | 131 +- vita3k/gui-qt/src/settings_dialog.cpp | 97 +- vita3k/gui-qt/src/settings_dialog.ui | 72 ++ .../gui-qt/src/settings_dialog_tooltips.cpp | 2 + vita3k/gui-qt/src/stylesheets.cpp | 321 ----- vita3k/gui-qt/src/theme_manager.cpp | 1066 +++++++++++++++ vita3k/gui-qt/src/theme_surface.cpp | 37 + .../gui-qt/src/trophy_collection_dialog.cpp | 2 +- vita3k/gui-qt/src/vita_theme.cpp | 921 +++++++++++++ vita3k/gui-qt/src/vita_themes_dialog.cpp | 1139 +++++++++++++++++ vita3k/icons/check_dark.svg | 3 + vita3k/icons/check_light.svg | 3 + vita3k/icons/radio_dot_dark.svg | 3 + vita3k/icons/radio_dot_light.svg | 3 + vita3k/interface.cpp | 52 +- vita3k/resources.qrc | 11 + vita3k/util/CMakeLists.txt | 1 + vita3k/util/include/util/string_utils.h | 1 + vita3k/util/include/util/vita_theme_utils.h | 34 + vita3k/util/src/string_utils.cpp | 14 + vita3k/util/src/vita_theme_utils.cpp | 81 ++ 51 files changed, 6300 insertions(+), 730 deletions(-) create mode 100644 vita3k/gui-qt/include/gui-qt/theme_manager.h rename vita3k/gui-qt/include/gui-qt/{stylesheets.h => theme_surface.h} (81%) create mode 100644 vita3k/gui-qt/include/gui-qt/vita_theme.h create mode 100644 vita3k/gui-qt/include/gui-qt/vita_themes_dialog.h create mode 100644 vita3k/gui-qt/resources/themes/dark/dark.qss create mode 100644 vita3k/gui-qt/resources/themes/light/light.qss create mode 100644 vita3k/gui-qt/resources/themes/vita/generated.qss.in create mode 100644 vita3k/gui-qt/resources/themes/waves/shell_background.svg create mode 100644 vita3k/gui-qt/resources/themes/waves/theme.qss delete mode 100644 vita3k/gui-qt/src/stylesheets.cpp create mode 100644 vita3k/gui-qt/src/theme_manager.cpp create mode 100644 vita3k/gui-qt/src/theme_surface.cpp create mode 100644 vita3k/gui-qt/src/vita_theme.cpp create mode 100644 vita3k/gui-qt/src/vita_themes_dialog.cpp create mode 100644 vita3k/icons/check_dark.svg create mode 100644 vita3k/icons/check_light.svg create mode 100644 vita3k/icons/radio_dot_dark.svg create mode 100644 vita3k/icons/radio_dot_light.svg create mode 100644 vita3k/util/include/util/vita_theme_utils.h create mode 100644 vita3k/util/src/vita_theme_utils.cpp diff --git a/.ci/setup-linux.sh b/.ci/setup-linux.sh index d991040cb..e506a7ada 100644 --- a/.ci/setup-linux.sh +++ b/.ci/setup-linux.sh @@ -42,7 +42,9 @@ sudo apt -y install \ libpipewire-0.3-dev \ libwayland-dev \ libdecor-0-dev \ - liburing-dev + liburing-dev \ + libgstreamer-plugins-bad1.0-0 \ + libgstreamer-plugins-good1.0-0 if [[ "$APPIMAGE_MODE" == "with-appimage" ]]; then curl -sLO "https://github.com/linuxdeploy/linuxdeploy/releases/latest/download/linuxdeploy-${ARCH}.AppImage" diff --git a/.github/workflows/c-cpp.yml b/.github/workflows/c-cpp.yml index 42d32e5ba..ed9749acc 100644 --- a/.github/workflows/c-cpp.yml +++ b/.github/workflows/c-cpp.yml @@ -134,6 +134,7 @@ jobs: curl:p zlib:p qt6-base:p + qt6-multimedia:p qt6-svg:p qt6-tools:p @@ -146,6 +147,7 @@ jobs: if: matrix.platform == 'windows-x64' with: version: ${{ env.QT_VERSION }} + modules: qtmultimedia host: windows target: desktop arch: win64_msvc2022_64 @@ -158,6 +160,7 @@ jobs: if: matrix.platform != 'windows-arm64' && matrix.platform != 'windows-x64' with: version: ${{ env.QT_VERSION }} + modules: qtmultimedia cache: true install-deps: ${{ matrix.qt_install_deps }} diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 7245ac6c3..9890bf40f 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -59,6 +59,7 @@ jobs: uses: jurplel/install-qt-action@v4 with: version: ${{ env.QT_VERSION }} + modules: qtmultimedia cache: true install-deps: 'true' diff --git a/cmake/qt6.cmake b/cmake/qt6.cmake index 1a2765c5d..136fcc164 100644 --- a/cmake/qt6.cmake +++ b/cmake/qt6.cmake @@ -4,7 +4,7 @@ add_library(vita3k_qt6 INTERFACE) set(VITA3K_QT_MIN_VER 6.11.0) -set(VITA3K_QT_COMPONENTS Core Gui Widgets Network Concurrent Svg LinguistTools) +set(VITA3K_QT_COMPONENTS Core Gui Widgets Network Concurrent Svg Multimedia LinguistTools) if(CMAKE_SYSTEM_NAME STREQUAL "Linux") list(APPEND VITA3K_QT_COMPONENTS GuiPrivate) endif() @@ -39,6 +39,7 @@ target_link_libraries(vita3k_qt6 INTERFACE Qt6::Network Qt6::Concurrent Qt6::Svg + Qt6::Multimedia ) if(CMAKE_SYSTEM_NAME STREQUAL "Linux" AND TARGET Qt6::GuiPrivate) diff --git a/vita3k/CMakeLists.txt b/vita3k/CMakeLists.txt index 756448c8d..9ac2df1be 100644 --- a/vita3k/CMakeLists.txt +++ b/vita3k/CMakeLists.txt @@ -226,6 +226,8 @@ if(NOT ANDROID) "${CMAKE_SOURCE_DIR}/i18n/qt/*.ts") set(VITA3K_QT_QM_DIR "${CMAKE_BINARY_DIR}/qt_translations") set(VITA3K_QT_QM_FILES "") + set(VITA3K_GUI_CONFIG_THEME_QSS "${CMAKE_CURRENT_SOURCE_DIR}/gui-qt/resources/themes/waves/theme.qss") + set(VITA3K_GUI_CONFIG_THEME_BG "${CMAKE_CURRENT_SOURCE_DIR}/gui-qt/resources/themes/waves/shell_background.svg") foreach(ts_file IN LISTS VITA3K_QT_TS_FILES) get_filename_component(ts_name "${ts_file}" NAME_WE) @@ -285,9 +287,12 @@ if(APPLE) add_custom_command( TARGET vita3k POST_BUILD + COMMAND ${CMAKE_COMMAND} -E make_directory "$/../Resources/gui-configs" COMMAND ${CMAKE_COMMAND} -E copy_directory "${CMAKE_CURRENT_SOURCE_DIR}/../data" "$/../Resources/data" COMMAND ${CMAKE_COMMAND} -E copy_directory "${VITA3K_QT_QM_DIR}" "$/../Resources/translations" - COMMAND ${CMAKE_COMMAND} -E copy_directory "${CMAKE_CURRENT_SOURCE_DIR}/shaders-builtin/" "$/../Resources/shaders-builtin") + COMMAND ${CMAKE_COMMAND} -E copy_directory "${CMAKE_CURRENT_SOURCE_DIR}/shaders-builtin/" "$/../Resources/shaders-builtin" + COMMAND ${CMAKE_COMMAND} -E copy_if_different "${VITA3K_GUI_CONFIG_THEME_QSS}" "$/../Resources/gui-configs/Waves.qss" + COMMAND ${CMAKE_COMMAND} -E copy_if_different "${VITA3K_GUI_CONFIG_THEME_BG}" "$/../Resources/gui-configs/shell_background.svg") if(USE_DISCORD_RICH_PRESENCE) if(ARCHITECTURE STREQUAL "x86_64") set(discord_dylib "${CMAKE_BINARY_DIR}/external/discord_game_sdk/lib/x86_64/discord_game_sdk.dylib") @@ -352,10 +357,13 @@ elseif(LINUX) add_custom_command( TARGET vita3k POST_BUILD + COMMAND ${CMAKE_COMMAND} -E make_directory "$/gui-configs" COMMAND ${CMAKE_COMMAND} -E copy_directory "${CMAKE_CURRENT_SOURCE_DIR}/../data" "$/data" COMMAND ${CMAKE_COMMAND} -E copy_directory "${VITA3K_QT_QM_DIR}" "$/translations" COMMAND ${CMAKE_COMMAND} -E copy_directory "${CMAKE_CURRENT_SOURCE_DIR}/shaders-builtin" "$/shaders-builtin" - COMMAND ${CMAKE_COMMAND} -E copy_directory "${CMAKE_CURRENT_SOURCE_DIR}/icons" "$/icons") + COMMAND ${CMAKE_COMMAND} -E copy_directory "${CMAKE_CURRENT_SOURCE_DIR}/icons" "$/icons" + COMMAND ${CMAKE_COMMAND} -E copy_if_different "${VITA3K_GUI_CONFIG_THEME_QSS}" "$/gui-configs/Waves.qss" + COMMAND ${CMAKE_COMMAND} -E copy_if_different "${VITA3K_GUI_CONFIG_THEME_BG}" "$/gui-configs/shell_background.svg") if(USE_DISCORD_RICH_PRESENCE) add_custom_command( TARGET vita3k @@ -370,10 +378,13 @@ elseif(WIN32) add_custom_command( TARGET vita3k POST_BUILD + COMMAND ${CMAKE_COMMAND} -E make_directory "$/gui-configs" COMMAND ${CMAKE_COMMAND} -E copy_directory "${CMAKE_CURRENT_SOURCE_DIR}/../data" "$/data" COMMAND ${CMAKE_COMMAND} -E copy_directory "${VITA3K_QT_QM_DIR}" "$/translations" COMMAND ${CMAKE_COMMAND} -E copy_directory "${CMAKE_CURRENT_SOURCE_DIR}/shaders-builtin" "$/shaders-builtin" - COMMAND ${CMAKE_COMMAND} -E copy_directory "${CMAKE_CURRENT_SOURCE_DIR}/icons" "$/icons") + COMMAND ${CMAKE_COMMAND} -E copy_directory "${CMAKE_CURRENT_SOURCE_DIR}/icons" "$/icons" + COMMAND ${CMAKE_COMMAND} -E copy_if_different "${VITA3K_GUI_CONFIG_THEME_QSS}" "$/gui-configs/Waves.qss" + COMMAND ${CMAKE_COMMAND} -E copy_if_different "${VITA3K_GUI_CONFIG_THEME_BG}" "$/gui-configs/shell_background.svg") if(USE_DISCORD_RICH_PRESENCE) add_custom_command( TARGET vita3k diff --git a/vita3k/gui-qt/CMakeLists.txt b/vita3k/gui-qt/CMakeLists.txt index ed66d882d..1b02414bd 100644 --- a/vita3k/gui-qt/CMakeLists.txt +++ b/vita3k/gui-qt/CMakeLists.txt @@ -24,7 +24,10 @@ add_library(gui-qt STATIC src/qt_utils.cpp src/settings_dialog.cpp src/settings_dialog_tooltips.cpp - src/stylesheets.cpp + src/theme_manager.cpp + src/theme_surface.cpp + src/vita_theme.cpp + src/vita_themes_dialog.cpp src/trophy_collection_dialog.cpp src/update_manager.cpp src/gui_language.cpp @@ -66,7 +69,10 @@ add_library(gui-qt STATIC include/gui-qt/qt_utils.h include/gui-qt/settings_dialog.h include/gui-qt/settings_dialog_tooltips.h - include/gui-qt/stylesheets.h + include/gui-qt/theme_manager.h + include/gui-qt/theme_surface.h + include/gui-qt/vita_theme.h + include/gui-qt/vita_themes_dialog.h include/gui-qt/trophy_collection_dialog.h include/gui-qt/update_manager.h include/gui-qt/gui_language.h diff --git a/vita3k/gui-qt/include/gui-qt/controls_dialog.h b/vita3k/gui-qt/include/gui-qt/controls_dialog.h index 43b4d1993..b4ce9b034 100644 --- a/vita3k/gui-qt/include/gui-qt/controls_dialog.h +++ b/vita3k/gui-qt/include/gui-qt/controls_dialog.h @@ -65,6 +65,7 @@ public: bool handle_sdl_event(const SDL_Event &event); protected: + void changeEvent(QEvent *event) override; void closeEvent(QCloseEvent *event) override; void done(int result) override; void keyPressEvent(QKeyEvent *event) override; @@ -216,6 +217,7 @@ private: void refresh_controller_tabs(); void refresh_controller_page_labels(int index); + void refresh_controller_art(); void set_controller_page_enabled(int index, bool enabled); void set_controller_page_view(int index, bool show_bindings); void start_controller_capture(int tab_index, int binding_index); diff --git a/vita3k/gui-qt/include/gui-qt/gui_settings.h b/vita3k/gui-qt/include/gui-qt/gui_settings.h index 16d8c944e..224af5261 100644 --- a/vita3k/gui-qt/include/gui-qt/gui_settings.h +++ b/vita3k/gui-qt/include/gui-qt/gui_settings.h @@ -28,7 +28,6 @@ const QString Settings = QStringLiteral("CurrentSettings"); const QString LightStylesheet = QStringLiteral("light"); const QString DarkStylesheet = QStringLiteral("dark"); -const QString NoStylesheet = QStringLiteral("none"); const QString main_window = QStringLiteral("MainWindow"); const QString apps_list = QStringLiteral("GameList"); @@ -38,6 +37,7 @@ const QString trophy = QStringLiteral("Trophy"); const QString controls = QStringLiteral("Controls"); const QString settings = QStringLiteral("Settings"); const QString user_mgmt = QStringLiteral("UserManagement"); +const QString vita_themes = QStringLiteral("VitaThemes"); const QString meta = QStringLiteral("Meta"); const GuiSave m_currentStylesheet = GuiSave(meta, QStringLiteral("currentStylesheet"), DarkStylesheet); @@ -86,6 +86,11 @@ const GuiSave sd_lastTab = GuiSave(settings, QStringLiteral("lastTab"), 0); const GuiSave um_geometry = GuiSave(user_mgmt, QStringLiteral("geometry"), QByteArray()); +const GuiSave vt_geometry = GuiSave(vita_themes, QStringLiteral("geometry"), QByteArray()); +const GuiSave vt_appliedSelection = GuiSave(vita_themes, QStringLiteral("appliedSelection"), QString()); +const GuiSave vt_bgmEnabled = GuiSave(vita_themes, QStringLiteral("bgmEnabled"), false); +const GuiSave vt_bgmVolume = GuiSave(vita_themes, QStringLiteral("bgmVolume"), 50); + } // namespace gui class GuiSettings : public GuiSettingsBase { @@ -93,6 +98,4 @@ class GuiSettings : public GuiSettingsBase { public: explicit GuiSettings(const QString &settings_dir, QObject *parent = nullptr); - - QStringList get_stylesheet_entries() const; }; diff --git a/vita3k/gui-qt/include/gui-qt/log_widget.h b/vita3k/gui-qt/include/gui-qt/log_widget.h index 271d2fd9a..63803dfb1 100644 --- a/vita3k/gui-qt/include/gui-qt/log_widget.h +++ b/vita3k/gui-qt/include/gui-qt/log_widget.h @@ -24,11 +24,13 @@ #include #include #include +#include #include +#include #include #include -#include +class ThemeSurface; class LogWidget : public custom_dock_widget { Q_OBJECT @@ -51,8 +53,6 @@ private: void on_log_message(const QString &msg, int level); void drain_pending_messages(); void append_log_entry(const QString &msg, int level, bool preserve_scroll = true); - QColor color_for_level(int level) const; - void trim_entries(); void show_search_bar(); void hide_search_bar(); void find_text(bool backwards = false); @@ -63,11 +63,11 @@ private: int effective_buffer_size(int requested_size) const; QPlainTextEdit *m_log; - QWidget *m_search_container = nullptr; + QPointer m_search_container; QLineEdit *m_search_edit = nullptr; QToolButton *m_search_prev_button = nullptr; QToolButton *m_search_next_button = nullptr; QToolButton *m_search_close_button = nullptr; QTextCursor m_current_search_cursor; - std::deque> m_entries; + QTimer *m_drain_timer = nullptr; }; diff --git a/vita3k/gui-qt/include/gui-qt/main_window.h b/vita3k/gui-qt/include/gui-qt/main_window.h index 6dbc87efd..b17aa98cd 100644 --- a/vita3k/gui-qt/include/gui-qt/main_window.h +++ b/vita3k/gui-qt/include/gui-qt/main_window.h @@ -22,6 +22,7 @@ #include #include +#include #include #include @@ -43,9 +44,11 @@ class GameCompatibility; class QWindow; class QWidget; class QLineEdit; +class QPaintEvent; class QPushButton; class QSlider; class QToolBar; +class QVariantAnimation; class TrophyCollectionDialog; class CtrlKeyboardFilter; class DebugWidget; @@ -54,6 +57,8 @@ class UserManagementDialog; class UpdateManager; class ControlsDialog; class SettingsDialog; +class ThemeManager; +class VitaThemesDialog; namespace app { struct SettingsCommitResult; } @@ -77,6 +82,7 @@ protected: void closeEvent(QCloseEvent *event) override; void dragEnterEvent(QDragEnterEvent *event) override; void dropEvent(QDropEvent *event) override; + void paintEvent(QPaintEvent *event) override; private slots: void on_about_action_triggered(); @@ -91,6 +97,7 @@ private slots: void pump_sdl_events(); void open_trophy_collection(); void open_user_management(); + void open_vita_themes(); void show_live_area(const std::string &title_id); void on_live_area_play(); void on_live_area_closed(); @@ -155,15 +162,19 @@ private: void open_debug_widget(int tab); void apply_log_gui_settings(); void repaint_gui(); - void init_default_style(); void init_first_run_stylesheet(); void apply_stylesheet(); + void apply_theme_background_presentation(); + void clear_theme_background_presentation(); + void rescale_theme_background_pixmaps(); + void advance_theme_background_presentation(); EmuEnvState &emuenv; app::AppSessionController m_app_session; std::unique_ptr m_ui; std::shared_ptr m_gui_settings; std::shared_ptr m_persistent_settings; + std::unique_ptr m_theme_manager; QMainWindow *m_dock_host = nullptr; AppsList *m_apps_list_widget = nullptr; @@ -173,25 +184,34 @@ private: QWindow *m_game_container = nullptr; QTimer *m_sdl_pump_timer = nullptr; - TrophyCollectionDialog *m_trophy_dialog = nullptr; - UserManagementDialog *m_user_mgmt_dialog = nullptr; + QPointer m_trophy_dialog; + QPointer m_user_mgmt_dialog; CtrlKeyboardFilter *m_kb_filter = nullptr; - DebugWidget *m_debug_widget = nullptr; + QPointer m_debug_widget; LiveAreaWidget *m_live_area_widget = nullptr; UpdateManager *m_update_manager = nullptr; - ControlsDialog *m_controls_dialog = nullptr; - SettingsDialog *m_settings_dialog = nullptr; + QPointer m_controls_dialog; + QPointer m_settings_dialog; + QPointer m_vita_themes_dialog; std::string m_live_area_title_id; QLineEdit *m_search_bar = nullptr; QSlider *m_icon_size_slider = nullptr; QWidget *m_slider_container = nullptr; std::vector> m_auxiliary_windows; + QTimer *m_theme_background_cycle_timer = nullptr; + QVariantAnimation *m_theme_background_fade_animation = nullptr; + std::vector m_theme_background_original_pixmaps; + std::vector m_theme_background_scaled_pixmaps; + QSize m_theme_background_scaled_size; + int m_theme_background_cycle_interval_ms = 15000; + int m_theme_background_current_index = 0; + int m_theme_background_next_index = -1; + qreal m_theme_background_fade_progress = 0.0; bool m_is_app_closing = false; bool m_fullscreen = false; bool m_app_selected = false; bool m_admin_privileged = false; - QString m_default_style; #if USE_DISCORD bool m_discord_rich_presence_old = false; #endif diff --git a/vita3k/gui-qt/include/gui-qt/qt_utils.h b/vita3k/gui-qt/include/gui-qt/qt_utils.h index b7c822b14..33214f535 100644 --- a/vita3k/gui-qt/include/gui-qt/qt_utils.h +++ b/vita3k/gui-qt/include/gui-qt/qt_utils.h @@ -50,8 +50,18 @@ void open_dir(const fs::path &path); fs::path to_fs_path(const QString &path); QString to_qt_path(const fs::path &path); +QString sanitize_relative_path_reference(const QString &path); +QString resolve_relative_path_in_root(const QString &relative_path, const QString &root_path); -QColor get_foreground_color(QWidget *widget = nullptr); +QColor foreground_color(QWidget *widget = nullptr); +QColor muted_text_color(); +QColor toolbar_icon_tint(); +QColor theme_role_color( + const QString &role, + QPalette::ColorRole color_role = QPalette::WindowText); +QString format_help_html(const QString &title, const QString &body); +void invalidate_theme_cache(); +void refresh_theme_state(QWidget *widget); QIcon get_colorized_icon(const QIcon &icon, const QColor &source_color, @@ -59,16 +69,6 @@ QIcon get_colorized_icon(const QIcon &icon, bool dark_mode_active(); -QColor get_label_color( - const QString &object_name, - const QColor &fallback_light, - const QColor &fallback_dark, - QPalette::ColorRole color_role = QPalette::WindowText); - -QColor get_label_color( - const QColor &fallback_light, - const QColor &fallback_dark); - MessageDialogResult show_message_box( QWidget *parent, QMessageBox::Icon icon, diff --git a/vita3k/gui-qt/include/gui-qt/settings_dialog.h b/vita3k/gui-qt/include/gui-qt/settings_dialog.h index 462bffbed..229803fef 100644 --- a/vita3k/gui-qt/include/gui-qt/settings_dialog.h +++ b/vita3k/gui-qt/include/gui-qt/settings_dialog.h @@ -30,6 +30,7 @@ struct EmuEnvState; class GuiSettings; class SettingsDialogTooltips; +class ThemeManager; enum class SettingsTab : int { Core = 0, @@ -54,6 +55,7 @@ class SettingsDialog final : public QDialog { public: explicit SettingsDialog(EmuEnvState &emuenv, std::shared_ptr gui_settings, + ThemeManager &theme_manager, const std::string &app_path = {}, int initial_tab = 0); ~SettingsDialog(); @@ -69,6 +71,7 @@ public: Q_SIGNALS: void gui_stylesheet_request(); + void gui_theme_music_settings_request(); void gui_log_settings_request(); void storage_path_changed(); void ui_language_request(const QString &locale_tag); @@ -78,6 +81,7 @@ private slots: void on_save_clicked(); void on_apply_clicked(); void on_close_clicked(); + void refresh_themes(); private: bool apply_pending_storage_path(); @@ -103,7 +107,7 @@ private: void mark_dirty(); void set_pending_pref_path(const fs::path &pref_path); void set_description(QWidget *tab, const QString &title, const QString &text); - void add_stylesheets(); + void add_stylesheets(const QString &preferred_name = {}); void apply_stylesheet(bool reset = false); void closeEvent(QCloseEvent *event) override; @@ -114,6 +118,7 @@ private: EmuEnvState &emuenv; std::shared_ptr m_gui_settings; + ThemeManager &m_theme_manager; std::unique_ptr m_tooltips; std::string m_app_path; int m_initial_tab; diff --git a/vita3k/gui-qt/include/gui-qt/settings_dialog_tooltips.h b/vita3k/gui-qt/include/gui-qt/settings_dialog_tooltips.h index 344d94553..301198183 100644 --- a/vita3k/gui-qt/include/gui-qt/settings_dialog_tooltips.h +++ b/vita3k/gui-qt/include/gui-qt/settings_dialog_tooltips.h @@ -60,6 +60,8 @@ public: QString memory_mapping; QString audio_backend; QString audio_volume; + QString theme_music_enable; + QString theme_music_volume; QString ngs_enable; QString front_camera; QString back_camera; diff --git a/vita3k/gui-qt/include/gui-qt/theme_manager.h b/vita3k/gui-qt/include/gui-qt/theme_manager.h new file mode 100644 index 000000000..eedf73e2c --- /dev/null +++ b/vita3k/gui-qt/include/gui-qt/theme_manager.h @@ -0,0 +1,133 @@ +// Vita3K emulator project +// Copyright (C) 2026 Vita3K team +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along +// with this program; if not, write to the Free Software Foundation, Inc., +// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +#pragma once + +#include + +#include + +#include +#include +#include +#include +#include + +#include +#include +#include + +class GuiSettings; +class QAudioOutput; +class QMediaPlayer; + +namespace gui { + +struct ThemeEntry { + QString name; + QString display_name; + QString qss_source; + QString base_path; +}; + +struct VitaThemeSelection { + QString theme_id; + QString background_id; + int readability = 55; + bool normalize_font_colors = true; + bool cycle_enabled = false; + int cycle_interval_seconds = 15; + QStringList cycle_background_ids; +}; + +VitaThemeSelection normalize_vita_theme_selection( + const VitaThemeInfo &theme, + VitaThemeSelection selection); + +struct ThemeBackgroundPresentation { + QStringList image_paths; + int interval_ms = 0; + bool animated = false; +}; + +struct AppliedThemeState { + bool is_vita_theme = false; + ThemeBackgroundPresentation background; +}; + +} // namespace gui + +class ThemeManager final : public QObject { + Q_OBJECT + +public: + explicit ThemeManager(std::shared_ptr gui_settings, + QObject *parent = nullptr); + + QList available_themes() const; + bool apply_theme(const QString &name = {}, bool force_reload = false); + void set_pref_path(const fs::path &pref_path); + void set_vita_themes_root(const fs::path &themes_root); + void refresh_vita_theme_catalog(); + const std::vector &installed_vita_themes(bool force_reload = false) const; + void ensure_generated_theme_ready(const fs::path &themes_root); + std::optional applied_vita_theme_selection() const; + bool apply_vita_theme_selection( + const gui::VitaThemeSelection &selection, + bool force_reload = true); + std::optional current_applied_theme_state() const; + void clear_applied_vita_theme(); + void refresh_vita_theme_bgm(); + void set_vita_theme_bgm_blocked(bool blocked); + +Q_SIGNALS: + void theme_state_changed(); + void theme_catalog_changed(); + +private: + void save_applied_selection(const gui::VitaThemeSelection &selection) const; + void switch_to_fallback_theme(const QString &reason); + QString requested_theme_name() const; + const gui::VitaThemeInfo *find_installed_vita_theme(const QString &theme_id, bool force_reload = false) const; + std::optional builtin_theme(const QString &name) const; + std::optional generated_vita_theme() const; + QList custom_themes() const; + std::optional resolve_theme(const QString &name) const; + std::optional build_applied_theme_state(const gui::ThemeEntry &theme, const QString &stylesheet) const; + bool apply_theme_entry(const gui::ThemeEntry &theme, bool force_reload); + bool apply_theme_entry(const gui::ThemeEntry &theme, const QString &stylesheet, bool force_reload); + std::optional load_stylesheet(const gui::ThemeEntry &theme) const; + std::optional parse_palette(const QString &stylesheet) const; + QString resolve_stylesheet_urls(const QString &stylesheet, const gui::ThemeEntry &theme) const; + QString resolve_stylesheet_url(const QString &url, const gui::ThemeEntry &theme) const; + fs::path generated_vita_theme_path(const QString &theme_id) const; + QString generated_vita_theme_display_name(const QString &theme_id) const; + void ensure_fusion_style() const; + void stop_vita_theme_bgm(); + + std::shared_ptr m_gui_settings; + QString m_current_theme_name; + fs::path m_pref_path; + fs::path m_vita_themes_root; + QAudioOutput *m_vita_theme_bgm_audio_output = nullptr; + QMediaPlayer *m_vita_theme_bgm_player = nullptr; + QString m_current_vita_theme_bgm_source_path; + std::optional m_applied_theme_state; + mutable std::vector m_installed_vita_theme_cache; + mutable bool m_vita_theme_cache_loaded = false; + bool m_vita_theme_bgm_blocked = false; +}; diff --git a/vita3k/gui-qt/include/gui-qt/stylesheets.h b/vita3k/gui-qt/include/gui-qt/theme_surface.h similarity index 81% rename from vita3k/gui-qt/include/gui-qt/stylesheets.h rename to vita3k/gui-qt/include/gui-qt/theme_surface.h index 9eef3d22f..66b1cb6b7 100644 --- a/vita3k/gui-qt/include/gui-qt/stylesheets.h +++ b/vita3k/gui-qt/include/gui-qt/theme_surface.h @@ -17,11 +17,12 @@ #pragma once -#include +#include -namespace gui::stylesheets { +class ThemeSurface : public QWidget { +public: + explicit ThemeSurface(QWidget *parent = nullptr); -extern const QString light_style_sheet; -extern const QString dark_style_sheet; - -} // namespace gui::stylesheets +protected: + void paintEvent(QPaintEvent *event) override; +}; diff --git a/vita3k/gui-qt/include/gui-qt/vita_theme.h b/vita3k/gui-qt/include/gui-qt/vita_theme.h new file mode 100644 index 000000000..4c6d70c65 --- /dev/null +++ b/vita3k/gui-qt/include/gui-qt/vita_theme.h @@ -0,0 +1,93 @@ +// Vita3K emulator project +// Copyright (C) 2026 Vita3K team +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along +// with this program; if not, write to the Free Software Foundation, Inc., +// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +#pragma once + +#include + +#include +#include +#include +#include +#include +#include + +namespace gui { + +enum class VitaThemeBackgroundSource { + Home, + Lockscreen, +}; + +struct VitaThemeColor { + std::uint8_t red = 0; + std::uint8_t green = 0; + std::uint8_t blue = 0; + std::uint8_t alpha = 255; +}; + +struct VitaThemeBackgroundOption { + std::string id; + std::string label; + fs::path asset_path; + VitaThemeBackgroundSource source = VitaThemeBackgroundSource::Home; + std::optional primary_text_color; +}; + +struct VitaThemePalette { + std::optional information_bar_color; + std::optional information_indicator_color; + std::optional notify_background_color; + std::optional notify_border_color; + std::optional notify_font_color; + std::optional date_color; +}; + +struct VitaThemeInfo { + std::string theme_id; + std::string content_id; + std::string title; + std::string provider; + std::string content_version; + fs::path installed_path; + std::optional bgm_path; + fs::path package_thumbnail_path; + std::uintmax_t installed_size = 0; + std::time_t updated_time = 0; + std::vector background_options; + VitaThemePalette palette; +}; + +std::optional load_vita_theme( + const fs::path &theme_dir, + const std::string &preferred_locale = {}); +std::vector enumerate_installed_vita_themes( + const fs::path &themes_root, + const std::string &preferred_locale = {}); + +std::optional synthesize_vita_theme_qss( + const VitaThemeInfo &theme, + const std::string &background_option_id, + int readability_percent, + bool normalize_font_colors, + const std::vector &background_image_paths, + int cycle_interval_seconds, + bool cycle_enabled, + const fs::path &pref_path, + const fs::path &gui_settings_dir); + +} // namespace gui diff --git a/vita3k/gui-qt/include/gui-qt/vita_themes_dialog.h b/vita3k/gui-qt/include/gui-qt/vita_themes_dialog.h new file mode 100644 index 000000000..193307abd --- /dev/null +++ b/vita3k/gui-qt/include/gui-qt/vita_themes_dialog.h @@ -0,0 +1,149 @@ +// Vita3K emulator project +// Copyright (C) 2026 Vita3K team +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along +// with this program; if not, write to the Free Software Foundation, Inc., +// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +#pragma once + +#include +#include + +#include + +#include +#include +#include + +class GuiSettings; +class ThemeManager; +class QListWidget; +class QLabel; +class QPushButton; +class QWidget; +class QHBoxLayout; +class QVBoxLayout; +class QSlider; +class QCheckBox; +class QSpinBox; +class QCloseEvent; +class QResizeEvent; +class QEvent; +class QButtonGroup; +class QRadioButton; +class QTextBrowser; +class QTreeWidget; +class QTreeWidgetItem; + +class VitaThemesDialog final : public QDialog { + Q_OBJECT + +public: + explicit VitaThemesDialog( + std::shared_ptr gui_settings, + ThemeManager &theme_manager, + QWidget *parent = nullptr); + +public Q_SLOTS: + void reload(); + +protected: + void closeEvent(QCloseEvent *event) override; + void resizeEvent(QResizeEvent *event) override; + bool eventFilter(QObject *watched, QEvent *event) override; + +private: + struct BackgroundRow { + QString id; + QTreeWidgetItem *item = nullptr; + QRadioButton *primary_radio = nullptr; + QCheckBox *cycle_checkbox = nullptr; + }; + + void build_ui(); + void restore_persistent_state(); + void save_persistent_state() const; + void repopulate_theme_list(const QString &preferred_theme_id = {}); + void apply_theme_selection(int row); + void clear_selection_state(); + gui::VitaThemeSelection selection_from_ui() const; + void load_selection_into_ui( + gui::VitaThemeSelection selection, + std::optional preview_background_id = std::nullopt, + bool follow_primary_preview = false, + bool reload_theme_ui = false); + void load_theme_into_ui(const gui::VitaThemeInfo *theme); + void sync_background_rows(); + void sync_preview(); + void sync_metadata(); + void sync_actions(); + void sync_cycle_controls(); + void set_selected_background(const QString &background_id); + void set_selected_cycle_backgrounds(const QStringList &background_ids); + void update_preview_pixmap(); + void update_package_thumbnail(); + void update_readability_label(); + void apply_selected_theme(); + void delete_selected_theme(); + void open_selected_theme_folder() const; + void register_description(QWidget *widget, const QString &title, const QString &description); + void set_description(const QString &title, const QString &text); + void reset_description(); + QString default_description() const; + void set_preview_background(const QString &background_id); + QString resolve_preview_background_id( + const gui::VitaThemeInfo &theme, + const QString &background_id) const; + + const gui::VitaThemeInfo *current_theme() const; + const gui::VitaThemeBackgroundOption *current_background() const; + const gui::VitaThemeBackgroundOption *current_preview_background() const; + + std::shared_ptr m_gui_settings; + ThemeManager &m_theme_manager; + std::vector m_themes; + + QListWidget *m_theme_list = nullptr; + QLabel *m_package_thumbnail = nullptr; + QLabel *m_theme_title = nullptr; + QLabel *m_theme_provider = nullptr; + QLabel *m_preview = nullptr; + QTreeWidget *m_background_list = nullptr; + QCheckBox *m_cycle_checkbox = nullptr; + QSpinBox *m_cycle_interval_spinbox = nullptr; + QLabel *m_cycle_summary_value = nullptr; + QLabel *m_version_value = nullptr; + QLabel *m_theme_id_value = nullptr; + QLabel *m_size_value = nullptr; + QLabel *m_updated_value = nullptr; + QLabel *m_background_value = nullptr; + QLabel *m_cycle_value = nullptr; + QSlider *m_readability_slider = nullptr; + QLabel *m_readability_value = nullptr; + QCheckBox *m_normalize_fonts_checkbox = nullptr; + QPushButton *m_refresh_button = nullptr; + QPushButton *m_apply_button = nullptr; + QPushButton *m_delete_button = nullptr; + QPushButton *m_open_folder_button = nullptr; + QTextBrowser *m_help_text = nullptr; + QButtonGroup *m_primary_background_group = nullptr; + QString m_loaded_theme_id; + std::vector m_background_rows; + + QString m_selected_background_id; + QString m_preview_background_id; + QString m_loaded_preview_background_id; + QStringList m_selected_cycle_background_ids; + QPixmap m_preview_pixmap; +}; diff --git a/vita3k/gui-qt/resources/themes/dark/dark.qss b/vita3k/gui-qt/resources/themes/dark/dark.qss new file mode 100644 index 000000000..d2d90fd73 --- /dev/null +++ b/vita3k/gui-qt/resources/themes/dark/dark.qss @@ -0,0 +1,285 @@ +/*!PALETTE + * dark: dark + * window: #2d2d30 + * window-text: #f3f3f3 + * base: #1e1e1e + * alternate-base: #262629 + * tooltip-base: #2d2d30 + * tooltip-text: #f3f3f3 + * text: #f3f3f3 + * button: #2d2d30 + * button-text: #f3f3f3 + * bright-text: #ffffff + * link: #429cff + * highlight: #21ffffff + * inactive-highlight: #21ffffff + * disabled-highlight: #14ffffff + * highlight-text: #ffffff + * inactive-highlight-text: #ffffff + * disabled-highlight-text: #c8c8c8 + * placeholder-text: #a0a0a0 + * disabled-button-text: #737373 + * disabled-window-text: #737373 + * disabled-text: #737373 + */ +QWidget { color: #e0e0e0; background-color: #353535; } +QWidget:disabled { color: #777777; } +QMainWindow { background-color: #353535; } +QMainWindow#main_window { background-color: #353535; } +QWidget#mw_frame { background-color: #353535; } +QMainWindow#mw_dock_host { background: transparent; } +QDialog { background-color: #353535; } +QMenuBar { background-color: #353535; color: #e0e0e0; } +QMenuBar::item { background: transparent; padding: 4px 8px; } +QMenuBar::item:selected { background: #4a4a4a; } +QMenu { background-color: #353535; color: #e0e0e0; border: 1px solid #555555; } +QMenu::item:selected { background-color: rgba(255, 255, 255, 0.12); color: #ffffff; } +QMenu::item:disabled { color: #808080; } +QMenu::separator { height: 1px; background: #555555; margin: 4px 8px; } +QToolBar#mw_toolbar { background-color: #353535; border: none; border-top: 1px solid #555555; } +QToolBar#mw_toolbar::separator { background-color: rgba(100, 100, 100, 235); width: 0.125em; margin-top: 0.250em; margin-bottom: 0.250em; } +QToolButton { color: #e0e0e0; background: transparent; border: 1px solid transparent; padding: 3px; } +QToolButton:hover { background-color: #4a4a4a; border: 1px solid #666666; } +QToolButton:pressed { background-color: rgba(255, 255, 255, 0.12); } +QToolButton:disabled { color: #808080; } +QToolButton#welcome_firmware_button { background-color: #353535; color: #e0e0e0; border: 1px solid #555555; border-radius: 2px; padding: 4px 32px 4px 12px; text-align: left; } +QToolButton#welcome_firmware_button:hover { background-color: #4a4a4a; border-color: #666666; } +QToolButton#welcome_firmware_button:pressed { background-color: rgba(255, 255, 255, 0.12); } +QToolButton#welcome_firmware_button::menu-button { subcontrol-origin: padding; subcontrol-position: top right; width: 24px; border-left: 1px solid #555555; border-top-right-radius: 2px; border-bottom-right-radius: 2px; background: #303030; } +QToolButton#welcome_firmware_button::menu-button:hover { background: #444444; border-left-color: #666666; } +QToolButton#welcome_firmware_button::menu-button:pressed { background: rgba(255, 255, 255, 0.12); } +QLabel[themeRole="toolbarIconTint"] { color: #f3f3f3; } +QLabel[themeRole="mutedText"] { color: #a8a8a8; } +QPushButton { background-color: #353535; color: #e0e0e0; border: 1px solid #555555; border-radius: 2px; padding: 4px 16px; min-height: 1.2em; } +QPushButton:hover { background-color: #4a4a4a; border-color: #666666; } +QPushButton:pressed { background-color: rgba(255, 255, 255, 0.12); } +QPushButton:disabled { color: #808080; background-color: #303030; border-color: #444444; } +QPushButton:default { border-color: rgba(255, 255, 255, 0.24); } +QDialog#vita_themes_dialog { color: #e0e0e0; } +QDialog#vita_themes_dialog QLabel, +QDialog#vita_themes_dialog QCheckBox, +QDialog#vita_themes_dialog QRadioButton { color: #e0e0e0; background: transparent; } +QDialog#vita_themes_dialog QLabel[themeRole="mutedText"] { color: #a8a8a8; } +QDialog#vita_themes_dialog QWidget[themeRole="backgroundCell"] { background: transparent; } +QDialog#vita_themes_dialog QCheckBox[themeRole="backgroundCellControl"], +QDialog#vita_themes_dialog QRadioButton[themeRole="backgroundCellControl"] { spacing: 0px; padding: 0px; margin: 0px; } +QDialog#vita_themes_dialog QListWidget#vitaThemesList { + background-color: #2a2a2a; + color: #e0e0e0; + border: none; + border-radius: 10px; + padding: 6px; + outline: none; +} +QDialog#vita_themes_dialog QListWidget#vitaThemesList::item { + background: transparent; + border: none; + border-radius: 10px; + padding: 8px 12px; + margin: 0px; +} +QDialog#vita_themes_dialog QListWidget#vitaThemesList::item:selected, +QDialog#vita_themes_dialog QListWidget#vitaThemesList::item:selected:active, +QDialog#vita_themes_dialog QListWidget#vitaThemesList::item:selected:!active { background-color: rgba(255, 255, 255, 0.12); color: #ffffff; } +QDialog#vita_themes_dialog QListWidget#vitaThemesList::item:hover:!selected { background-color: #3a3a3a; } +QDialog#vita_themes_dialog QTreeWidget#vitaThemesBackgroundList { + background-color: #2a2a2a; + color: #e0e0e0; + border: 1px solid #555555; + border-radius: 8px; + padding: 2px; + outline: none; +} +QDialog#vita_themes_dialog QTreeWidget#vitaThemesBackgroundList::item { + padding: 5px 8px; + border-radius: 0px; + margin: 0px; +} +QDialog#vita_themes_dialog QTreeWidget#vitaThemesBackgroundList::item:selected, +QDialog#vita_themes_dialog QTreeWidget#vitaThemesBackgroundList::item:selected:active, +QDialog#vita_themes_dialog QTreeWidget#vitaThemesBackgroundList::item:selected:!active { background-color: rgba(255, 255, 255, 0.12); color: #ffffff; } +QDialog#vita_themes_dialog QTreeWidget#vitaThemesBackgroundList::item:hover:!selected { background-color: #3a3a3a; } +QDialog#vita_themes_dialog QTextBrowser#vitaThemesHelpText { + background-color: #2a2a2a; + color: #e0e0e0; + border: none; + border-radius: 10px; + padding: 3px 4px; +} +QDialog#vita_themes_dialog QScrollArea { background: transparent; border: none; } +QDialog#vita_themes_dialog QCheckBox::indicator { + width: 12px; + height: 12px; + padding: 0px; + margin: 0px; + border-radius: 3px; + border: 1px solid #666666; + background-color: #2a2a2a; +} +QDialog#vita_themes_dialog QCheckBox::indicator:hover { border-color: #888888; background-color: #323232; } +QDialog#vita_themes_dialog QCheckBox::indicator:checked, +QDialog#vita_themes_dialog QCheckBox::indicator:indeterminate { + background-color: rgba(255, 255, 255, 0.78); + border-color: rgba(255, 255, 255, 0.82); + image: url(":/icons/check_dark.svg"); +} +QDialog#vita_themes_dialog QCheckBox::indicator:disabled { border-color: #4a4a4a; background-color: #252525; } +QDialog#vita_themes_dialog QCheckBox::indicator:checked:disabled, +QDialog#vita_themes_dialog QCheckBox::indicator:indeterminate:disabled { + background-color: rgba(255, 255, 255, 0.38); + border-color: rgba(255, 255, 255, 0.42); +} +QDialog#vita_themes_dialog QRadioButton::indicator { + width: 12px; + height: 12px; + padding: 0px; + margin: 0px; + border-radius: 6px; + border: 1px solid #666666; + background-color: #2a2a2a; +} +QDialog#vita_themes_dialog QRadioButton::indicator:hover { border-color: #888888; background-color: #323232; } +QDialog#vita_themes_dialog QRadioButton::indicator:checked { + background-color: rgba(255, 255, 255, 0.78); + border-color: rgba(255, 255, 255, 0.82); + image: url(":/icons/radio_dot_dark.svg"); +} +QDialog#vita_themes_dialog QRadioButton::indicator:disabled { border-color: #4a4a4a; background-color: #252525; } +QDialog#vita_themes_dialog QRadioButton::indicator:checked:disabled { + background-color: rgba(255, 255, 255, 0.38); + border-color: rgba(255, 255, 255, 0.42); +} +QDialog#controls_dialog QToolButton[modeButton="true"] { background: transparent; border: none; border-radius: 10px; padding: 6px 12px; } +QDialog#controls_dialog QToolButton[modeButton="true"]:hover { background-color: rgba(255, 255, 255, 0.08); } +QDialog#controls_dialog QToolButton[modeButton="true"]:checked { background-color: rgba(255, 255, 255, 0.14); } +QDialog#controls_dialog QToolButton[modeButton="true"]:pressed { background-color: rgba(255, 255, 255, 0.20); } +QDialog#controls_dialog QPushButton { background-color: #303030; color: #e0e0e0; border: none; border-radius: 6px; padding: 6px 12px; outline: none; } +QDialog#controls_dialog QPushButton:hover { background-color: #3a3a3a; border: none; } +QDialog#controls_dialog QPushButton:pressed { background-color: #464646; border: none; } +QDialog#controls_dialog QPushButton:disabled { color: #808080; background-color: #2c2c2c; border: none; } +QDialog#controls_dialog QPushButton[bindButton="true"] { background-color: #2f2f2f; border-radius: 6px; padding: 6px 12px; } +QDialog#controls_dialog QPushButton[bindButton="true"]:hover { background-color: #393939; } +QDialog#controls_dialog QPushButton[bindButton="true"]:pressed { background-color: #454545; } +QDialog#controls_dialog QPushButton[bindButton="true"][captureActive="true"] { background-color: rgba(255, 255, 255, 0.18); color: #ffffff; } +QDialog#controls_dialog QPushButton[bindButton="true"][captureActive="true"]:hover { background-color: rgba(255, 255, 255, 0.24); } +QDialog#controls_dialog QPushButton[bindButton="true"][captureActive="true"]:pressed { background-color: rgba(255, 255, 255, 0.30); } +QStatusBar#emulator_status_bar { background-color: #353535; color: #e0e0e0; } +QStatusBar#emulator_status_bar::item { border: none; } +QStatusBar#emulator_status_bar QPushButton#status_button { border: 1px solid transparent; padding: 2px 8px; font-weight: bold; font-size: 11px; background: transparent; border-radius: 6px; color: #e0e0e0; } +QStatusBar#emulator_status_bar QPushButton#status_button:hover { border: 1px solid #666666; background: rgba(255, 255, 255, 0.04); } +QStatusBar#emulator_status_bar QPushButton#status_button:pressed { border: 1px solid #888888; background: rgba(255, 255, 255, 0.07); } +QStatusBar#emulator_status_bar QPushButton#status_button[accent="renderer_vulkan"] { color: #ff4444; } +QStatusBar#emulator_status_bar QPushButton#status_button[accent="renderer_opengl"] { color: #4488ff; } +QStatusBar#emulator_status_bar QPushButton#status_button[accent="high"] { color: #ff8800; } +QStatusBar#emulator_status_bar QPushButton#status_button[accent="standard"] { color: #aaaaaa; } +QStatusBar#emulator_status_bar QPushButton#status_button[accent="neutral"] { color: #cccccc; } +QStatusBar#emulator_status_bar QPushButton#status_button[accent="audio"] { color: #88dd44; } +QStatusBar#emulator_status_bar QPushButton#status_button[accent="muted"] { color: #777777; } +QStatusBar#emulator_status_bar QPushButton#status_button[accent="update"] { color: #6de089; } +QLineEdit { background-color: #2a2a2a; color: #e0e0e0; border: 1px solid #555555; border-radius: 2px; padding: 2px 4px; selection-background-color: rgba(255, 255, 255, 0.18); selection-color: #ffffff; } +QLineEdit:focus { border-color: rgba(255, 255, 255, 0.24); } +QLineEdit:disabled { color: #777777; background-color: #303030; border-color: #444444; } +QComboBox { background-color: #353535; color: #e0e0e0; border: 1px solid #555555; border-radius: 2px; padding: 2px 28px 2px 6px; } +QComboBox:hover { border-color: #666666; } +QComboBox:disabled { color: #777777; background-color: #303030; border-color: #444444; } +QComboBox::drop-down { subcontrol-origin: padding; subcontrol-position: top right; width: 24px; border: none; background: transparent; } +QComboBox::down-arrow { image: url("chevron_down_light.svg"); width: 10px; height: 6px; } +QComboBox::down-arrow:on { top: 1px; } +QComboBox QAbstractItemView { background-color: #2a2a2a; color: #e0e0e0; border: 1px solid #555555; selection-background-color: rgba(255, 255, 255, 0.12); selection-color: #ffffff; } +QSpinBox, QDoubleSpinBox { background-color: #2a2a2a; color: #e0e0e0; border: 1px solid #555555; border-radius: 2px; padding: 2px 24px 2px 6px; selection-background-color: rgba(255, 255, 255, 0.18); selection-color: #ffffff; } +QSpinBox:disabled, QDoubleSpinBox:disabled { color: #777777; background-color: #303030; border-color: #444444; } +QSpinBox::up-button, QDoubleSpinBox::up-button { subcontrol-origin: border; subcontrol-position: top right; width: 18px; border: none; background: transparent; } +QSpinBox::down-button, QDoubleSpinBox::down-button { subcontrol-origin: border; subcontrol-position: bottom right; width: 18px; border: none; background: transparent; } +QSpinBox::up-arrow, QDoubleSpinBox::up-arrow { image: url("chevron_up_light.svg"); width: 10px; height: 6px; } +QSpinBox::down-arrow, QDoubleSpinBox::down-arrow { image: url("chevron_down_light.svg"); width: 10px; height: 6px; } +QCheckBox { color: #e0e0e0; spacing: 6px; } +QCheckBox:disabled { color: #777777; } +QRadioButton { color: #e0e0e0; spacing: 6px; } +QRadioButton:disabled { color: #777777; } +QGroupBox { color: #e0e0e0; border: 1px solid #555555; border-radius: 4px; margin-top: 0.5em; padding-top: 0.6em; } +QGroupBox:disabled { color: #7f7f7f; border-color: #4a4a4a; } +QGroupBox::title { subcontrol-origin: margin; subcontrol-position: top left; padding: 0 4px; color: #e0e0e0; } +QLabel { color: #e0e0e0; background: transparent; } +QLabel:disabled { color: #777777; } +QTableWidget, QTableView { alternate-background-color: #3a3a3a; background-color: #2a2a2a; border: none; color: #e0e0e0; gridline-color: #444444; } +QTableWidget:disabled, QTableView:disabled { color: #777777; background-color: #303030; } +QTableCornerButton::section { background-color: #353535; border: 1px solid #555555; } +QTableView::item { border-left: 0.063em solid #2a2a2a; border-right: 0.063em solid #2a2a2a; padding-left: 0.313em; } +QTableView::item:selected { background-color: rgba(255, 255, 255, 0.12); color: #ffffff; } +QTableWidget#apps_list_table::item:selected, QTableView#apps_list_table::item:selected { background-color: rgba(255, 255, 255, 0.12); color: #f0f0f0; border-left: 0.063em solid rgba(255, 255, 255, 0); border-right: 0.063em solid rgba(255, 255, 255, 0); } +QTableView#apps_list_table QHeaderView::section, QTableWidget#apps_list_table QHeaderView::section { padding-top: 0; padding-bottom: 0; } +QTableView#trophy_app_table::item:selected, QTableView#trophy_table::item:selected { background-color: rgba(255, 255, 255, 0.13); color: #ffffff; border-left: 0.063em solid rgba(255, 255, 255, 0); border-right: 0.063em solid rgba(255, 255, 255, 0); } +QHeaderView::section { background-color: #353535; color: #ffffff; padding-left: .5em; padding-right: .5em; padding-top: .4em; padding-bottom: -.1em; border: 0.063em solid #2a2a2a; } +QHeaderView::section:hover { background: #4a4a4a; } +QDockWidget { background: transparent; color: #ffffff; } +QDockWidget#apps_list[floating="true"], +QDockWidget#logger[floating="true"] { background-color: #353535; } +QDockWidget::title { background: #353535; border: none; padding-top: 0.2em; padding-left: 0.2em; } +QDockWidget::close-button, QDockWidget::float-button { background-color: #353535; border: none; } +QDockWidget::close-button:hover, QDockWidget::float-button:hover { background-color: #4a4a4a; } +QWidget#apps_list_frame { background-color: #353535; } +QWidget#log_surface { background-color: #353535; } +QWidget#apps_list_viewport { background-color: #2a2a2a; } +QWidget#log_frame_viewport { background-color: #2a2a2a; } +QPlainTextEdit#log_frame { background-color: #2a2a2a; color: #ffffff; } +QWidget#log_search_container { background-color: #2a2a2a; border: 1px solid #555555; border-radius: 4px; } +QWidget#toolbar_icon_size_container { background-color: #353535; } +QSlider#toolbar_icon_size_slider { background: transparent; } +QSlider#toolbar_icon_size_slider::groove:horizontal { background: transparent; height: 4px; border-radius: 2px; } +QSlider#toolbar_icon_size_slider::sub-page:horizontal { background: rgba(255, 255, 255, 0.82); height: 4px; border-radius: 2px; } +QSlider#toolbar_icon_size_slider::add-page:horizontal { background: #555555; height: 4px; border-radius: 2px; } +QSlider#toolbar_icon_size_slider::handle:horizontal { background: #f3f3f3; width: 14px; margin: -5px 0; border-radius: 7px; } +QSlider#toolbar_icon_size_slider::handle:horizontal:hover { background: #ffffff; } +QLabel[themeRole="logFatal"] { color: #ff66ff; } +QLabel[themeRole="logError"] { color: #ff8080; } +QLabel[themeRole="logWarning"] { color: #ffd27f; } +QLabel[themeRole="logNotice"] { color: #ffffff; } +QLabel[themeRole="logDebug"] { color: #8bb9ff; } +QLabel[themeRole="logTrace"] { color: #ffffff; } +QLabel[themeRole="sectionHeader"] { font-weight: 600; margin-top: 8px; } +QLabel[severity="success"] { border-radius: 10px; padding: 4px 10px; background-color: #255a2e; color: #b9f5bf; } +QLabel[severity="error"] { border-radius: 10px; padding: 4px 10px; background-color: #6d2a25; color: #ffb1aa; } +QTabWidget::pane { border: 1px solid #555555; background-color: #353535; } +QTabBar::tab { background-color: #2d2d2d; color: #e0e0e0; padding: 6px 14px; border: 1px solid #555555; border-bottom: none; border-top-left-radius: 3px; border-top-right-radius: 3px; } +QTabBar::tab:selected { background-color: #353535; } +QTabBar::tab:!selected { margin-top: 2px; } +QTabBar::tab:hover:!selected { background-color: #3a3a3a; } +QListWidget { background-color: #2a2a2a; color: #e0e0e0; border: 1px solid #555555; } +QListWidget:disabled { color: #777777; background-color: #303030; border-color: #444444; } +QListWidget::item:selected { background-color: rgba(255, 255, 255, 0.12); color: #ffffff; } +QListWidget::item:hover:!selected { background-color: #3a3a3a; } +QDialog[class="settings-window"] QListWidget#settingsCategory { background-color: #2a2a2a; border: none; border-radius: 10px; padding: 6px; outline: none; } +QDialog[class="settings-window"] QListWidget#settingsCategory::item { background: transparent; border: none; border-radius: 10px; padding: 8px 12px; margin: 0px; } +QDialog[class="settings-window"] QListWidget#settingsCategory::item:selected, +QDialog[class="settings-window"] QListWidget#settingsCategory::item:selected:active, +QDialog[class="settings-window"] QListWidget#settingsCategory::item:selected:!active { background-color: rgba(255, 255, 255, 0.12); color: #ffffff; } +QDialog[class="settings-window"] QListWidget#settingsCategory::item:hover:!selected { background-color: #3a3a3a; } +QDialog[class="settings-window"] QTextBrowser#helpText { background-color: #2a2a2a; color: #e0e0e0; border: none; border-radius: 10px; padding: 3px 4px; } +QScrollBar:vertical { background: #2a2a2a; width: 14px; margin: 0; } +QScrollBar::handle:vertical { background: #5a5a5a; min-height: 20px; border-radius: 3px; margin: 2px; } +QScrollBar::handle:vertical:hover { background: #6a6a6a; } +QScrollBar::add-line:vertical, QScrollBar::sub-line:vertical { height: 0px; } +QScrollBar::add-page:vertical, QScrollBar::sub-page:vertical { background: none; } +QScrollBar:horizontal { background: #2a2a2a; height: 14px; margin: 0; } +QScrollBar::handle:horizontal { background: #5a5a5a; min-width: 20px; border-radius: 3px; margin: 2px; } +QScrollBar::handle:horizontal:hover { background: #6a6a6a; } +QScrollBar::add-line:horizontal, QScrollBar::sub-line:horizontal { width: 0px; } +QScrollBar::add-page:horizontal, QScrollBar::sub-page:horizontal { background: none; } +QAbstractScrollArea::corner { background: #2a2a2a; } +QSlider::groove:horizontal { background: transparent; height: 4px; border-radius: 2px; } +QSlider::sub-page:horizontal { background: rgba(255, 255, 255, 0.82); height: 4px; border-radius: 2px; } +QSlider::add-page:horizontal { background: #555555; height: 4px; border-radius: 2px; } +QSlider::handle:horizontal { background: #c0c0c0; width: 14px; margin: -5px 0; border-radius: 7px; } +QSlider::handle:horizontal:hover { background: #ffffff; } +QSlider::groove:vertical { background: transparent; width: 4px; border-radius: 2px; } +QSlider::sub-page:vertical { background: rgba(255, 255, 255, 0.82); width: 4px; border-radius: 2px; } +QSlider::add-page:vertical { background: #555555; width: 4px; border-radius: 2px; } +QSlider::handle:vertical { background: #c0c0c0; height: 14px; margin: 0 -5px; border-radius: 7px; } +QSlider::handle:vertical:hover { background: #ffffff; } +QProgressBar { background-color: #2a2a2a; color: #e0e0e0; border: 1px solid #555555; text-align: center; border-radius: 2px; } +QProgressBar::chunk { background-color: #6a6a6a; border-radius: 2px; } +QSplitter::handle { background-color: #555555; } +QToolTip { background-color: #353535; color: #e0e0e0; border: 1px solid #555555; padding: 2px; } +QPlainTextEdit, QTextEdit, QTextBrowser { background-color: #2a2a2a; color: #e0e0e0; border: 1px solid #555555; } +QPlainTextEdit:disabled, QTextEdit:disabled, QTextBrowser:disabled { color: #777777; background-color: #303030; border-color: #444444; } +QDialogButtonBox QPushButton { min-width: 80px; } diff --git a/vita3k/gui-qt/resources/themes/light/light.qss b/vita3k/gui-qt/resources/themes/light/light.qss new file mode 100644 index 000000000..291116541 --- /dev/null +++ b/vita3k/gui-qt/resources/themes/light/light.qss @@ -0,0 +1,281 @@ +/*!PALETTE + * dark: light + * window: #f5f5f5 + * window-text: #202020 + * base: #ffffff + * alternate-base: #f2f2f2 + * tooltip-base: #f7f7f7 + * tooltip-text: #202020 + * text: #202020 + * button: #ffffff + * button-text: #202020 + * bright-text: #ffffff + * link: #148aff + * highlight: #148aff + * inactive-highlight: #148aff + * disabled-highlight: #40148aff + * highlight-text: #ffffff + * inactive-highlight-text: #ffffff + * disabled-highlight-text: #d0d0d0 + * placeholder-text: #8a8a8a + * disabled-button-text: #8a8a8a + * disabled-window-text: #8a8a8a + * disabled-text: #8a8a8a + */ +QWidget { color: #202020; background-color: #f5f5f5; } +QWidget:disabled { color: #8a8a8a; } +QMainWindow { background-color: #f5f5f5; } +QMainWindow#main_window { background-color: #f5f5f5; } +QWidget#mw_frame { background-color: #f5f5f5; } +QMainWindow#mw_dock_host { background: transparent; } +QDialog { background-color: #f5f5f5; } +QMenuBar { background-color: #f0f0f0; color: #202020; } +QMenuBar::item { background: transparent; padding: 4px 8px; } +QMenuBar::item:selected { background: #e1e1e1; } +QMenu { background-color: #f7f7f7; color: #202020; border: 1px solid #c6c6c6; } +QMenu::item:selected { background-color: #d6e8ff; color: #202020; } +QMenu::item:disabled { color: #808080; } +QMenu::separator { height: 1px; background: #d0d0d0; margin: 4px 8px; } +QToolBar#mw_toolbar { background-color: #f0f0f0; border: none; border-top: 1px solid #cfcfcf; } +QToolBar#mw_toolbar::separator { background-color: rgba(207, 207, 207, 235); width: 0.125em; margin-top: 0.250em; margin-bottom: 0.250em; } +QToolButton { color: #202020; background: transparent; border: 1px solid transparent; padding: 3px; } +QToolButton:hover { background-color: #e8e8e8; border: 1px solid #bdbdbd; } +QToolButton:pressed { background-color: #d8d8d8; color: #202020; } +QToolButton:disabled { color: #808080; } +QToolButton#welcome_firmware_button { background-color: #ffffff; color: #202020; border: 1px solid #b8b8b8; border-radius: 2px; padding: 4px 32px 4px 12px; text-align: left; } +QToolButton#welcome_firmware_button:hover { background-color: #f3f3f3; border-color: #9f9f9f; } +QToolButton#welcome_firmware_button:pressed { background-color: #e3e3e3; } +QToolButton#welcome_firmware_button::menu-button { subcontrol-origin: padding; subcontrol-position: top right; width: 24px; border-left: 1px solid #b8b8b8; border-top-right-radius: 2px; border-bottom-right-radius: 2px; background: #f3f3f3; } +QToolButton#welcome_firmware_button::menu-button:hover { background: #e8e8e8; border-left-color: #9f9f9f; } +QToolButton#welcome_firmware_button::menu-button:pressed { background: #dcdcdc; } +QLabel[themeRole="toolbarIconTint"] { color: #5b5b5b; } +QLabel[themeRole="mutedText"] { color: #5b5b5b; } +QPushButton { background-color: #ffffff; color: #202020; border: 1px solid #b8b8b8; border-radius: 2px; padding: 4px 16px; min-height: 1.2em; } +QPushButton:hover { background-color: #f3f3f3; border-color: #9f9f9f; } +QPushButton:pressed { background-color: #e3e3e3; } +QPushButton:disabled { color: #8a8a8a; background-color: #efefef; border-color: #d4d4d4; } +QPushButton:default { border-color: #148aff; } +QDialog#vita_themes_dialog { color: #202020; } +QDialog#vita_themes_dialog QLabel, +QDialog#vita_themes_dialog QCheckBox, +QDialog#vita_themes_dialog QRadioButton { color: #202020; background: transparent; } +QDialog#vita_themes_dialog QLabel[themeRole="mutedText"] { color: #5b5b5b; } +QDialog#vita_themes_dialog QWidget[themeRole="backgroundCell"] { background: transparent; } +QDialog#vita_themes_dialog QCheckBox[themeRole="backgroundCellControl"], +QDialog#vita_themes_dialog QRadioButton[themeRole="backgroundCellControl"] { spacing: 0px; padding: 0px; margin: 0px; } +QDialog#vita_themes_dialog QListWidget#vitaThemesList { + background-color: #ffffff; + color: #202020; + border: none; + border-radius: 10px; + padding: 6px; + outline: none; +} +QDialog#vita_themes_dialog QListWidget#vitaThemesList::item { + background: transparent; + border: none; + border-radius: 10px; + padding: 8px 12px; + margin: 0px; +} +QDialog#vita_themes_dialog QListWidget#vitaThemesList::item:selected, +QDialog#vita_themes_dialog QListWidget#vitaThemesList::item:selected:active, +QDialog#vita_themes_dialog QListWidget#vitaThemesList::item:selected:!active { background-color: #148aff; color: #ffffff; } +QDialog#vita_themes_dialog QListWidget#vitaThemesList::item:hover:!selected { background-color: #f2f2f2; } +QDialog#vita_themes_dialog QTreeWidget#vitaThemesBackgroundList { + background-color: #ffffff; + color: #202020; + border: 1px solid #c8c8c8; + border-radius: 8px; + padding: 2px; + outline: none; +} +QDialog#vita_themes_dialog QTreeWidget#vitaThemesBackgroundList::item { + padding: 5px 8px; + border-radius: 0px; + margin: 0px; +} +QDialog#vita_themes_dialog QTreeWidget#vitaThemesBackgroundList::item:selected, +QDialog#vita_themes_dialog QTreeWidget#vitaThemesBackgroundList::item:selected:active, +QDialog#vita_themes_dialog QTreeWidget#vitaThemesBackgroundList::item:selected:!active { background-color: #148aff; color: #ffffff; } +QDialog#vita_themes_dialog QTreeWidget#vitaThemesBackgroundList::item:hover:!selected { background-color: #f2f2f2; } +QDialog#vita_themes_dialog QTextBrowser#vitaThemesHelpText { + background-color: #ffffff; + color: #202020; + border: none; + border-radius: 10px; + padding: 3px 4px; +} +QDialog#vita_themes_dialog QScrollArea { background: transparent; border: none; } +QDialog#vita_themes_dialog QCheckBox::indicator { + width: 12px; + height: 12px; + padding: 0px; + margin: 0px; + border-radius: 3px; + border: 1px solid #b8b8b8; + background-color: #ffffff; +} +QDialog#vita_themes_dialog QCheckBox::indicator:hover { border-color: #148aff; background-color: #f2f8ff; } +QDialog#vita_themes_dialog QCheckBox::indicator:checked, +QDialog#vita_themes_dialog QCheckBox::indicator:indeterminate { + background-color: #148aff; + border-color: #148aff; + image: url(":/icons/check_light.svg"); +} +QDialog#vita_themes_dialog QCheckBox::indicator:disabled { border-color: #d0d0d0; background-color: #f0f0f0; } +QDialog#vita_themes_dialog QCheckBox::indicator:checked:disabled, +QDialog#vita_themes_dialog QCheckBox::indicator:indeterminate:disabled { + background-color: rgba(20, 138, 255, 0.25); + border-color: rgba(20, 138, 255, 0.35); +} +QDialog#vita_themes_dialog QRadioButton::indicator { + width: 12px; + height: 12px; + padding: 0px; + margin: 0px; + border-radius: 6px; + border: 1px solid #b8b8b8; + background-color: #ffffff; +} +QDialog#vita_themes_dialog QRadioButton::indicator:hover { border-color: #148aff; background-color: #f2f8ff; } +QDialog#vita_themes_dialog QRadioButton::indicator:checked { + background-color: #148aff; + border-color: #148aff; + image: url(":/icons/radio_dot_light.svg"); +} +QDialog#vita_themes_dialog QRadioButton::indicator:disabled { border-color: #d0d0d0; background-color: #f0f0f0; } +QDialog#vita_themes_dialog QRadioButton::indicator:checked:disabled { + background-color: rgba(20, 138, 255, 0.25); + border-color: rgba(20, 138, 255, 0.35); +} +QDialog#controls_dialog QToolButton[modeButton="true"] { background: transparent; border: none; border-radius: 10px; padding: 6px 12px; } +QDialog#controls_dialog QToolButton[modeButton="true"]:hover { background-color: rgba(20, 138, 255, 0.08); } +QDialog#controls_dialog QToolButton[modeButton="true"]:checked { background-color: rgba(20, 138, 255, 0.14); } +QDialog#controls_dialog QToolButton[modeButton="true"]:pressed { background-color: rgba(20, 138, 255, 0.18); } +QDialog#controls_dialog QPushButton { background-color: #ececec; color: #202020; border: none; border-radius: 6px; padding: 6px 12px; outline: none; } +QDialog#controls_dialog QPushButton:hover { background-color: #e3e3e3; border: none; } +QDialog#controls_dialog QPushButton:pressed { background-color: #d8d8d8; border: none; } +QDialog#controls_dialog QPushButton:disabled { color: #8a8a8a; background-color: #e4e4e4; border: none; } +QDialog#controls_dialog QPushButton[bindButton="true"] { background-color: #e6e6e6; border-radius: 6px; padding: 6px 12px; } +QDialog#controls_dialog QPushButton[bindButton="true"]:hover { background-color: #dddddd; } +QDialog#controls_dialog QPushButton[bindButton="true"]:pressed { background-color: #d2d2d2; } +QDialog#controls_dialog QPushButton[bindButton="true"][captureActive="true"] { background-color: #148aff; color: #ffffff; } +QDialog#controls_dialog QPushButton[bindButton="true"][captureActive="true"]:hover { background-color: #2a97ff; } +QDialog#controls_dialog QPushButton[bindButton="true"][captureActive="true"]:pressed { background-color: #0f74d6; } +QStatusBar#emulator_status_bar { background-color: #f0f0f0; color: #202020; } +QStatusBar#emulator_status_bar::item { border: none; } +QStatusBar#emulator_status_bar QPushButton#status_button { border: 1px solid transparent; padding: 2px 8px; font-weight: bold; font-size: 11px; background: transparent; border-radius: 6px; color: #202020; } +QStatusBar#emulator_status_bar QPushButton#status_button:hover { border: 1px solid #a0a0a0; background: rgba(0, 0, 0, 0.03); } +QStatusBar#emulator_status_bar QPushButton#status_button:pressed { border: 1px solid #8a8a8a; background: rgba(0, 0, 0, 0.06); } +QStatusBar#emulator_status_bar QPushButton#status_button[accent="renderer_vulkan"] { color: #ff4444; } +QStatusBar#emulator_status_bar QPushButton#status_button[accent="renderer_opengl"] { color: #4488ff; } +QStatusBar#emulator_status_bar QPushButton#status_button[accent="high"] { color: #ff8800; } +QStatusBar#emulator_status_bar QPushButton#status_button[accent="standard"] { color: #aaaaaa; } +QStatusBar#emulator_status_bar QPushButton#status_button[accent="neutral"] { color: #5f5f5f; } +QStatusBar#emulator_status_bar QPushButton#status_button[accent="audio"] { color: #4b9b2f; } +QStatusBar#emulator_status_bar QPushButton#status_button[accent="muted"] { color: #777777; } +QStatusBar#emulator_status_bar QPushButton#status_button[accent="update"] { color: #1f8f4e; } +QLineEdit { background-color: #ffffff; color: #202020; border: 1px solid #b8b8b8; border-radius: 2px; padding: 2px 4px; selection-background-color: #148aff; selection-color: #ffffff; } +QLineEdit:focus { border-color: #148aff; } +QLineEdit:disabled { color: #8a8a8a; background-color: #f0f0f0; border-color: #d0d0d0; } +QComboBox { background-color: #ffffff; color: #202020; border: 1px solid #b8b8b8; border-radius: 2px; padding: 2px 28px 2px 6px; } +QComboBox:hover { border-color: #9f9f9f; } +QComboBox:disabled { color: #8a8a8a; background-color: #f0f0f0; border-color: #d0d0d0; } +QComboBox::drop-down { subcontrol-origin: padding; subcontrol-position: top right; width: 24px; border: none; background: transparent; } +QComboBox::down-arrow { image: url("chevron_down_dark.svg"); width: 10px; height: 6px; } +QComboBox::down-arrow:on { top: 1px; } +QComboBox QAbstractItemView { background-color: #ffffff; color: #202020; border: 1px solid #b8b8b8; selection-background-color: #148aff; selection-color: #ffffff; } +QSpinBox, QDoubleSpinBox { background-color: #ffffff; color: #202020; border: 1px solid #b8b8b8; border-radius: 2px; padding: 2px 24px 2px 6px; selection-background-color: #148aff; selection-color: #ffffff; } +QSpinBox:disabled, QDoubleSpinBox:disabled { color: #8a8a8a; background-color: #f0f0f0; border-color: #d0d0d0; } +QSpinBox::up-button, QDoubleSpinBox::up-button { subcontrol-origin: border; subcontrol-position: top right; width: 18px; border: none; background: transparent; } +QSpinBox::down-button, QDoubleSpinBox::down-button { subcontrol-origin: border; subcontrol-position: bottom right; width: 18px; border: none; background: transparent; } +QSpinBox::up-arrow, QDoubleSpinBox::up-arrow { image: url("chevron_up_dark.svg"); width: 10px; height: 6px; } +QSpinBox::down-arrow, QDoubleSpinBox::down-arrow { image: url("chevron_down_dark.svg"); width: 10px; height: 6px; } +QCheckBox { color: #202020; spacing: 6px; } +QCheckBox:disabled { color: #8a8a8a; } +QRadioButton { color: #202020; spacing: 6px; } +QRadioButton:disabled { color: #8a8a8a; } +QGroupBox { color: #202020; border: 1px solid #c8c8c8; border-radius: 4px; margin-top: 0.5em; padding-top: 0.6em; } +QGroupBox:disabled { color: #9a9a9a; border-color: #d8d8d8; } +QGroupBox::title { subcontrol-origin: margin; subcontrol-position: top left; padding: 0 4px; color: #202020; } +QLabel { color: #202020; background: transparent; } +QLabel:disabled { color: #8a8a8a; } +QTableWidget, QTableView { alternate-background-color: #f2f2f2; background-color: #ffffff; border: none; color: #202020; gridline-color: #e0e0e0; } +QTableWidget:disabled, QTableView:disabled { color: #8a8a8a; background-color: #f0f0f0; } +QTableCornerButton::section { background-color: #ececec; border: 1px solid #d4d4d4; } +QTableView::item { border-left: 0.063em solid #ffffff; border-right: 0.063em solid #ffffff; padding-left: 0.313em; } +QTableView::item:selected { background-color: #148aff; color: #ffffff; } +QTableWidget#apps_list_table::item:selected, QTableView#apps_list_table::item:selected { background-color: rgba(84, 140, 188, 48); color: #202020; border-left: 0.063em solid rgba(84, 140, 188, 0); border-right: 0.063em solid rgba(84, 140, 188, 0); } +QTableView#apps_list_table QHeaderView::section, QTableWidget#apps_list_table QHeaderView::section { padding-top: 0; padding-bottom: 0; } +QTableView#trophy_app_table::item:selected, QTableView#trophy_table::item:selected { background-color: rgba(84, 140, 188, 48); color: #202020; border-left: 0.063em solid rgba(84, 140, 188, 0); border-right: 0.063em solid rgba(84, 140, 188, 0); } +QHeaderView::section { background-color: #ececec; color: #202020; padding-left: .5em; padding-right: .5em; padding-top: .4em; padding-bottom: -.1em; border: 0.063em solid #ffffff; } +QHeaderView::section:hover { background: #e0e0e0; } +QDockWidget { background: transparent; color: #202020; } +QDockWidget#apps_list[floating="true"], +QDockWidget#logger[floating="true"] { background-color: #f5f5f5; } +QDockWidget::title { background: #ececec; border: none; padding-top: 0.2em; padding-left: 0.2em; } +QDockWidget::close-button, QDockWidget::float-button { background-color: #ececec; border: none; } +QDockWidget::close-button:hover, QDockWidget::float-button:hover { background-color: #e0e0e0; } +QWidget#apps_list_frame { background-color: #f5f5f5; } +QWidget#log_surface { background-color: #f5f5f5; } +QWidget#apps_list_viewport { background-color: #ffffff; } +QWidget#log_frame_viewport { background-color: #ffffff; } +QPlainTextEdit#log_frame { background-color: #ffffff; color: #202020; } +QWidget#log_search_container { background-color: #ffffff; border: 1px solid #c8c8c8; border-radius: 4px; } +QWidget#toolbar_icon_size_container { background-color: #f0f0f0; } +QSlider#toolbar_icon_size_slider { background: transparent; } +QSlider#toolbar_icon_size_slider::groove:horizontal { background: transparent; height: 4px; border-radius: 2px; } +QSlider#toolbar_icon_size_slider::sub-page:horizontal { background: #7f7f7f; height: 4px; border-radius: 2px; } +QSlider#toolbar_icon_size_slider::add-page:horizontal { background: #c8c8c8; height: 4px; border-radius: 2px; } +QSlider#toolbar_icon_size_slider::handle:horizontal { background: #5b5b5b; width: 14px; margin: -5px 0; border-radius: 7px; } +QSlider#toolbar_icon_size_slider::handle:horizontal:hover { background: #404040; } +QLabel[themeRole="logFatal"] { color: #ff00ff; } +QLabel[themeRole="logError"] { color: #C02F1D; } +QLabel[themeRole="logWarning"] { color: #BA8745; } +QLabel[themeRole="logNotice"] { color: #000000; } +QLabel[themeRole="logDebug"] { color: #2467c8; } +QLabel[themeRole="logTrace"] { color: #808080; } +QLabel[themeRole="sectionHeader"] { font-weight: 600; margin-top: 8px; } +QLabel[severity="success"] { border-radius: 10px; padding: 4px 10px; background-color: #d9f2dd; color: #1f7a1f; } +QLabel[severity="error"] { border-radius: 10px; padding: 4px 10px; background-color: #f7dcd9; color: #a32a1c; } +QTabWidget::pane { border: 1px solid #c8c8c8; background-color: #f5f5f5; } +QTabBar::tab { background-color: #e9e9e9; color: #202020; padding: 6px 14px; border: 1px solid #c8c8c8; border-bottom: none; border-top-left-radius: 3px; border-top-right-radius: 3px; } +QTabBar::tab:selected { background-color: #f5f5f5; } +QTabBar::tab:!selected { margin-top: 2px; } +QTabBar::tab:hover:!selected { background-color: #dfdfdf; } +QListWidget { background-color: #ffffff; color: #202020; border: 1px solid #c8c8c8; } +QListWidget:disabled { color: #8a8a8a; background-color: #f0f0f0; border-color: #d6d6d6; } +QListWidget::item:selected { background-color: #148aff; color: #ffffff; } +QListWidget::item:hover:!selected { background-color: #f2f2f2; } +QDialog[class="settings-window"] QListWidget#settingsCategory { background-color: #ffffff; border: none; border-radius: 10px; padding: 6px; outline: none; } +QDialog[class="settings-window"] QListWidget#settingsCategory::item { background: transparent; border: none; border-radius: 10px; padding: 8px 12px; margin: 0px; } +QDialog[class="settings-window"] QListWidget#settingsCategory::item:selected, +QDialog[class="settings-window"] QListWidget#settingsCategory::item:selected:active, +QDialog[class="settings-window"] QListWidget#settingsCategory::item:selected:!active { background-color: #148aff; color: #ffffff; } +QDialog[class="settings-window"] QListWidget#settingsCategory::item:hover:!selected { background-color: #f2f2f2; } +QDialog[class="settings-window"] QTextBrowser#helpText { background-color: #ffffff; color: #202020; border: none; border-radius: 10px; padding: 3px 4px; } +QScrollBar:vertical { background: #f0f0f0; width: 14px; margin: 0; } +QScrollBar::handle:vertical { background: #b0b0b0; min-height: 20px; border-radius: 3px; margin: 2px; } +QScrollBar::handle:vertical:hover { background: #989898; } +QScrollBar::add-line:vertical, QScrollBar::sub-line:vertical { height: 0px; } +QScrollBar::add-page:vertical, QScrollBar::sub-page:vertical { background: none; } +QScrollBar:horizontal { background: #f0f0f0; height: 14px; margin: 0; } +QScrollBar::handle:horizontal { background: #b0b0b0; min-width: 20px; border-radius: 3px; margin: 2px; } +QScrollBar::handle:horizontal:hover { background: #989898; } +QScrollBar::add-line:horizontal, QScrollBar::sub-line:horizontal { width: 0px; } +QScrollBar::add-page:horizontal, QScrollBar::sub-page:horizontal { background: none; } +QAbstractScrollArea::corner { background: #f0f0f0; } +QSlider::groove:horizontal { background: #c8c8c8; height: 4px; border-radius: 2px; } +QSlider::handle:horizontal { background: #7f7f7f; width: 14px; margin: -5px 0; border-radius: 7px; } +QSlider::handle:horizontal:hover { background: #666666; } +QSlider::groove:vertical { background: #c8c8c8; width: 4px; border-radius: 2px; } +QSlider::handle:vertical { background: #7f7f7f; height: 14px; margin: 0 -5px; border-radius: 7px; } +QSlider::handle:vertical:hover { background: #666666; } +QProgressBar { background-color: #ffffff; color: #202020; border: 1px solid #c8c8c8; text-align: center; border-radius: 2px; } +QProgressBar::chunk { background-color: #148aff; border-radius: 2px; } +QSplitter::handle { background-color: #d4d4d4; } +QToolTip { background-color: #f7f7f7; color: #202020; border: 1px solid #c8c8c8; padding: 2px; } +QPlainTextEdit, QTextEdit, QTextBrowser { background-color: #ffffff; color: #202020; border: 1px solid #c8c8c8; } +QPlainTextEdit:disabled, QTextEdit:disabled, QTextBrowser:disabled { color: #8a8a8a; background-color: #f0f0f0; border-color: #d0d0d0; } +QDialogButtonBox QPushButton { min-width: 80px; } diff --git a/vita3k/gui-qt/resources/themes/vita/generated.qss.in b/vita3k/gui-qt/resources/themes/vita/generated.qss.in new file mode 100644 index 000000000..7a22ec178 --- /dev/null +++ b/vita3k/gui-qt/resources/themes/vita/generated.qss.in @@ -0,0 +1,664 @@ +/*!VITA3K_THEME +background-images: {{generated_background_images}} +background-interval: {{generated_background_interval_seconds}} +background-animated: {{generated_background_animated}} +*/ +/*!PALETTE + * dark: {{palette_dark}} + * window: {{window_hex}} + * window-text: {{text_hex}} + * base: {{base_hex}} + * alternate-base: {{alternate_base_hex}} + * tooltip-base: {{base_hex}} + * tooltip-text: {{text_hex}} + * text: {{text_hex}} + * button: {{button_hex}} + * button-text: {{button_text_hex}} + * bright-text: {{button_bright_text_hex}} + * link: {{accent_hex}} + * highlight: {{highlight_argb}} + * inactive-highlight: {{inactive_highlight_argb}} + * disabled-highlight: {{disabled_highlight_argb}} + * highlight-text: {{highlight_text_hex}} + * inactive-highlight-text: {{highlight_text_hex}} + * disabled-highlight-text: {{overlay_disabled_text_hex}} + * placeholder-text: {{muted_text_hex}} + * disabled-button-text: {{disabled_text_hex}} + * disabled-window-text: {{disabled_text_hex}} + * disabled-text: {{disabled_text_hex}} + */ +QWidget { color: {{text_hex}}; background: transparent; } +QWidget:disabled { color: {{disabled_text_hex}}; } +QMainWindow { background-color: {{window_hex}}; } +QMainWindow#main_window { + background-color: {{window_hex}}; +} +QWidget#mw_frame, +QMainWindow#mw_dock_host { + background: transparent; +} +QDialog { background-color: {{dialog_bg}}; } +QMenuBar { + background-color: {{menu_bar_bg}}; + color: {{text_hex}}; + border-bottom: 1px solid {{menu_bar_border}}; +} +QMenuBar::item { background: transparent; padding: 4px 8px; } +QMenuBar::item:selected { background: {{menu_bar_item_hover_bg}}; } +QMenu { + background-color: {{menu_bg}}; + color: {{text_hex}}; + border: 1px solid {{menu_border}}; +} +QMenu::item:selected { background-color: {{menu_selected_bg}}; color: {{selected_text_hex}}; } +QMenu::item:disabled { color: {{disabled_text_hex}}; } +QMenu::separator { height: 1px; background: {{menu_separator_bg}}; margin: 4px 8px; } +QToolBar#mw_toolbar { + background-color: {{toolbar_bg}}; + border: none; + border-top: 1px solid {{toolbar_border_top}}; + border-bottom: 1px solid {{toolbar_border_bottom}}; +} +QToolBar#mw_toolbar::separator { + background-color: {{toolbar_separator_bg}}; + width: 0.125em; + margin-top: 0.250em; + margin-bottom: 0.250em; +} +QToolBar#mw_toolbar > QWidget { background: transparent; } +QToolButton { + color: {{text_hex}}; + background: transparent; + border: 1px solid transparent; + padding: 3px; +} +QToolButton:hover { background-color: {{tool_button_hover_bg}}; border: 1px solid {{tool_button_hover_border}}; } +QToolButton:pressed { background-color: {{tool_button_pressed_bg}}; } +QToolButton:disabled { color: {{disabled_text_hex}}; } +QToolButton#welcome_firmware_button { + background-color: {{welcome_button_bg}}; + color: {{text_hex}}; + border: 1px solid {{welcome_button_border}}; + border-radius: 2px; + padding: 4px 32px 4px 12px; + text-align: left; +} +QToolButton#welcome_firmware_button:hover { + background-color: {{welcome_button_hover_bg}}; + border-color: {{welcome_button_hover_border}}; +} +QToolButton#welcome_firmware_button:pressed { background-color: {{welcome_button_pressed_bg}}; } +QToolButton#welcome_firmware_button::menu-button { + subcontrol-origin: padding; + subcontrol-position: top right; + width: 24px; + border-left: 1px solid {{welcome_menu_button_border}}; + background: {{welcome_menu_button_bg}}; +} +QToolButton#welcome_firmware_button::menu-button:hover { + background: {{welcome_menu_button_hover_bg}}; + border-left-color: {{welcome_menu_button_hover_border}}; +} +QToolButton#welcome_firmware_button::menu-button:pressed { background: {{welcome_menu_button_pressed_bg}}; } +QLabel[themeRole="toolbarIconTint"] { color: {{toolbar_icon_tint_hex}}; } +QLabel[themeRole="mutedText"] { color: {{muted_text_hex}}; } +QPushButton { + background-color: {{push_button_bg}}; + color: {{text_hex}}; + border: 1px solid {{push_button_border}}; + border-radius: 2px; + padding: 4px 16px; + min-height: 1.2em; +} +QPushButton:hover { background-color: {{push_button_hover_bg}}; border-color: {{push_button_hover_border}}; } +QPushButton:pressed { background-color: {{push_button_pressed_bg}}; color: {{push_button_pressed_text_hex}}; } +QPushButton:disabled { + color: {{disabled_text_hex}}; + background-color: {{push_button_disabled_bg}}; + border-color: {{push_button_disabled_border}}; +} +QPushButton:default { border-color: {{push_button_default_border}}; } +QDialog#vita_themes_dialog { color: {{dialog_text_hex}}; } +QDialog#vita_themes_dialog QLabel, +QDialog#vita_themes_dialog QCheckBox, +QDialog#vita_themes_dialog QRadioButton { color: {{dialog_text_hex}}; background: transparent; } +QDialog#vita_themes_dialog QLabel[themeRole="mutedText"] { color: {{dialog_muted_text_hex}}; } +QDialog#vita_themes_dialog QWidget[themeRole="backgroundCell"] { background: transparent; } +QDialog#vita_themes_dialog QCheckBox[themeRole="backgroundCellControl"], +QDialog#vita_themes_dialog QRadioButton[themeRole="backgroundCellControl"] { spacing: 0px; padding: 0px; margin: 0px; } +QDialog#vita_themes_dialog QListWidget#vitaThemesList { + background-color: {{vita_list_bg}}; + color: {{dialog_text_hex}}; + border: none; + border-radius: 10px; + padding: 6px; + outline: none; +} +QDialog#vita_themes_dialog QListWidget#vitaThemesList::item { + background: transparent; + border: none; + border-radius: 10px; + padding: 8px 12px; + margin: 0px; +} +QDialog#vita_themes_dialog QListWidget#vitaThemesList::item:selected, +QDialog#vita_themes_dialog QListWidget#vitaThemesList::item:selected:active, +QDialog#vita_themes_dialog QListWidget#vitaThemesList::item:selected:!active { background-color: {{vita_list_selected_bg}}; color: {{dialog_selected_text_hex}}; } +QDialog#vita_themes_dialog QListWidget#vitaThemesList::item:hover:!selected { background-color: {{vita_list_hover_bg}}; } +QDialog#vita_themes_dialog QTreeWidget#vitaThemesBackgroundList { + background-color: {{background_list_bg}}; + color: {{dialog_text_hex}}; + border: 1px solid {{background_list_border}}; + border-radius: 8px; + padding: 2px; + outline: none; +} +QDialog#vita_themes_dialog QTreeWidget#vitaThemesBackgroundList::item { + padding: 5px 8px; + border-radius: 0px; + margin: 0px; +} +QDialog#vita_themes_dialog QTreeWidget#vitaThemesBackgroundList::item:selected, +QDialog#vita_themes_dialog QTreeWidget#vitaThemesBackgroundList::item:selected:active, +QDialog#vita_themes_dialog QTreeWidget#vitaThemesBackgroundList::item:selected:!active { background-color: {{background_list_selected_bg}}; color: {{dialog_selected_text_hex}}; } +QDialog#vita_themes_dialog QTreeWidget#vitaThemesBackgroundList::item:hover:!selected { background-color: {{background_list_hover_bg}}; } +QDialog#vita_themes_dialog QTextBrowser#vitaThemesHelpText { + background-color: {{vita_help_bg}}; + color: {{dialog_text_hex}}; + border: none; + border-radius: 10px; + padding: 3px 4px; +} +QDialog#vita_themes_dialog QScrollArea { background: transparent; border: none; } +QDialog#vita_themes_dialog QCheckBox::indicator { + width: 12px; + height: 12px; + padding: 0px; + margin: 0px; + border-radius: 3px; + border: 1px solid {{indicator_border}}; + background-color: {{indicator_bg}}; +} +QDialog#vita_themes_dialog QCheckBox::indicator:hover { border-color: {{indicator_hover_border}}; background-color: {{indicator_hover_bg}}; } +QDialog#vita_themes_dialog QCheckBox::indicator:checked, +QDialog#vita_themes_dialog QCheckBox::indicator:indeterminate { + background-color: {{indicator_checked_bg}}; + border-color: {{indicator_checked_border}}; + image: url(":/icons/check_{{check_icon_suffix}}.svg"); +} +QDialog#vita_themes_dialog QCheckBox::indicator:disabled { border-color: {{indicator_disabled_border}}; background-color: {{indicator_disabled_bg}}; } +QDialog#vita_themes_dialog QCheckBox::indicator:checked:disabled, +QDialog#vita_themes_dialog QCheckBox::indicator:indeterminate:disabled { + background-color: {{indicator_checked_disabled_bg}}; + border-color: {{indicator_checked_disabled_border}}; +} +QDialog#vita_themes_dialog QRadioButton::indicator { + width: 12px; + height: 12px; + padding: 0px; + margin: 0px; + border-radius: 6px; + border: 1px solid {{radio_indicator_border}}; + background-color: {{radio_indicator_bg}}; +} +QDialog#vita_themes_dialog QRadioButton::indicator:hover { border-color: {{radio_indicator_hover_border}}; background-color: {{radio_indicator_hover_bg}}; } +QDialog#vita_themes_dialog QRadioButton::indicator:checked { + background-color: {{radio_checked_bg}}; + border-color: {{radio_checked_border}}; + image: url(":/icons/radio_dot_{{check_icon_suffix}}.svg"); +} +QDialog#vita_themes_dialog QRadioButton::indicator:disabled { border-color: {{indicator_disabled_border}}; background-color: {{indicator_disabled_bg}}; } +QDialog#vita_themes_dialog QRadioButton::indicator:checked:disabled { + background-color: {{radio_checked_disabled_bg}}; + border-color: {{radio_checked_disabled_border}}; +} +QDialog#controls_dialog QToolButton[modeButton="true"] { + background: transparent; + border: none; + border-radius: 10px; + padding: 6px 12px; +} +QDialog#controls_dialog QToolButton[modeButton="true"]:hover { background-color: {{controls_mode_hover_bg}}; } +QDialog#controls_dialog QToolButton[modeButton="true"]:checked { background-color: {{controls_mode_checked_bg}}; color: {{dialog_mode_checked_text_hex}}; } +QDialog#controls_dialog QToolButton[modeButton="true"]:pressed { background-color: {{controls_mode_pressed_bg}}; color: {{dialog_mode_checked_text_hex}}; } +QDialog#controls_dialog QPushButton { + background-color: {{controls_button_bg}}; + color: {{dialog_text_hex}}; + border: none; + border-radius: 6px; + padding: 6px 12px; + outline: none; +} +QDialog#controls_dialog QPushButton:hover { background-color: {{controls_button_hover_bg}}; border: none; } +QDialog#controls_dialog QPushButton:pressed { background-color: {{controls_button_pressed_bg}}; border: none; } +QDialog#controls_dialog QPushButton:disabled { color: {{dialog_disabled_text_hex}}; background-color: {{controls_button_disabled_bg}}; border: none; } +QDialog#controls_dialog QPushButton[bindButton="true"] { + background-color: {{controls_bind_bg}}; + border-radius: 6px; + padding: 6px 12px; +} +QDialog#controls_dialog QPushButton[bindButton="true"]:hover { background-color: {{controls_bind_hover_bg}}; } +QDialog#controls_dialog QPushButton[bindButton="true"]:pressed { background-color: {{controls_bind_pressed_bg}}; } +QDialog#controls_dialog QPushButton[bindButton="true"][captureActive="true"] { + background-color: {{controls_capture_bg}}; + color: {{dialog_selected_text_hex}}; +} +QDialog#controls_dialog QPushButton[bindButton="true"][captureActive="true"]:hover { background-color: {{controls_capture_hover_bg}}; } +QDialog#controls_dialog QPushButton[bindButton="true"][captureActive="true"]:pressed { background-color: {{controls_capture_pressed_bg}}; } +QStatusBar#emulator_status_bar { + background-color: {{status_bar_bg}}; + color: {{text_hex}}; + border-top: 1px solid {{status_bar_border}}; +} +QStatusBar#emulator_status_bar::item { border: none; } +QStatusBar#emulator_status_bar QPushButton#status_button { + border: 1px solid transparent; + padding: 2px 8px; + font-weight: bold; + font-size: 11px; + background: transparent; + border-radius: 6px; + color: {{text_hex}}; +} +QStatusBar#emulator_status_bar QPushButton#status_button:hover { border: 1px solid {{status_button_hover_border}}; background: {{status_button_hover_bg}}; } +QStatusBar#emulator_status_bar QPushButton#status_button:pressed { border: 1px solid {{status_button_pressed_border}}; background: {{status_button_pressed_bg}}; } +QStatusBar#emulator_status_bar QPushButton#status_button[accent="renderer_vulkan"] { color: {{renderer_vulkan_hex}}; } +QStatusBar#emulator_status_bar QPushButton#status_button[accent="renderer_opengl"] { color: {{renderer_opengl_hex}}; } +QStatusBar#emulator_status_bar QPushButton#status_button[accent="high"] { color: {{status_high_hex}}; } +QStatusBar#emulator_status_bar QPushButton#status_button[accent="standard"] { color: {{status_standard_hex}}; } +QStatusBar#emulator_status_bar QPushButton#status_button[accent="neutral"] { color: {{status_neutral_hex}}; } +QStatusBar#emulator_status_bar QPushButton#status_button[accent="audio"] { color: {{status_audio_hex}}; } +QStatusBar#emulator_status_bar QPushButton#status_button[accent="muted"] { color: {{status_muted_hex}}; } +QStatusBar#emulator_status_bar QPushButton#status_button[accent="update"] { color: {{status_update_hex}}; } +QLineEdit { + background-color: {{line_edit_bg}}; + color: {{text_hex}}; + border: 1px solid {{line_edit_border}}; + border-radius: 2px; + padding: 2px 4px; + selection-background-color: {{line_edit_selection_bg}}; + selection-color: {{selected_text_hex}}; +} +QLineEdit:focus { border-color: {{line_edit_focus_border}}; } +QLineEdit:disabled { color: {{disabled_text_hex}}; background-color: {{line_edit_disabled_bg}}; border-color: {{line_edit_disabled_border}}; } +QLineEdit#mw_searchbar { + background: transparent; + border: 1px solid {{main_search_border}}; + border-radius: 10px; + color: {{text_hex}}; + padding: 5px 12px; + selection-background-color: {{main_search_selection_bg}}; + selection-color: {{selected_text_hex}}; +} +QLineEdit#mw_searchbar:focus { + background: {{main_search_focus_bg}}; + border-color: {{main_search_focus_border}}; +} +QComboBox { + background-color: {{combo_bg}}; + color: {{text_hex}}; + border: 1px solid {{combo_border}}; + border-radius: 2px; + padding: 2px 28px 2px 6px; +} +QComboBox:hover { border-color: {{combo_hover_border}}; } +QComboBox:disabled { color: {{disabled_text_hex}}; background-color: {{combo_disabled_bg}}; border-color: {{combo_disabled_border}}; } +QComboBox::drop-down { subcontrol-origin: padding; subcontrol-position: top right; width: 24px; border: none; background: transparent; } +QComboBox::down-arrow { image: url(":/icons/chevron_down_{{icon_suffix}}.svg"); width: 10px; height: 6px; } +QComboBox::down-arrow:on { top: 1px; } +QComboBox QAbstractItemView { + background-color: {{combo_popup_bg}}; + color: {{text_hex}}; + border: 1px solid {{combo_popup_border}}; + selection-background-color: {{combo_popup_selection_bg}}; + selection-color: {{selected_text_hex}}; +} +QSpinBox, QDoubleSpinBox { + background-color: {{spin_bg}}; + color: {{text_hex}}; + border: 1px solid {{spin_border}}; + border-radius: 2px; + padding: 2px 24px 2px 6px; + selection-background-color: {{spin_selection_bg}}; + selection-color: {{selected_text_hex}}; +} +QSpinBox:disabled, QDoubleSpinBox:disabled { color: {{disabled_text_hex}}; background-color: {{spin_disabled_bg}}; border-color: {{spin_disabled_border}}; } +QSpinBox::up-button, QDoubleSpinBox::up-button { subcontrol-origin: border; subcontrol-position: top right; width: 18px; border: none; background: transparent; } +QSpinBox::down-button, QDoubleSpinBox::down-button { subcontrol-origin: border; subcontrol-position: bottom right; width: 18px; border: none; background: transparent; } +QSpinBox::up-arrow, QDoubleSpinBox::up-arrow { image: url(":/icons/chevron_up_{{icon_suffix}}.svg"); width: 10px; height: 6px; } +QSpinBox::down-arrow, QDoubleSpinBox::down-arrow { image: url(":/icons/chevron_down_{{icon_suffix}}.svg"); width: 10px; height: 6px; } +QCheckBox { color: {{text_hex}}; spacing: 6px; } +QCheckBox:disabled { color: {{disabled_text_hex}}; } +QRadioButton { color: {{text_hex}}; spacing: 6px; } +QRadioButton:disabled { color: {{disabled_text_hex}}; } +QDialog#controls_dialog, +QDialog[class="settings-window"] { color: {{dialog_text_hex}}; } +QDialog#controls_dialog QLabel, +QDialog#controls_dialog QCheckBox, +QDialog#controls_dialog QRadioButton, +QDialog#controls_dialog QGroupBox, +QDialog[class="settings-window"] QLabel, +QDialog[class="settings-window"] QCheckBox, +QDialog[class="settings-window"] QRadioButton, +QDialog[class="settings-window"] QGroupBox { color: {{dialog_text_hex}}; background: transparent; } +QDialog#controls_dialog QToolButton[modeButton="true"] { color: {{dialog_text_hex}}; } +QCheckBox::indicator, +QGroupBox::indicator, +QListView::indicator, +QListWidget::indicator { + width: 12px; + height: 12px; + padding: 0px; + margin: 0px; + border-radius: 3px; + border: 1px solid {{indicator_border}}; + background-color: {{indicator_bg}}; +} +QCheckBox::indicator:hover, +QGroupBox::indicator:hover, +QListView::indicator:hover, +QListWidget::indicator:hover { border-color: {{indicator_hover_border}}; background-color: {{indicator_hover_bg}}; } +QCheckBox::indicator:checked, +QCheckBox::indicator:indeterminate, +QGroupBox::indicator:checked, +QGroupBox::indicator:indeterminate, +QListView::indicator:checked, +QListView::indicator:indeterminate, +QListWidget::indicator:checked, +QListWidget::indicator:indeterminate { + background-color: {{indicator_checked_bg}}; + border-color: {{indicator_checked_border}}; + image: url(":/icons/check_{{check_icon_suffix}}.svg"); +} +QCheckBox::indicator:disabled, +QGroupBox::indicator:disabled, +QListView::indicator:disabled, +QListWidget::indicator:disabled { border-color: {{indicator_disabled_border}}; background-color: {{indicator_disabled_bg}}; } +QCheckBox::indicator:checked:disabled, +QCheckBox::indicator:indeterminate:disabled, +QGroupBox::indicator:checked:disabled, +QGroupBox::indicator:indeterminate:disabled, +QListView::indicator:checked:disabled, +QListView::indicator:indeterminate:disabled, +QListWidget::indicator:checked:disabled, +QListWidget::indicator:indeterminate:disabled { + background-color: {{indicator_checked_disabled_bg}}; + border-color: {{indicator_checked_disabled_border}}; +} +QRadioButton::indicator { + width: 12px; + height: 12px; + border-radius: 6px; + border: 1px solid {{radio_indicator_border}}; + background-color: {{radio_indicator_bg}}; +} +QRadioButton::indicator:hover { border-color: {{radio_indicator_hover_border}}; background-color: {{radio_indicator_hover_bg}}; } +QRadioButton::indicator:checked { + background-color: {{radio_checked_bg}}; + border-color: {{radio_checked_border}}; + image: url(":/icons/radio_dot_{{check_icon_suffix}}.svg"); +} +QRadioButton::indicator:disabled { border-color: {{indicator_disabled_border}}; background-color: {{indicator_disabled_bg}}; } +QRadioButton::indicator:checked:disabled { + background-color: {{radio_checked_disabled_bg}}; + border-color: {{radio_checked_disabled_border}}; +} +QGroupBox { + color: {{text_hex}}; + border: 1px solid {{group_border}}; + border-radius: 4px; + margin-top: 0.5em; + padding-top: 0.6em; +} +QGroupBox:disabled { color: {{disabled_text_hex}}; border-color: {{group_disabled_border}}; } +QGroupBox::title { subcontrol-origin: margin; subcontrol-position: top left; padding: 0 4px; color: {{text_hex}}; } +QLabel { color: {{text_hex}}; background: transparent; } +QLabel:disabled { color: {{disabled_text_hex}}; } +QDockWidget#apps_list, +QDockWidget#logger { + background: transparent; + color: {{selected_text_hex}}; +} +QDockWidget#apps_list::title, +QDockWidget#logger::title { + background: {{dock_title_bg}}; + border: none; + padding-top: 0.2em; + padding-left: 0.2em; +} +QDockWidget#apps_list::close-button, +QDockWidget#apps_list::float-button, +QDockWidget#logger::close-button, +QDockWidget#logger::float-button { + background-color: {{dock_title_bg}}; + border: none; +} +QDockWidget#apps_list::close-button:hover, +QDockWidget#apps_list::float-button:hover, +QDockWidget#logger::close-button:hover, +QDockWidget#logger::float-button:hover { + background-color: {{dock_button_hover_bg}}; +} +QDockWidget#apps_list[floating="true"], +QDockWidget#logger[floating="true"] { + background-color: {{window_hex}}; +} +QWidget#apps_list_frame, +QWidget#log_surface { + background-color: {{apps_surface_bg}}; +} +QTableWidget#apps_list_table, +QTableView#apps_list_table, +QWidget#apps_list_viewport, +QPlainTextEdit#log_frame, +QWidget#log_frame_viewport { background: transparent; } +QTableWidget, QTableView { + alternate-background-color: {{alt_row_bg}}; + background-color: transparent; + border: none; + color: {{text_hex}}; + gridline-color: {{table_gridline}}; +} +QTableWidget:disabled, QTableView:disabled { color: {{disabled_text_hex}}; background-color: {{table_disabled_bg}}; } +QTableCornerButton::section { background-color: {{table_corner_bg}}; border: 1px solid {{table_corner_border}}; } +QTableView::item { border-left: 0.063em solid transparent; border-right: 0.063em solid transparent; padding-left: 0.313em; } +QTableView::item:selected { background-color: {{table_selected_bg}}; color: {{selected_text_hex}}; } +QTableWidget#apps_list_table::item:selected, QTableView#apps_list_table::item:selected { + background-color: {{table_selected_bg}}; + color: {{selected_text_hex}}; + border-left: 0.063em solid transparent; + border-right: 0.063em solid transparent; +} +QTableView#apps_list_table QHeaderView, +QTableWidget#apps_list_table QHeaderView { + background-color: {{header_bg}}; +} +QTableView#apps_list_table QHeaderView::section, +QTableWidget#apps_list_table QHeaderView::section { + background-color: {{header_bg}}; + color: {{selected_text_hex}}; + border: 0.063em solid {{header_border}}; + padding-top: 0; + padding-bottom: 0; +} +QTableView#trophy_app_table::item:selected, QTableView#trophy_table::item:selected { + background-color: {{table_selected_bg}}; + color: {{selected_text_hex}}; + border-left: 0.063em solid transparent; + border-right: 0.063em solid transparent; +} +QHeaderView::section { + background-color: {{header_bg}}; + color: {{selected_text_hex}}; + padding-left: .5em; + padding-right: .5em; + padding-top: .4em; + padding-bottom: -.1em; + border: 0.063em solid {{header_border}}; +} +QHeaderView::section:hover { background: {{header_hover_bg}}; } +QPlainTextEdit#log_frame { + color: {{selected_text_hex}}; + border: none; +} +QWidget#log_search_container { + background-color: {{log_search_bg}}; + border: 1px solid {{log_search_border}}; + border-radius: 4px; +} +QWidget#toolbar_icon_size_container { + background: transparent; + border: none; +} +QSlider#toolbar_icon_size_slider { + background: transparent; + margin: 0; + min-height: 20px; +} +QSlider#toolbar_icon_size_slider::groove:horizontal { + background: {{toolbar_slider_groove_bg}}; + border: none; + height: 6px; + border-radius: 3px; +} +QSlider#toolbar_icon_size_slider::sub-page:horizontal { + background: {{toolbar_slider_subpage_bg}}; + border: none; + height: 6px; + border-radius: 3px; +} +QSlider#toolbar_icon_size_slider::add-page:horizontal { + background: {{toolbar_slider_addpage_bg}}; + border: none; + height: 6px; + border-radius: 3px; +} +QSlider#toolbar_icon_size_slider::handle:horizontal { + background: {{toolbar_slider_handle_bg}}; + border: none; + width: 14px; + height: 14px; + margin: -4px 0; + border-radius: 7px; +} +QSlider#toolbar_icon_size_slider::handle:horizontal:hover { background: {{toolbar_slider_handle_hover_bg}}; } +QLabel[themeRole="logFatal"] { color: {{log_fatal_hex}}; } +QLabel[themeRole="logError"] { color: {{log_error_hex}}; } +QLabel[themeRole="logWarning"] { color: {{log_warning_hex}}; } +QLabel[themeRole="logNotice"] { color: {{selected_text_hex}}; } +QLabel[themeRole="logDebug"] { color: {{log_debug_hex}}; } +QLabel[themeRole="logTrace"] { color: {{muted_text_hex}}; } +QLabel[themeRole="sectionHeader"] { font-weight: 600; margin-top: 8px; } +QLabel[severity="success"] { border-radius: 10px; padding: 4px 10px; background-color: {{severity_success_bg}}; color: {{severity_success_text_hex}}; } +QLabel[severity="error"] { border-radius: 10px; padding: 4px 10px; background-color: {{severity_error_bg}}; color: {{severity_error_text_hex}}; } +QTabWidget::pane { border: 1px solid {{tab_pane_border}}; background-color: {{tab_pane_bg}}; } +QTabBar::tab { + background-color: {{tab_bg}}; + color: {{text_hex}}; + padding: 6px 14px; + border: 1px solid {{tab_border}}; + border-bottom: none; + border-top-left-radius: 3px; + border-top-right-radius: 3px; +} +QTabBar::tab:selected { background-color: {{tab_selected_bg}}; } +QTabBar::tab:!selected { margin-top: 2px; } +QTabBar::tab:hover:!selected { background-color: {{tab_hover_bg}}; } +QListWidget { + background-color: transparent; + color: {{text_hex}}; + border: 1px solid {{list_border}}; +} +QListWidget:disabled { color: {{disabled_text_hex}}; background-color: {{list_disabled_bg}}; border-color: {{list_disabled_border}}; } +QListWidget::item:selected { background-color: {{list_selected_bg}}; color: {{selected_text_hex}}; } +QListWidget::item:hover:!selected { background-color: {{list_hover_bg}}; } +QDialog[class="settings-window"] QListWidget#settingsCategory { + background-color: {{settings_category_bg}}; + border: none; + border-radius: 10px; + padding: 6px; + outline: none; +} +QDialog[class="settings-window"] QListWidget#settingsCategory::item { + background: transparent; + border: none; + border-radius: 10px; + padding: 8px 12px; + margin: 0px; +} +QDialog[class="settings-window"] QListWidget#settingsCategory::item:selected, +QDialog[class="settings-window"] QListWidget#settingsCategory::item:selected:active, +QDialog[class="settings-window"] QListWidget#settingsCategory::item:selected:!active { + background-color: {{settings_category_selected_bg}}; + color: {{dialog_selected_text_hex}}; +} +QDialog[class="settings-window"] QListWidget#settingsCategory::item:hover:!selected { background-color: {{settings_category_hover_bg}}; } +QDialog[class="settings-window"] QTextBrowser#helpText { + background-color: {{settings_help_bg}}; + color: {{dialog_text_hex}}; + border: none; + border-radius: 10px; + padding: 3px 4px; +} +QDialog[class="settings-window"] QListWidget#modules_list { + background-color: {{modules_list_bg}}; + color: {{dialog_text_hex}}; + border: 1px solid {{modules_list_border}}; + border-radius: 6px; + padding: 4px; + outline: none; +} +QDialog[class="settings-window"] QListWidget#modules_list::item { + padding: 3px 8px; + border-radius: 4px; + margin: 0px; +} +QDialog[class="settings-window"] QListWidget#modules_list::item:hover { background-color: {{modules_list_hover_bg}}; } +QDialog[class="settings-window"] QListWidget#modules_list::item:selected { background: transparent; color: {{dialog_text_hex}}; } +QScrollBar:vertical { background: {{scrollbar_bg}}; width: 14px; margin: 0; } +QScrollBar::handle:vertical { background: {{scrollbar_handle_bg}}; min-height: 20px; border-radius: 3px; margin: 2px; } +QScrollBar::handle:vertical:hover { background: {{scrollbar_handle_hover_bg}}; } +QScrollBar::add-line:vertical, QScrollBar::sub-line:vertical { height: 0px; } +QScrollBar::add-page:vertical, QScrollBar::sub-page:vertical { background: none; } +QScrollBar:horizontal { background: {{scrollbar_bg}}; height: 14px; margin: 0; } +QScrollBar::handle:horizontal { background: {{scrollbar_handle_bg}}; min-width: 20px; border-radius: 3px; margin: 2px; } +QScrollBar::handle:horizontal:hover { background: {{scrollbar_handle_hover_bg}}; } +QScrollBar::add-line:horizontal, QScrollBar::sub-line:horizontal { width: 0px; } +QScrollBar::add-page:horizontal, QScrollBar::sub-page:horizontal { background: none; } +QAbstractScrollArea::corner { background: {{scrollbar_bg}}; } +QSlider::groove:horizontal { background: transparent; height: 4px; border-radius: 2px; } +QSlider::sub-page:horizontal { background: {{slider_subpage_bg}}; height: 4px; border-radius: 2px; } +QSlider::add-page:horizontal { background: {{slider_addpage_bg}}; height: 4px; border-radius: 2px; } +QSlider::handle:horizontal { background: {{slider_handle_bg}}; width: 14px; margin: -5px 0; border-radius: 7px; } +QSlider::handle:horizontal:hover { background: {{slider_handle_hover_bg}}; } +QSlider::groove:vertical { background: transparent; width: 4px; border-radius: 2px; } +QSlider::sub-page:vertical { background: {{slider_subpage_bg}}; width: 4px; border-radius: 2px; } +QSlider::add-page:vertical { background: {{slider_addpage_bg}}; width: 4px; border-radius: 2px; } +QSlider::handle:vertical { background: {{slider_handle_bg}}; height: 14px; margin: 0 -5px; border-radius: 7px; } +QSlider::handle:vertical:hover { background: {{slider_handle_hover_bg}}; } +QProgressBar { + background-color: {{progress_bg}}; + color: {{text_hex}}; + border: 1px solid {{progress_border}}; + text-align: center; + border-radius: 2px; +} +QProgressBar::chunk { background-color: {{progress_chunk_bg}}; border-radius: 2px; } +QSplitter::handle { background-color: {{splitter_handle_bg}}; } +QToolTip { + background-color: {{tooltip_bg}}; + color: {{text_hex}}; + border: 1px solid {{tooltip_border}}; + padding: 2px; +} +QPlainTextEdit, QTextEdit, QTextBrowser { + background-color: {{text_edit_bg}}; + color: {{text_hex}}; + border: 1px solid {{text_edit_border}}; +} +QPlainTextEdit:disabled, QTextEdit:disabled, QTextBrowser:disabled { + color: {{disabled_text_hex}}; + background-color: {{text_edit_disabled_bg}}; + border-color: {{text_edit_disabled_border}}; +} +QDialogButtonBox QPushButton { min-width: 80px; } diff --git a/vita3k/gui-qt/resources/themes/waves/shell_background.svg b/vita3k/gui-qt/resources/themes/waves/shell_background.svg new file mode 100644 index 000000000..ab92eb40a --- /dev/null +++ b/vita3k/gui-qt/resources/themes/waves/shell_background.svg @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/vita3k/gui-qt/resources/themes/waves/theme.qss b/vita3k/gui-qt/resources/themes/waves/theme.qss new file mode 100644 index 000000000..b1100eb80 --- /dev/null +++ b/vita3k/gui-qt/resources/themes/waves/theme.qss @@ -0,0 +1,539 @@ +/*!PALETTE + * dark: dark + * window: #102030 + * window-text: #f2f3f6 + * base: #112334 + * alternate-base: #173047 + * tooltip-base: #102030 + * tooltip-text: #f2f3f6 + * text: #f2f3f6 + * button: #173047 + * button-text: #f2f3f6 + * bright-text: #ffffff + * link: #8cd4ff + * highlight: #56b2ff + * inactive-highlight: #56b2ff + * disabled-highlight: #3356b2ff + * highlight-text: #061018 + * inactive-highlight-text: #061018 + * disabled-highlight-text: #ced7de + * placeholder-text: #a9bbc8 + * disabled-button-text: #788791 + * disabled-window-text: #788791 + * disabled-text: #788791 + */ +QWidget { color: #edf3f8; background-color: #102030; } +QWidget:disabled { color: #788791; } +QMainWindow#main_window { + border-image: url("shell_background.svg") 0 0 0 0 stretch stretch; + background-color: #102030; +} +QWidget#mw_frame, +QMainWindow#mw_dock_host { + background: transparent; +} +QMenuBar { + background-color: rgba(11, 20, 31, 0.42); + color: #eef4f8; + border-bottom: 1px solid rgba(255, 255, 255, 0.10); +} +QMenuBar::item { background: transparent; padding: 4px 8px; } +QMenuBar::item:selected { background: rgba(255, 255, 255, 0.11); } +QMenu { + background-color: rgba(10, 19, 30, 0.95); + color: #eef4f8; + border: 1px solid rgba(255, 255, 255, 0.16); +} +QMenu::item:selected { background-color: rgba(86, 178, 255, 0.28); color: #ffffff; } +QMenu::item:disabled { color: #7f8d97; } +QMenu::separator { height: 1px; background: rgba(255, 255, 255, 0.12); margin: 4px 8px; } +QToolBar#mw_toolbar { + background-color: rgba(8, 17, 27, 0.45); + border: none; + border-top: 1px solid rgba(255, 255, 255, 0.12); + border-bottom: 1px solid rgba(255, 255, 255, 0.10); +} +QToolBar#mw_toolbar::separator { + background-color: rgba(255, 255, 255, 0.16); + width: 0.125em; + margin-top: 0.250em; + margin-bottom: 0.250em; +} +QToolBar#mw_toolbar > QWidget { background: transparent; } +QToolButton { + color: #eef4f8; + background: transparent; + border: 1px solid transparent; + padding: 3px; +} +QToolButton:hover { background-color: rgba(255, 255, 255, 0.10); border: 1px solid rgba(255, 255, 255, 0.12); } +QToolButton:pressed { background-color: rgba(86, 178, 255, 0.18); } +QToolButton:disabled { color: #7f8d97; } +QToolButton#welcome_firmware_button { + background-color: rgba(9, 18, 29, 0.68); + color: #eef4f8; + border: 1px solid rgba(255, 255, 255, 0.14); + border-radius: 2px; + padding: 4px 32px 4px 12px; + text-align: left; +} +QToolButton#welcome_firmware_button:hover { background-color: rgba(255, 255, 255, 0.10); } +QToolButton#welcome_firmware_button:pressed { background-color: rgba(86, 178, 255, 0.18); } +QToolButton#welcome_firmware_button::menu-button { + subcontrol-origin: padding; + subcontrol-position: top right; + width: 24px; + border-left: 1px solid rgba(255, 255, 255, 0.14); + background: rgba(255, 255, 255, 0.05); +} +QLabel[themeRole="toolbarIconTint"] { color: #f2f3f6; } +QLabel[themeRole="mutedText"] { color: #9eb2c1; } +QPushButton { + background-color: rgba(10, 20, 31, 0.66); + color: #eef4f8; + border: 1px solid rgba(255, 255, 255, 0.14); + border-radius: 2px; + padding: 4px 16px; + min-height: 1.2em; +} +QPushButton:hover { background-color: rgba(255, 255, 255, 0.10); border-color: rgba(255, 255, 255, 0.18); } +QPushButton:pressed { background-color: rgba(86, 178, 255, 0.20); } +QPushButton:disabled { color: #7f8d97; background-color: rgba(8, 15, 24, 0.56); border-color: rgba(255, 255, 255, 0.08); } +QPushButton:default { border-color: rgba(140, 212, 255, 0.60); } +QDialog#vita_themes_dialog { color: #eef4f8; } +QDialog#vita_themes_dialog QLabel, +QDialog#vita_themes_dialog QCheckBox, +QDialog#vita_themes_dialog QRadioButton { color: #eef4f8; background: transparent; } +QDialog#vita_themes_dialog QLabel[themeRole="mutedText"] { color: #9eb2c1; } +QDialog#vita_themes_dialog QWidget[themeRole="backgroundCell"] { background: transparent; } +QDialog#vita_themes_dialog QCheckBox[themeRole="backgroundCellControl"], +QDialog#vita_themes_dialog QRadioButton[themeRole="backgroundCellControl"] { spacing: 0px; padding: 0px; margin: 0px; } +QDialog#vita_themes_dialog QListWidget#vitaThemesList { + background-color: rgba(10, 19, 30, 0.96); + color: #eef4f8; + border: none; + border-radius: 10px; + padding: 6px; + outline: none; +} +QDialog#vita_themes_dialog QListWidget#vitaThemesList::item { + background: transparent; + border: none; + border-radius: 10px; + padding: 8px 12px; + margin: 0px; +} +QDialog#vita_themes_dialog QListWidget#vitaThemesList::item:selected, +QDialog#vita_themes_dialog QListWidget#vitaThemesList::item:selected:active, +QDialog#vita_themes_dialog QListWidget#vitaThemesList::item:selected:!active { background-color: rgba(86, 178, 255, 0.28); color: #ffffff; } +QDialog#vita_themes_dialog QListWidget#vitaThemesList::item:hover:!selected { background-color: rgba(255, 255, 255, 0.06); } +QDialog#vita_themes_dialog QTreeWidget#vitaThemesBackgroundList { + background-color: rgba(9, 18, 29, 0.72); + color: #eef4f8; + border: 1px solid rgba(255, 255, 255, 0.14); + border-radius: 8px; + padding: 2px; + outline: none; +} +QDialog#vita_themes_dialog QTreeWidget#vitaThemesBackgroundList::item { + padding: 5px 8px; + border-radius: 0px; + margin: 0px; +} +QDialog#vita_themes_dialog QTreeWidget#vitaThemesBackgroundList::item:selected, +QDialog#vita_themes_dialog QTreeWidget#vitaThemesBackgroundList::item:selected:active, +QDialog#vita_themes_dialog QTreeWidget#vitaThemesBackgroundList::item:selected:!active { background-color: rgba(86, 178, 255, 0.28); color: #ffffff; } +QDialog#vita_themes_dialog QTreeWidget#vitaThemesBackgroundList::item:hover:!selected { background-color: rgba(255, 255, 255, 0.08); } +QDialog#vita_themes_dialog QTextBrowser#vitaThemesHelpText { + background-color: rgba(10, 19, 30, 0.96); + color: #eef4f8; + border: none; + border-radius: 10px; + padding: 3px 4px; +} +QDialog#vita_themes_dialog QScrollArea { background: transparent; border: none; } +QDialog#vita_themes_dialog QCheckBox::indicator { + width: 12px; + height: 12px; + padding: 0px; + margin: 0px; + border-radius: 3px; + border: 1px solid rgba(255, 255, 255, 0.20); + background-color: rgba(9, 18, 29, 0.72); +} +QDialog#vita_themes_dialog QCheckBox::indicator:hover { border-color: rgba(140, 212, 255, 0.54); background-color: rgba(255, 255, 255, 0.08); } +QDialog#vita_themes_dialog QCheckBox::indicator:checked, +QDialog#vita_themes_dialog QCheckBox::indicator:indeterminate { + background-color: rgba(140, 212, 255, 0.86); + border-color: rgba(140, 212, 255, 0.92); + image: url(":/icons/check_dark.svg"); +} +QDialog#vita_themes_dialog QCheckBox::indicator:disabled { border-color: rgba(255, 255, 255, 0.10); background-color: rgba(8, 15, 24, 0.60); } +QDialog#vita_themes_dialog QCheckBox::indicator:checked:disabled, +QDialog#vita_themes_dialog QCheckBox::indicator:indeterminate:disabled { + background-color: rgba(140, 212, 255, 0.32); + border-color: rgba(140, 212, 255, 0.40); +} +QDialog#vita_themes_dialog QRadioButton::indicator { + width: 12px; + height: 12px; + padding: 0px; + margin: 0px; + border-radius: 6px; + border: 1px solid rgba(255, 255, 255, 0.20); + background-color: rgba(9, 18, 29, 0.72); +} +QDialog#vita_themes_dialog QRadioButton::indicator:hover { border-color: rgba(140, 212, 255, 0.54); background-color: rgba(255, 255, 255, 0.08); } +QDialog#vita_themes_dialog QRadioButton::indicator:checked { + background-color: rgba(140, 212, 255, 0.86); + border-color: rgba(140, 212, 255, 0.92); + image: url(":/icons/radio_dot_dark.svg"); +} +QDialog#vita_themes_dialog QRadioButton::indicator:disabled { border-color: rgba(255, 255, 255, 0.10); background-color: rgba(8, 15, 24, 0.60); } +QDialog#vita_themes_dialog QRadioButton::indicator:checked:disabled { + background-color: rgba(140, 212, 255, 0.32); + border-color: rgba(140, 212, 255, 0.40); +} +QDialog#controls_dialog QToolButton[modeButton="true"] { + background: transparent; + border: none; + border-radius: 10px; + padding: 6px 12px; +} +QDialog#controls_dialog QToolButton[modeButton="true"]:hover { background-color: rgba(255, 255, 255, 0.08); } +QDialog#controls_dialog QToolButton[modeButton="true"]:checked { background-color: rgba(255, 255, 255, 0.14); } +QDialog#controls_dialog QToolButton[modeButton="true"]:pressed { background-color: rgba(255, 255, 255, 0.20); } +QDialog#controls_dialog QPushButton { + background-color: rgba(9, 18, 29, 0.66); + color: #eef4f8; + border: none; + border-radius: 6px; + padding: 6px 12px; + outline: none; +} +QDialog#controls_dialog QPushButton:hover { background-color: rgba(255, 255, 255, 0.10); border: none; } +QDialog#controls_dialog QPushButton:pressed { background-color: rgba(86, 178, 255, 0.20); border: none; } +QDialog#controls_dialog QPushButton:disabled { color: #7f8d97; background-color: rgba(8, 15, 24, 0.56); border: none; } +QDialog#controls_dialog QPushButton[bindButton="true"] { + background-color: rgba(9, 18, 29, 0.72); + border-radius: 6px; + padding: 6px 12px; +} +QDialog#controls_dialog QPushButton[bindButton="true"]:hover { background-color: rgba(255, 255, 255, 0.08); } +QDialog#controls_dialog QPushButton[bindButton="true"]:pressed { background-color: rgba(86, 178, 255, 0.18); } +QDialog#controls_dialog QPushButton[bindButton="true"][captureActive="true"] { + background-color: rgba(140, 212, 255, 0.22); + color: #ffffff; +} +QDialog#controls_dialog QPushButton[bindButton="true"][captureActive="true"]:hover { background-color: rgba(140, 212, 255, 0.28); } +QDialog#controls_dialog QPushButton[bindButton="true"][captureActive="true"]:pressed { background-color: rgba(140, 212, 255, 0.34); } +QStatusBar#emulator_status_bar { + background-color: rgba(8, 17, 27, 0.52); + color: #eef4f8; + border-top: 1px solid rgba(255, 255, 255, 0.10); +} +QStatusBar#emulator_status_bar::item { border: none; } +QStatusBar#emulator_status_bar QPushButton#status_button { + border: 1px solid transparent; + padding: 2px 8px; + font-weight: bold; + font-size: 11px; + background: transparent; + border-radius: 6px; + color: #eef4f8; +} +QStatusBar#emulator_status_bar QPushButton#status_button:hover { border: 1px solid rgba(255, 255, 255, 0.14); background: rgba(255, 255, 255, 0.06); } +QStatusBar#emulator_status_bar QPushButton#status_button:pressed { border: 1px solid rgba(255, 255, 255, 0.20); background: rgba(86, 178, 255, 0.18); } +QStatusBar#emulator_status_bar QPushButton#status_button[accent="renderer_vulkan"] { color: #ff8f7a; } +QStatusBar#emulator_status_bar QPushButton#status_button[accent="renderer_opengl"] { color: #7fb2ff; } +QStatusBar#emulator_status_bar QPushButton#status_button[accent="high"] { color: #ffd278; } +QStatusBar#emulator_status_bar QPushButton#status_button[accent="standard"] { color: #b8c5cf; } +QStatusBar#emulator_status_bar QPushButton#status_button[accent="neutral"] { color: #d6e0e7; } +QStatusBar#emulator_status_bar QPushButton#status_button[accent="audio"] { color: #8ef0a2; } +QStatusBar#emulator_status_bar QPushButton#status_button[accent="muted"] { color: #7f8d97; } +QStatusBar#emulator_status_bar QPushButton#status_button[accent="update"] { color: #a3f0b5; } +QLineEdit { + background-color: rgba(9, 18, 29, 0.72); + color: #eef4f8; + border: 1px solid rgba(255, 255, 255, 0.14); + border-radius: 2px; + padding: 2px 4px; + selection-background-color: rgba(140, 212, 255, 0.50); + selection-color: #061018; +} +QLineEdit:focus { border-color: rgba(140, 212, 255, 0.65); } +QLineEdit:disabled { color: #788791; background-color: rgba(8, 15, 24, 0.60); border-color: rgba(255, 255, 255, 0.08); } +QLineEdit#mw_searchbar { + background: transparent; + border: 1px solid rgba(255, 255, 255, 0.14); + border-radius: 10px; + color: #eef4f8; + padding: 5px 12px; + selection-background-color: rgba(140, 212, 255, 0.42); + selection-color: #061018; +} +QLineEdit#mw_searchbar:focus { + background: rgba(255, 255, 255, 0.05); + border-color: rgba(140, 212, 255, 0.42); +} +QComboBox { + background-color: rgba(9, 18, 29, 0.72); + color: #eef4f8; + border: 1px solid rgba(255, 255, 255, 0.14); + border-radius: 2px; + padding: 2px 28px 2px 6px; +} +QComboBox:hover { border-color: rgba(255, 255, 255, 0.20); } +QComboBox:disabled { color: #788791; background-color: rgba(8, 15, 24, 0.60); border-color: rgba(255, 255, 255, 0.08); } +QComboBox::drop-down { subcontrol-origin: padding; subcontrol-position: top right; width: 24px; border: none; background: transparent; } +QComboBox::down-arrow { image: url(":/icons/chevron_down_light.svg"); width: 10px; height: 6px; } +QComboBox QAbstractItemView { + background-color: rgba(10, 19, 30, 0.96); + color: #eef4f8; + border: 1px solid rgba(255, 255, 255, 0.16); + selection-background-color: rgba(86, 178, 255, 0.28); + selection-color: #ffffff; +} +QSpinBox, QDoubleSpinBox { + background-color: rgba(9, 18, 29, 0.72); + color: #eef4f8; + border: 1px solid rgba(255, 255, 255, 0.14); + border-radius: 2px; + padding: 2px 24px 2px 6px; + selection-background-color: rgba(140, 212, 255, 0.50); + selection-color: #061018; +} +QSpinBox:disabled, QDoubleSpinBox:disabled { color: #788791; background-color: rgba(8, 15, 24, 0.60); border-color: rgba(255, 255, 255, 0.08); } +QSpinBox::up-button, QDoubleSpinBox::up-button { subcontrol-origin: border; subcontrol-position: top right; width: 18px; border: none; background: transparent; } +QSpinBox::down-button, QDoubleSpinBox::down-button { subcontrol-origin: border; subcontrol-position: bottom right; width: 18px; border: none; background: transparent; } +QSpinBox::up-arrow, QDoubleSpinBox::up-arrow { image: url(":/icons/chevron_up_light.svg"); width: 10px; height: 6px; } +QSpinBox::down-arrow, QDoubleSpinBox::down-arrow { image: url(":/icons/chevron_down_light.svg"); width: 10px; height: 6px; } +QCheckBox { color: #eef4f8; spacing: 6px; } +QCheckBox:disabled { color: #788791; } +QRadioButton { color: #eef4f8; spacing: 6px; } +QRadioButton:disabled { color: #788791; } +QGroupBox { + color: #eef4f8; + border: 1px solid rgba(255, 255, 255, 0.14); + border-radius: 4px; + margin-top: 0.5em; + padding-top: 0.6em; +} +QGroupBox:disabled { color: #7f8d97; border-color: rgba(255, 255, 255, 0.08); } +QGroupBox::title { subcontrol-origin: margin; subcontrol-position: top left; padding: 0 4px; color: #eef4f8; } +QLabel { color: #eef4f8; background: transparent; } +QLabel:disabled { color: #788791; } +QDockWidget#apps_list, +QDockWidget#logger { + background: transparent; + color: #ffffff; +} +QDockWidget#apps_list::title, +QDockWidget#logger::title { + background: rgba(8, 17, 27, 0.62); + border: none; + padding-top: 0.2em; + padding-left: 0.2em; +} +QDockWidget#apps_list::close-button, +QDockWidget#apps_list::float-button, +QDockWidget#logger::close-button, +QDockWidget#logger::float-button { + background-color: rgba(8, 17, 27, 0.62); + border: none; +} +QDockWidget#apps_list::close-button:hover, +QDockWidget#apps_list::float-button:hover, +QDockWidget#logger::close-button:hover, +QDockWidget#logger::float-button:hover { + background-color: rgba(255, 255, 255, 0.12); +} +QDockWidget#apps_list[floating="true"], +QDockWidget#logger[floating="true"] { + border-image: url("shell_background.svg") 0 0 0 0 stretch stretch; + background-color: #102030; +} +QWidget#apps_list_frame, +QWidget#log_surface { + background-color: rgba(8, 17, 27, 0.38); +} +QTableWidget#apps_list_table, +QWidget#apps_list_viewport, +QPlainTextEdit#log_frame, +QWidget#log_frame_viewport { + background: transparent; +} +QTableWidget, QTableView { + alternate-background-color: rgba(255, 255, 255, 0.04); + background-color: rgba(9, 18, 29, 0.54); + border: none; + color: #eef4f8; + gridline-color: rgba(255, 255, 255, 0.08); +} +QTableWidget:disabled, QTableView:disabled { color: #788791; background-color: rgba(8, 15, 24, 0.56); } +QTableCornerButton::section { background-color: rgba(12, 25, 38, 0.88); border: 1px solid rgba(255, 255, 255, 0.10); } +QTableView::item { border-left: 0.063em solid transparent; border-right: 0.063em solid transparent; padding-left: 0.313em; } +QTableView::item:selected { background-color: rgba(86, 178, 255, 0.20); color: #ffffff; } +QTableWidget#apps_list_table::item:selected, QTableView#apps_list_table::item:selected { + background-color: rgba(86, 178, 255, 0.22); + color: #f7fbfe; + border-left: 0.063em solid transparent; + border-right: 0.063em solid transparent; +} +QTableView#apps_list_table QHeaderView::section, QTableWidget#apps_list_table QHeaderView::section { padding-top: 0; padding-bottom: 0; } +QHeaderView::section { + background-color: rgba(12, 25, 38, 0.84); + color: #ffffff; + padding-left: .5em; + padding-right: .5em; + padding-top: .4em; + padding-bottom: -.1em; + border: 0.063em solid rgba(255, 255, 255, 0.06); +} +QHeaderView::section:hover { background: rgba(255, 255, 255, 0.10); } +QPlainTextEdit#log_frame { + color: #f7fbfe; + border: none; +} +QWidget#log_search_container { + background-color: rgba(8, 17, 27, 0.82); + border: 1px solid rgba(255, 255, 255, 0.16); + border-radius: 4px; +} +QWidget#toolbar_icon_size_container { + background: transparent; + border: none; +} +QSlider#toolbar_icon_size_slider { + background: transparent; + margin: 0; + min-height: 20px; +} +QSlider#toolbar_icon_size_slider::groove:horizontal { + background: rgba(255, 255, 255, 0.18); + border: none; + height: 6px; + border-radius: 3px; +} +QSlider#toolbar_icon_size_slider::sub-page:horizontal { + background: rgba(242, 243, 246, 0.92); + border: none; + height: 6px; + border-radius: 3px; +} +QSlider#toolbar_icon_size_slider::add-page:horizontal { + background: rgba(255, 255, 255, 0.16); + border: none; + height: 6px; + border-radius: 3px; +} +QSlider#toolbar_icon_size_slider::handle:horizontal { + background: #f2f3f6; + border: none; + width: 14px; + height: 14px; + margin: -4px 0; + border-radius: 7px; +} +QSlider#toolbar_icon_size_slider::handle:horizontal:hover { background: #ffffff; } +QLabel[themeRole="logFatal"] { color: #ff9ff3; } +QLabel[themeRole="logError"] { color: #ff9a7f; } +QLabel[themeRole="logWarning"] { color: #ffd37f; } +QLabel[themeRole="logNotice"] { color: #f7fbfe; } +QLabel[themeRole="logDebug"] { color: #8cd4ff; } +QLabel[themeRole="logTrace"] { color: #bdd0de; } +QLabel[themeRole="sectionHeader"] { font-weight: 600; margin-top: 8px; } +QLabel[severity="success"] { border-radius: 10px; padding: 4px 10px; background-color: rgba(71, 125, 92, 0.58); color: #d3ffe0; } +QLabel[severity="error"] { border-radius: 10px; padding: 4px 10px; background-color: rgba(134, 72, 60, 0.62); color: #ffd5cd; } +QTabWidget::pane { border: 1px solid rgba(255, 255, 255, 0.12); background-color: rgba(8, 17, 27, 0.44); } +QTabBar::tab { + background-color: rgba(9, 18, 29, 0.62); + color: #eef4f8; + padding: 6px 14px; + border: 1px solid rgba(255, 255, 255, 0.12); + border-bottom: none; + border-top-left-radius: 3px; + border-top-right-radius: 3px; +} +QTabBar::tab:selected { background-color: rgba(255, 255, 255, 0.10); } +QTabBar::tab:!selected { margin-top: 2px; } +QTabBar::tab:hover:!selected { background-color: rgba(255, 255, 255, 0.08); } +QListWidget { + background-color: rgba(9, 18, 29, 0.62); + color: #eef4f8; + border: 1px solid rgba(255, 255, 255, 0.12); +} +QListWidget:disabled { color: #788791; background-color: rgba(8, 15, 24, 0.56); border-color: rgba(255, 255, 255, 0.08); } +QListWidget::item:selected { background-color: rgba(86, 178, 255, 0.26); color: #ffffff; } +QListWidget::item:hover:!selected { background-color: rgba(255, 255, 255, 0.06); } +QDialog[class="settings-window"] QListWidget#settingsCategory { + background-color: rgba(9, 18, 29, 0.62); + border: none; + border-radius: 10px; + padding: 6px; + outline: none; +} +QDialog[class="settings-window"] QListWidget#settingsCategory::item { + background: transparent; + border: none; + border-radius: 10px; + padding: 8px 12px; + margin: 0px; +} +QDialog[class="settings-window"] QListWidget#settingsCategory::item:selected, +QDialog[class="settings-window"] QListWidget#settingsCategory::item:selected:active, +QDialog[class="settings-window"] QListWidget#settingsCategory::item:selected:!active { + background-color: rgba(86, 178, 255, 0.26); + color: #ffffff; +} +QDialog[class="settings-window"] QListWidget#settingsCategory::item:hover:!selected { background-color: rgba(255, 255, 255, 0.06); } +QDialog[class="settings-window"] QTextBrowser#helpText { + background-color: rgba(9, 18, 29, 0.62); + color: #eef4f8; + border: none; + border-radius: 10px; + padding: 3px 4px; +} +QScrollBar:vertical { background: rgba(9, 18, 29, 0.52); width: 14px; margin: 0; } +QScrollBar::handle:vertical { background: rgba(255, 255, 255, 0.20); min-height: 20px; border-radius: 3px; margin: 2px; } +QScrollBar::handle:vertical:hover { background: rgba(255, 255, 255, 0.28); } +QScrollBar::add-line:vertical, QScrollBar::sub-line:vertical { height: 0px; } +QScrollBar::add-page:vertical, QScrollBar::sub-page:vertical { background: none; } +QScrollBar:horizontal { background: rgba(9, 18, 29, 0.52); height: 14px; margin: 0; } +QScrollBar::handle:horizontal { background: rgba(255, 255, 255, 0.20); min-width: 20px; border-radius: 3px; margin: 2px; } +QScrollBar::handle:horizontal:hover { background: rgba(255, 255, 255, 0.28); } +QScrollBar::add-line:horizontal, QScrollBar::sub-line:horizontal { width: 0px; } +QScrollBar::add-page:horizontal, QScrollBar::sub-page:horizontal { background: none; } +QAbstractScrollArea::corner { background: rgba(9, 18, 29, 0.52); } +QSlider::groove:horizontal { background: transparent; height: 4px; border-radius: 2px; } +QSlider::sub-page:horizontal { background: rgba(242, 243, 246, 0.88); height: 4px; border-radius: 2px; } +QSlider::add-page:horizontal { background: rgba(255, 255, 255, 0.20); height: 4px; border-radius: 2px; } +QSlider::handle:horizontal { background: #ced7de; width: 14px; margin: -5px 0; border-radius: 7px; } +QSlider::handle:horizontal:hover { background: #ffffff; } +QSlider::groove:vertical { background: transparent; width: 4px; border-radius: 2px; } +QSlider::sub-page:vertical { background: rgba(242, 243, 246, 0.88); width: 4px; border-radius: 2px; } +QSlider::add-page:vertical { background: rgba(255, 255, 255, 0.20); width: 4px; border-radius: 2px; } +QSlider::handle:vertical { background: #ced7de; height: 14px; margin: 0 -5px; border-radius: 7px; } +QSlider::handle:vertical:hover { background: #ffffff; } +QProgressBar { + background-color: rgba(9, 18, 29, 0.70); + color: #eef4f8; + border: 1px solid rgba(255, 255, 255, 0.12); + text-align: center; + border-radius: 2px; +} +QProgressBar::chunk { background-color: #8cd4ff; border-radius: 2px; } +QSplitter::handle { background-color: rgba(255, 255, 255, 0.14); } +QToolTip { + background-color: rgba(10, 19, 30, 0.96); + color: #eef4f8; + border: 1px solid rgba(255, 255, 255, 0.16); + padding: 2px; +} +QPlainTextEdit, QTextEdit, QTextBrowser { + background-color: rgba(9, 18, 29, 0.62); + color: #eef4f8; + border: 1px solid rgba(255, 255, 255, 0.12); +} +QPlainTextEdit:disabled, QTextEdit:disabled, QTextBrowser:disabled { color: #788791; background-color: rgba(8, 15, 24, 0.56); border-color: rgba(255, 255, 255, 0.08); } +QDialogButtonBox QPushButton { min-width: 80px; } diff --git a/vita3k/gui-qt/src/apps_list.cpp b/vita3k/gui-qt/src/apps_list.cpp index 835ea9363..1f7371935 100644 --- a/vita3k/gui-qt/src/apps_list.cpp +++ b/vita3k/gui-qt/src/apps_list.cpp @@ -18,6 +18,7 @@ #include #include #include +#include #include #include @@ -33,15 +34,17 @@ AppsList::AppsList(EmuEnvState &emuenv, QWidget *parent) : custom_dock_widget(tr("App Library"), parent) , m_emuenv(emuenv) { - setObjectName(QStringLiteral("AppsList")); + setObjectName(QStringLiteral("apps_list")); setFeatures(QDockWidget::DockWidgetMovable | QDockWidget::DockWidgetFloatable); - auto *central = new QWidget(this); + auto *central = new ThemeSurface(this); + central->setObjectName(QStringLiteral("apps_list_frame")); auto *vbox = new QVBoxLayout(central); vbox->setContentsMargins(0, 4, 0, 0); vbox->setSpacing(4); m_table = new AppsListTable(central); + m_table->viewport()->setObjectName(QStringLiteral("apps_list_viewport")); vbox->addWidget(m_table); setWidget(central); diff --git a/vita3k/gui-qt/src/archive_install_dialog.cpp b/vita3k/gui-qt/src/archive_install_dialog.cpp index 4e65e7629..1b49a8ba0 100644 --- a/vita3k/gui-qt/src/archive_install_dialog.cpp +++ b/vita3k/gui-qt/src/archive_install_dialog.cpp @@ -72,10 +72,7 @@ QLabel *make_muted_label(const QString &text, QWidget *parent, Qt::Alignment ali auto *label = new QLabel(text, parent); label->setAlignment(alignment); label->setWordWrap(true); - const QColor color = gui::utils::get_label_color(QColor(0x5b, 0x5b, 0x5b), QColor(0xa8, 0xa8, 0xa8)); - QPalette palette = label->palette(); - palette.setColor(QPalette::WindowText, color); - label->setPalette(palette); + label->setProperty("themeRole", QStringLiteral("mutedText")); return label; } @@ -109,13 +106,7 @@ QFrame *make_result_row(const ArchiveInstallResult &result, QWidget *parent) { status->setAlignment(Qt::AlignCenter); status->setMinimumWidth(84); status->setMargin(6); - const QColor status_bg = result.success - ? gui::utils::get_label_color(QColor(0xd9, 0xf2, 0xdd), QColor(0x25, 0x5a, 0x2e)) - : gui::utils::get_label_color(QColor(0xf7, 0xdc, 0xd9), QColor(0x6d, 0x2a, 0x25)); - const QColor status_fg = result.success - ? gui::utils::get_label_color(QColor(0x1f, 0x7a, 0x1f), QColor(0xb9, 0xf5, 0xbf)) - : gui::utils::get_label_color(QColor(0xa3, 0x2a, 0x1c), QColor(0xff, 0xb1, 0xaa)); - status->setStyleSheet(QStringLiteral("border-radius: 10px; padding: 4px 10px; background-color: %1; color: %2;").arg(status_bg.name(), status_fg.name())); + status->setProperty("severity", result.success ? QStringLiteral("success") : QStringLiteral("error")); layout->addWidget(status, 0, Qt::AlignTop); return row; diff --git a/vita3k/gui-qt/src/controls_dialog.cpp b/vita3k/gui-qt/src/controls_dialog.cpp index 416305c6d..d7ac208f6 100644 --- a/vita3k/gui-qt/src/controls_dialog.cpp +++ b/vita3k/gui-qt/src/controls_dialog.cpp @@ -58,7 +58,6 @@ #include #include -#include namespace { @@ -67,22 +66,6 @@ constexpr int STACK_PAGE_MIN_WIDTH = 980; constexpr int CAPTURE_TIMEOUT_MS = 5'000; constexpr int16_t AXIS_DEADZONE = 16000; -QColor mix_colors(const QColor &a, const QColor &b, float amount_b) { - const float amount_a = 1.0f - amount_b; - return QColor( - std::lround(a.red() * amount_a + b.red() * amount_b), - std::lround(a.green() * amount_a + b.green() * amount_b), - std::lround(a.blue() * amount_a + b.blue() * amount_b), - std::lround(a.alpha() * amount_a + b.alpha() * amount_b)); -} - -QIcon theme_icon(QWidget *widget, const QString &theme_name, QStyle::StandardPixmap fallback) { - QIcon icon = QIcon::fromTheme(theme_name); - if (icon.isNull() && widget) - icon = widget->style()->standardIcon(fallback); - return icon; -} - QPixmap render_vita_svg(const QColor &color) { QSvgRenderer renderer(QStringLiteral(":/icons/PSV_Layout.svg")); QPixmap px(renderer.defaultSize() * 4); @@ -112,51 +95,22 @@ QLabel *make_wrapped_label(const QString &text, QWidget *parent = nullptr) { return label; } -QToolButton *make_header_tool_button(QWidget *parent, const QString &text, const QString &theme_name, - QStyle::StandardPixmap fallback) { - Q_UNUSED(theme_name); - Q_UNUSED(fallback); +QToolButton *make_header_tool_button(QWidget *parent, const QString &text) { auto *button = new QToolButton(parent); button->setText(text); button->setCheckable(true); button->setToolButtonStyle(Qt::ToolButtonTextOnly); button->setAutoRaise(true); + button->setProperty("modeButton", true); return button; } -void refresh_widget_style(QWidget *widget) { - if (!widget) - return; - widget->style()->unpolish(widget); - widget->style()->polish(widget); - widget->update(); -} - -void style_mode_button(QToolButton *button) { - if (!button) - return; - const QPalette pal = button->palette(); - const QColor base = pal.color(QPalette::Base); - const QColor hover = mix_colors(base, pal.color(QPalette::Highlight), 0.10f); - const QColor checked = mix_colors(base, pal.color(QPalette::Highlight), 0.18f); - button->setStyleSheet(QStringLiteral( - "QToolButton {" - " background: transparent;" - " border: none;" - " border-radius: 10px;" - " padding: 6px 12px;" - " }" - "QToolButton:hover { background-color: %1; }" - "QToolButton:checked { background-color: %2; }") - .arg(hover.name(QColor::HexArgb), checked.name(QColor::HexArgb))); -} - void style_binding_button(QPushButton *button, bool capturing = false) { if (!button) return; button->setProperty("bindButton", true); button->setProperty("captureActive", capturing); - refresh_widget_style(button); + gui::utils::refresh_theme_state(button); } QPushButton *make_bind_button() { @@ -375,6 +329,7 @@ ControlsDialog::ControlsDialog(EmuEnvState &emuenv, , m_gui_settings(std::move(gui_settings)) , m_ui(std::make_unique()) { m_ui->setupUi(this); + setObjectName(QStringLiteral("controls_dialog")); setWindowTitle(tr("Controls")); setWindowModality(Qt::NonModal); setWindowFlag(Qt::Window, true); @@ -425,6 +380,13 @@ ControlsDialog::~ControlsDialog() { cancel_controller_capture(); } +void ControlsDialog::changeEvent(QEvent *event) { + if (event->type() == QEvent::PaletteChange || event->type() == QEvent::StyleChange) + refresh_controller_art(); + + QDialog::changeEvent(event); +} + void ControlsDialog::closeEvent(QCloseEvent *event) { if (m_gui_settings) { m_gui_settings->set_value(gui::cd_geometry, saveGeometry(), false); @@ -542,12 +504,8 @@ void ControlsDialog::build_keyboard_page() { header_layout->addWidget(m_keyboard_page.lbl_summary, 1); auto *view_layout = new QHBoxLayout(); - m_keyboard_page.btn_primary = make_header_tool_button(header_group, tr("Primary"), - QStringLiteral("input-keyboard"), QStyle::SP_ArrowRight); - m_keyboard_page.btn_alt = make_header_tool_button(header_group, tr("Alternate"), - QStringLiteral("input-keyboard-shortcut"), QStyle::SP_FileDialogDetailedView); - style_mode_button(m_keyboard_page.btn_primary); - style_mode_button(m_keyboard_page.btn_alt); + m_keyboard_page.btn_primary = make_header_tool_button(header_group, tr("Primary")); + m_keyboard_page.btn_alt = make_header_tool_button(header_group, tr("Alternate")); view_layout->addWidget(m_keyboard_page.btn_primary); view_layout->addWidget(m_keyboard_page.btn_alt); header_layout->addLayout(view_layout); @@ -561,7 +519,7 @@ void ControlsDialog::build_keyboard_page() { m_keyboard_page.stack = new QStackedWidget(m_keyboard_page.page); root_layout->addWidget(m_keyboard_page.stack, 1); - const QPixmap vita_pixmap = render_vita_svg(gui::utils::get_foreground_color(this)); + const QPixmap vita_pixmap = render_vita_svg(gui::utils::foreground_color(this)); { QWidget *content_widget = nullptr; @@ -705,10 +663,8 @@ void ControlsDialog::build_controller_page(int index) { header_layout->addWidget(t.lbl_summary, 1); auto *view_layout = new QHBoxLayout(); - t.btn_bindings = make_header_tool_button(header_group, tr("Bindings"), QStringLiteral("controller-digital-line"), QStyle::SP_ArrowRight); - t.btn_settings = make_header_tool_button(header_group, tr("Settings"), QStringLiteral("settings-3-line"), QStyle::SP_FileDialogDetailedView); - style_mode_button(t.btn_bindings); - style_mode_button(t.btn_settings); + t.btn_bindings = make_header_tool_button(header_group, tr("Bindings")); + t.btn_settings = make_header_tool_button(header_group, tr("Settings")); view_layout->addWidget(t.btn_bindings); view_layout->addWidget(t.btn_settings); header_layout->addLayout(view_layout); @@ -750,7 +706,7 @@ void ControlsDialog::build_controller_page(int index) { top_grid->addWidget(make_button_group(tr("R1"), t.btn_r1), 0, 3); center_column->addLayout(top_grid); - t.l_controller_image = make_vita_image_label(this, render_vita_svg(gui::utils::get_foreground_color(this))); + t.l_controller_image = make_vita_image_label(this, render_vita_svg(gui::utils::foreground_color(this))); center_column->addWidget(make_centered_widget(t.l_controller_image)); center_column->addStretch(); @@ -1190,6 +1146,18 @@ void ControlsDialog::refresh_controller_page_labels(int index) { } } +void ControlsDialog::refresh_controller_art() { + const QPixmap vita_pixmap = render_vita_svg(gui::utils::foreground_color(this)); + + if (m_keyboard_page.l_controller_image) + m_keyboard_page.l_controller_image->setPixmap(vita_pixmap); + + for (auto &tab : m_ctrl_tabs) { + if (tab.l_controller_image) + tab.l_controller_image->setPixmap(vita_pixmap); + } +} + void ControlsDialog::set_controller_page_enabled(int index, bool enabled) { auto &tab = m_ctrl_tabs[index]; for (auto &binding : tab.bindings) diff --git a/vita3k/gui-qt/src/debug_widget.cpp b/vita3k/gui-qt/src/debug_widget.cpp index 58f223f02..bb69e9873 100644 --- a/vita3k/gui-qt/src/debug_widget.cpp +++ b/vita3k/gui-qt/src/debug_widget.cpp @@ -399,7 +399,7 @@ void DebugWidget::on_thread_double_clicked(QTreeWidgetItem *item, int /*column*/ layout->addLayout(form); auto *reg_label = new QLabel(tr("Registers"), dlg); - reg_label->setStyleSheet(QStringLiteral("font-weight: bold; margin-top: 8px;")); + reg_label->setProperty("themeRole", QStringLiteral("sectionHeader")); layout->addWidget(reg_label); auto *reg_tree = new QTreeWidget(dlg); @@ -432,7 +432,7 @@ void DebugWidget::on_thread_double_clicked(QTreeWidgetItem *item, int /*column*/ layout->addWidget(reg_tree); auto *stack_label = new QLabel(tr("Stack"), dlg); - stack_label->setStyleSheet(QStringLiteral("font-weight: bold; margin-top: 8px;")); + stack_label->setProperty("themeRole", QStringLiteral("sectionHeader")); layout->addWidget(stack_label); auto *stack_tree = new QTreeWidget(dlg); diff --git a/vita3k/gui-qt/src/gui_settings.cpp b/vita3k/gui-qt/src/gui_settings.cpp index 64e45342b..da80a262c 100644 --- a/vita3k/gui-qt/src/gui_settings.cpp +++ b/vita3k/gui-qt/src/gui_settings.cpp @@ -18,8 +18,6 @@ #include #include -#include - GuiSettings::GuiSettings(const QString &settings_dir, QObject *parent) : GuiSettingsBase(parent) { m_settings_dir = QDir(settings_dir); @@ -30,20 +28,3 @@ GuiSettings::GuiSettings(const QString &settings_dir, QObject *parent) m_settings_dir.filePath(gui::Settings + QStringLiteral(".ini")), QSettings::IniFormat, parent); } - -QStringList GuiSettings::get_stylesheet_entries() const { - const QStringList name_filter = QStringList(QStringLiteral("*.qss")); - QStringList res; - - { - QDirIterator it(m_settings_dir.absolutePath(), name_filter, QDir::Files); - while (it.hasNext()) { - it.next(); - res.append(it.fileInfo().completeBaseName()); - } - } - - res.removeDuplicates(); - res.sort(); - return res; -} diff --git a/vita3k/gui-qt/src/log_widget.cpp b/vita3k/gui-qt/src/log_widget.cpp index 52be01134..4374525ca 100644 --- a/vita3k/gui-qt/src/log_widget.cpp +++ b/vita3k/gui-qt/src/log_widget.cpp @@ -17,11 +17,13 @@ #include #include +#include #include #include +#include #include #include #include @@ -34,8 +36,11 @@ #include #include #include +#include +#include #include #include +#include #include #include @@ -47,7 +52,9 @@ namespace { constexpr int SAFE_UNBOUNDED_LOG_BUFFER_SIZE = 50000; -constexpr size_t MAX_LOG_DRAIN_BATCH = 256; +constexpr int LOG_DRAIN_INTERVAL_MS = 10; +constexpr qint64 LOG_DRAIN_TIME_BUDGET_MS = 4; +constexpr size_t MAX_LOG_DRAIN_BATCH = 64; constexpr size_t MIN_PENDING_LOG_LINES = 512; constexpr size_t MAX_PENDING_LOG_LINES = 8192; @@ -57,6 +64,84 @@ struct PendingLogMessage { size_t repeat_count = 1; }; +class LogBlockData final : public QTextBlockUserData { +public: + explicit LogBlockData(int level_value) + : level(level_value) { + } + + int level = 0; +}; + +struct RenderedLogMessage { + QString msg; + int level = 0; +}; + +QColor log_level_color(int level) { + using L = spdlog::level::level_enum; + + switch (static_cast(level)) { + case L::critical: + return gui::utils::theme_role_color(QStringLiteral("logFatal")); + case L::err: + return gui::utils::theme_role_color(QStringLiteral("logError")); + case L::warn: + return gui::utils::theme_role_color(QStringLiteral("logWarning")); + case L::info: + return gui::utils::theme_role_color(QStringLiteral("logNotice")); + case L::debug: + return gui::utils::theme_role_color(QStringLiteral("logDebug")); + case L::trace: + default: + return gui::utils::theme_role_color(QStringLiteral("logTrace")); + } +} + +void apply_block_color(const QTextBlock &block, int level) { + if (!block.isValid()) + return; + + QTextCursor cursor(block); + cursor.movePosition(QTextCursor::EndOfBlock, QTextCursor::KeepAnchor); + + QTextCharFormat format; + format.setForeground(log_level_color(level)); + cursor.mergeCharFormat(format); +} + +void append_log_entries(QPlainTextEdit *log, const std::vector &entries, bool preserve_scroll = true) { + if (!log || entries.empty()) + return; + + QScrollBar *bar = log->verticalScrollBar(); + const bool at_bottom = !preserve_scroll || bar->value() == bar->maximum(); + + QTextCursor cursor(log->document()); + cursor.movePosition(QTextCursor::End); + cursor.beginEditBlock(); + + for (const RenderedLogMessage &entry : entries) { + const QString normalized = entry.msg; + const QStringList lines = normalized.split(QLatin1Char('\n'), Qt::KeepEmptyParts); + + QTextCharFormat format; + format.setForeground(log_level_color(entry.level)); + + for (const QString &line : lines) { + cursor.insertText(line, format); + if (QTextBlock block = cursor.block(); block.isValid()) + block.setUserData(new LogBlockData(entry.level)); + cursor.insertBlock(); + } + } + + cursor.endEditBlock(); + + if (at_bottom) + bar->setValue(bar->maximum()); +} + QFont make_log_font(const QString &family = {}) { QFont font = QFontDatabase::systemFont(QFontDatabase::FixedFont); font.setPointSize(9); @@ -83,7 +168,10 @@ static struct { LogWidget::LogWidget(QWidget *parent) : custom_dock_widget(tr("Log"), parent) { - auto *container = new QWidget(this); + setObjectName(QStringLiteral("logger")); + + auto *container = new ThemeSurface(this); + container->setObjectName(QStringLiteral("log_surface")); auto *layout = new QVBoxLayout(container); layout->setContentsMargins(0, 0, 0, 0); layout->setSpacing(0); @@ -93,10 +181,11 @@ LogWidget::LogWidget(QWidget *parent) m_log->setFont(make_log_font()); m_log->document()->setMaximumBlockCount(effective_buffer_size(1000)); m_log->setObjectName(QStringLiteral("log_frame")); + m_log->viewport()->setObjectName(QStringLiteral("log_frame_viewport")); layout->addWidget(m_log); setWidget(container); - m_search_container = new QWidget(m_log); + m_search_container = new ThemeSurface(m_log); m_search_container->setObjectName(QStringLiteral("log_search_container")); auto *search_layout = new QHBoxLayout(m_search_container); search_layout->setContentsMargins(6, 6, 6, 6); @@ -147,6 +236,10 @@ LogWidget::LogWidget(QWidget *parent) connect(m_search_next_button, &QToolButton::clicked, this, [this]() { find_text(false); }); connect(m_search_close_button, &QToolButton::clicked, this, &LogWidget::hide_search_bar); + m_drain_timer = new QTimer(this); + m_drain_timer->setSingleShot(true); + connect(m_drain_timer, &QTimer::timeout, this, &LogWidget::drain_pending_messages); + update_search_ui_text(); } @@ -209,15 +302,15 @@ void LogWidget::attach() { } void LogWidget::on_log_message(const QString &msg, int level) { - m_entries.emplace_back(msg, level); - trim_entries(); append_log_entry(msg, level); } void LogWidget::drain_pending_messages() { + if (m_drain_timer && m_drain_timer->isActive()) + m_drain_timer->stop(); + std::deque batch; size_t dropped_lines = 0; - bool should_schedule_next = false; { const std::lock_guard lock(s_log_state.mutex); @@ -228,95 +321,76 @@ void LogWidget::drain_pending_messages() { batch.push_back(std::move(s_log_state.backlog.front())); s_log_state.backlog.pop_front(); } - - should_schedule_next = !s_log_state.backlog.empty(); - s_log_state.drain_posted = should_schedule_next; + s_log_state.drain_posted = false; } + std::vector rendered_entries; + rendered_entries.reserve(batch.size() + (dropped_lines > 0 ? 1 : 0)); + if (dropped_lines > 0) { - on_log_message( - QStringLiteral("[Qt log] Dropped %1 queued log lines before they could be rendered.") - .arg(static_cast(dropped_lines)), - static_cast(spdlog::level::warn)); + rendered_entries.push_back({ QStringLiteral("[Qt log] Dropped %1 queued log lines before they could be rendered.") + .arg(static_cast(dropped_lines)), + static_cast(spdlog::level::warn) }); } - if (!batch.empty()) { + QElapsedTimer timer; + timer.start(); + + size_t consumed = 0; + for (; consumed < batch.size(); ++consumed) { + if (consumed > 0 && timer.elapsed() >= LOG_DRAIN_TIME_BUDGET_MS) + break; + + QString qmsg = QString::fromStdString(batch[consumed].msg).trimmed(); + if (qmsg.isEmpty()) + continue; + + qmsg.replace(QStringLiteral("\r\n"), QStringLiteral("\n")); + qmsg.replace(QLatin1Char('\r'), QLatin1Char('\n')); + + if (batch[consumed].repeat_count > 1) + qmsg += QStringLiteral(" [x%1]").arg(static_cast(batch[consumed].repeat_count)); + + rendered_entries.push_back({ std::move(qmsg), batch[consumed].level }); + } + + if (consumed < batch.size()) { + const std::lock_guard lock(s_log_state.mutex); + for (auto it = batch.rbegin(); it != batch.rbegin() + static_cast(batch.size() - consumed); ++it) + s_log_state.backlog.push_front(std::move(*it)); + } + + if (!rendered_entries.empty()) { m_log->setUpdatesEnabled(false); - for (const auto &entry : batch) { - QString qmsg = QString::fromStdString(entry.msg).trimmed(); - if (qmsg.isEmpty()) - continue; - - if (entry.repeat_count > 1) { - qmsg += QStringLiteral(" [x%1]").arg(static_cast(entry.repeat_count)); - } - - on_log_message(qmsg, entry.level); - } - + append_log_entries(m_log, rendered_entries); m_log->setUpdatesEnabled(true); m_log->viewport()->update(); } + bool should_schedule_next = false; + { + const std::lock_guard lock(s_log_state.mutex); + should_schedule_next = !s_log_state.backlog.empty(); + s_log_state.drain_posted = should_schedule_next; + } + if (m_search_container && m_search_container->isVisible() && m_search_edit && !m_search_edit->text().isEmpty()) update_search_highlights(); - if (should_schedule_next) - QMetaObject::invokeMethod(this, &LogWidget::drain_pending_messages, Qt::QueuedConnection); -} - -void LogWidget::append_log_entry(const QString &msg, int level, bool preserve_scroll) { - QScrollBar *bar = m_log->verticalScrollBar(); - const bool at_bottom = !preserve_scroll || bar->value() == bar->maximum(); - - QTextCursor cursor = m_log->textCursor(); - cursor.movePosition(QTextCursor::End); - - QTextCharFormat fmt; - fmt.setForeground(color_for_level(level)); - - cursor.insertText(msg + "\n", fmt); - - if (at_bottom) - bar->setValue(bar->maximum()); -} - -QColor LogWidget::color_for_level(int level) const { - using L = spdlog::level::level_enum; - - switch (static_cast(level)) { - case L::critical: - return gui::utils::get_label_color( - QStringLiteral("log_level_fatal"), - QColor(QStringLiteral("#ff00ff")), - QColor(QStringLiteral("#ff66ff"))); - case L::err: - return gui::utils::get_label_color( - QStringLiteral("log_level_error"), - QColor(QStringLiteral("#C02F1D")), - QColor(QStringLiteral("#ff8080"))); - case L::warn: - return gui::utils::get_label_color( - QStringLiteral("log_level_warning"), - QColor(QStringLiteral("#BA8745")), - QColor(QStringLiteral("#ffd27f"))); - case L::info: - return gui::utils::get_label_color( - QStringLiteral("log_level_notice"), - QColor(Qt::black), - QColor(Qt::white)); - default: - return gui::utils::get_label_color( - QStringLiteral("log_level_trace"), - QColor(QStringLiteral("#808080")), - QColor(Qt::white)); + if (should_schedule_next && m_drain_timer) { + m_drain_timer->start(LOG_DRAIN_INTERVAL_MS); } } -void LogWidget::trim_entries() { - const int max_blocks = m_log->document()->maximumBlockCount(); - while (max_blocks > 0 && static_cast(m_entries.size()) > max_blocks) - m_entries.pop_front(); +void LogWidget::append_log_entry(const QString &msg, int level, bool preserve_scroll) { + QString normalized = msg.trimmed(); + if (normalized.isEmpty()) + return; + + normalized.replace(QStringLiteral("\r\n"), QStringLiteral("\n")); + normalized.replace(QLatin1Char('\r'), QLatin1Char('\n')); + + append_log_entries(m_log, { RenderedLogMessage{ std::move(normalized), level } }, preserve_scroll); } void LogWidget::set_log_buffer_size(int size) { @@ -329,7 +403,6 @@ void LogWidget::set_log_buffer_size(int size) { s_log_state.backlog.pop_front(); } } - trim_entries(); if (m_search_edit && !m_search_edit->text().isEmpty()) update_search_highlights(); } @@ -354,14 +427,23 @@ QString LogWidget::log_font_family() const { } void LogWidget::repaint_text_colors() { + if (!m_log) + return; + const QScrollBar *bar = m_log->verticalScrollBar(); const int old_value = bar->value(); const bool at_bottom = old_value == bar->maximum(); - m_log->clear(); + m_log->setUpdatesEnabled(false); + for (QTextBlock block = m_log->document()->begin(); block.isValid(); block = block.next()) { + const auto *data = static_cast(block.userData()); + if (!data) + continue; - for (const auto &[msg, level] : m_entries) - append_log_entry(msg, level, false); + apply_block_color(block, data->level); + } + m_log->setUpdatesEnabled(true); + m_log->viewport()->update(); if (!at_bottom) m_log->verticalScrollBar()->setValue(old_value); @@ -521,7 +603,7 @@ void LogWidget::update_search_ui_icons() { if (!m_search_prev_button || !m_search_next_button || !m_search_close_button) return; - const QColor foreground = gui::utils::get_foreground_color(m_search_container); + const QColor foreground = gui::utils::foreground_color(m_search_container); const QString theme_suffix = foreground.lightnessF() > 0.5f ? QStringLiteral("light") : QStringLiteral("dark"); diff --git a/vita3k/gui-qt/src/main_window.cpp b/vita3k/gui-qt/src/main_window.cpp index fb7c4d821..b7cd41335 100644 --- a/vita3k/gui-qt/src/main_window.cpp +++ b/vita3k/gui-qt/src/main_window.cpp @@ -39,10 +39,12 @@ #include #include #include -#include +#include +#include #include #include #include +#include #include #include @@ -90,7 +92,7 @@ #include #include #include -#include +#include #include #include #include @@ -99,6 +101,7 @@ #include #include #include +#include #include #include @@ -127,35 +130,6 @@ void copy_config_for_edit(Config &dst, const Config &src) { dst.current_config = src.current_config; } -QPalette make_dark_palette() { - QPalette palette; - const QColor selection_highlight(255, 255, 255, 33); - const QColor disabled_selection_highlight(255, 255, 255, 20); - - palette.setColor(QPalette::Window, QColor(45, 45, 48)); - palette.setColor(QPalette::WindowText, QColor(243, 243, 243)); - palette.setColor(QPalette::Base, QColor(30, 30, 30)); - palette.setColor(QPalette::AlternateBase, QColor(38, 38, 41)); - palette.setColor(QPalette::ToolTipBase, QColor(45, 45, 48)); - palette.setColor(QPalette::ToolTipText, QColor(243, 243, 243)); - palette.setColor(QPalette::Text, QColor(243, 243, 243)); - palette.setColor(QPalette::Button, QColor(45, 45, 48)); - palette.setColor(QPalette::ButtonText, QColor(243, 243, 243)); - palette.setColor(QPalette::BrightText, Qt::white); - palette.setColor(QPalette::Link, QColor(66, 156, 255)); - palette.setColor(QPalette::Highlight, selection_highlight); - palette.setColor(QPalette::Inactive, QPalette::Highlight, selection_highlight); - palette.setColor(QPalette::Disabled, QPalette::Highlight, disabled_selection_highlight); - palette.setColor(QPalette::HighlightedText, Qt::white); - palette.setColor(QPalette::Inactive, QPalette::HighlightedText, Qt::white); - palette.setColor(QPalette::Disabled, QPalette::HighlightedText, QColor(200, 200, 200)); - palette.setColor(QPalette::PlaceholderText, QColor(160, 160, 160)); - palette.setColor(QPalette::Disabled, QPalette::Text, QColor(115, 115, 115)); - palette.setColor(QPalette::Disabled, QPalette::ButtonText, QColor(115, 115, 115)); - palette.setColor(QPalette::Disabled, QPalette::WindowText, QColor(115, 115, 115)); - return palette; -} - void update_status_button_accent(QPushButton *button, const QString &accent) { if (!button) return; @@ -165,11 +139,8 @@ void update_status_button_accent(QPushButton *button, const QString &accent) { if (button->property("accent").toString() != accent) { button->setProperty("accent", accent); - button->style()->unpolish(button); - button->style()->polish(button); + gui::utils::refresh_theme_state(button); } - - button->update(); } } // namespace @@ -212,9 +183,6 @@ void MainWindow::register_auxiliary_window(QWidget *window) { return; m_auxiliary_windows.push_back(window); - connect(window, &QObject::destroyed, this, [this](QObject *) { - prune_auxiliary_windows(); - }); } void MainWindow::prune_auxiliary_windows() { @@ -287,6 +255,39 @@ void MainWindow::closeEvent(QCloseEvent *event) { QMainWindow::closeEvent(event); } +void MainWindow::paintEvent(QPaintEvent *event) { + QMainWindow::paintEvent(event); + + if (m_theme_background_scaled_pixmaps.empty()) + return; + + if (m_theme_background_scaled_size != size()) + rescale_theme_background_pixmaps(); + if (m_theme_background_scaled_pixmaps.empty()) + return; + + const int current_index = std::clamp(m_theme_background_current_index, 0, + static_cast(m_theme_background_scaled_pixmaps.size()) - 1); + + QPainter painter(this); + painter.setRenderHint(QPainter::SmoothPixmapTransform, true); + + const auto draw_index = [this, &painter](const int index, const qreal opacity) { + if (index < 0 || index >= static_cast(m_theme_background_scaled_pixmaps.size()) || opacity <= 0.0) + return; + + painter.setOpacity(opacity); + painter.drawPixmap(rect(), m_theme_background_scaled_pixmaps[static_cast(index)]); + }; + + if (m_theme_background_next_index >= 0 && m_theme_background_next_index < static_cast(m_theme_background_scaled_pixmaps.size())) { + draw_index(current_index, 1.0 - m_theme_background_fade_progress); + draw_index(m_theme_background_next_index, m_theme_background_fade_progress); + } else { + draw_index(current_index, 1.0); + } +} + void MainWindow::save_window_state() { if (!m_gui_settings) return; @@ -374,6 +375,7 @@ void MainWindow::restore_window_state() { void MainWindow::initialize() { Q_INIT_RESOURCE(resources); m_ui->setupUi(this); + setObjectName(QStringLiteral("main_window")); this->resize(1280, 720); this->setWindowIcon(QIcon(":/Vita3K.png")); @@ -386,11 +388,19 @@ void MainWindow::initialize() { init_current_user(); - m_dock_host = new QMainWindow(this); + auto *shell_frame = new ThemeSurface(this); + shell_frame->setObjectName(QStringLiteral("mw_frame")); + auto *shell_layout = new QVBoxLayout(shell_frame); + shell_layout->setContentsMargins(0, 0, 0, 0); + shell_layout->setSpacing(0); + setCentralWidget(shell_frame); + + m_dock_host = new QMainWindow(shell_frame); + m_dock_host->setObjectName(QStringLiteral("mw_dock_host")); m_dock_host->setWindowFlags(Qt::Widget); m_dock_host->setContextMenuPolicy(Qt::PreventContextMenu); m_dock_host->setDockNestingEnabled(true); - setCentralWidget(m_dock_host); + shell_layout->addWidget(m_dock_host); m_apps_list_widget = new AppsList(emuenv, m_dock_host); m_apps_list_widget->refresh(); @@ -423,6 +433,9 @@ void MainWindow::initialize() { connect(m_ui->trophy_collection_action, &QAction::triggered, this, &MainWindow::open_trophy_collection); + connect(m_ui->vita_themes_action, &QAction::triggered, + this, &MainWindow::open_vita_themes); + connect(m_ui->user_management_action, &QAction::triggered, this, &MainWindow::open_user_management); @@ -500,7 +513,35 @@ void MainWindow::initialize() { apply_log_gui_settings(); - init_default_style(); + m_theme_manager = std::make_unique(m_gui_settings); + m_theme_manager->set_pref_path(emuenv.pref_path); + m_theme_manager->set_vita_themes_root(emuenv.pref_path / "ux0/theme"); + connect(m_theme_manager.get(), &ThemeManager::theme_state_changed, this, &MainWindow::repaint_gui); + connect(m_theme_manager.get(), &ThemeManager::theme_state_changed, this, &MainWindow::apply_theme_background_presentation); + + m_theme_background_cycle_timer = new QTimer(this); + connect(m_theme_background_cycle_timer, &QTimer::timeout, this, &MainWindow::advance_theme_background_presentation); + + m_theme_background_fade_animation = new QVariantAnimation(this); + m_theme_background_fade_animation->setDuration(900); + m_theme_background_fade_animation->setStartValue(0.0); + m_theme_background_fade_animation->setEndValue(1.0); + m_theme_background_fade_animation->setEasingCurve(QEasingCurve::InOutSine); + connect(m_theme_background_fade_animation, &QVariantAnimation::valueChanged, this, [this](const QVariant &value) { + m_theme_background_fade_progress = value.toReal(); + update(); + }); + connect(m_theme_background_fade_animation, &QVariantAnimation::finished, this, [this] { + if (m_theme_background_next_index >= 0) + m_theme_background_current_index = m_theme_background_next_index; + + m_theme_background_next_index = -1; + m_theme_background_fade_progress = 0.0; + if (m_theme_background_cycle_timer && m_theme_background_scaled_pixmaps.size() > 1) + m_theme_background_cycle_timer->start(m_theme_background_cycle_interval_ms); + update(); + }); + init_first_run_stylesheet(); apply_stylesheet(); @@ -580,6 +621,10 @@ void MainWindow::on_install_license_triggered() { void MainWindow::on_install_finished() { m_apps_list_widget->refresh(true); + if (m_theme_manager) + m_theme_manager->refresh_vita_theme_catalog(); + if (m_vita_themes_dialog) + m_vita_themes_dialog->reload(); } bool MainWindow::prompt_startup_warnings() { @@ -747,9 +792,6 @@ void MainWindow::on_controls_action_triggered() { if (!m_controls_dialog) { m_controls_dialog = new ControlsDialog(emuenv, m_gui_settings); register_auxiliary_window(m_controls_dialog); - connect(m_controls_dialog, &QObject::destroyed, this, [this] { - m_controls_dialog = nullptr; - }); } present_tool_window(m_controls_dialog); } @@ -761,13 +803,18 @@ void MainWindow::on_check_for_updates_action_triggered() { void MainWindow::open_settings(int tab_index) { if (!m_settings_dialog) { - m_settings_dialog = new SettingsDialog(emuenv, m_gui_settings, {}, tab_index); + m_settings_dialog = new SettingsDialog(emuenv, m_gui_settings, *m_theme_manager, {}, tab_index); register_auxiliary_window(m_settings_dialog); connect(m_settings_dialog, &SettingsDialog::gui_stylesheet_request, this, &MainWindow::apply_stylesheet); + connect(m_settings_dialog, &SettingsDialog::gui_theme_music_settings_request, this, [this] { + if (m_theme_manager) + m_theme_manager->refresh_vita_theme_bgm(); + }); connect(m_settings_dialog, &SettingsDialog::gui_log_settings_request, this, &MainWindow::apply_log_gui_settings); connect(m_settings_dialog, &SettingsDialog::storage_path_changed, this, [this] { if (m_apps_list_widget) m_apps_list_widget->refresh(true); + apply_stylesheet(); }); connect(m_settings_dialog, &SettingsDialog::ui_language_request, this, &MainWindow::apply_ui_language); connect(m_settings_dialog, &SettingsDialog::restart_game_requested, this, &MainWindow::restart_running_app); @@ -776,9 +823,6 @@ void MainWindow::open_settings(int tab_index) { refresh_status_bar(); apply_log_gui_settings(); }); - connect(m_settings_dialog, &QObject::destroyed, this, [this] { - m_settings_dialog = nullptr; - }); } m_settings_dialog->set_storage_path_locked(m_game_window != nullptr); @@ -904,6 +948,8 @@ std::optional MainWindow::boot_game_once(const AppLaunchReques tr("Could not find app '%1' in apps list.").arg(QString::fromStdString(launch_request.app_path))); return std::nullopt; } + if (m_theme_manager) + m_theme_manager->set_vita_theme_bgm_blocked(true); bool launch_fullscreen = emuenv.cfg.boot_apps_full_screen; if (m_game_container) { @@ -941,6 +987,8 @@ std::optional MainWindow::boot_game_once(const AppLaunchReques const auto abort_boot = [&](const QString &msg) { QMessageBox::critical(this, tr("Error"), msg); m_app_session.stop(app::AppSessionStopReason::LaunchFailure); + if (m_theme_manager) + m_theme_manager->set_vita_theme_bgm_blocked(false); if (m_game_window) { delete m_game_window; @@ -1156,6 +1204,8 @@ void MainWindow::on_game_closed() { if (m_settings_dialog) m_settings_dialog->set_storage_path_locked(false); + if (m_theme_manager && !m_is_app_closing) + m_theme_manager->set_vita_theme_bgm_blocked(false); refresh_emulation_actions(); if (!m_is_app_closing && m_apps_list_widget) @@ -1451,13 +1501,19 @@ void MainWindow::open_trophy_collection() { if (!m_trophy_dialog) { m_trophy_dialog = new TrophyCollectionDialog(emuenv, m_gui_settings); register_auxiliary_window(m_trophy_dialog); - connect(m_trophy_dialog, &QObject::destroyed, this, [this] { - m_trophy_dialog = nullptr; - }); } present_tool_window(m_trophy_dialog); } +void MainWindow::open_vita_themes() { + if (!m_vita_themes_dialog) { + m_vita_themes_dialog = new VitaThemesDialog(m_gui_settings, *m_theme_manager, this); + register_auxiliary_window(m_vita_themes_dialog); + } + + present_tool_window(m_vita_themes_dialog); +} + void MainWindow::open_user_management() { if (m_game_window) { return; @@ -1472,9 +1528,6 @@ void MainWindow::open_user_management() { if (m_trophy_dialog) m_trophy_dialog->reload(); }); - connect(m_user_mgmt_dialog, &QObject::destroyed, this, [this] { - m_user_mgmt_dialog = nullptr; - }); } present_tool_window(m_user_mgmt_dialog); } @@ -1483,9 +1536,6 @@ void MainWindow::open_debug_widget(int tab) { if (!m_debug_widget) { m_debug_widget = new DebugWidget(emuenv, this); m_debug_widget->setFloating(true); - connect(m_debug_widget, &QObject::destroyed, this, [this] { - m_debug_widget = nullptr; - }); } m_debug_widget->show_tab(tab); } @@ -1537,7 +1587,7 @@ void MainWindow::setup_toolbar() { connect(m_ui->toolbar_controls, &QAction::triggered, this, &MainWindow::on_controls_action_triggered); - m_slider_container = new QWidget(this); + m_slider_container = new ThemeSurface(this); m_slider_container->setObjectName(QStringLiteral("toolbar_icon_size_container")); auto *slider_layout = new QHBoxLayout(m_slider_container); slider_layout->setContentsMargins(14, 0, 14, 0); @@ -1558,6 +1608,7 @@ void MainWindow::setup_toolbar() { this, &MainWindow::resize_icons); m_search_bar = new QLineEdit(this); + m_search_bar->setObjectName(QStringLiteral("mw_searchbar")); m_search_bar->setPlaceholderText(tr("Search...")); m_search_bar->setFocusPolicy(Qt::ClickFocus); m_search_bar->setClearButtonEnabled(true); @@ -1591,20 +1642,6 @@ void MainWindow::repaint_gui() { update(); } -void MainWindow::init_default_style() { - if (QStyleFactory::keys().contains(QStringLiteral("Fusion"), Qt::CaseInsensitive)) - m_default_style = QStringLiteral("Fusion"); - - if (m_default_style.isEmpty()) { - if (const QStyle *style = QApplication::style()) - m_default_style = style->name(); - } - if (m_default_style.isEmpty()) { - if (const QStringList styles = QStyleFactory::keys(); !styles.empty()) - m_default_style = styles.front(); - } -} - void MainWindow::init_first_run_stylesheet() { if (!m_gui_settings || m_gui_settings->contains_value(gui::m_currentStylesheet)) return; @@ -1617,69 +1654,116 @@ void MainWindow::init_first_run_stylesheet() { } void MainWindow::apply_stylesheet() { - if (!m_gui_settings) + if (!m_theme_manager) + return; + m_theme_manager->ensure_generated_theme_ready(emuenv.pref_path / "ux0/theme"); + m_theme_manager->apply_theme({}, true); +} + +void MainWindow::apply_theme_background_presentation() { + if (!m_theme_manager) { + clear_theme_background_presentation(); + return; + } + + const auto state = m_theme_manager->current_applied_theme_state(); + if (!state || state->background.image_paths.isEmpty()) { + clear_theme_background_presentation(); + return; + } + + std::vector next_pixmaps; + next_pixmaps.reserve(static_cast(state->background.image_paths.size())); + for (const QString &path : state->background.image_paths) { + QPixmap pixmap(path); + if (!pixmap.isNull()) + next_pixmaps.push_back(std::move(pixmap)); + } + + if (next_pixmaps.empty()) { + clear_theme_background_presentation(); + return; + } + + if (m_theme_background_cycle_timer) + m_theme_background_cycle_timer->stop(); + if (m_theme_background_fade_animation) + m_theme_background_fade_animation->stop(); + + m_theme_background_original_pixmaps = std::move(next_pixmaps); + m_theme_background_scaled_pixmaps.clear(); + m_theme_background_scaled_size = {}; + m_theme_background_cycle_interval_ms = state->background.interval_ms; + m_theme_background_current_index = 0; + m_theme_background_next_index = -1; + m_theme_background_fade_progress = 0.0; + rescale_theme_background_pixmaps(); + + if (m_theme_background_cycle_timer && state->background.animated && m_theme_background_scaled_pixmaps.size() > 1) + m_theme_background_cycle_timer->start(m_theme_background_cycle_interval_ms); + update(); +} + +void MainWindow::clear_theme_background_presentation() { + if (m_theme_background_cycle_timer) + m_theme_background_cycle_timer->stop(); + if (m_theme_background_fade_animation) + m_theme_background_fade_animation->stop(); + + m_theme_background_original_pixmaps.clear(); + m_theme_background_scaled_pixmaps.clear(); + m_theme_background_scaled_size = {}; + m_theme_background_current_index = 0; + m_theme_background_next_index = -1; + m_theme_background_fade_progress = 0.0; + update(); +} + +void MainWindow::rescale_theme_background_pixmaps() { + m_theme_background_scaled_pixmaps.clear(); + if (m_theme_background_original_pixmaps.empty() || size().isEmpty()) { + m_theme_background_scaled_size = {}; + return; + } + + m_theme_background_scaled_size = size(); + m_theme_background_scaled_pixmaps.reserve(m_theme_background_original_pixmaps.size()); + for (const QPixmap &pixmap : m_theme_background_original_pixmaps) { + m_theme_background_scaled_pixmaps.push_back(pixmap.scaled( + m_theme_background_scaled_size, + Qt::IgnoreAspectRatio, + Qt::SmoothTransformation)); + } +} + +void MainWindow::advance_theme_background_presentation() { + if (!m_theme_background_fade_animation || m_theme_background_scaled_pixmaps.size() < 2) return; - const QString stylesheet_name = m_gui_settings->get_value(gui::m_currentStylesheet).toString(); - - if (QStyle *style = QStyleFactory::create(m_default_style)) { - QApplication::setStyle(style); - QApplication::setPalette(style->standardPalette()); + if (!isVisible() || isMinimized()) { + if (m_theme_background_cycle_timer) + m_theme_background_cycle_timer->start(1000); + return; } - if (stylesheet_name.isEmpty() || stylesheet_name == gui::LightStylesheet) { - LOG_INFO("Using light stylesheet"); - if (QStyle *fusion = QStyleFactory::create(QStringLiteral("Fusion"))) { - QApplication::setStyle(fusion); - QApplication::setPalette(fusion->standardPalette()); - } - qApp->setStyleSheet(gui::stylesheets::light_style_sheet); - } else if (stylesheet_name == gui::DarkStylesheet) { - LOG_INFO("Using dark stylesheet"); - if (QStyle *fusion = QStyleFactory::create(QStringLiteral("Fusion"))) { - QApplication::setStyle(fusion); - QApplication::setPalette(fusion->standardPalette()); - } - QApplication::setPalette(make_dark_palette()); - qApp->setStyleSheet(gui::stylesheets::dark_style_sheet); - } else if (stylesheet_name == gui::NoStylesheet) { - LOG_INFO("Using empty stylesheet"); - qApp->setStyleSheet(QStringLiteral("/* none */")); - } else { - const QString settings_dir = m_gui_settings->get_settings_dir(); - const QString stylesheet_path = settings_dir + QStringLiteral("/") + stylesheet_name + QStringLiteral(".qss"); + if (m_theme_background_fade_animation->state() == QAbstractAnimation::Running) + return; - if (QFile file(stylesheet_path); file.open(QIODevice::ReadOnly | QIODevice::Text)) { - QString qss = file.readAll(); - file.close(); - - qss.replace( - QStringLiteral("url(\"gui-configs/"), - QStringLiteral("url(\"") + settings_dir + QStringLiteral("/")); - - LOG_INFO("Loaded stylesheet '{}' from {}", stylesheet_name.toUtf8().constData(), stylesheet_path.toUtf8().constData()); - qApp->setStyleSheet(qss); - } else { - LOG_ERROR("Could not find stylesheet '{}', falling back to light", stylesheet_name.toUtf8().constData()); - qApp->setStyleSheet(gui::stylesheets::light_style_sheet); - } - } - - repaint_gui(); + m_theme_background_next_index = (m_theme_background_current_index + 1) + % static_cast(m_theme_background_scaled_pixmaps.size()); + m_theme_background_fade_progress = 0.0; + m_theme_background_fade_animation->start(); } void MainWindow::repaint_toolbar_icons() { auto *tb = m_ui->toolBar; - const QColor fg = gui::utils::get_label_color( - QStringLiteral("toolbar_icon_color"), - QColor(QStringLiteral("#5b5b5b")), - QColor(QStringLiteral("#f3f3f3"))); + const QColor fg = gui::utils::toolbar_icon_tint(); std::map new_colors; - new_colors[QIcon::Normal] = gui::utils::get_label_color(fg, fg); - new_colors[QIcon::Disabled] = gui::utils::get_label_color(Qt::gray, Qt::darkGray); - new_colors[QIcon::Active] = gui::utils::get_label_color(fg, fg); - new_colors[QIcon::Selected] = gui::utils::get_label_color(fg, fg); + new_colors[QIcon::Normal] = fg; + new_colors[QIcon::Disabled] = gui::utils::muted_text_color(); + new_colors[QIcon::Active] = fg; + new_colors[QIcon::Selected] = fg; const auto icon = [&new_colors](const QString &path) { return gui::utils::get_colorized_icon(QIcon(path), Qt::black, new_colors); @@ -1776,9 +1860,13 @@ void MainWindow::on_context_menu_requested(const QPoint &global_pos, const std:: }); connect(&menu, &AppsListContextMenu::open_settings_requested, this, [this](const std::string &app_path, int tab) { - auto *dlg = new SettingsDialog(emuenv, m_gui_settings, app_path, tab); + auto *dlg = new SettingsDialog(emuenv, m_gui_settings, *m_theme_manager, app_path, tab); register_auxiliary_window(dlg); connect(dlg, &SettingsDialog::gui_stylesheet_request, this, &MainWindow::apply_stylesheet); + connect(dlg, &SettingsDialog::gui_theme_music_settings_request, this, [this] { + if (m_theme_manager) + m_theme_manager->refresh_vita_theme_bgm(); + }); connect(dlg, &SettingsDialog::gui_log_settings_request, this, &MainWindow::apply_log_gui_settings); connect(dlg, &SettingsDialog::ui_language_request, this, &MainWindow::apply_ui_language); connect(dlg, &SettingsDialog::restart_game_requested, this, &MainWindow::restart_running_app); @@ -1807,7 +1895,7 @@ void MainWindow::resize_icons(int index) { void MainWindow::setup_status_bar() { auto *sb = statusBar(); - sb->setObjectName(QStringLiteral("emulatorStatusBar")); + sb->setObjectName(QStringLiteral("emulator_status_bar")); m_renderer_button = new QPushButton(this); m_renderer_button->setObjectName(QStringLiteral("status_button")); diff --git a/vita3k/gui-qt/src/main_window.ui b/vita3k/gui-qt/src/main_window.ui index a8d23505a..0a249e842 100644 --- a/vita3k/gui-qt/src/main_window.ui +++ b/vita3k/gui-qt/src/main_window.ui @@ -72,6 +72,7 @@ Manage + @@ -241,6 +242,11 @@ Trophy Collection + + + Vita Themes + + User Management diff --git a/vita3k/gui-qt/src/qt_utils.cpp b/vita3k/gui-qt/src/qt_utils.cpp index ce02a9221..0a8bdebae 100644 --- a/vita3k/gui-qt/src/qt_utils.cpp +++ b/vita3k/gui-qt/src/qt_utils.cpp @@ -20,6 +20,8 @@ #include #include #include +#include +#include #include #include #include @@ -28,7 +30,9 @@ #include #include #include +#include #include +#include #include #include #include @@ -37,6 +41,22 @@ namespace gui::utils { +namespace { + +QColor default_label_color(QPalette::ColorRole color_role) { + QLabel dummy; + dummy.ensurePolished(); + return dummy.palette().color(color_role); +} + +QHash s_theme_role_colors; + +QString theme_role_cache_key(const QString &role, QPalette::ColorRole color_role) { + return role + QLatin1Char(':') + QString::number(static_cast(color_role)); +} + +} // namespace + void open_dir(const fs::path &path) { QDesktopServices::openUrl(QUrl::fromLocalFile(to_qt_path(path))); } @@ -49,40 +69,107 @@ QString to_qt_path(const fs::path &path) { return QString::fromStdWString(path.wstring()); } -QColor get_foreground_color(QWidget *widget) { +QString sanitize_relative_path_reference(const QString &path) { + QString normalized = path.trimmed(); + normalized.replace(QLatin1Char('\\'), QLatin1Char('/')); + if (normalized.isEmpty()) + return {}; + + static const QRegularExpression windows_drive_prefix(QStringLiteral(R"(^[A-Za-z]:/)")); + static const QRegularExpression url_scheme_prefix( + QStringLiteral(R"(^[A-Za-z][A-Za-z0-9+.-]*:)"), + QRegularExpression::CaseInsensitiveOption); + + if (windows_drive_prefix.match(normalized).hasMatch() || url_scheme_prefix.match(normalized).hasMatch()) + return {}; + + QString relative = normalized; + while (relative.startsWith(QStringLiteral("./"))) + relative.remove(0, 2); + while (relative.startsWith(QLatin1Char('/'))) + relative.remove(0, 1); + + relative = QDir::cleanPath(relative); + if (relative.isEmpty() || relative == QStringLiteral(".") + || relative == QStringLiteral("..") + || relative.startsWith(QStringLiteral("../"))) { + return {}; + } + + return relative; +} + +QString resolve_relative_path_in_root(const QString &relative_path, const QString &root_path) { + if (relative_path.isEmpty() || root_path.isEmpty()) + return {}; + + const QDir root_dir(QDir::cleanPath(root_path)); + const QString resolved_path = QDir::cleanPath(root_dir.absoluteFilePath(relative_path)); + const QString root_relative = QDir::cleanPath(root_dir.relativeFilePath(resolved_path)); + if (root_relative == QStringLiteral("..") || root_relative.startsWith(QStringLiteral("../"))) + return {}; + + return QDir::fromNativeSeparators(resolved_path); +} + +QColor foreground_color(QWidget *widget) { if (widget) { widget->ensurePolished(); return widget->palette().color(QPalette::ColorRole::WindowText); } + return default_label_color(QPalette::ColorRole::WindowText); +} + +QColor theme_role_color( + const QString &role, + QPalette::ColorRole color_role) { + const QString cache_key = theme_role_cache_key(role, color_role); + if (const auto it = s_theme_role_colors.constFind(cache_key); it != s_theme_role_colors.cend()) + return *it; + QLabel dummy; + dummy.setProperty("themeRole", role); dummy.ensurePolished(); - return dummy.palette().color(QPalette::ColorRole::WindowText); + const QColor color = dummy.palette().color(color_role); + s_theme_role_colors.insert(cache_key, color); + return color; +} + +QString format_help_html(const QString &title, const QString &body) { + QString escaped = body.toHtmlEscaped(); + escaped.replace(QLatin1Char('\n'), QStringLiteral("
")); + return QStringLiteral( + "
%1
" + "
" + "
%2
") + .arg(title.toHtmlEscaped(), escaped); +} + +void invalidate_theme_cache() { + s_theme_role_colors.clear(); +} + +QColor muted_text_color() { + return theme_role_color(QStringLiteral("mutedText")); +} + +QColor toolbar_icon_tint() { + return theme_role_color(QStringLiteral("toolbarIconTint")); +} + +void refresh_theme_state(QWidget *widget) { + if (!widget) + return; + + widget->style()->unpolish(widget); + widget->style()->polish(widget); + widget->update(); } bool dark_mode_active() { return QApplication::styleHints()->colorScheme() == Qt::ColorScheme::Dark; } -QColor get_label_color( - const QString &object_name, - const QColor &fallback_light, - const QColor &fallback_dark, - QPalette::ColorRole color_role) { - if (!qApp || qApp->styleSheet().isEmpty() || !qApp->styleSheet().contains(object_name)) - return dark_mode_active() ? fallback_dark : fallback_light; - - QLabel dummy; - dummy.setObjectName(object_name); - dummy.ensurePolished(); - return dummy.palette().color(color_role); -} - -QColor get_label_color( - const QColor &fallback_light, - const QColor &fallback_dark) { - return dark_mode_active() ? fallback_dark : fallback_light; -} - QIcon get_colorized_icon(const QIcon &icon, const QColor &source_color, const std::map &new_colors) { diff --git a/vita3k/gui-qt/src/settings_dialog.cpp b/vita3k/gui-qt/src/settings_dialog.cpp index 5917db554..07dd434e9 100644 --- a/vita3k/gui-qt/src/settings_dialog.cpp +++ b/vita3k/gui-qt/src/settings_dialog.cpp @@ -29,6 +29,7 @@ #include #include #include +#include #include #include #include @@ -59,19 +60,12 @@ #include #include #include +#include + +#include namespace { -QString format_help_html(const QString &title, const QString &body) { - QString escaped = body.toHtmlEscaped(); - escaped.replace(QLatin1Char('\n'), QStringLiteral("
")); - return QStringLiteral( - "
%1
" - "
" - "
%2
") - .arg(title.toHtmlEscaped(), escaped); -} - int first_visible_category(const QListWidget *list, int preferred) { const int count = list->count(); if (preferred >= 0 && preferred < count) { @@ -91,11 +85,13 @@ int first_visible_category(const QListWidget *list, int preferred) { SettingsDialog::SettingsDialog(EmuEnvState &emuenv, std::shared_ptr gui_settings, + ThemeManager &theme_manager, const std::string &app_path, int initial_tab) : QDialog(nullptr) , emuenv(emuenv) , m_gui_settings(std::move(gui_settings)) + , m_theme_manager(theme_manager) , m_tooltips(std::make_unique(this)) , m_app_path(app_path) , m_initial_tab(initial_tab) @@ -123,7 +119,12 @@ SettingsDialog::SettingsDialog(EmuEnvState &emuenv, } if (!m_gui_settings || !restoreGeometry(m_gui_settings->get_value(gui::sd_geometry).toByteArray())) resize(780, 676); + connect(&m_theme_manager, &ThemeManager::theme_state_changed, this, &SettingsDialog::refresh_themes); + connect(&m_theme_manager, &ThemeManager::theme_catalog_changed, this, &SettingsDialog::refresh_themes); m_ui->settingsCategory->setSpacing(0); + m_ui->modules_list->setSelectionMode(QAbstractItemView::NoSelection); + m_ui->modules_list->setFocusPolicy(Qt::NoFocus); + m_ui->modules_list->setAlternatingRowColors(true); for (int i = 0; i < m_ui->tab_widget_settings->count(); ++i) { auto *item = new QListWidgetItem(m_ui->tab_widget_settings->tabText(i), m_ui->settingsCategory); @@ -169,6 +170,7 @@ SettingsDialog::SettingsDialog(EmuEnvState &emuenv, m_ui->show_mode->setVisible(false); m_ui->demo_mode->setVisible(false); + m_ui->gb_theme_music_section->setVisible(false); m_ui->boot_apps_fullscreen->setVisible(false); m_ui->show_live_area_screen->setVisible(false); m_ui->show_compile_shaders->setVisible(false); @@ -198,6 +200,13 @@ SettingsDialog::SettingsDialog(EmuEnvState &emuenv, bb->button(QDialogButtonBox::Apply)->setEnabled(false); } +void SettingsDialog::refresh_themes() { + const QString preferred_name = m_ui->combo_stylesheets + ? m_ui->combo_stylesheets->currentData().toString() + : m_current_stylesheet; + add_stylesheets(preferred_name); +} + SettingsDialog::~SettingsDialog() { } @@ -360,6 +369,17 @@ void SettingsDialog::load_config() { m_ui->audio_volume->setValue(m_config.audio_volume); m_ui->audio_volume_label->setText(tr("Current volume: %1%").arg(m_config.audio_volume)); + const bool theme_music_enabled = m_gui_settings + ? m_gui_settings->get_value(gui::vt_bgmEnabled).toBool() + : gui::vt_bgmEnabled.def.toBool(); + const int theme_music_volume = std::clamp( + m_gui_settings + ? m_gui_settings->get_value(gui::vt_bgmVolume).toInt() + : gui::vt_bgmVolume.def.toInt(), + 0, 100); + m_ui->theme_music_enable->setChecked(theme_music_enabled); + m_ui->theme_music_volume->setValue(theme_music_volume); + m_ui->theme_music_volume_label->setText(tr("Theme music volume: %1%").arg(theme_music_volume)); m_ui->ngs_enable->setChecked(m_config.ngs_enable); @@ -707,6 +727,12 @@ bool SettingsDialog::commit_changes(bool close_after) { const bool previous_windows_rounded_corners = update_gui_settings ? m_gui_settings->get_value(gui::gw_roundedCorners).toBool() : false; + const bool previous_theme_music_enabled = update_gui_settings + ? m_gui_settings->get_value(gui::vt_bgmEnabled).toBool() + : gui::vt_bgmEnabled.def.toBool(); + const int previous_theme_music_volume = update_gui_settings + ? m_gui_settings->get_value(gui::vt_bgmVolume).toInt() + : gui::vt_bgmVolume.def.toInt(); if (storage_path_changed && !apply_pending_storage_path()) return false; @@ -737,8 +763,19 @@ bool SettingsDialog::commit_changes(bool close_after) { m_gui_settings->set_value(gui::mw_warnAdminPrivileges, m_ui->warn_admin_privileges->isChecked()); if (update_gui_settings && previous_windows_rounded_corners != m_ui->windows_rounded_corners->isChecked()) m_gui_settings->set_value(gui::gw_roundedCorners, m_ui->windows_rounded_corners->isChecked()); + bool theme_music_settings_changed = false; + if (update_gui_settings && previous_theme_music_enabled != m_ui->theme_music_enable->isChecked()) { + m_gui_settings->set_value(gui::vt_bgmEnabled, m_ui->theme_music_enable->isChecked(), false); + theme_music_settings_changed = true; + } + if (update_gui_settings && previous_theme_music_volume != m_ui->theme_music_volume->value()) { + m_gui_settings->set_value(gui::vt_bgmVolume, m_ui->theme_music_volume->value(), false); + theme_music_settings_changed = true; + } if (log_settings_changed) Q_EMIT gui_log_settings_request(); + if (theme_music_settings_changed) + Q_EMIT gui_theme_music_settings_request(); emuenv.kernel.debugger.log_imports = m_ui->log_imports->isChecked(); emuenv.kernel.debugger.log_exports = m_ui->log_exports->isChecked(); @@ -756,7 +793,7 @@ void SettingsDialog::populate_modules_list() { const auto modules = config::get_modules_list(emuenv.pref_path, m_config.lle_modules); for (const auto &[name, enabled] : modules) { auto *item = new QListWidgetItem(QString::fromStdString(name), m_ui->modules_list); - item->setFlags(item->flags() | Qt::ItemIsUserCheckable); + item->setFlags((item->flags() | Qt::ItemIsUserCheckable) & ~Qt::ItemIsSelectable); item->setCheckState(enabled ? Qt::Checked : Qt::Unchecked); } update_modules_list_enabled(); @@ -951,6 +988,9 @@ void SettingsDialog::setup_connections() { connect(m_ui->audio_volume, &QSlider::valueChanged, this, [this](int val) { m_ui->audio_volume_label->setText(tr("Current volume: %1%").arg(val)); }); + connect(m_ui->theme_music_volume, &QSlider::valueChanged, this, [this](int val) { + m_ui->theme_music_volume_label->setText(tr("Theme music volume: %1%").arg(val)); + }); connect(m_ui->perf_overlay_enabled, &QCheckBox::toggled, this, [this](bool on) { m_ui->label_perf_detail->setEnabled(on); @@ -1122,6 +1162,8 @@ void SettingsDialog::setup_connections() { // Audio { m_ui->audio_backend_box, tr("Audio Backend"), m_tooltips->audio_backend }, { m_ui->audio_volume, tr("Volume"), m_tooltips->audio_volume }, + { m_ui->theme_music_enable, tr("Enable Vita Theme Background Music"), m_tooltips->theme_music_enable }, + { m_ui->theme_music_volume, tr("Theme Music Volume"), m_tooltips->theme_music_volume }, { m_ui->ngs_enable, tr("Enable NGS Support"), m_tooltips->ngs_enable }, // Camera { m_ui->front_camera_box, tr("Front Camera Source"), m_tooltips->front_camera }, @@ -1243,9 +1285,9 @@ void SettingsDialog::set_description(QWidget *tab, const QString &title_override }(); if (text == m_tooltips->default_description) { - m_ui->helpText->setHtml(format_help_html(title, m_tooltips->category_summary(static_cast(index)))); + m_ui->helpText->setHtml(gui::utils::format_help_html(title, m_tooltips->category_summary(static_cast(index)))); } else { - m_ui->helpText->setHtml(format_help_html(title, text)); + m_ui->helpText->setHtml(gui::utils::format_help_html(title, text)); } } @@ -1392,30 +1434,27 @@ void SettingsDialog::update_gpu_visibility() { } } -void SettingsDialog::add_stylesheets() { +void SettingsDialog::add_stylesheets(const QString &preferred_name) { m_ui->combo_stylesheets->blockSignals(true); m_ui->combo_stylesheets->clear(); - m_ui->combo_stylesheets->addItem(tr("None", "Stylesheets"), gui::NoStylesheet); + const auto themes = m_theme_manager.available_themes(); + for (const auto &theme : themes) + m_ui->combo_stylesheets->addItem(theme.display_name, theme.name); - m_ui->combo_stylesheets->addItem(tr("Light", "Stylesheets"), gui::LightStylesheet); - m_ui->combo_stylesheets->addItem(tr("Dark", "Stylesheets"), gui::DarkStylesheet); - - if (m_gui_settings) { - for (const QString &entry : m_gui_settings->get_stylesheet_entries()) { - if (entry != gui::LightStylesheet && entry != gui::DarkStylesheet) { - m_ui->combo_stylesheets->addItem(entry, entry); - } - } - } - - m_current_stylesheet = m_gui_settings - ? m_gui_settings->get_value(gui::m_currentStylesheet).toString() - : gui::DarkStylesheet; + if (!preferred_name.isEmpty()) + m_current_stylesheet = preferred_name; + else if (m_gui_settings) + m_current_stylesheet = m_gui_settings->get_value(gui::m_currentStylesheet).toString(); + else + m_current_stylesheet.clear(); const int index = m_ui->combo_stylesheets->findData(m_current_stylesheet); if (index >= 0) { m_ui->combo_stylesheets->setCurrentIndex(index); + } else if (m_ui->combo_stylesheets->count() > 0) { + m_ui->combo_stylesheets->setCurrentIndex(0); + m_current_stylesheet = m_ui->combo_stylesheets->currentData().toString(); } m_ui->combo_stylesheets->blockSignals(false); } diff --git a/vita3k/gui-qt/src/settings_dialog.ui b/vita3k/gui-qt/src/settings_dialog.ui index e2486ef28..332c75ac5 100644 --- a/vita3k/gui-qt/src/settings_dialog.ui +++ b/vita3k/gui-qt/src/settings_dialog.ui @@ -1266,6 +1266,78 @@ QGroupBox[flat="true"]::title { + + + + Theme Music + + + + + + Vita Theme Music + + + true + + + + 8 + + + 0 + + + 6 + + + 0 + + + 0 + + + + + Enable Vita theme background music + + + + + + + Theme music volume: 50% + + + + + + + 0 + + + 100 + + + 50 + + + Qt::Orientation::Horizontal + + + QSlider::TickPosition::TicksBelow + + + 25 + + + + + + + + + diff --git a/vita3k/gui-qt/src/settings_dialog_tooltips.cpp b/vita3k/gui-qt/src/settings_dialog_tooltips.cpp index a63e87b88..505c8ca98 100644 --- a/vita3k/gui-qt/src/settings_dialog_tooltips.cpp +++ b/vita3k/gui-qt/src/settings_dialog_tooltips.cpp @@ -124,6 +124,8 @@ SettingsDialogTooltips::SettingsDialogTooltips(QObject *parent) , memory_mapping(tr("Memory mapping improved performance, reduces memory usage and fixes many graphical issues. However, it may be unstable on some GPUs.")) , audio_backend(tr("Select the audio backend. Cubeb is recommended for most systems.")) , audio_volume(tr("Set the in-game audio volume.")) + , theme_music_enable(tr("Enable background music for generated Vita themes when the selected theme provides an ATRAC9 BGM file.")) + , theme_music_volume(tr("Set the playback volume for Vita theme background music. This only affects interface theme music.")) , ngs_enable(tr("Enable advanced audio library NGS support.")) , front_camera(tr("Select the front camera source.\nSolid Color or Static Image can be used as substitutes.")) , back_camera(tr("Select the back camera source.\nSolid Color or Static Image can be used as substitutes.")) diff --git a/vita3k/gui-qt/src/stylesheets.cpp b/vita3k/gui-qt/src/stylesheets.cpp deleted file mode 100644 index 3b74f8297..000000000 --- a/vita3k/gui-qt/src/stylesheets.cpp +++ /dev/null @@ -1,321 +0,0 @@ -// Vita3K emulator project -// Copyright (C) 2026 Vita3K team -// -// This program is free software; you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation; either version 2 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License along -// with this program; if not, write to the Free Software Foundation, Inc., -// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. - -#include - -namespace gui::stylesheets { - -// Light built-in stylesheet -const QString light_style_sheet( - "QWidget { color: #202020; background-color: #f5f5f5; }" - "QWidget:disabled { color: #8a8a8a; }" - "QMainWindow { background-color: #f5f5f5; }" - "QDialog { background-color: #f5f5f5; }" - "QMenuBar { background-color: #f0f0f0; color: #202020; }" - "QMenuBar::item { background: transparent; padding: 4px 8px; }" - "QMenuBar::item:selected { background: #e1e1e1; }" - "QMenu { background-color: #f7f7f7; color: #202020; border: 1px solid #c6c6c6; }" - "QMenu::item:selected { background-color: #d6e8ff; color: #202020; }" - "QMenu::item:disabled { color: #808080; }" - "QMenu::separator { height: 1px; background: #d0d0d0; margin: 4px 8px; }" - "QToolBar#mw_toolbar { background-color: #f0f0f0; border: none; border-top: 1px solid #cfcfcf; }" - "QToolBar#mw_toolbar::separator { background-color: rgba(207, 207, 207, 235); width: 0.125em; margin-top: 0.250em; margin-bottom: 0.250em; }" - "QToolButton { color: #202020; background: transparent; border: 1px solid transparent; padding: 3px; }" - "QToolButton:hover { background-color: #e8e8e8; border: 1px solid #bdbdbd; }" - "QToolButton:pressed { background-color: #d8d8d8; color: #202020; }" - "QToolButton:disabled { color: #808080; }" - "QToolButton#welcome_firmware_button { background-color: #ffffff; color: #202020; border: 1px solid #b8b8b8; border-radius: 2px; padding: 4px 32px 4px 12px; text-align: left; }" - "QToolButton#welcome_firmware_button:hover { background-color: #f3f3f3; border-color: #9f9f9f; }" - "QToolButton#welcome_firmware_button:pressed { background-color: #e3e3e3; }" - "QToolButton#welcome_firmware_button::menu-button { subcontrol-origin: padding; subcontrol-position: top right; width: 24px; border-left: 1px solid #b8b8b8; border-top-right-radius: 2px; border-bottom-right-radius: 2px; background: #f3f3f3; }" - "QToolButton#welcome_firmware_button::menu-button:hover { background: #e8e8e8; border-left-color: #9f9f9f; }" - "QToolButton#welcome_firmware_button::menu-button:pressed { background: #dcdcdc; }" - "QLabel#toolbar_icon_color { color: #5b5b5b; }" - "QPushButton { background-color: #ffffff; color: #202020; border: 1px solid #b8b8b8; border-radius: 2px; padding: 4px 16px; min-height: 1.2em; }" - "QPushButton:hover { background-color: #f3f3f3; border-color: #9f9f9f; }" - "QPushButton:pressed { background-color: #e3e3e3; }" - "QPushButton:disabled { color: #8a8a8a; background-color: #efefef; border-color: #d4d4d4; }" - "QPushButton:default { border-color: #148aff; }" - "QDialog#ControlsDialog QPushButton { background-color: #ececec; color: #202020; border: none; border-radius: 6px; padding: 6px 12px; outline: none; }" - "QDialog#ControlsDialog QPushButton:hover { background-color: #e3e3e3; border: none; }" - "QDialog#ControlsDialog QPushButton:pressed { background-color: #d8d8d8; border: none; }" - "QDialog#ControlsDialog QPushButton:disabled { color: #8a8a8a; background-color: #e4e4e4; border: none; }" - "QDialog#ControlsDialog QPushButton[bindButton=\"true\"] { background-color: #e6e6e6; border-radius: 6px; padding: 6px 12px; }" - "QDialog#ControlsDialog QPushButton[bindButton=\"true\"]:hover { background-color: #dddddd; }" - "QDialog#ControlsDialog QPushButton[bindButton=\"true\"]:pressed { background-color: #d2d2d2; }" - "QDialog#ControlsDialog QPushButton[bindButton=\"true\"][captureActive=\"true\"] { background-color: #148aff; color: #ffffff; }" - "QDialog#ControlsDialog QPushButton[bindButton=\"true\"][captureActive=\"true\"]:hover { background-color: #2a97ff; }" - "QDialog#ControlsDialog QPushButton[bindButton=\"true\"][captureActive=\"true\"]:pressed { background-color: #0f74d6; }" - "QStatusBar#emulatorStatusBar { background-color: #f0f0f0; color: #202020; }" - "QStatusBar#emulatorStatusBar::item { border: none; }" - "QStatusBar#emulatorStatusBar QPushButton#status_button { border: 1px solid transparent; padding: 2px 8px; font-weight: bold; font-size: 11px; background: transparent; border-radius: 6px; color: #202020; }" - "QStatusBar#emulatorStatusBar QPushButton#status_button:hover { border: 1px solid #a0a0a0; background: rgba(0, 0, 0, 0.03); }" - "QStatusBar#emulatorStatusBar QPushButton#status_button:pressed { border: 1px solid #8a8a8a; background: rgba(0, 0, 0, 0.06); }" - "QStatusBar#emulatorStatusBar QPushButton#status_button[accent=\"renderer_vulkan\"] { color: #ff4444; }" - "QStatusBar#emulatorStatusBar QPushButton#status_button[accent=\"renderer_opengl\"] { color: #4488ff; }" - "QStatusBar#emulatorStatusBar QPushButton#status_button[accent=\"high\"] { color: #ff8800; }" - "QStatusBar#emulatorStatusBar QPushButton#status_button[accent=\"standard\"] { color: #aaaaaa; }" - "QStatusBar#emulatorStatusBar QPushButton#status_button[accent=\"neutral\"] { color: #5f5f5f; }" - "QStatusBar#emulatorStatusBar QPushButton#status_button[accent=\"audio\"] { color: #4b9b2f; }" - "QStatusBar#emulatorStatusBar QPushButton#status_button[accent=\"muted\"] { color: #777777; }" - "QStatusBar#emulatorStatusBar QPushButton#status_button[accent=\"update\"] { color: #1f8f4e; }" - "QLineEdit { background-color: #ffffff; color: #202020; border: 1px solid #b8b8b8; border-radius: 2px; padding: 2px 4px; selection-background-color: #148aff; selection-color: #ffffff; }" - "QLineEdit:focus { border-color: #148aff; }" - "QLineEdit:disabled { color: #8a8a8a; background-color: #f0f0f0; border-color: #d0d0d0; }" - "QComboBox { background-color: #ffffff; color: #202020; border: 1px solid #b8b8b8; border-radius: 2px; padding: 2px 28px 2px 6px; }" - "QComboBox:hover { border-color: #9f9f9f; }" - "QComboBox:disabled { color: #8a8a8a; background-color: #f0f0f0; border-color: #d0d0d0; }" - "QComboBox::drop-down { subcontrol-origin: padding; subcontrol-position: top right; width: 24px; border: none; background: transparent; }" - "QComboBox::down-arrow { image: url(:/icons/chevron_down_dark.svg); width: 10px; height: 6px; }" - "QComboBox::down-arrow:on { top: 1px; }" - "QComboBox QAbstractItemView { background-color: #ffffff; color: #202020; border: 1px solid #b8b8b8; selection-background-color: #148aff; selection-color: #ffffff; }" - "QSpinBox, QDoubleSpinBox { background-color: #ffffff; color: #202020; border: 1px solid #b8b8b8; border-radius: 2px; padding: 2px 24px 2px 6px; selection-background-color: #148aff; selection-color: #ffffff; }" - "QSpinBox:disabled, QDoubleSpinBox:disabled { color: #8a8a8a; background-color: #f0f0f0; border-color: #d0d0d0; }" - "QSpinBox::up-button, QDoubleSpinBox::up-button { subcontrol-origin: border; subcontrol-position: top right; width: 18px; border: none; background: transparent; }" - "QSpinBox::down-button, QDoubleSpinBox::down-button { subcontrol-origin: border; subcontrol-position: bottom right; width: 18px; border: none; background: transparent; }" - "QSpinBox::up-arrow, QDoubleSpinBox::up-arrow { image: url(:/icons/chevron_up_dark.svg); width: 10px; height: 6px; }" - "QSpinBox::down-arrow, QDoubleSpinBox::down-arrow { image: url(:/icons/chevron_down_dark.svg); width: 10px; height: 6px; }" - "QCheckBox { color: #202020; spacing: 6px; }" - "QCheckBox:disabled { color: #8a8a8a; }" - "QRadioButton { color: #202020; spacing: 6px; }" - "QRadioButton:disabled { color: #8a8a8a; }" - "QGroupBox { color: #202020; border: 1px solid #c8c8c8; border-radius: 4px; margin-top: 0.5em; padding-top: 0.6em; }" - "QGroupBox:disabled { color: #9a9a9a; border-color: #d8d8d8; }" - "QGroupBox::title { subcontrol-origin: margin; subcontrol-position: top left; padding: 0 4px; color: #202020; }" - "QLabel { color: #202020; background: transparent; }" - "QLabel:disabled { color: #8a8a8a; }" - "QTableWidget, QTableView { alternate-background-color: #f2f2f2; background-color: #ffffff; border: none; color: #202020; gridline-color: #e0e0e0; }" - "QTableWidget:disabled, QTableView:disabled { color: #8a8a8a; background-color: #f0f0f0; }" - "QTableCornerButton::section { background-color: #ececec; border: 1px solid #d4d4d4; }" - "QTableView::item { border-left: 0.063em solid #ffffff; border-right: 0.063em solid #ffffff; padding-left: 0.313em; }" - "QTableView::item:selected { background-color: #148aff; color: #ffffff; }" - "QTableWidget#apps_list_table::item:selected, QTableView#apps_list_table::item:selected { background-color: rgba(84, 140, 188, 48); color: #202020; border-left: 0.063em solid rgba(84, 140, 188, 0); border-right: 0.063em solid rgba(84, 140, 188, 0); }" - "QTableView#apps_list_table QHeaderView::section, QTableWidget#apps_list_table QHeaderView::section { padding-top: 0; padding-bottom: 0; }" - "QTableView#trophy_app_table::item:selected, QTableView#trophy_table::item:selected { background-color: rgba(84, 140, 188, 48); color: #202020; border-left: 0.063em solid rgba(84, 140, 188, 0); border-right: 0.063em solid rgba(84, 140, 188, 0); }" - "QHeaderView::section { background-color: #ececec; color: #202020; padding-left: .5em; padding-right: .5em; padding-top: .4em; padding-bottom: -.1em; border: 0.063em solid #ffffff; }" - "QHeaderView::section:hover { background: #e0e0e0; }" - "QDockWidget { background: transparent; color: #202020; }" - "QDockWidget[floating=\"true\"] { background: #f5f5f5; }" - "QDockWidget::title { background: #ececec; border: none; padding-top: 0.2em; padding-left: 0.2em; }" - "QDockWidget::close-button, QDockWidget::float-button { background-color: #ececec; border: none; }" - "QDockWidget::close-button:hover, QDockWidget::float-button:hover { background-color: #e0e0e0; }" - "QPlainTextEdit#log_frame { background-color: #ffffff; color: #202020; }" - "QWidget#log_search_container { background-color: #ffffff; border: 1px solid #c8c8c8; border-radius: 4px; }" - "QWidget#toolbar_icon_size_container { background-color: #f0f0f0; }" - "QSlider#toolbar_icon_size_slider { background: transparent; }" - "QSlider#toolbar_icon_size_slider::groove:horizontal { background: transparent; height: 4px; border-radius: 2px; }" - "QSlider#toolbar_icon_size_slider::sub-page:horizontal { background: #7f7f7f; height: 4px; border-radius: 2px; }" - "QSlider#toolbar_icon_size_slider::add-page:horizontal { background: #c8c8c8; height: 4px; border-radius: 2px; }" - "QSlider#toolbar_icon_size_slider::handle:horizontal { background: #5b5b5b; width: 14px; margin: -5px 0; border-radius: 7px; }" - "QSlider#toolbar_icon_size_slider::handle:horizontal:hover { background: #404040; }" - "QLabel#log_level_fatal { color: #ff00ff; }" - "QLabel#log_level_error { color: #C02F1D; }" - "QLabel#log_level_warning { color: #BA8745; }" - "QLabel#log_level_notice { color: #000000; }" - "QLabel#log_level_trace { color: #808080; }" - "QTabWidget::pane { border: 1px solid #c8c8c8; background-color: #f5f5f5; }" - "QTabBar::tab { background-color: #e9e9e9; color: #202020; padding: 6px 14px; border: 1px solid #c8c8c8; border-bottom: none; border-top-left-radius: 3px; border-top-right-radius: 3px; }" - "QTabBar::tab:selected { background-color: #f5f5f5; }" - "QTabBar::tab:!selected { margin-top: 2px; }" - "QTabBar::tab:hover:!selected { background-color: #dfdfdf; }" - "QListWidget { background-color: #ffffff; color: #202020; border: 1px solid #c8c8c8; }" - "QListWidget:disabled { color: #8a8a8a; background-color: #f0f0f0; border-color: #d6d6d6; }" - "QListWidget::item:selected { background-color: #148aff; color: #ffffff; }" - "QListWidget::item:hover:!selected { background-color: #f2f2f2; }" - "QDialog[class=\"settings-window\"] QListWidget#settingsCategory { background-color: #ffffff; border: none; border-radius: 10px; padding: 6px; outline: none; }" - "QDialog[class=\"settings-window\"] QListWidget#settingsCategory::item { background: transparent; border: none; border-radius: 10px; padding: 8px 12px; margin: 0px; }" - "QDialog[class=\"settings-window\"] QListWidget#settingsCategory::item:selected," - "QDialog[class=\"settings-window\"] QListWidget#settingsCategory::item:selected:active," - "QDialog[class=\"settings-window\"] QListWidget#settingsCategory::item:selected:!active { background-color: #148aff; color: #ffffff; }" - "QDialog[class=\"settings-window\"] QListWidget#settingsCategory::item:hover:!selected { background-color: #f2f2f2; }" - "QDialog[class=\"settings-window\"] QTextBrowser#helpText { background-color: #ffffff; color: #202020; border: none; border-radius: 10px; padding: 3px 4px; }" - "QScrollBar:vertical { background: #f0f0f0; width: 14px; margin: 0; }" - "QScrollBar::handle:vertical { background: #b0b0b0; min-height: 20px; border-radius: 3px; margin: 2px; }" - "QScrollBar::handle:vertical:hover { background: #989898; }" - "QScrollBar::add-line:vertical, QScrollBar::sub-line:vertical { height: 0px; }" - "QScrollBar::add-page:vertical, QScrollBar::sub-page:vertical { background: none; }" - "QScrollBar:horizontal { background: #f0f0f0; height: 14px; margin: 0; }" - "QScrollBar::handle:horizontal { background: #b0b0b0; min-width: 20px; border-radius: 3px; margin: 2px; }" - "QScrollBar::handle:horizontal:hover { background: #989898; }" - "QScrollBar::add-line:horizontal, QScrollBar::sub-line:horizontal { width: 0px; }" - "QScrollBar::add-page:horizontal, QScrollBar::sub-page:horizontal { background: none; }" - "QSlider::groove:horizontal { background: #c8c8c8; height: 4px; border-radius: 2px; }" - "QSlider::handle:horizontal { background: #7f7f7f; width: 14px; margin: -5px 0; border-radius: 7px; }" - "QSlider::handle:horizontal:hover { background: #666666; }" - "QSlider::groove:vertical { background: #c8c8c8; width: 4px; border-radius: 2px; }" - "QSlider::handle:vertical { background: #7f7f7f; height: 14px; margin: 0 -5px; border-radius: 7px; }" - "QSlider::handle:vertical:hover { background: #666666; }" - "QProgressBar { background-color: #ffffff; color: #202020; border: 1px solid #c8c8c8; text-align: center; border-radius: 2px; }" - "QProgressBar::chunk { background-color: #148aff; border-radius: 2px; }" - "QSplitter::handle { background-color: #d4d4d4; }" - "QToolTip { background-color: #f7f7f7; color: #202020; border: 1px solid #c8c8c8; padding: 2px; }" - "QPlainTextEdit, QTextEdit, QTextBrowser { background-color: #ffffff; color: #202020; border: 1px solid #c8c8c8; }" - "QPlainTextEdit:disabled, QTextEdit:disabled, QTextBrowser:disabled { color: #8a8a8a; background-color: #f0f0f0; border-color: #d0d0d0; }" - "QDialogButtonBox QPushButton { min-width: 80px; }"); - -const QString dark_style_sheet( - "QWidget { color: #e0e0e0; background-color: #353535; }" - "QWidget:disabled { color: #777777; }" - "QMainWindow { background-color: #353535; }" - "QDialog { background-color: #353535; }" - "QMenuBar { background-color: #353535; color: #e0e0e0; }" - "QMenuBar::item { background: transparent; padding: 4px 8px; }" - "QMenuBar::item:selected { background: #4a4a4a; }" - "QMenu { background-color: #353535; color: #e0e0e0; border: 1px solid #555555; }" - "QMenu::item:selected { background-color: rgba(255, 255, 255, 0.12); color: #ffffff; }" - "QMenu::item:disabled { color: #808080; }" - "QMenu::separator { height: 1px; background: #555555; margin: 4px 8px; }" - "QToolBar#mw_toolbar { background-color: #353535; border: none; border-top: 1px solid #555555; }" - "QToolBar#mw_toolbar::separator { background-color: rgba(100, 100, 100, 235); width: 0.125em; margin-top: 0.250em; margin-bottom: 0.250em; }" - "QToolButton { color: #e0e0e0; background: transparent; border: 1px solid transparent; padding: 3px; }" - "QToolButton:hover { background-color: #4a4a4a; border: 1px solid #666666; }" - "QToolButton:pressed { background-color: rgba(255, 255, 255, 0.12); }" - "QToolButton:disabled { color: #808080; }" - "QToolButton#welcome_firmware_button { background-color: #353535; color: #e0e0e0; border: 1px solid #555555; border-radius: 2px; padding: 4px 32px 4px 12px; text-align: left; }" - "QToolButton#welcome_firmware_button:hover { background-color: #4a4a4a; border-color: #666666; }" - "QToolButton#welcome_firmware_button:pressed { background-color: rgba(255, 255, 255, 0.12); }" - "QToolButton#welcome_firmware_button::menu-button { subcontrol-origin: padding; subcontrol-position: top right; width: 24px; border-left: 1px solid #555555; border-top-right-radius: 2px; border-bottom-right-radius: 2px; background: #303030; }" - "QToolButton#welcome_firmware_button::menu-button:hover { background: #444444; border-left-color: #666666; }" - "QToolButton#welcome_firmware_button::menu-button:pressed { background: rgba(255, 255, 255, 0.12); }" - "QLabel#toolbar_icon_color { color: #f3f3f3; }" - "QPushButton { background-color: #353535; color: #e0e0e0; border: 1px solid #555555; border-radius: 2px; padding: 4px 16px; min-height: 1.2em; }" - "QPushButton:hover { background-color: #4a4a4a; border-color: #666666; }" - "QPushButton:pressed { background-color: rgba(255, 255, 255, 0.12); }" - "QPushButton:disabled { color: #808080; background-color: #303030; border-color: #444444; }" - "QPushButton:default { border-color: rgba(255, 255, 255, 0.24); }" - "QDialog#ControlsDialog QPushButton { background-color: #303030; color: #e0e0e0; border: none; border-radius: 6px; padding: 6px 12px; outline: none; }" - "QDialog#ControlsDialog QPushButton:hover { background-color: #3a3a3a; border: none; }" - "QDialog#ControlsDialog QPushButton:pressed { background-color: #464646; border: none; }" - "QDialog#ControlsDialog QPushButton:disabled { color: #808080; background-color: #2c2c2c; border: none; }" - "QDialog#ControlsDialog QPushButton[bindButton=\"true\"] { background-color: #2f2f2f; border-radius: 6px; padding: 6px 12px; }" - "QDialog#ControlsDialog QPushButton[bindButton=\"true\"]:hover { background-color: #393939; }" - "QDialog#ControlsDialog QPushButton[bindButton=\"true\"]:pressed { background-color: #454545; }" - "QDialog#ControlsDialog QPushButton[bindButton=\"true\"][captureActive=\"true\"] { background-color: rgba(255, 255, 255, 0.18); color: #ffffff; }" - "QDialog#ControlsDialog QPushButton[bindButton=\"true\"][captureActive=\"true\"]:hover { background-color: rgba(255, 255, 255, 0.24); }" - "QDialog#ControlsDialog QPushButton[bindButton=\"true\"][captureActive=\"true\"]:pressed { background-color: rgba(255, 255, 255, 0.30); }" - "QStatusBar#emulatorStatusBar { background-color: #353535; color: #e0e0e0; }" - "QStatusBar#emulatorStatusBar::item { border: none; }" - "QStatusBar#emulatorStatusBar QPushButton#status_button { border: 1px solid transparent; padding: 2px 8px; font-weight: bold; font-size: 11px; background: transparent; border-radius: 6px; color: #e0e0e0; }" - "QStatusBar#emulatorStatusBar QPushButton#status_button:hover { border: 1px solid #666666; background: rgba(255, 255, 255, 0.04); }" - "QStatusBar#emulatorStatusBar QPushButton#status_button:pressed { border: 1px solid #888888; background: rgba(255, 255, 255, 0.07); }" - "QStatusBar#emulatorStatusBar QPushButton#status_button[accent=\"renderer_vulkan\"] { color: #ff4444; }" - "QStatusBar#emulatorStatusBar QPushButton#status_button[accent=\"renderer_opengl\"] { color: #4488ff; }" - "QStatusBar#emulatorStatusBar QPushButton#status_button[accent=\"high\"] { color: #ff8800; }" - "QStatusBar#emulatorStatusBar QPushButton#status_button[accent=\"standard\"] { color: #aaaaaa; }" - "QStatusBar#emulatorStatusBar QPushButton#status_button[accent=\"neutral\"] { color: #cccccc; }" - "QStatusBar#emulatorStatusBar QPushButton#status_button[accent=\"audio\"] { color: #88dd44; }" - "QStatusBar#emulatorStatusBar QPushButton#status_button[accent=\"muted\"] { color: #777777; }" - "QStatusBar#emulatorStatusBar QPushButton#status_button[accent=\"update\"] { color: #6de089; }" - "QLineEdit { background-color: #2a2a2a; color: #e0e0e0; border: 1px solid #555555; border-radius: 2px; padding: 2px 4px; selection-background-color: rgba(255, 255, 255, 0.18); selection-color: #ffffff; }" - "QLineEdit:focus { border-color: rgba(255, 255, 255, 0.24); }" - "QLineEdit:disabled { color: #777777; background-color: #303030; border-color: #444444; }" - "QComboBox { background-color: #353535; color: #e0e0e0; border: 1px solid #555555; border-radius: 2px; padding: 2px 28px 2px 6px; }" - "QComboBox:hover { border-color: #666666; }" - "QComboBox:disabled { color: #777777; background-color: #303030; border-color: #444444; }" - "QComboBox::drop-down { subcontrol-origin: padding; subcontrol-position: top right; width: 24px; border: none; background: transparent; }" - "QComboBox::down-arrow { image: url(:/icons/chevron_down_light.svg); width: 10px; height: 6px; }" - "QComboBox::down-arrow:on { top: 1px; }" - "QComboBox QAbstractItemView { background-color: #2a2a2a; color: #e0e0e0; border: 1px solid #555555; selection-background-color: rgba(255, 255, 255, 0.12); selection-color: #ffffff; }" - "QSpinBox, QDoubleSpinBox { background-color: #2a2a2a; color: #e0e0e0; border: 1px solid #555555; border-radius: 2px; padding: 2px 24px 2px 6px; selection-background-color: rgba(255, 255, 255, 0.18); selection-color: #ffffff; }" - "QSpinBox:disabled, QDoubleSpinBox:disabled { color: #777777; background-color: #303030; border-color: #444444; }" - "QSpinBox::up-button, QDoubleSpinBox::up-button { subcontrol-origin: border; subcontrol-position: top right; width: 18px; border: none; background: transparent; }" - "QSpinBox::down-button, QDoubleSpinBox::down-button { subcontrol-origin: border; subcontrol-position: bottom right; width: 18px; border: none; background: transparent; }" - "QSpinBox::up-arrow, QDoubleSpinBox::up-arrow { image: url(:/icons/chevron_up_light.svg); width: 10px; height: 6px; }" - "QSpinBox::down-arrow, QDoubleSpinBox::down-arrow { image: url(:/icons/chevron_down_light.svg); width: 10px; height: 6px; }" - "QCheckBox { color: #e0e0e0; spacing: 6px; }" - "QCheckBox:disabled { color: #777777; }" - "QRadioButton { color: #e0e0e0; spacing: 6px; }" - "QRadioButton:disabled { color: #777777; }" - "QGroupBox { color: #e0e0e0; border: 1px solid #555555; border-radius: 4px; margin-top: 0.5em; padding-top: 0.6em; }" - "QGroupBox:disabled { color: #7f7f7f; border-color: #4a4a4a; }" - "QGroupBox::title { subcontrol-origin: margin; subcontrol-position: top left; padding: 0 4px; color: #e0e0e0; }" - "QLabel { color: #e0e0e0; background: transparent; }" - "QLabel:disabled { color: #777777; }" - "QTableWidget, QTableView { alternate-background-color: #3a3a3a; background-color: #2a2a2a; border: none; color: #e0e0e0; gridline-color: #444444; }" - "QTableWidget:disabled, QTableView:disabled { color: #777777; background-color: #303030; }" - "QTableCornerButton::section { background-color: #353535; border: 1px solid #555555; }" - "QTableView::item { border-left: 0.063em solid #2a2a2a; border-right: 0.063em solid #2a2a2a; padding-left: 0.313em; }" - "QTableView::item:selected { background-color: rgba(255, 255, 255, 0.12); color: #ffffff; }" - "QTableWidget#apps_list_table::item:selected, QTableView#apps_list_table::item:selected { background-color: rgba(255, 255, 255, 0.12); color: #f0f0f0; border-left: 0.063em solid rgba(255, 255, 255, 0); border-right: 0.063em solid rgba(255, 255, 255, 0); }" - "QTableView#apps_list_table QHeaderView::section, QTableWidget#apps_list_table QHeaderView::section { padding-top: 0; padding-bottom: 0; }" - "QTableView#trophy_app_table::item:selected, QTableView#trophy_table::item:selected { background-color: rgba(255, 255, 255, 0.13); color: #ffffff; border-left: 0.063em solid rgba(255, 255, 255, 0); border-right: 0.063em solid rgba(255, 255, 255, 0); }" - "QHeaderView::section { background-color: #353535; color: #ffffff; padding-left: .5em; padding-right: .5em; padding-top: .4em; padding-bottom: -.1em; border: 0.063em solid #2a2a2a; }" - "QHeaderView::section:hover { background: #4a4a4a; }" - "QDockWidget { background: transparent; color: #ffffff; }" - "QDockWidget[floating=\"true\"] { background: #353535; }" - "QDockWidget::title { background: #353535; border: none; padding-top: 0.2em; padding-left: 0.2em; }" - "QDockWidget::close-button, QDockWidget::float-button { background-color: #353535; border: none; }" - "QDockWidget::close-button:hover, QDockWidget::float-button:hover { background-color: #4a4a4a; }" - "QPlainTextEdit#log_frame { background-color: #2a2a2a; color: #ffffff; }" - "QWidget#log_search_container { background-color: #2a2a2a; border: 1px solid #555555; border-radius: 4px; }" - "QWidget#toolbar_icon_size_container { background-color: #353535; }" - "QSlider#toolbar_icon_size_slider { background: transparent; }" - "QSlider#toolbar_icon_size_slider::groove:horizontal { background: transparent; height: 4px; border-radius: 2px; }" - "QSlider#toolbar_icon_size_slider::sub-page:horizontal { background: rgba(255, 255, 255, 0.82); height: 4px; border-radius: 2px; }" - "QSlider#toolbar_icon_size_slider::add-page:horizontal { background: #555555; height: 4px; border-radius: 2px; }" - "QSlider#toolbar_icon_size_slider::handle:horizontal { background: #f3f3f3; width: 14px; margin: -5px 0; border-radius: 7px; }" - "QSlider#toolbar_icon_size_slider::handle:horizontal:hover { background: #ffffff; }" - "QLabel#log_level_fatal { color: #ff66ff; }" - "QLabel#log_level_error { color: #ff8080; }" - "QLabel#log_level_warning { color: #ffd27f; }" - "QLabel#log_level_notice { color: #ffffff; }" - "QLabel#log_level_trace { color: #ffffff; }" - "QTabWidget::pane { border: 1px solid #555555; background-color: #353535; }" - "QTabBar::tab { background-color: #2d2d2d; color: #e0e0e0; padding: 6px 14px; border: 1px solid #555555; border-bottom: none; border-top-left-radius: 3px; border-top-right-radius: 3px; }" - "QTabBar::tab:selected { background-color: #353535; }" - "QTabBar::tab:!selected { margin-top: 2px; }" - "QTabBar::tab:hover:!selected { background-color: #3a3a3a; }" - "QListWidget { background-color: #2a2a2a; color: #e0e0e0; border: 1px solid #555555; }" - "QListWidget:disabled { color: #777777; background-color: #303030; border-color: #444444; }" - "QListWidget::item:selected { background-color: rgba(255, 255, 255, 0.12); color: #ffffff; }" - "QListWidget::item:hover:!selected { background-color: #3a3a3a; }" - "QDialog[class=\"settings-window\"] QListWidget#settingsCategory { background-color: #2a2a2a; border: none; border-radius: 10px; padding: 6px; outline: none; }" - "QDialog[class=\"settings-window\"] QListWidget#settingsCategory::item { background: transparent; border: none; border-radius: 10px; padding: 8px 12px; margin: 0px; }" - "QDialog[class=\"settings-window\"] QListWidget#settingsCategory::item:selected," - "QDialog[class=\"settings-window\"] QListWidget#settingsCategory::item:selected:active," - "QDialog[class=\"settings-window\"] QListWidget#settingsCategory::item:selected:!active { background-color: rgba(255, 255, 255, 0.12); color: #ffffff; }" - "QDialog[class=\"settings-window\"] QListWidget#settingsCategory::item:hover:!selected { background-color: #3a3a3a; }" - "QDialog[class=\"settings-window\"] QTextBrowser#helpText { background-color: #2a2a2a; color: #e0e0e0; border: none; border-radius: 10px; padding: 3px 4px; }" - "QScrollBar:vertical { background: #2a2a2a; width: 14px; margin: 0; }" - "QScrollBar::handle:vertical { background: #5a5a5a; min-height: 20px; border-radius: 3px; margin: 2px; }" - "QScrollBar::handle:vertical:hover { background: #6a6a6a; }" - "QScrollBar::add-line:vertical, QScrollBar::sub-line:vertical { height: 0px; }" - "QScrollBar::add-page:vertical, QScrollBar::sub-page:vertical { background: none; }" - "QScrollBar:horizontal { background: #2a2a2a; height: 14px; margin: 0; }" - "QScrollBar::handle:horizontal { background: #5a5a5a; min-width: 20px; border-radius: 3px; margin: 2px; }" - "QScrollBar::handle:horizontal:hover { background: #6a6a6a; }" - "QScrollBar::add-line:horizontal, QScrollBar::sub-line:horizontal { width: 0px; }" - "QScrollBar::add-page:horizontal, QScrollBar::sub-page:horizontal { background: none; }" - "QSlider::groove:horizontal { background: transparent; height: 4px; border-radius: 2px; }" - "QSlider::sub-page:horizontal { background: rgba(255, 255, 255, 0.82); height: 4px; border-radius: 2px; }" - "QSlider::add-page:horizontal { background: #555555; height: 4px; border-radius: 2px; }" - "QSlider::handle:horizontal { background: #c0c0c0; width: 14px; margin: -5px 0; border-radius: 7px; }" - "QSlider::handle:horizontal:hover { background: #ffffff; }" - "QSlider::groove:vertical { background: transparent; width: 4px; border-radius: 2px; }" - "QSlider::sub-page:vertical { background: rgba(255, 255, 255, 0.82); width: 4px; border-radius: 2px; }" - "QSlider::add-page:vertical { background: #555555; width: 4px; border-radius: 2px; }" - "QSlider::handle:vertical { background: #c0c0c0; height: 14px; margin: 0 -5px; border-radius: 7px; }" - "QSlider::handle:vertical:hover { background: #ffffff; }" - "QProgressBar { background-color: #2a2a2a; color: #e0e0e0; border: 1px solid #555555; text-align: center; border-radius: 2px; }" - "QProgressBar::chunk { background-color: #6a6a6a; border-radius: 2px; }" - "QSplitter::handle { background-color: #555555; }" - "QToolTip { background-color: #353535; color: #e0e0e0; border: 1px solid #555555; padding: 2px; }" - "QPlainTextEdit, QTextEdit, QTextBrowser { background-color: #2a2a2a; color: #e0e0e0; border: 1px solid #555555; }" - "QPlainTextEdit:disabled, QTextEdit:disabled, QTextBrowser:disabled { color: #777777; background-color: #303030; border-color: #444444; }" - "QDialogButtonBox QPushButton { min-width: 80px; }"); - -} // namespace gui::stylesheets diff --git a/vita3k/gui-qt/src/theme_manager.cpp b/vita3k/gui-qt/src/theme_manager.cpp new file mode 100644 index 000000000..6ff2f12b0 --- /dev/null +++ b/vita3k/gui-qt/src/theme_manager.cpp @@ -0,0 +1,1066 @@ +// Vita3K emulator project +// Copyright (C) 2026 Vita3K team +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along +// with this program; if not, write to the Free Software Foundation, Inc., +// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +#include +#include +#include +#include + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +namespace { + +constexpr int default_vita_theme_readability = 55; +constexpr bool default_vita_theme_normalize_fonts = true; +constexpr int default_vita_theme_cycle_interval_seconds = 15; +constexpr int default_theme_bgm_volume = 50; + +const QPalette &fusion_palette() { + static const QPalette palette = []() { + if (QStyle *fusion = QStyleFactory::create(QStringLiteral("Fusion"))) { + const QPalette result = fusion->standardPalette(); + delete fusion; + return result; + } + + return QApplication::palette(); + }(); + + return palette; +} + +QString fallback_theme_name() { + return gui::DarkStylesheet; +} + +QString generated_vita_theme_name_for_id(const QString &theme_id) { + return QStringLiteral("vita-theme-%1").arg(theme_id); +} + +const gui::VitaThemeBackgroundOption *find_background_option( + const gui::VitaThemeInfo &theme, + const QString &background_id); + +QStringList sanitize_cycle_background_ids(const QStringList &ids, const QString &anchor_background_id) { + QStringList sanitized; + QSet seen; + + const auto append_unique = [&sanitized, &seen](const QString &id) { + if (!id.isEmpty() && !seen.contains(id)) { + sanitized.append(id); + seen.insert(id); + } + }; + + append_unique(anchor_background_id); + for (const QString &id : ids) + append_unique(id); + + return sanitized; +} + +gui::VitaThemeSelection sanitize_vita_theme_selection(gui::VitaThemeSelection selection) { + selection.readability = std::clamp(selection.readability, 0, 100); + selection.cycle_interval_seconds = std::clamp( + selection.cycle_interval_seconds, + 5, + 120); + selection.cycle_background_ids = sanitize_cycle_background_ids( + selection.cycle_background_ids, + selection.background_id); + return selection; +} + +QString resolve_theme_background_id( + const gui::VitaThemeInfo &theme, + const QString &background_id) { + if (theme.background_options.empty()) + return {}; + + if (const auto *background = find_background_option(theme, background_id)) + return QString::fromStdString(background->id); + + return QString::fromStdString(theme.background_options.front().id); +} + +QStringList canonicalize_cycle_background_ids( + const gui::VitaThemeInfo &theme, + const QStringList &background_ids, + const QString &primary_background_id) { + QStringList canonical_ids; + + if (theme.background_options.empty()) + return canonical_ids; + + if (background_ids.isEmpty()) { + for (const auto &background : theme.background_options) + canonical_ids.append(QString::fromStdString(background.id)); + } else { + QSet requested_ids; + for (const QString &id : background_ids) { + if (!id.isEmpty()) + requested_ids.insert(id); + } + + for (const auto &background : theme.background_options) { + const QString id = QString::fromStdString(background.id); + if (requested_ids.contains(id)) + canonical_ids.append(id); + } + } + + if (canonical_ids.isEmpty()) { + if (!primary_background_id.isEmpty()) { + canonical_ids.append(primary_background_id); + } else { + canonical_ids.append(QString::fromStdString(theme.background_options.front().id)); + } + } + + if (!primary_background_id.isEmpty()) { + canonical_ids.removeAll(primary_background_id); + canonical_ids.prepend(primary_background_id); + } + + return canonical_ids; +} + +gui::VitaThemeSelection normalize_selection( + const gui::VitaThemeInfo &theme, + gui::VitaThemeSelection selection) { + selection = sanitize_vita_theme_selection(std::move(selection)); + selection.theme_id = QString::fromStdString(theme.theme_id); + + if (theme.background_options.empty()) { + selection.background_id.clear(); + selection.cycle_background_ids.clear(); + selection.cycle_enabled = false; + return selection; + } + + selection.background_id = resolve_theme_background_id(theme, selection.background_id); + selection.cycle_background_ids = canonicalize_cycle_background_ids( + theme, + selection.cycle_background_ids, + selection.background_id); + return selection; +} + +gui::ThemeEntry generated_theme_entry( + const QString &theme_id, + const QString &display_name, + const fs::path &qss_path) { + const QFileInfo info(gui::utils::to_qt_path(qss_path)); + return gui::ThemeEntry{ + generated_vita_theme_name_for_id(theme_id), + display_name, + info.absoluteFilePath(), + info.absolutePath(), + }; +} + +gui::ThemeBackgroundPresentation build_background_presentation( + const gui::VitaThemeInfo &theme, + const gui::VitaThemeSelection &selection) { + const gui::VitaThemeSelection canonical_selection = normalize_selection(theme, selection); + + gui::ThemeBackgroundPresentation presentation; + presentation.interval_ms = std::clamp(canonical_selection.cycle_interval_seconds, 5, 120) * 1000; + presentation.animated = canonical_selection.cycle_enabled; + + QSet added_paths; + for (const QString &background_id : canonical_selection.cycle_background_ids) { + const auto *background = find_background_option(theme, background_id); + if (!background) + continue; + + const QString path = QDir::fromNativeSeparators(gui::utils::to_qt_path(background->asset_path)); + if (!path.isEmpty() && !added_paths.contains(path)) { + presentation.image_paths.append(path); + added_paths.insert(path); + } + } + + if (presentation.image_paths.isEmpty()) { + if (const auto *background = find_background_option(theme, canonical_selection.background_id)) + presentation.image_paths.append(QDir::fromNativeSeparators(gui::utils::to_qt_path(background->asset_path))); + } + + if (!presentation.animated && presentation.image_paths.size() > 1) + presentation.image_paths = QStringList{ presentation.image_paths.front() }; + + if (presentation.image_paths.size() < 2) + presentation.animated = false; + + return presentation; +} + +std::vector to_fs_paths(const QStringList &paths) { + std::vector result; + result.reserve(static_cast(paths.size())); + for (const QString &path : paths) { + if (!path.isEmpty()) + result.push_back(gui::utils::to_fs_path(path)); + } + return result; +} + +bool is_qrc_stylesheet_url(const QString &url) { + return url.startsWith(QStringLiteral(":/")) + || url.startsWith(QStringLiteral("qrc:/")); +} + +QJsonObject serialize_vita_theme_selection(const gui::VitaThemeSelection &selection) { + QJsonObject object; + object.insert(QStringLiteral("themeId"), selection.theme_id); + object.insert(QStringLiteral("backgroundId"), selection.background_id); + object.insert(QStringLiteral("readability"), selection.readability); + object.insert(QStringLiteral("normalizeFontColors"), selection.normalize_font_colors); + object.insert(QStringLiteral("cycleEnabled"), selection.cycle_enabled); + object.insert(QStringLiteral("cycleIntervalSeconds"), selection.cycle_interval_seconds); + + QJsonArray cycle_background_ids; + for (const QString &id : selection.cycle_background_ids) + cycle_background_ids.append(id); + object.insert(QStringLiteral("cycleBackgroundIds"), cycle_background_ids); + return object; +} + +std::optional deserialize_vita_theme_selection(const QString &serialized) { + if (serialized.isEmpty()) + return std::nullopt; + + QJsonParseError error; + const QJsonDocument document = QJsonDocument::fromJson(serialized.toUtf8(), &error); + if (error.error != QJsonParseError::NoError || !document.isObject()) + return std::nullopt; + + const QJsonObject object = document.object(); + gui::VitaThemeSelection selection; + selection.theme_id = object.value(QStringLiteral("themeId")).toString(); + selection.background_id = object.value(QStringLiteral("backgroundId")).toString(); + selection.readability = object.contains(QStringLiteral("readability")) + ? object.value(QStringLiteral("readability")).toInt(default_vita_theme_readability) + : default_vita_theme_readability; + selection.normalize_font_colors = object.contains(QStringLiteral("normalizeFontColors")) + ? object.value(QStringLiteral("normalizeFontColors")).toBool(default_vita_theme_normalize_fonts) + : default_vita_theme_normalize_fonts; + selection.cycle_enabled = object.contains(QStringLiteral("cycleEnabled")) + ? object.value(QStringLiteral("cycleEnabled")).toBool(false) + : false; + selection.cycle_interval_seconds = object.contains(QStringLiteral("cycleIntervalSeconds")) + ? object.value(QStringLiteral("cycleIntervalSeconds")).toInt(default_vita_theme_cycle_interval_seconds) + : default_vita_theme_cycle_interval_seconds; + + const QJsonArray cycle_background_ids = object.value(QStringLiteral("cycleBackgroundIds")).toArray(); + for (const QJsonValue &id : cycle_background_ids) { + const QString value = id.toString(); + if (!value.isEmpty()) + selection.cycle_background_ids.append(value); + } + + if (selection.theme_id.isEmpty()) + return std::nullopt; + + return sanitize_vita_theme_selection(std::move(selection)); +} + +std::optional load_vita_theme_selection( + const std::shared_ptr &gui_settings, + const GuiSave &key) { + if (!gui_settings) + return std::nullopt; + + return deserialize_vita_theme_selection(gui_settings->get_value(key).toString()); +} + +void save_vita_theme_selection( + const std::shared_ptr &gui_settings, + const GuiSave &key, + const gui::VitaThemeSelection &selection, + const bool sync) { + if (!gui_settings) + return; + + const QString serialized = QString::fromUtf8( + QJsonDocument(serialize_vita_theme_selection(selection)).toJson(QJsonDocument::Compact)); + gui_settings->set_value(key, serialized, false); + if (sync) + gui_settings->sync(); +} + +void clear_vita_theme_selection( + const std::shared_ptr &gui_settings, + const GuiSave &key, + const bool sync) { + if (!gui_settings) + return; + + gui_settings->remove_value(key, false); + if (sync) + gui_settings->sync(); +} + +const gui::VitaThemeBackgroundOption *find_background_option( + const gui::VitaThemeInfo &theme, + const QString &background_id) { + const auto it = std::find_if(theme.background_options.begin(), theme.background_options.end(), + [&background_id](const gui::VitaThemeBackgroundOption &background) { + return QString::fromStdString(background.id) == background_id; + }); + if (it == theme.background_options.end()) + return nullptr; + + return &(*it); +} + +struct ThemeBackgroundMetadata { + QStringList image_urls; + int interval_seconds = default_vita_theme_cycle_interval_seconds; + std::optional animated; +}; + +std::optional parse_theme_background_metadata(const QString &stylesheet) { + const qsizetype start = stylesheet.indexOf(QStringLiteral("/*!VITA3K_THEME")); + if (start < 0) + return std::nullopt; + + const qsizetype end = stylesheet.indexOf(QStringLiteral("*/"), start); + if (end < 0) + return std::nullopt; + + ThemeBackgroundMetadata metadata; + const QString block = stylesheet.mid(start, end - start); + const QStringList lines = block.split(QLatin1Char('\n')); + for (const QString &raw_line : lines) { + QString line = raw_line.trimmed(); + if (line == QStringLiteral("/*!VITA3K_THEME")) + continue; + + if (line.startsWith(QLatin1Char('*'))) { + line.remove(0, 1); + line = line.trimmed(); + } + + if (line.isEmpty()) + continue; + + const qsizetype colon = line.indexOf(QLatin1Char(':')); + if (colon < 0) + continue; + + const QString key = line.first(colon).trimmed(); + const QString value = line.sliced(colon + 1).trimmed(); + if (key == QStringLiteral("background-images")) { + const QStringList images = value.split(QLatin1Char(','), Qt::SkipEmptyParts); + for (const QString &image : images) { + const QString trimmed_image = image.trimmed(); + if (!trimmed_image.isEmpty()) + metadata.image_urls.append(trimmed_image); + } + } else if (key == QStringLiteral("background-interval")) { + metadata.interval_seconds = value.toInt(); + } else if (key == QStringLiteral("background-animated")) { + const QString normalized = value.toLower(); + if (normalized == QStringLiteral("true")) + metadata.animated = true; + else if (normalized == QStringLiteral("false")) + metadata.animated = false; + } + } + + metadata.image_urls.removeAll(QString()); + if (metadata.image_urls.isEmpty()) + return std::nullopt; + + return metadata; +} + +} // namespace + +gui::VitaThemeSelection gui::normalize_vita_theme_selection( + const gui::VitaThemeInfo &theme, + gui::VitaThemeSelection selection) { + return normalize_selection(theme, std::move(selection)); +} + +ThemeManager::ThemeManager(std::shared_ptr gui_settings, + QObject *parent) + : QObject(parent) + , m_gui_settings(std::move(gui_settings)) + , m_vita_theme_bgm_audio_output(new QAudioOutput(this)) + , m_vita_theme_bgm_player(new QMediaPlayer(this)) { + m_vita_theme_bgm_player->setAudioOutput(m_vita_theme_bgm_audio_output); + m_vita_theme_bgm_player->setLoops(QMediaPlayer::Infinite); +} + +void ThemeManager::set_pref_path(const fs::path &pref_path) { + m_pref_path = pref_path; +} + +QList ThemeManager::available_themes() const { + QList themes = { + *builtin_theme(gui::LightStylesheet), + *builtin_theme(gui::DarkStylesheet), + }; + + if (const auto generated = generated_vita_theme()) + themes.append(*generated); + + for (const auto &theme : custom_themes()) + themes.append(theme); + + return themes; +} + +bool ThemeManager::apply_theme(const QString &name, const bool force_reload) { + const QString requested_name = name.isEmpty() ? requested_theme_name() : name; + if (requested_name.isEmpty()) { + LOG_ERROR("No GUI theme is configured"); + return false; + } + + auto try_apply = [this, force_reload](const QString &theme_name, const bool log_fallback_context) -> bool { + const auto theme = resolve_theme(theme_name); + if (!theme) { + if (log_fallback_context) + LOG_ERROR("Could not resolve GUI theme '{}'", theme_name.toUtf8().constData()); + return false; + } + + const auto stylesheet = load_stylesheet(*theme); + if (!stylesheet) { + if (log_fallback_context) { + LOG_ERROR("Could not load GUI theme '{}' from {}", + theme->name.toUtf8().constData(), + theme->qss_source.toUtf8().constData()); + } + return false; + } + + return apply_theme_entry(*theme, *stylesheet, force_reload); + }; + + if (try_apply(requested_name, true)) + return true; + + const QString fallback_name = fallback_theme_name(); + if (requested_name == fallback_name) + return false; + + LOG_WARN("Falling back to packaged GUI theme '{}'", fallback_name.toUtf8().constData()); + return try_apply(fallback_name, false); +} + +void ThemeManager::set_vita_themes_root(const fs::path &themes_root) { + if (m_vita_themes_root != themes_root) + refresh_vita_theme_catalog(); + m_vita_themes_root = themes_root; +} + +void ThemeManager::refresh_vita_theme_catalog() { + m_installed_vita_theme_cache.clear(); + m_vita_theme_cache_loaded = false; +} + +const std::vector &ThemeManager::installed_vita_themes(const bool force_reload) const { + if (force_reload || !m_vita_theme_cache_loaded) { + m_installed_vita_theme_cache = m_vita_themes_root.empty() + ? std::vector{} + : gui::enumerate_installed_vita_themes(m_vita_themes_root); + m_vita_theme_cache_loaded = true; + } + + return m_installed_vita_theme_cache; +} + +void ThemeManager::ensure_generated_theme_ready(const fs::path &themes_root) { + if (!m_gui_settings) + return; + + set_vita_themes_root(themes_root); + + const auto selection = applied_vita_theme_selection(); + if (!selection) + return; + + const QString expected_theme_name = generated_vita_theme_name_for_id(selection->theme_id); + if (requested_theme_name() != expected_theme_name) + return; + + const auto *theme = find_installed_vita_theme(selection->theme_id); + if (!theme) { + switch_to_fallback_theme(tr("the installed Vita theme \"%1\" could not be found").arg(selection->theme_id)); + return; + } + + const auto *background = find_background_option(*theme, selection->background_id); + if (!background) { + switch_to_fallback_theme(tr("the selected Vita background \"%1\" is no longer available").arg(selection->background_id)); + return; + } + + const auto background_presentation = build_background_presentation(*theme, *selection); + const auto synthesized = gui::synthesize_vita_theme_qss( + *theme, + background->id, + selection->readability, + selection->normalize_font_colors, + to_fs_paths(background_presentation.image_paths), + std::clamp(background_presentation.interval_ms / 1000, 5, 120), + background_presentation.animated, + m_pref_path, + gui::utils::to_fs_path(m_gui_settings->get_settings_dir())); + if (!synthesized) { + switch_to_fallback_theme(tr("the generated Vita theme stylesheet could not be rebuilt")); + return; + } +} + +std::optional ThemeManager::applied_vita_theme_selection() const { + auto selection = load_vita_theme_selection(m_gui_settings, gui::vt_appliedSelection); + if (!selection) + return std::nullopt; + + if (const auto *theme = find_installed_vita_theme(selection->theme_id)) + return gui::normalize_vita_theme_selection(*theme, std::move(*selection)); + + return sanitize_vita_theme_selection(std::move(*selection)); +} + +bool ThemeManager::apply_vita_theme_selection( + const gui::VitaThemeSelection &selection, + const bool force_reload) { + if (!m_gui_settings || m_vita_themes_root.empty()) { + LOG_ERROR("Cannot apply Vita theme selection without GUI settings and a theme root"); + return false; + } + + const auto *theme = find_installed_vita_theme(selection.theme_id); + if (!theme) { + LOG_ERROR("Could not find installed Vita theme '{}'", selection.theme_id.toUtf8().constData()); + return false; + } + + gui::VitaThemeSelection normalized_selection = gui::normalize_vita_theme_selection(*theme, selection); + if (normalized_selection.background_id.isEmpty()) { + LOG_ERROR("Could not resolve a background for Vita theme '{}'", + normalized_selection.theme_id.toUtf8().constData()); + return false; + } + + const auto *background = find_background_option(*theme, normalized_selection.background_id); + if (!background) { + LOG_ERROR("Could not find background '{}' for Vita theme '{}'", + normalized_selection.background_id.toUtf8().constData(), + normalized_selection.theme_id.toUtf8().constData()); + return false; + } + + const auto background_presentation = build_background_presentation(*theme, normalized_selection); + const auto synthesized = gui::synthesize_vita_theme_qss( + *theme, + background->id, + normalized_selection.readability, + normalized_selection.normalize_font_colors, + to_fs_paths(background_presentation.image_paths), + std::clamp(background_presentation.interval_ms / 1000, 5, 120), + background_presentation.animated, + m_pref_path, + gui::utils::to_fs_path(m_gui_settings->get_settings_dir())); + if (!synthesized) + return false; + + save_applied_selection(normalized_selection); + m_gui_settings->set_value( + gui::m_currentStylesheet, + generated_vita_theme_name_for_id(normalized_selection.theme_id), + false); + m_gui_settings->sync(); + Q_EMIT theme_catalog_changed(); + + const gui::ThemeEntry generated_theme = generated_theme_entry( + normalized_selection.theme_id, + generated_vita_theme_display_name(normalized_selection.theme_id), + *synthesized); + + return apply_theme_entry(generated_theme, force_reload); +} + +void ThemeManager::save_applied_selection(const gui::VitaThemeSelection &selection) const { + gui::VitaThemeSelection persisted_selection = sanitize_vita_theme_selection(selection); + if (const auto *theme = find_installed_vita_theme(persisted_selection.theme_id)) + persisted_selection = gui::normalize_vita_theme_selection(*theme, std::move(persisted_selection)); + + save_vita_theme_selection( + m_gui_settings, + gui::vt_appliedSelection, + persisted_selection, + false); +} + +void ThemeManager::clear_applied_vita_theme() { + const bool had_selection = applied_vita_theme_selection().has_value(); + clear_vita_theme_selection(m_gui_settings, gui::vt_appliedSelection, false); + if (m_gui_settings) + m_gui_settings->sync(); + m_current_theme_name.clear(); + m_applied_theme_state.reset(); + refresh_vita_theme_bgm(); + if (had_selection) { + Q_EMIT theme_catalog_changed(); + Q_EMIT theme_state_changed(); + } +} + +void ThemeManager::switch_to_fallback_theme(const QString &reason) { + const QString fallback_name = fallback_theme_name(); + LOG_WARN("Falling back to packaged GUI theme '{}' because {}", + fallback_name.toUtf8().constData(), + reason.toUtf8().constData()); + + clear_applied_vita_theme(); + if (!m_gui_settings) + return; + + m_gui_settings->set_value(gui::m_currentStylesheet, fallback_name, false); + m_gui_settings->sync(); +} + +QString ThemeManager::requested_theme_name() const { + if (!m_gui_settings) + return {}; + + return m_gui_settings->get_value(gui::m_currentStylesheet).toString(); +} + +std::optional ThemeManager::builtin_theme(const QString &name) const { + if (name == gui::LightStylesheet) { + return gui::ThemeEntry{ + gui::LightStylesheet, + tr("Light", "Stylesheets"), + QStringLiteral(":/themes/light/light.qss"), + QStringLiteral(":/themes/light"), + }; + } + + if (name == gui::DarkStylesheet) { + return gui::ThemeEntry{ + gui::DarkStylesheet, + tr("Dark", "Stylesheets"), + QStringLiteral(":/themes/dark/dark.qss"), + QStringLiteral(":/themes/dark"), + }; + } + + return std::nullopt; +} + +std::optional ThemeManager::generated_vita_theme() const { + const auto selection = applied_vita_theme_selection(); + if (!selection) + return std::nullopt; + + const fs::path path = generated_vita_theme_path(selection->theme_id); + const QFileInfo info(gui::utils::to_qt_path(path)); + if (!info.exists() || !info.isFile()) + return std::nullopt; + + return generated_theme_entry( + selection->theme_id, + generated_vita_theme_display_name(selection->theme_id), + path); +} + +const gui::VitaThemeInfo *ThemeManager::find_installed_vita_theme( + const QString &theme_id, + const bool force_reload) const { + const auto &themes = installed_vita_themes(force_reload); + const auto it = std::find_if(themes.begin(), themes.end(), + [&theme_id](const gui::VitaThemeInfo &theme) { + return QString::fromStdString(theme.theme_id) == theme_id; + }); + return (it != themes.end()) ? &(*it) : nullptr; +} + +QList ThemeManager::custom_themes() const { + QList themes; + if (!m_gui_settings) + return themes; + + const QStringList name_filter = { QStringLiteral("*.qss") }; + QDirIterator it(m_gui_settings->get_settings_dir(), name_filter, QDir::Files); + while (it.hasNext()) { + it.next(); + + const QString base_name = it.fileInfo().completeBaseName(); + if (base_name == gui::LightStylesheet || base_name == gui::DarkStylesheet) + continue; + + themes.append({ + base_name, + base_name, + it.fileInfo().absoluteFilePath(), + it.fileInfo().absolutePath(), + }); + } + + return themes; +} + +std::optional ThemeManager::resolve_theme(const QString &name) const { + if (const auto builtin = builtin_theme(name)) + return builtin; + + if (const auto generated = generated_vita_theme(); generated && generated->name == name) + return generated; + + const auto themes = custom_themes(); + for (const auto &theme : themes) { + if (theme.name == name) + return theme; + } + + return std::nullopt; +} + +std::optional ThemeManager::current_applied_theme_state() const { + return m_applied_theme_state; +} + +std::optional ThemeManager::build_applied_theme_state(const gui::ThemeEntry &theme, const QString &stylesheet) const { + gui::AppliedThemeState state{ + false, + {}, + }; + + const auto metadata = parse_theme_background_metadata(stylesheet); + if (metadata) { + QSet added_paths; + for (const QString &image_url : metadata->image_urls) { + const QString resolved_path = resolve_stylesheet_url(image_url, theme); + if (!resolved_path.isEmpty() && !added_paths.contains(resolved_path)) { + state.background.image_paths.append(resolved_path); + added_paths.insert(resolved_path); + } + } + + if (!state.background.image_paths.isEmpty()) { + state.background.interval_ms = std::clamp(metadata->interval_seconds, 5, 120) * 1000; + state.background.animated = metadata->animated.value_or(state.background.image_paths.size() > 1); + if (!state.background.animated && state.background.image_paths.size() > 1) + state.background.image_paths = QStringList{ state.background.image_paths.front() }; + if (state.background.image_paths.size() < 2) + state.background.animated = false; + } + } + + if (m_vita_themes_root.empty()) + return state; + + const auto selection = applied_vita_theme_selection(); + if (!selection || theme.name != generated_vita_theme_name_for_id(selection->theme_id)) + return state; + + state.is_vita_theme = true; + return state; +} + +bool ThemeManager::apply_theme_entry(const gui::ThemeEntry &theme, const bool force_reload) { + const auto stylesheet = load_stylesheet(theme); + if (!stylesheet) + return false; + + return apply_theme_entry(theme, *stylesheet, force_reload); +} + +bool ThemeManager::apply_theme_entry(const gui::ThemeEntry &theme, const QString &stylesheet, const bool force_reload) { + if (!force_reload && m_current_theme_name == theme.name) + return true; + + const auto palette = parse_palette(stylesheet); + const QString resolved_stylesheet = resolve_stylesheet_urls(stylesheet, theme); + + qApp->setStyleSheet(QString()); + ensure_fusion_style(); + QApplication::setPalette(palette.value_or(fusion_palette())); + + LOG_INFO("Loaded stylesheet '{}' from {}", theme.name.toUtf8().constData(), theme.qss_source.toUtf8().constData()); + qApp->setStyleSheet(resolved_stylesheet); + gui::utils::invalidate_theme_cache(); + m_current_theme_name = theme.name; + m_applied_theme_state = build_applied_theme_state(theme, stylesheet); + refresh_vita_theme_bgm(); + Q_EMIT theme_state_changed(); + return true; +} + +void ThemeManager::refresh_vita_theme_bgm() { + if (!m_vita_theme_bgm_audio_output || !m_vita_theme_bgm_player || m_vita_themes_root.empty()) { + stop_vita_theme_bgm(); + return; + } + + const bool bgm_enabled = m_gui_settings + ? m_gui_settings->get_value(gui::vt_bgmEnabled).toBool() + : false; + const auto selection = applied_vita_theme_selection(); + if (!bgm_enabled || m_vita_theme_bgm_blocked || !selection + || !m_applied_theme_state || !m_applied_theme_state->is_vita_theme) { + stop_vita_theme_bgm(); + return; + } + + const auto *theme = find_installed_vita_theme(selection->theme_id); + if (!theme || !theme->bgm_path || theme->bgm_path->empty()) { + stop_vita_theme_bgm(); + return; + } + + const QString source_path = gui::utils::to_qt_path(*theme->bgm_path); + const int volume = std::clamp( + m_gui_settings && m_gui_settings->contains_value(gui::vt_bgmVolume) + ? m_gui_settings->get_value(gui::vt_bgmVolume).toInt() + : default_theme_bgm_volume, + 0, 100); + m_vita_theme_bgm_audio_output->setVolume(static_cast(volume) / 100.0f); + + if (m_current_vita_theme_bgm_source_path != source_path) { + m_current_vita_theme_bgm_source_path = source_path; + m_vita_theme_bgm_player->stop(); + m_vita_theme_bgm_player->setSource(QUrl::fromLocalFile(source_path)); + } + + if (m_vita_theme_bgm_player->playbackState() != QMediaPlayer::PlayingState) { + LOG_INFO("Starting Vita theme BGM playback from {}", source_path.toUtf8().constData()); + m_vita_theme_bgm_player->play(); + } +} + +void ThemeManager::set_vita_theme_bgm_blocked(const bool blocked) { + if (m_vita_theme_bgm_blocked == blocked) + return; + + m_vita_theme_bgm_blocked = blocked; + refresh_vita_theme_bgm(); +} + +void ThemeManager::stop_vita_theme_bgm() { + if (!m_vita_theme_bgm_player) + return; + + if (m_vita_theme_bgm_player->playbackState() != QMediaPlayer::StoppedState) + m_vita_theme_bgm_player->stop(); + + if (!m_current_vita_theme_bgm_source_path.isEmpty()) { + m_vita_theme_bgm_player->setSource(QUrl()); + m_current_vita_theme_bgm_source_path.clear(); + } +} + +std::optional ThemeManager::load_stylesheet(const gui::ThemeEntry &theme) const { + QFile file(theme.qss_source); + if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) + return std::nullopt; + + return QString::fromUtf8(file.readAll()); +} + +std::optional ThemeManager::parse_palette(const QString &stylesheet) const { + const qsizetype start = stylesheet.indexOf(QStringLiteral("/*!PALETTE")); + if (start < 0) + return std::nullopt; + + const qsizetype end = stylesheet.indexOf(QStringLiteral("*/"), start); + if (end < 0) + return std::nullopt; + + QPalette palette = fusion_palette(); + const QString block = stylesheet.mid(start, end - start); + const QStringList lines = block.split(QLatin1Char('\n')); + for (const QString &raw_line : lines) { + const QString line = raw_line.trimmed(); + if (!line.startsWith(QStringLiteral("* "))) + continue; + + const QString entry = line.mid(2); + const qsizetype colon = entry.indexOf(QLatin1Char(':')); + if (colon < 0) + continue; + + const QString key = entry.first(colon).trimmed(); + const QString value = entry.sliced(colon + 1).trimmed(); + if (key == QStringLiteral("dark")) + continue; + + const QColor color(value); + if (!color.isValid()) { + LOG_WARN("Invalid palette color '{}' for key '{}'", value.toUtf8().constData(), key.toUtf8().constData()); + continue; + } + + if (key == QStringLiteral("window")) + palette.setColor(QPalette::Window, color); + else if (key == QStringLiteral("window-text")) + palette.setColor(QPalette::WindowText, color); + else if (key == QStringLiteral("base")) + palette.setColor(QPalette::Base, color); + else if (key == QStringLiteral("alternate-base")) + palette.setColor(QPalette::AlternateBase, color); + else if (key == QStringLiteral("tooltip-base")) + palette.setColor(QPalette::ToolTipBase, color); + else if (key == QStringLiteral("tooltip-text")) + palette.setColor(QPalette::ToolTipText, color); + else if (key == QStringLiteral("text")) + palette.setColor(QPalette::Text, color); + else if (key == QStringLiteral("button")) + palette.setColor(QPalette::Button, color); + else if (key == QStringLiteral("button-text")) + palette.setColor(QPalette::ButtonText, color); + else if (key == QStringLiteral("bright-text")) + palette.setColor(QPalette::BrightText, color); + else if (key == QStringLiteral("link")) + palette.setColor(QPalette::Link, color); + else if (key == QStringLiteral("highlight")) + palette.setColor(QPalette::Highlight, color); + else if (key == QStringLiteral("inactive-highlight")) + palette.setColor(QPalette::Inactive, QPalette::Highlight, color); + else if (key == QStringLiteral("disabled-highlight")) + palette.setColor(QPalette::Disabled, QPalette::Highlight, color); + else if (key == QStringLiteral("highlight-text")) + palette.setColor(QPalette::HighlightedText, color); + else if (key == QStringLiteral("inactive-highlight-text")) + palette.setColor(QPalette::Inactive, QPalette::HighlightedText, color); + else if (key == QStringLiteral("disabled-highlight-text")) + palette.setColor(QPalette::Disabled, QPalette::HighlightedText, color); + else if (key == QStringLiteral("placeholder-text")) + palette.setColor(QPalette::PlaceholderText, color); + else if (key == QStringLiteral("disabled-button-text")) + palette.setColor(QPalette::Disabled, QPalette::ButtonText, color); + else if (key == QStringLiteral("disabled-window-text")) + palette.setColor(QPalette::Disabled, QPalette::WindowText, color); + else if (key == QStringLiteral("disabled-text")) + palette.setColor(QPalette::Disabled, QPalette::Text, color); + else + LOG_WARN("Unknown palette key '{}'", key.toUtf8().constData()); + } + + return palette; +} + +QString ThemeManager::resolve_stylesheet_urls(const QString &stylesheet, const gui::ThemeEntry &theme) const { + static const QRegularExpression url_pattern( + QStringLiteral(R"(url\(\s*(['"]?)([^'")]+)\1\s*\))")); + + QString resolved; + resolved.reserve(stylesheet.size()); + + qsizetype last_end = 0; + auto it = url_pattern.globalMatch(stylesheet); + while (it.hasNext()) { + const QRegularExpressionMatch match = it.next(); + const qsizetype start = match.capturedStart(0); + const qsizetype finish = match.capturedEnd(0); + resolved.append(stylesheet.mid(last_end, start - last_end)); + resolved.append(QStringLiteral("url(\"%1\")").arg(resolve_stylesheet_url(match.captured(2), theme))); + last_end = finish; + } + + resolved.append(stylesheet.mid(last_end)); + return resolved; +} + +QString ThemeManager::resolve_stylesheet_url(const QString &url, const gui::ThemeEntry &theme) const { + QString normalized = url.trimmed(); + normalized.replace(QLatin1Char('\\'), QLatin1Char('/')); + if (normalized.isEmpty()) + return normalized; + + const bool qrc_theme = theme.base_path.startsWith(QStringLiteral(":/")) + || theme.base_path.startsWith(QStringLiteral("qrc:/")); + if (is_qrc_stylesheet_url(normalized)) + return normalized; + + const QString relative_url = gui::utils::sanitize_relative_path_reference(normalized); + if (relative_url.isEmpty()) + return {}; + + if (qrc_theme) { + const QString base_path = theme.base_path.endsWith(QLatin1Char('/')) + ? theme.base_path.chopped(1) + : theme.base_path; + return base_path + QStringLiteral("/") + relative_url; + } + + const QString theme_relative = gui::utils::resolve_relative_path_in_root(relative_url, theme.base_path); + if (theme_relative.isEmpty()) + return {}; + if (QFileInfo(theme_relative).exists() || m_pref_path.empty()) + return theme_relative; + + const QString pref_relative = gui::utils::resolve_relative_path_in_root( + relative_url, + gui::utils::to_qt_path(m_pref_path)); + if (!pref_relative.isEmpty() && QFileInfo(pref_relative).exists()) + return pref_relative; + + return theme_relative; +} + +fs::path ThemeManager::generated_vita_theme_path(const QString &theme_id) const { + if (!m_gui_settings || theme_id.isEmpty()) + return {}; + + return gui::utils::to_fs_path(m_gui_settings->get_settings_dir()) + / "vita-themes" / gui::utils::to_fs_path(theme_id) / "generated.qss"; +} + +QString ThemeManager::generated_vita_theme_display_name(const QString &theme_id) const { + if (!theme_id.isEmpty() && !m_vita_themes_root.empty()) { + const auto *theme = find_installed_vita_theme(theme_id); + if (theme) + return tr("Vita Theme: %1").arg(QString::fromStdString(theme->title)); + } + + return tr("Vita Theme: %1").arg(theme_id.isEmpty() ? tr("Generated") : theme_id); +} + +void ThemeManager::ensure_fusion_style() const { + if (const QStyle *style = QApplication::style(); style && style->name().compare(QStringLiteral("Fusion"), Qt::CaseInsensitive) == 0) + return; + + if (QStyle *fusion = QStyleFactory::create(QStringLiteral("Fusion"))) + QApplication::setStyle(fusion); +} diff --git a/vita3k/gui-qt/src/theme_surface.cpp b/vita3k/gui-qt/src/theme_surface.cpp new file mode 100644 index 000000000..b4579863c --- /dev/null +++ b/vita3k/gui-qt/src/theme_surface.cpp @@ -0,0 +1,37 @@ +// Vita3K emulator project +// Copyright (C) 2026 Vita3K team +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along +// with this program; if not, write to the Free Software Foundation, Inc., +// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +#include + +#include +#include +#include + +ThemeSurface::ThemeSurface(QWidget *parent) + : QWidget(parent) { + setAttribute(Qt::WA_StyledBackground, true); +} + +void ThemeSurface::paintEvent(QPaintEvent *event) { + Q_UNUSED(event); + + QStyleOption opt; + opt.initFrom(this); + + QPainter painter(this); + style()->drawPrimitive(QStyle::PE_Widget, &opt, &painter, this); +} diff --git a/vita3k/gui-qt/src/trophy_collection_dialog.cpp b/vita3k/gui-qt/src/trophy_collection_dialog.cpp index 5d5e4abe6..d0bfae661 100644 --- a/vita3k/gui-qt/src/trophy_collection_dialog.cpp +++ b/vita3k/gui-qt/src/trophy_collection_dialog.cpp @@ -128,7 +128,7 @@ TrophyCollectionDialog::TrophyCollectionDialog(EmuEnvState &emuenv, , m_emuenv(emuenv) , m_gui_settings(std::move(gui_settings)) { setWindowTitle(tr("Trophy Collection")); - setObjectName(QStringLiteral("TrophyCollectionDialog")); + setObjectName(QStringLiteral("trophy_collection_dialog")); setWindowModality(Qt::NonModal); setWindowFlag(Qt::WindowSystemMenuHint, true); setWindowFlag(Qt::WindowMinMaxButtonsHint, true); diff --git a/vita3k/gui-qt/src/vita_theme.cpp b/vita3k/gui-qt/src/vita_theme.cpp new file mode 100644 index 000000000..746f894bf --- /dev/null +++ b/vita3k/gui-qt/src/vita_theme.cpp @@ -0,0 +1,921 @@ +// Vita3K emulator project +// Copyright (C) 2026 Vita3K team +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along +// with this program; if not, write to the Free Software Foundation, Inc., +// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +#include +#include + +#include +#include +#include + +#include + +#include + +#include +#include +#include +#include +#include + +namespace gui { +namespace { + +std::string lowercase_copy(std::string value) { + std::transform(value.begin(), value.end(), value.begin(), [](const unsigned char ch) { + return static_cast(std::tolower(ch)); + }); + return value; +} + +std::string normalize_locale(std::string locale) { + locale = string_utils::trim_copy(std::move(locale)); + std::replace(locale.begin(), locale.end(), '_', '-'); + return lowercase_copy(std::move(locale)); +} + +std::string path_leaf_string(const fs::path &path) { + return fs_utils::path_to_utf8(path.filename()); +} + +std::string child_text(const pugi::xml_node &parent, const char *name) { + return string_utils::trim_copy(parent.child(name).text().as_string()); +} + +std::string localized_text(const pugi::xml_node &node, const std::string &preferred_locale) { + if (!node) + return {}; + + const std::string normalized_locale = normalize_locale(preferred_locale); + const auto try_key = [&node](const std::string &name) -> std::string { + return child_text(node, name.c_str()); + }; + const auto try_locale = [&try_key](const std::string &name) -> std::string { + if (name.empty()) + return {}; + + if (const std::string exact = try_key(name); !exact.empty()) + return exact; + + return try_key("m_" + name); + }; + + if (!normalized_locale.empty()) { + if (const std::string exact = try_locale(normalized_locale); !exact.empty()) + return exact; + + if (const auto separator = normalized_locale.find('-'); separator != std::string::npos) { + if (const std::string base = try_locale(normalized_locale.substr(0, separator)); !base.empty()) + return base; + } + } + + if (const std::string fallback = child_text(node, "m_default"); !fallback.empty()) + return fallback; + + for (const auto &child : node.children()) { + if (child.type() != pugi::node_element) + continue; + if (const std::string value = string_utils::trim_copy(child.text().as_string()); !value.empty()) + return value; + } + + return {}; +} + +fs::path resolve_asset_path(const fs::path &theme_dir, std::string asset_value) { + const QString relative_path = gui::utils::sanitize_relative_path_reference( + QString::fromStdString(string_utils::trim_copy(std::move(asset_value)))); + if (relative_path.isEmpty()) + return {}; + + return (theme_dir / gui::utils::to_fs_path(relative_path)).lexically_normal(); +} + +std::optional parse_theme_color(std::string raw_value) { + raw_value = string_utils::trim_copy(std::move(raw_value)); + if (raw_value.empty()) + return std::nullopt; + + if (raw_value.front() == '#') + raw_value.erase(raw_value.begin()); + else if (raw_value.starts_with("0x") || raw_value.starts_with("0X")) + raw_value.erase(0, 2); + + unsigned long long value = 0; + try { + value = std::stoull(raw_value, nullptr, 16); + } catch (const std::exception &) { + try { + value = std::stoull(raw_value, nullptr, 10); + } catch (const std::exception &) { + LOG_WARN("Unable to parse Vita theme color '{}'", raw_value); + return std::nullopt; + } + } + + if (raw_value.size() <= 6 && value <= 0xFFFFFFULL) { + return VitaThemeColor{ + static_cast((value >> 16) & 0xFF), + static_cast((value >> 8) & 0xFF), + static_cast(value & 0xFF), + 255, + }; + } + + return VitaThemeColor{ + static_cast((value >> 16) & 0xFF), + static_cast((value >> 8) & 0xFF), + static_cast(value & 0xFF), + static_cast((value >> 24) & 0xFF), + }; +} + +double luminance(const VitaThemeColor &color) { + return (0.2126 * color.red + 0.7152 * color.green + 0.0722 * color.blue) / 255.0; +} + +double color_distance(const VitaThemeColor &lhs, const VitaThemeColor &rhs) { + const auto component_delta = [](const std::uint8_t first, const std::uint8_t second) { + return static_cast(static_cast(first) - static_cast(second)) / 255.0; + }; + + const double red = component_delta(lhs.red, rhs.red); + const double green = component_delta(lhs.green, rhs.green); + const double blue = component_delta(lhs.blue, rhs.blue); + return std::sqrt((red * red + green * green + blue * blue) / 3.0); +} + +VitaThemeColor mix(const VitaThemeColor &lhs, const VitaThemeColor &rhs, const double ratio) { + const double clamped = std::clamp(ratio, 0.0, 1.0); + const auto blend = [clamped](const std::uint8_t first, const std::uint8_t second) { + return static_cast(std::lround((first * (1.0 - clamped)) + (second * clamped))); + }; + + return VitaThemeColor{ + blend(lhs.red, rhs.red), + blend(lhs.green, rhs.green), + blend(lhs.blue, rhs.blue), + blend(lhs.alpha, rhs.alpha), + }; +} + +VitaThemeColor darken(const VitaThemeColor &color, const double ratio) { + return mix(color, VitaThemeColor{}, ratio); +} + +VitaThemeColor lighten(const VitaThemeColor &color, const double ratio) { + return mix(color, VitaThemeColor{ 255, 255, 255, 255 }, ratio); +} + +VitaThemeColor contrasting_text(const VitaThemeColor &background) { + return luminance(background) > 0.55 + ? VitaThemeColor{ 10, 18, 24, 255 } + : VitaThemeColor{ 245, 247, 250, 255 }; +} + +VitaThemeColor ensure_readable(const VitaThemeColor &foreground, const VitaThemeColor &background) { + return std::abs(luminance(foreground) - luminance(background)) < 0.28 + ? contrasting_text(background) + : foreground; +} + +std::string css_hex(const VitaThemeColor &color) { + return fmt::format("#{:02X}{:02X}{:02X}", color.red, color.green, color.blue); +} + +std::string css_argb(const VitaThemeColor &color, const std::uint8_t alpha) { + return fmt::format("#{:02X}{:02X}{:02X}{:02X}", alpha, color.red, color.green, color.blue); +} + +std::string css_rgba(const VitaThemeColor &color, const std::uint8_t alpha) { + return fmt::format("rgba({}, {}, {}, {:.3f})", + color.red, + color.green, + color.blue, + static_cast(alpha) / 255.0); +} + +std::uint8_t readability_alpha(const int readability_percent, const std::uint8_t minimum, const std::uint8_t maximum) { + const double amount = std::clamp(static_cast(readability_percent) / 100.0, 0.0, 1.0); + return static_cast(std::lround((minimum * (1.0 - amount)) + (maximum * amount))); +} + +std::string stylesheet_path_url(const fs::path &path, const fs::path &pref_path) { + fs::path normalized_path = path.lexically_normal(); + if (!pref_path.empty()) { + const fs::path relative = normalized_path.lexically_relative(pref_path); + if (!relative.empty() && relative != normalized_path) + normalized_path = relative; + } + + std::string normalized = fs_utils::path_to_utf8(normalized_path); + std::replace(normalized.begin(), normalized.end(), '\\', '/'); + return normalized; +} + +std::string join_stylesheet_path_urls(const std::vector &paths, const fs::path &pref_path) { + std::string joined; + bool first = true; + for (const fs::path &path : paths) { + if (path.empty()) + continue; + + if (!first) + joined += ", "; + joined += stylesheet_path_url(path, pref_path); + first = false; + } + return joined; +} + +struct ThemeTemplateContext { + std::unordered_map strings; +}; + +std::optional load_vita_theme_stylesheet_template() { + static const std::optional stylesheet_template = []() -> std::optional { + QFile file(QStringLiteral(":/themes/vita/generated.qss.in")); + if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) { + LOG_ERROR("Failed to load Vita theme stylesheet template from resources"); + return std::nullopt; + } + + return std::string(file.readAll().constData(), static_cast(file.size())); + }(); + + return stylesheet_template; +} + +std::optional render_template_token( + const ThemeTemplateContext &context, + const std::string &token) { + const auto it = context.strings.find(token); + return (it != context.strings.end()) ? std::optional(it->second) : std::nullopt; +} + +std::optional render_stylesheet_template( + const ThemeTemplateContext &context) { + auto stylesheet_template = load_vita_theme_stylesheet_template(); + if (!stylesheet_template) + return std::nullopt; + + std::string rendered; + rendered.reserve(stylesheet_template->size()); + + std::size_t cursor = 0; + while (cursor < stylesheet_template->size()) { + const std::size_t start = stylesheet_template->find("{{", cursor); + if (start == std::string::npos) { + rendered.append(*stylesheet_template, cursor, std::string::npos); + break; + } + + rendered.append(*stylesheet_template, cursor, start - cursor); + const std::size_t finish = stylesheet_template->find("}}", start + 2); + if (finish == std::string::npos) { + rendered.append(*stylesheet_template, start, std::string::npos); + break; + } + + const std::string token = stylesheet_template->substr(start + 2, finish - (start + 2)); + const auto value = render_template_token(context, token); + if (!value) { + LOG_ERROR("Unknown Vita theme stylesheet template token '{}'", token); + return std::nullopt; + } + + rendered.append(*value); + cursor = finish + 2; + } + + return rendered; +} + +std::optional existing_asset_path(const fs::path &theme_dir, const std::string &raw_path) { + const fs::path resolved = resolve_asset_path(theme_dir, raw_path); + return (!resolved.empty() && fs::exists(resolved)) ? std::optional(resolved) : std::nullopt; +} + +std::vector collect_background_nodes(const pugi::xml_node &home_node) { + std::vector nodes; + const auto bg_parent = home_node.child("m_bgParam"); + if (!bg_parent) + return nodes; + + for (const auto &child : bg_parent.children()) { + if (child.type() != pugi::node_element) + continue; + if ((std::string_view(child.name()) == "BackgroundParam") || child.child("m_imageFilePath")) + nodes.push_back(child); + else if (const auto nested = child.child("BackgroundParam")) + nodes.push_back(nested); + } + + return nodes; +} + +void collect_directory_metrics(const fs::path &root, std::uintmax_t &total_size, std::time_t &latest_write_time) { + if (!fs::exists(root)) + return; + + boost::system::error_code error_code; + if (const auto root_time = fs::last_write_time(root, error_code); !error_code) + latest_write_time = std::max(latest_write_time, root_time); + + fs::recursive_directory_iterator end; + for (fs::recursive_directory_iterator it(root, error_code); !error_code && it != end; it.increment(error_code)) { + const fs::path current = it->path(); + + if (fs::is_regular_file(current, error_code)) { + total_size += fs::file_size(current, error_code); + error_code.clear(); + } + + if (const auto write_time = fs::last_write_time(current, error_code); !error_code) + latest_write_time = std::max(latest_write_time, write_time); + error_code.clear(); + } +} + +const VitaThemeBackgroundOption *find_background_option( + const VitaThemeInfo &theme, + const std::string &background_option_id) { + if (theme.background_options.empty()) + return nullptr; + + if (background_option_id.empty()) + return &theme.background_options.front(); + + const auto it = std::find_if(theme.background_options.begin(), theme.background_options.end(), + [&background_option_id](const VitaThemeBackgroundOption &option) { + return option.id == background_option_id; + }); + return (it != theme.background_options.end()) ? &(*it) : nullptr; +} + +std::string build_stylesheet( + const VitaThemeInfo &theme, + const VitaThemeBackgroundOption &background_option, + const int readability_percent, + const bool normalize_font_colors, + const std::vector &background_image_paths, + const int cycle_interval_seconds, + const bool cycle_enabled, + const fs::path &pref_path) { + const VitaThemeColor window = theme.palette.information_bar_color.value_or(VitaThemeColor{ 18, 26, 34, 255 }); + const bool dark_theme = luminance(window) < 0.5; + const VitaThemeColor base = darken(window, 0.18); + const VitaThemeColor alternate_base = lighten(base, 0.08); + const VitaThemeColor accent_hint = dark_theme + ? VitaThemeColor{ 140, 212, 255, 255 } + : VitaThemeColor{ 20, 138, 255, 255 }; + VitaThemeColor accent = theme.palette.information_indicator_color.value_or(accent_hint); + const VitaThemeColor normalized_text = contrasting_text(window); + const VitaThemeColor neutral_border = mix(normalized_text, window, dark_theme ? 0.34 : 0.56); + VitaThemeColor border = theme.palette.notify_border_color + ? mix(*theme.palette.notify_border_color, neutral_border, dark_theme ? 0.42 : 0.58) + : neutral_border; + const VitaThemeColor theme_text = normalize_font_colors + ? normalized_text + : ensure_readable( + background_option.primary_text_color.value_or( + theme.palette.notify_font_color.value_or(normalized_text)), + window); + const VitaThemeColor dialog_text = dark_theme ? VitaThemeColor{ 224, 224, 224, 255 } : VitaThemeColor{ 32, 32, 32, 255 }; + const VitaThemeColor dialog_disabled_text = dark_theme ? VitaThemeColor{ 119, 119, 119, 255 } : VitaThemeColor{ 138, 138, 138, 255 }; + const VitaThemeColor dialog_muted_text = dark_theme ? VitaThemeColor{ 168, 168, 168, 255 } : VitaThemeColor{ 91, 91, 91, 255 }; + const VitaThemeColor overlay_text = (normalize_font_colors && !dark_theme) + ? VitaThemeColor{ 245, 247, 250, 255 } + : theme_text; + const VitaThemeColor text = overlay_text; + VitaThemeColor muted_text = ensure_readable( + normalize_font_colors + ? mix(dialog_text, window, dark_theme ? 0.32 : 0.48) + : theme.palette.date_color.value_or(mix(theme_text, window, dark_theme ? 0.42 : 0.52)), + window); + const VitaThemeColor overlay_muted_text = normalize_font_colors + ? (dark_theme ? VitaThemeColor{ 168, 168, 168, 255 } : VitaThemeColor{ 208, 220, 230, 255 }) + : muted_text; + if ((std::abs(luminance(accent) - luminance(window)) < 0.16) + && (std::abs(luminance(accent) - luminance(theme_text)) < 0.10)) { + accent = accent_hint; + } + accent = ensure_readable(accent, window); + + const VitaThemeColor button = theme.palette.notify_background_color.value_or(mix(window, accent, 0.10)); + const VitaThemeColor button_text = normalize_font_colors + ? ensure_readable(dialog_text, button) + : ensure_readable( + theme.palette.notify_font_color.value_or(text), + button); + const VitaThemeColor highlight_text = contrasting_text(accent); + const VitaThemeColor checkbox_fill = mix(base, window, dark_theme ? 0.22 : 0.14); + const VitaThemeColor disabled_text = mix(muted_text, window, 0.32); + const VitaThemeColor overlay_disabled_text = normalize_font_colors + ? (dark_theme ? VitaThemeColor{ 119, 119, 119, 255 } : VitaThemeColor{ 166, 180, 194, 255 }) + : disabled_text; + const VitaThemeColor line_edit_fill = mix(base, window, 0.35); + const VitaThemeColor subtle_surface = mix(base, window, 0.40); + const VitaThemeColor overlay_selection_fill = dark_theme + ? VitaThemeColor{ 255, 255, 255, 255 } + : VitaThemeColor{ 10, 18, 24, 255 }; + const VitaThemeColor overlay_selection_text = VitaThemeColor{ 255, 255, 255, 255 }; + const VitaThemeColor hover_surface = overlay_selection_fill; + const VitaThemeColor selected_text = overlay_selection_text; + const VitaThemeColor toolbar_icon_tint = normalize_font_colors + ? overlay_text + : (dark_theme ? text : mix(text, window, 0.48)); + const VitaThemeColor dialog_button_fill = dark_theme ? VitaThemeColor{ 48, 48, 48, 255 } : VitaThemeColor{ 236, 236, 236, 255 }; + const VitaThemeColor dialog_bind_fill = dark_theme ? VitaThemeColor{ 47, 47, 47, 255 } : VitaThemeColor{ 230, 230, 230, 255 }; + const VitaThemeColor dialog_sidebar_fill = dark_theme ? VitaThemeColor{ 42, 42, 42, 255 } : VitaThemeColor{ 255, 255, 255, 255 }; + const VitaThemeColor dialog_help_fill = dark_theme ? VitaThemeColor{ 42, 42, 42, 255 } : VitaThemeColor{ 255, 255, 255, 255 }; + const VitaThemeColor dialog_hover_fill = dark_theme ? VitaThemeColor{ 58, 58, 58, 255 } : VitaThemeColor{ 242, 242, 242, 255 }; + const VitaThemeColor dialog_selected_fill = dark_theme ? VitaThemeColor{ 255, 255, 255, 255 } : VitaThemeColor{ 20, 138, 255, 255 }; + const VitaThemeColor dialog_selected_text = VitaThemeColor{ 255, 255, 255, 255 }; + const VitaThemeColor dialog_mode_checked_text = dark_theme ? dialog_text : dialog_selected_text; + const VitaThemeColor renderer_vulkan = VitaThemeColor{ 255, 68, 68, 255 }; + const VitaThemeColor renderer_opengl = VitaThemeColor{ 68, 136, 255, 255 }; + const VitaThemeColor status_high = VitaThemeColor{ 255, 136, 0, 255 }; + const VitaThemeColor status_standard = text; + const VitaThemeColor status_neutral = text; + const VitaThemeColor status_audio = dark_theme ? VitaThemeColor{ 136, 221, 68, 255 } : VitaThemeColor{ 75, 155, 47, 255 }; + const VitaThemeColor status_muted = text; + const VitaThemeColor status_update = dark_theme ? VitaThemeColor{ 109, 224, 137, 255 } : VitaThemeColor{ 31, 143, 78, 255 }; + const VitaThemeColor log_fatal = ensure_readable(VitaThemeColor{ 255, 159, 243, 255 }, window); + const VitaThemeColor log_error = ensure_readable(VitaThemeColor{ 255, 154, 127, 255 }, window); + const VitaThemeColor log_warning = ensure_readable(VitaThemeColor{ 255, 211, 127, 255 }, window); + const VitaThemeColor log_debug = ensure_readable(VitaThemeColor{ 140, 212, 255, 255 }, window); + + if (std::abs(luminance(muted_text) - luminance(text)) < 0.08) + muted_text = ensure_readable(mix(text, window, dark_theme ? 0.32 : 0.48), window); + + if ((std::abs(luminance(border) - luminance(window)) < 0.11) + || (color_distance(border, window) < 0.22)) { + border = neutral_border; + } + + const std::uint8_t dialog_alpha = readability_alpha(readability_percent, 188, 252); + const std::uint8_t chrome_alpha = readability_alpha(readability_percent, 58, 196); + const std::uint8_t chrome_border_alpha = readability_alpha(readability_percent, 18, 64); + const std::uint8_t control_alpha = readability_alpha(readability_percent, 26, 176); + const std::uint8_t input_alpha = readability_alpha(readability_percent, 32, 188); + const std::uint8_t sidebar_alpha = readability_alpha(readability_percent, 28, 210); + const std::uint8_t surface_alpha = readability_alpha(readability_percent, 10, 156); + const std::uint8_t pane_alpha = readability_alpha(readability_percent, 12, 190); + const std::uint8_t search_alpha = readability_alpha(readability_percent, 20, 210); + const std::uint8_t progress_alpha = readability_alpha(readability_percent, 28, 140); + const std::uint8_t scroll_alpha = readability_alpha(readability_percent, 18, 110); + const std::uint8_t hover_alpha = readability_alpha(readability_percent, 44, 96); + const std::uint8_t overlay_selected_alpha = dark_theme + ? readability_alpha(readability_percent, 28, 82) + : readability_alpha(readability_percent, 42, 132); + const std::uint8_t overlay_selected_soft_alpha = dark_theme + ? readability_alpha(readability_percent, 18, 54) + : readability_alpha(readability_percent, 28, 94); + const std::uint8_t overlay_hover_alpha = dark_theme + ? readability_alpha(readability_percent, 16, 42) + : readability_alpha(readability_percent, 12, 34); + const std::uint8_t dialog_selected_alpha = dark_theme + ? readability_alpha(readability_percent, 28, 78) + : readability_alpha(readability_percent, 96, 220); + + const std::string icon_suffix = dark_theme ? "light" : "dark"; + const std::string check_icon_suffix = luminance(highlight_text) > 0.5 ? "light" : "dark"; + const std::string background_url = stylesheet_path_url(background_option.asset_path, pref_path); + ThemeTemplateContext context; + const auto put = [&context](const std::string &key, std::string value) { + context.strings[key] = std::move(value); + }; + const auto put_hex = [&put](const std::string &key, const VitaThemeColor &color) { + put(key, css_hex(color)); + }; + const auto put_rgba = [&put](const std::string &key, const VitaThemeColor &color, const int alpha) { + put(key, css_rgba(color, static_cast(std::clamp(alpha, 0, 255)))); + }; + const auto put_argb = [&put](const std::string &key, const VitaThemeColor &color, const int alpha) { + put(key, css_argb(color, static_cast(std::clamp(alpha, 0, 255)))); + }; + + put("generated_background_images", join_stylesheet_path_urls(background_image_paths, pref_path)); + put("generated_background_interval_seconds", std::to_string(std::clamp(cycle_interval_seconds, 5, 120))); + put("generated_background_animated", cycle_enabled ? "true" : "false"); + + put("palette_dark", dark_theme ? "dark" : "light"); + put_hex("window_hex", window); + put_hex("text_hex", text); + put_hex("base_hex", base); + put_hex("alternate_base_hex", alternate_base); + put_hex("button_hex", button); + put_hex("button_text_hex", button_text); + put_hex("button_bright_text_hex", contrasting_text(button)); + put_hex("accent_hex", accent); + put_argb("highlight_argb", overlay_selection_fill, overlay_selected_alpha); + put_argb("inactive_highlight_argb", overlay_selection_fill, overlay_selected_soft_alpha); + put_argb("disabled_highlight_argb", overlay_selection_fill, overlay_hover_alpha); + put_hex("highlight_text_hex", overlay_selection_text); + put_hex("disabled_text_hex", disabled_text); + put_hex("muted_text_hex", muted_text); + put_hex("overlay_disabled_text_hex", overlay_disabled_text); + put_hex("dialog_text_hex", dialog_text); + put_hex("dialog_muted_text_hex", dialog_muted_text); + put_hex("dialog_disabled_text_hex", dialog_disabled_text); + put_hex("selected_text_hex", selected_text); + put_hex("push_button_pressed_text_hex", highlight_text); + put_hex("dialog_selected_text_hex", dialog_selected_text); + put_hex("dialog_mode_checked_text_hex", dialog_mode_checked_text); + put_hex("toolbar_icon_tint_hex", toolbar_icon_tint); + put_hex("renderer_vulkan_hex", renderer_vulkan); + put_hex("renderer_opengl_hex", renderer_opengl); + put_hex("status_high_hex", status_high); + put_hex("status_standard_hex", status_standard); + put_hex("status_neutral_hex", status_neutral); + put_hex("status_audio_hex", status_audio); + put_hex("status_muted_hex", status_muted); + put_hex("status_update_hex", status_update); + put_hex("log_fatal_hex", log_fatal); + put_hex("log_error_hex", log_error); + put_hex("log_warning_hex", log_warning); + put_hex("log_debug_hex", log_debug); + put_hex("severity_success_text_hex", contrasting_text(status_audio)); + put_hex("severity_error_text_hex", contrasting_text(log_error)); + + put("background_url", background_url); + put("icon_suffix", icon_suffix); + put("check_icon_suffix", check_icon_suffix); + + put_rgba("dialog_bg", base, dialog_alpha); + put_rgba("menu_bar_bg", base, chrome_alpha); + put_rgba("menu_bar_border", border, chrome_border_alpha); + put_rgba("menu_bar_item_hover_bg", hover_surface, hover_alpha); + put_rgba("menu_bg", base, 245); + put_rgba("menu_border", border, 40); + put_rgba("menu_selected_bg", overlay_selection_fill, overlay_selected_soft_alpha); + put_rgba("menu_separator_bg", border, 30); + put_rgba("toolbar_bg", base, chrome_alpha); + put_rgba("toolbar_border_top", border, chrome_border_alpha + 8); + put_rgba("toolbar_border_bottom", border, chrome_border_alpha); + put_rgba("toolbar_separator_bg", border, 40); + put_rgba("tool_button_hover_bg", hover_surface, hover_alpha); + put_rgba("tool_button_hover_border", border, chrome_border_alpha + 6); + put_rgba("tool_button_pressed_bg", accent, 54); + put_rgba("welcome_button_bg", button, 88); + put_rgba("welcome_button_border", border, 36); + put_rgba("welcome_button_hover_bg", hover_surface, 88); + put_rgba("welcome_button_hover_border", border, 48); + put_rgba("welcome_button_pressed_bg", accent, 56); + put_rgba("welcome_menu_button_border", border, 36); + put_rgba("welcome_menu_button_bg", base, 28); + put_rgba("welcome_menu_button_hover_bg", hover_surface, 74); + put_rgba("welcome_menu_button_hover_border", border, 48); + put_rgba("welcome_menu_button_pressed_bg", accent, 48); + put_rgba("push_button_bg", button, control_alpha); + put_rgba("push_button_border", border, 36); + put_rgba("push_button_hover_bg", hover_surface, hover_alpha + 12); + put_rgba("push_button_hover_border", border, chrome_border_alpha + 18); + put_rgba("push_button_pressed_bg", accent, 62); + put_rgba("push_button_disabled_bg", base, 72); + put_rgba("push_button_disabled_border", border, 20); + put_rgba("push_button_default_border", accent, 110); + put_rgba("vita_list_bg", dialog_sidebar_fill, sidebar_alpha); + put_rgba("vita_list_selected_bg", dialog_selected_fill, dialog_selected_alpha); + put_rgba("vita_list_hover_bg", dialog_hover_fill, dark_theme ? hover_alpha + 8 : hover_alpha); + put_rgba("background_list_bg", line_edit_fill, pane_alpha); + put_rgba("background_list_border", border, 26); + put_rgba("background_list_selected_bg", dialog_selected_fill, dialog_selected_alpha); + put_rgba("background_list_hover_bg", dialog_hover_fill, dark_theme ? hover_alpha + 6 : hover_alpha + 4); + put_rgba("vita_help_bg", dialog_help_fill, pane_alpha); + put_rgba("controls_mode_hover_bg", dialog_hover_fill, dark_theme ? hover_alpha : hover_alpha + 10); + put_rgba("controls_mode_checked_bg", dialog_selected_fill, dialog_selected_alpha); + put_rgba("controls_mode_pressed_bg", dialog_selected_fill, dark_theme ? dialog_selected_alpha + 16 : std::min(255, dialog_selected_alpha + 24)); + put_rgba("controls_button_bg", dialog_button_fill, control_alpha); + put_rgba("controls_button_hover_bg", dialog_hover_fill, dark_theme ? hover_alpha + 10 : hover_alpha + 6); + put_rgba("controls_button_pressed_bg", dialog_hover_fill, dark_theme ? hover_alpha + 20 : hover_alpha + 18); + put_rgba("controls_button_disabled_bg", dialog_button_fill, std::max(40, control_alpha - 28)); + put_rgba("controls_bind_bg", dialog_bind_fill, input_alpha); + put_rgba("controls_bind_hover_bg", dialog_hover_fill, dark_theme ? hover_alpha + 6 : hover_alpha + 4); + put_rgba("controls_bind_pressed_bg", dialog_hover_fill, dark_theme ? hover_alpha + 16 : hover_alpha + 14); + put_rgba("controls_capture_bg", dialog_selected_fill, dark_theme ? dialog_selected_alpha + 18 : dialog_selected_alpha); + put_rgba("controls_capture_hover_bg", dialog_selected_fill, dark_theme ? dialog_selected_alpha + 28 : std::min(255, dialog_selected_alpha + 18)); + put_rgba("controls_capture_pressed_bg", dialog_selected_fill, dark_theme ? dialog_selected_alpha + 38 : std::min(255, dialog_selected_alpha + 28)); + put_rgba("status_bar_bg", base, chrome_alpha + 18); + put_rgba("status_bar_border", border, 26); + put_rgba("status_button_hover_border", border, 32); + put_rgba("status_button_hover_bg", hover_surface, overlay_hover_alpha); + put_rgba("status_button_pressed_border", border, 40); + put_rgba("status_button_pressed_bg", overlay_selection_fill, overlay_selected_soft_alpha); + put_rgba("line_edit_bg", line_edit_fill, input_alpha); + put_rgba("line_edit_border", border, 36); + put_rgba("line_edit_selection_bg", overlay_selection_fill, overlay_selected_alpha); + put_rgba("line_edit_focus_border", accent, 96); + put_rgba("line_edit_disabled_bg", base, 76); + put_rgba("line_edit_disabled_border", border, 20); + put_rgba("main_search_border", border, 36); + put_rgba("main_search_selection_bg", overlay_selection_fill, overlay_selected_soft_alpha); + put_rgba("main_search_focus_bg", base, 18); + put_rgba("main_search_focus_border", accent, 72); + put_rgba("combo_bg", line_edit_fill, input_alpha); + put_rgba("combo_border", border, 36); + put_rgba("combo_hover_border", border, 54); + put_rgba("combo_disabled_bg", base, 76); + put_rgba("combo_disabled_border", border, 20); + put_rgba("combo_popup_bg", base, 244); + put_rgba("combo_popup_border", border, 40); + put_rgba("combo_popup_selection_bg", overlay_selection_fill, overlay_selected_soft_alpha); + put_rgba("spin_bg", line_edit_fill, input_alpha); + put_rgba("spin_border", border, 36); + put_rgba("spin_selection_bg", overlay_selection_fill, overlay_selected_alpha); + put_rgba("spin_disabled_bg", base, 76); + put_rgba("spin_disabled_border", border, 20); + put_rgba("indicator_border", border, 92); + put_rgba("indicator_bg", checkbox_fill, std::max(62, static_cast(input_alpha) - 12)); + put_rgba("indicator_hover_border", accent, 138); + put_rgba("indicator_hover_bg", checkbox_fill, std::max(82, static_cast(input_alpha) + 6)); + put_rgba("indicator_checked_bg", accent, 196); + put_rgba("indicator_checked_border", accent, 204); + put_rgba("indicator_disabled_border", border, 54); + put_rgba("indicator_disabled_bg", base, 88); + put_rgba("indicator_checked_disabled_bg", accent, 96); + put_rgba("indicator_checked_disabled_border", accent, 108); + put_rgba("radio_indicator_border", border, 92); + put_rgba("radio_indicator_bg", checkbox_fill, std::max(62, static_cast(input_alpha) - 12)); + put_rgba("radio_indicator_hover_border", accent, 138); + put_rgba("radio_indicator_hover_bg", checkbox_fill, std::max(82, static_cast(input_alpha) + 6)); + put_rgba("radio_checked_bg", accent, 196); + put_rgba("radio_checked_border", accent, 204); + put_rgba("radio_checked_disabled_bg", accent, 96); + put_rgba("radio_checked_disabled_border", accent, 108); + put_rgba("group_border", border, 32); + put_rgba("group_disabled_border", border, 18); + put_rgba("dock_title_bg", base, chrome_alpha); + put_rgba("dock_button_hover_bg", hover_surface, hover_alpha + 8); + put_rgba("apps_surface_bg", base, surface_alpha); + put_rgba("alt_row_bg", text, std::max(8, surface_alpha / 2)); + put_rgba("table_gridline", border, 18); + put_rgba("table_disabled_bg", base, 54); + put_rgba("table_corner_bg", darken(base, 0.08), 80); + put_rgba("table_corner_border", border, 24); + put_rgba("table_selected_bg", overlay_selection_fill, overlay_selected_alpha); + put_rgba("header_bg", darken(base, 0.08), surface_alpha); + put_rgba("header_border", border, 18); + put_rgba("header_hover_bg", hover_surface, 84); + put_rgba("log_search_bg", subtle_surface, search_alpha); + put_rgba("log_search_border", border, 30); + put_rgba("toolbar_slider_groove_bg", text, 24); + put_rgba("toolbar_slider_subpage_bg", text, 228); + put_rgba("toolbar_slider_addpage_bg", text, 20); + put_hex("toolbar_slider_handle_bg", lighten(text, dark_theme ? 0.0 : 0.12)); + put_hex("toolbar_slider_handle_hover_bg", lighten(text, 0.12)); + put_rgba("severity_success_bg", status_audio, 124); + put_rgba("severity_error_bg", log_error, 124); + put_rgba("tab_pane_border", border, 24); + put_rgba("tab_pane_bg", base, pane_alpha); + put_rgba("tab_bg", subtle_surface, sidebar_alpha); + put_rgba("tab_border", border, 24); + put_rgba("tab_selected_bg", base, pane_alpha); + put_rgba("tab_hover_bg", hover_surface, hover_alpha); + put_rgba("list_bg", base, pane_alpha); + put_rgba("list_border", border, 22); + put_rgba("list_disabled_bg", base, 54); + put_rgba("list_disabled_border", border, 16); + put_rgba("list_selected_bg", overlay_selection_fill, overlay_selected_alpha); + put_rgba("list_hover_bg", hover_surface, hover_alpha); + put_rgba("settings_category_bg", dialog_sidebar_fill, sidebar_alpha); + put_rgba("settings_category_selected_bg", dialog_selected_fill, dialog_selected_alpha); + put_rgba("settings_category_hover_bg", dialog_hover_fill, dark_theme ? hover_alpha + 8 : hover_alpha); + put_rgba("modules_list_bg", line_edit_fill, pane_alpha); + put_rgba("modules_list_border", border, 30); + put_rgba("modules_list_hover_bg", dialog_hover_fill, dark_theme ? hover_alpha + 4 : hover_alpha + 8); + put_rgba("settings_help_bg", dialog_help_fill, pane_alpha); + put_rgba("scrollbar_bg", base, scroll_alpha); + put_rgba("scrollbar_handle_bg", text, scroll_alpha + 22); + put_rgba("scrollbar_handle_hover_bg", text, scroll_alpha + 40); + put_rgba("slider_subpage_bg", text, 224); + put_rgba("slider_addpage_bg", text, 28); + put_hex("slider_handle_bg", lighten(text, dark_theme ? 0.0 : 0.12)); + put_hex("slider_handle_hover_bg", lighten(text, 0.12)); + put_rgba("progress_bg", base, progress_alpha); + put_rgba("progress_border", border, 22); + put_hex("progress_chunk_bg", accent); + put_rgba("splitter_handle_bg", border, 34); + put_rgba("tooltip_bg", base, 245); + put_rgba("tooltip_border", border, 40); + put_rgba("text_edit_bg", base, pane_alpha); + put_rgba("text_edit_border", border, 22); + put_rgba("text_edit_disabled_bg", base, 54); + put_rgba("text_edit_disabled_border", border, 16); + + const auto rendered = render_stylesheet_template(context); + return rendered.value_or(std::string()); +} + +} // namespace + +std::optional load_vita_theme( + const fs::path &theme_dir, + const std::string &preferred_locale) { + const fs::path theme_xml_path = theme_dir / "theme.xml"; + if (!fs::exists(theme_xml_path)) + return std::nullopt; + + std::vector xml_buffer; + if (!fs_utils::read_data(theme_xml_path, xml_buffer)) { + LOG_WARN("Failed to read Vita theme metadata from {}", theme_xml_path); + return std::nullopt; + } + + pugi::xml_document document; + const auto parse_result = document.load_buffer(xml_buffer.data(), xml_buffer.size()); + if (!parse_result) { + LOG_WARN("Failed to parse Vita theme metadata from {}: {}", theme_xml_path, parse_result.description()); + return std::nullopt; + } + + const auto theme_root = document.child("theme"); + if (!theme_root) { + LOG_WARN("Theme metadata at {} is missing the root node", theme_xml_path); + return std::nullopt; + } + + VitaThemeInfo theme; + theme.installed_path = theme_dir; + const auto info_node = theme_root.child("InfomationProperty"); + const auto info_bar_node = theme_root.child("InfomationBarProperty"); + const auto home_node = theme_root.child("HomeProperty"); + const auto start_node = theme_root.child("StartScreenProperty"); + + theme.content_id = child_text(info_node, "m_contentId"); + theme.title = localized_text(info_node.child("m_title"), preferred_locale); + theme.provider = localized_text(info_node.child("m_provider"), preferred_locale); + theme.content_version = child_text(info_node, "m_contentVer"); + theme.theme_id = vita_theme_utils::resolve_theme_id(theme.content_id, path_leaf_string(theme_dir), theme.title); + + if (theme.title.empty()) + theme.title = theme.theme_id; + + const std::string package_thumbnail_raw = [&]() { + if (const std::string package_image = child_text(info_node, "m_packageImageFilePath"); !package_image.empty()) + return package_image; + return child_text(info_node, "m_previewFilePath"); + }(); + if (const auto package_thumbnail = existing_asset_path(theme_dir, package_thumbnail_raw)) + theme.package_thumbnail_path = *package_thumbnail; + + const std::string bgm_raw_path = child_text(home_node, "m_bgmFilePath"); + if (const auto bgm_path = existing_asset_path(theme_dir, bgm_raw_path)) { + theme.bgm_path = *bgm_path; + } else if (!bgm_raw_path.empty()) { + LOG_WARN("Skipping missing Vita theme BGM '{}' for theme '{}'", bgm_raw_path, theme.theme_id); + } + + theme.palette.information_bar_color = parse_theme_color(child_text(info_bar_node, "m_barColor")); + theme.palette.information_indicator_color = parse_theme_color(child_text(info_bar_node, "m_indicatorColor")); + theme.palette.notify_background_color = parse_theme_color(child_text(start_node, "m_notifyBgColor")); + theme.palette.notify_border_color = parse_theme_color(child_text(start_node, "m_notifyBorderColor")); + theme.palette.notify_font_color = parse_theme_color(child_text(start_node, "m_notifyFontColor")); + theme.palette.date_color = parse_theme_color(child_text(start_node, "m_dateColor")); + + int home_index = 0; + for (const auto &background_node : collect_background_nodes(home_node)) { + const std::string raw_path = child_text(background_node, "m_imageFilePath"); + if (raw_path.empty()) + continue; + + const auto asset_path = existing_asset_path(theme_dir, raw_path); + if (!asset_path) { + LOG_WARN("Skipping missing Vita home background '{}' for theme '{}'", raw_path, theme.theme_id); + continue; + } + + VitaThemeBackgroundOption option; + option.id = fmt::format("home-{}", home_index); + option.label = fmt::format("Home Background {}", home_index + 1); + option.asset_path = *asset_path; + option.source = VitaThemeBackgroundSource::Home; + option.primary_text_color = parse_theme_color(child_text(background_node, "m_fontColor")); + theme.background_options.push_back(std::move(option)); + ++home_index; + } + + const std::string raw_lockscreen_path = child_text(start_node, "m_filePath"); + if (const auto lockscreen_path = existing_asset_path(theme_dir, raw_lockscreen_path)) { + VitaThemeBackgroundOption option; + option.id = "lockscreen"; + option.label = "Lockscreen"; + option.asset_path = *lockscreen_path; + option.source = VitaThemeBackgroundSource::Lockscreen; + option.primary_text_color = theme.palette.notify_font_color; + theme.background_options.push_back(std::move(option)); + } else if (!raw_lockscreen_path.empty()) { + LOG_WARN("Skipping missing Vita lockscreen background '{}' for theme '{}'", raw_lockscreen_path, theme.theme_id); + } + + collect_directory_metrics(theme_dir, theme.installed_size, theme.updated_time); + + if (theme.background_options.empty()) { + LOG_WARN("Theme '{}' has no usable background assets, skipping", theme.theme_id); + return std::nullopt; + } + + return theme; +} + +std::vector enumerate_installed_vita_themes( + const fs::path &themes_root, + const std::string &preferred_locale) { + std::vector themes; + if (!fs::exists(themes_root) || !fs::is_directory(themes_root)) + return themes; + + std::vector theme_paths; + for (fs::directory_iterator it(themes_root), end; it != end; ++it) { + if (fs::is_directory(it->path())) + theme_paths.push_back(it->path()); + } + + std::sort(theme_paths.begin(), theme_paths.end(), [](const fs::path &lhs, const fs::path &rhs) { + return fs_utils::path_to_utf8(lhs.filename()) < fs_utils::path_to_utf8(rhs.filename()); + }); + + for (const auto &theme_path : theme_paths) { + if (auto theme = load_vita_theme(theme_path, preferred_locale)) + themes.push_back(std::move(*theme)); + } + + return themes; +} + +std::optional synthesize_vita_theme_qss( + const VitaThemeInfo &theme, + const std::string &background_option_id, + const int readability_percent, + const bool normalize_font_colors, + const std::vector &background_image_paths, + const int cycle_interval_seconds, + const bool cycle_enabled, + const fs::path &pref_path, + const fs::path &gui_settings_dir) { + const auto *selected_background = find_background_option(theme, background_option_id); + if (!selected_background) { + LOG_ERROR("Unable to find Vita theme background option '{}' for theme '{}'", + background_option_id, + theme.theme_id); + return std::nullopt; + } + + if (!fs::exists(selected_background->asset_path)) { + LOG_ERROR("Vita theme asset is missing: {}", selected_background->asset_path); + return std::nullopt; + } + + const fs::path output_directory = gui_settings_dir / "vita-themes" / theme.theme_id; + const fs::path qss_path = output_directory / "generated.qss"; + const std::string stylesheet = build_stylesheet( + theme, + *selected_background, + std::clamp(readability_percent, 0, 100), + normalize_font_colors, + background_image_paths.empty() ? std::vector{ selected_background->asset_path } : background_image_paths, + cycle_interval_seconds, + cycle_enabled, + pref_path); + + boost::system::error_code error_code; + fs::create_directories(output_directory, error_code); + if (error_code) { + LOG_ERROR("Failed to create Vita theme output directory {}: {}", + output_directory, + error_code.message()); + return std::nullopt; + } + + fs::ofstream output(qss_path, fs::ofstream::binary | fs::ofstream::trunc); + if (!output) { + LOG_ERROR("Failed to open generated Vita theme stylesheet path {}", qss_path); + return std::nullopt; + } + + output.write(stylesheet.data(), static_cast(stylesheet.size())); + if (!output.good()) { + LOG_ERROR("Failed to write generated Vita theme stylesheet {}", qss_path); + return std::nullopt; + } + + return qss_path; +} + +} // namespace gui diff --git a/vita3k/gui-qt/src/vita_themes_dialog.cpp b/vita3k/gui-qt/src/vita_themes_dialog.cpp new file mode 100644 index 000000000..4fd477de9 --- /dev/null +++ b/vita3k/gui-qt/src/vita_themes_dialog.cpp @@ -0,0 +1,1139 @@ +// Vita3K emulator project +// Copyright (C) 2026 Vita3K team +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along +// with this program; if not, write to the Free Software Foundation, Inc., +// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +#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 QSize k_default_preview_aspect(960, 544); + +QString to_qstring(const std::string &value) { + return QString::fromStdString(value); +} + +QString background_source_label(const gui::VitaThemeBackgroundOption &background) { + return (background.source == gui::VitaThemeBackgroundSource::Lockscreen) + ? QObject::tr("Lockscreen") + : QObject::tr("Home"); +} + +QLabel *make_section_label(const QString &text, QWidget *parent) { + auto *label = new QLabel(text, parent); + label->setProperty("themeRole", QStringLiteral("sectionHeader")); + gui::utils::refresh_theme_state(label); + return label; +} + +class AspectRatioPreviewLabel final : public QLabel { +public: + explicit AspectRatioPreviewLabel(QWidget *parent = nullptr) + : QLabel(parent) { + } + + bool hasHeightForWidth() const override { + return true; + } + + int heightForWidth(int width) const override { + const int safe_width = std::max(width, minimumSizeHint().width()); + return std::max( + minimumSizeHint().height(), + (safe_width * m_aspect_size.height() + (m_aspect_size.width() / 2)) / m_aspect_size.width()); + } + + QSize sizeHint() const override { + return minimumSizeHint(); + } + + QSize minimumSizeHint() const override { + return QSize(420, 236); + } + + void set_aspect_size(const QSize &aspect_size) { + const QSize normalized = aspect_size.expandedTo(QSize(1, 1)); + if (m_aspect_size == normalized) + return; + + m_aspect_size = normalized; + updateGeometry(); + } + +private: + QSize m_aspect_size = k_default_preview_aspect; +}; + +} // namespace + +VitaThemesDialog::VitaThemesDialog( + std::shared_ptr gui_settings, + ThemeManager &theme_manager, + QWidget *parent) + : QDialog(parent) + , m_gui_settings(std::move(gui_settings)) + , m_theme_manager(theme_manager) { + setObjectName(QStringLiteral("vita_themes_dialog")); + setWindowTitle(tr("Vita Themes")); + setAttribute(Qt::WA_DeleteOnClose, false); + + build_ui(); + restore_persistent_state(); + reload(); +} + +void VitaThemesDialog::reload() { + m_theme_manager.refresh_vita_theme_catalog(); + const auto preferred_selection = m_theme_manager.applied_vita_theme_selection(); + const QString preferred_theme_id = preferred_selection + ? preferred_selection->theme_id + : QString(); + repopulate_theme_list(preferred_theme_id); +} + +void VitaThemesDialog::closeEvent(QCloseEvent *event) { + save_persistent_state(); + QDialog::closeEvent(event); +} + +void VitaThemesDialog::resizeEvent(QResizeEvent *event) { + QDialog::resizeEvent(event); + update_preview_pixmap(); +} + +bool VitaThemesDialog::eventFilter(QObject *watched, QEvent *event) { + if (!watched || !event) + return QDialog::eventFilter(watched, event); + + if ((watched == m_preview || watched == m_package_thumbnail) + && (event->type() == QEvent::Resize || event->type() == QEvent::Show)) { + if (watched == m_preview) + update_preview_pixmap(); + else + update_package_thumbnail(); + } + + if (event->type() == QEvent::Enter || event->type() == QEvent::FocusIn) { + if (auto *widget = qobject_cast(watched)) { + const QString description = widget->property("_desc").toString(); + if (!description.isEmpty()) + set_description(widget->property("_desc_title").toString(), description); + } + } else if (event->type() == QEvent::Leave || event->type() == QEvent::FocusOut) { + if (auto *widget = qobject_cast(watched)) { + if (!widget->property("_desc").toString().isEmpty()) + reset_description(); + } + } + + return QDialog::eventFilter(watched, event); +} + +void VitaThemesDialog::build_ui() { + auto *root_layout = new QVBoxLayout(this); + root_layout->setContentsMargins(6, 6, 6, 6); + root_layout->setSpacing(6); + + auto *splitter = new QSplitter(Qt::Horizontal, this); + splitter->setObjectName(QStringLiteral("vitaThemesSplitter")); + splitter->setChildrenCollapsible(false); + splitter->setHandleWidth(10); + splitter->setStyleSheet(QStringLiteral( + "QSplitter#vitaThemesSplitter::handle { background: transparent; }")); + root_layout->addWidget(splitter, 1); + + auto *left_panel = new QWidget(splitter); + auto *left_layout = new QVBoxLayout(left_panel); + left_layout->setContentsMargins(0, 0, 0, 0); + left_layout->setSpacing(6); + + auto *left_header = make_section_label(tr("Installed Themes"), left_panel); + left_layout->addWidget(left_header); + + m_theme_list = new QListWidget(left_panel); + m_theme_list->setObjectName(QStringLiteral("vitaThemesList")); + m_theme_list->setSelectionMode(QAbstractItemView::SingleSelection); + left_layout->addWidget(m_theme_list, 1); + register_description( + m_theme_list, + tr("Installed Themes"), + tr("Select an installed Vita theme to preview it, choose a primary background, and prepare a generated emulator theme.")); + + auto *card = new QWidget(left_panel); + auto *card_layout = new QVBoxLayout(card); + card_layout->setContentsMargins(0, 0, 0, 0); + card_layout->setSpacing(4); + + m_package_thumbnail = new QLabel(card); + m_package_thumbnail->setAlignment(Qt::AlignCenter); + m_package_thumbnail->setMinimumHeight(96); + m_package_thumbnail->setMaximumHeight(96); + m_package_thumbnail->setFrameShape(QFrame::StyledPanel); + m_package_thumbnail->installEventFilter(this); + card_layout->addWidget(m_package_thumbnail); + + m_theme_title = new QLabel(card); + m_theme_title->setWordWrap(true); + card_layout->addWidget(m_theme_title); + + m_theme_provider = new QLabel(card); + m_theme_provider->setWordWrap(true); + m_theme_provider->setProperty("themeRole", QStringLiteral("mutedText")); + gui::utils::refresh_theme_state(m_theme_provider); + card_layout->addWidget(m_theme_provider); + left_layout->addWidget(card); + + auto *right_panel = new QWidget(splitter); + auto *right_layout = new QVBoxLayout(right_panel); + right_layout->setContentsMargins(0, 0, 0, 0); + right_layout->setSpacing(6); + + auto *preview_card = new QWidget(right_panel); + preview_card->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Maximum); + auto *preview_layout = new QVBoxLayout(preview_card); + preview_layout->setContentsMargins(0, 0, 0, 0); + preview_layout->setSpacing(4); + + preview_layout->addWidget(make_section_label(tr("Preview"), preview_card)); + + auto *preview_content = new QWidget(preview_card); + preview_content->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Maximum); + auto *preview_content_layout = new QHBoxLayout(preview_content); + preview_content_layout->setContentsMargins(0, 0, 0, 0); + preview_content_layout->setSpacing(6); + + auto *preview_stage = new QWidget(preview_content); + preview_stage->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred); + auto *preview_stage_layout = new QVBoxLayout(preview_stage); + preview_stage_layout->setContentsMargins(0, 0, 0, 0); + preview_stage_layout->setSpacing(0); + preview_stage_layout->addStretch(1); + + m_preview = new AspectRatioPreviewLabel(preview_stage); + m_preview->setAlignment(Qt::AlignCenter); + m_preview->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred); + m_preview->setFrameShape(QFrame::StyledPanel); + m_preview->setText(tr("No preview available")); + preview_stage_layout->addWidget(m_preview); + preview_stage_layout->addStretch(1); + preview_content_layout->addWidget(preview_stage, 1); + register_description( + m_preview, + tr("Preview"), + tr("Shows the currently previewed Vita background. Select a row in the list to compare slides, or use Primary to choose which one gets applied.")); + + auto *backgrounds_side = new QWidget(preview_content); + backgrounds_side->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Preferred); + backgrounds_side->setMinimumWidth(288); + backgrounds_side->setMaximumWidth(360); + auto *backgrounds_layout = new QVBoxLayout(backgrounds_side); + backgrounds_layout->setContentsMargins(0, 0, 0, 0); + backgrounds_layout->setSpacing(4); + + backgrounds_layout->addWidget(make_section_label(tr("Slides"), backgrounds_side)); + + auto *cycle_row = new QWidget(backgrounds_side); + auto *cycle_layout = new QHBoxLayout(cycle_row); + cycle_layout->setContentsMargins(0, 0, 0, 0); + cycle_layout->setSpacing(4); + + m_cycle_checkbox = new QCheckBox(cycle_row); + cycle_layout->addWidget(m_cycle_checkbox, 0, Qt::AlignVCenter); + register_description( + m_cycle_checkbox, + tr("Cycle Backgrounds"), + tr("Rotate through the backgrounds checked in the list below. The primary background always remains part of the cycle order.")); + + auto *cycle_label = new QLabel(tr("Cycle"), cycle_row); + cycle_layout->addWidget(cycle_label, 0, Qt::AlignVCenter); + + auto *cycle_interval_label = new QLabel(tr("Every"), cycle_row); + cycle_layout->addWidget(cycle_interval_label, 0, Qt::AlignVCenter); + + m_cycle_interval_spinbox = new QSpinBox(cycle_row); + m_cycle_interval_spinbox->setRange(5, 120); + m_cycle_interval_spinbox->setSuffix(tr(" s")); + m_cycle_interval_spinbox->setValue(15); + m_cycle_interval_spinbox->setFixedWidth(70); + cycle_layout->addWidget(m_cycle_interval_spinbox, 0, Qt::AlignVCenter); + register_description( + m_cycle_interval_spinbox, + tr("Cycle Interval"), + tr("Choose how long each selected background stays on screen before the next fade begins.")); + + cycle_layout->addStretch(1); + + m_cycle_summary_value = new QLabel(cycle_row); + m_cycle_summary_value->setProperty("themeRole", QStringLiteral("mutedText")); + m_cycle_summary_value->setMinimumWidth(68); + gui::utils::refresh_theme_state(m_cycle_summary_value); + cycle_layout->addWidget(m_cycle_summary_value, 0, Qt::AlignVCenter); + backgrounds_layout->addWidget(cycle_row); + + m_background_list = new QTreeWidget(backgrounds_side); + m_background_list->setObjectName(QStringLiteral("vitaThemesBackgroundList")); + m_background_list->setColumnCount(3); + m_background_list->setHeaderLabels({ tr("Background"), tr("Primary"), tr("Cycle") }); + m_background_list->setRootIsDecorated(false); + m_background_list->setItemsExpandable(false); + m_background_list->setIndentation(0); + m_background_list->setSelectionMode(QAbstractItemView::SingleSelection); + m_background_list->setSelectionBehavior(QAbstractItemView::SelectRows); + m_background_list->setAlternatingRowColors(false); + m_background_list->setUniformRowHeights(false); + m_background_list->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Preferred); + m_background_list->setMinimumHeight(212); + if (auto *header = m_background_list->header()) { + header->setStretchLastSection(false); + header->setSectionResizeMode(0, QHeaderView::Stretch); + header->setSectionResizeMode(1, QHeaderView::ResizeToContents); + header->setSectionResizeMode(2, QHeaderView::ResizeToContents); + } + backgrounds_layout->addWidget(m_background_list, 1); + register_description( + m_background_list, + tr("Slides"), + tr("Select a slide to preview it. Use the Primary column to choose the applied background and the Cycle column to decide which slides rotate.")); + + connect(m_background_list, &QTreeWidget::itemSelectionChanged, this, [this] { + if (!m_background_list) + return; + if (auto *item = m_background_list->currentItem()) + set_preview_background(item->data(0, Qt::UserRole).toString()); + }); + + preview_content_layout->addWidget(backgrounds_side); + preview_layout->addWidget(preview_content); + right_layout->addWidget(preview_card); + + auto *appearance_card = new QGroupBox(tr("Appearance"), right_panel); + appearance_card->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Maximum); + auto *appearance_layout = new QVBoxLayout(appearance_card); + appearance_layout->setContentsMargins(8, 8, 8, 8); + appearance_layout->setSpacing(4); + + auto *readability_row = new QWidget(appearance_card); + auto *readability_layout = new QHBoxLayout(readability_row); + readability_layout->setContentsMargins(0, 0, 0, 0); + readability_layout->setSpacing(6); + + auto *readability_label = new QLabel(tr("Background Opaqueness"), readability_row); + readability_layout->addWidget(readability_label); + + m_readability_slider = new QSlider(Qt::Horizontal, readability_row); + m_readability_slider->setRange(0, 100); + m_readability_slider->setValue(55); + readability_layout->addWidget(m_readability_slider, 1); + register_description( + m_readability_slider, + tr("Background Opaqueness"), + tr("Adjust how strongly the generated theme dims the background behind Vita shell panels for readability.")); + + m_readability_value = new QLabel(readability_row); + readability_layout->addWidget(m_readability_value); + appearance_layout->addWidget(readability_row); + + m_normalize_fonts_checkbox = new QCheckBox(tr("Normalize Font Colors"), appearance_card); + m_normalize_fonts_checkbox->setChecked(true); + appearance_layout->addWidget(m_normalize_fonts_checkbox); + register_description( + m_normalize_fonts_checkbox, + tr("Normalize Font Colors"), + tr("Use a more reliable light or dark text palette instead of the original Vita theme font colors when generating the stylesheet.")); + right_layout->addWidget(appearance_card); + + auto *meta_card = new QGroupBox(tr("Details"), right_panel); + meta_card->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Maximum); + auto *meta_card_layout = new QVBoxLayout(meta_card); + meta_card_layout->setContentsMargins(8, 8, 8, 8); + meta_card_layout->setSpacing(4); + + auto *meta_layout = new QFormLayout(); + meta_layout->setContentsMargins(0, 0, 0, 0); + meta_layout->setHorizontalSpacing(20); + meta_layout->setVerticalSpacing(6); + meta_layout->setLabelAlignment(Qt::AlignLeft | Qt::AlignTop); + meta_layout->setFormAlignment(Qt::AlignLeft | Qt::AlignTop); + + m_version_value = new QLabel(meta_card); + m_version_value->setWordWrap(true); + meta_layout->addRow(tr("Version"), m_version_value); + + m_theme_id_value = new QLabel(meta_card); + m_theme_id_value->setWordWrap(true); + meta_layout->addRow(tr("Theme ID"), m_theme_id_value); + + m_size_value = new QLabel(meta_card); + m_size_value->setWordWrap(true); + meta_layout->addRow(tr("Installed Size"), m_size_value); + + m_updated_value = new QLabel(meta_card); + m_updated_value->setWordWrap(true); + meta_layout->addRow(tr("Updated"), m_updated_value); + + m_background_value = new QLabel(meta_card); + m_background_value->setWordWrap(true); + meta_layout->addRow(tr("Background"), m_background_value); + + m_cycle_value = new QLabel(meta_card); + m_cycle_value->setWordWrap(true); + meta_layout->addRow(tr("Cycle"), m_cycle_value); + + meta_card_layout->addLayout(meta_layout); + right_layout->addWidget(meta_card); + + auto *help_card = new QWidget(right_panel); + help_card->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Maximum); + auto *help_layout = new QVBoxLayout(help_card); + help_layout->setContentsMargins(0, 0, 0, 0); + help_layout->setSpacing(4); + + m_help_text = new QTextBrowser(help_card); + m_help_text->setObjectName(QStringLiteral("vitaThemesHelpText")); + m_help_text->setMinimumHeight(104); + m_help_text->setMaximumHeight(104); + m_help_text->setFocusPolicy(Qt::NoFocus); + m_help_text->setFrameShape(QFrame::NoFrame); + m_help_text->setReadOnly(true); + m_help_text->setOpenExternalLinks(true); + m_help_text->document()->setDocumentMargin(0); + help_layout->addWidget(m_help_text); + right_layout->addWidget(help_card); + + right_layout->addStretch(1); + + auto *buttons = new QWidget(this); + auto *buttons_layout = new QHBoxLayout(buttons); + buttons_layout->setContentsMargins(0, 0, 0, 0); + buttons_layout->setSpacing(8); + + m_refresh_button = new QPushButton(tr("Refresh"), buttons); + buttons_layout->addWidget(m_refresh_button); + register_description( + m_refresh_button, + tr("Refresh"), + tr("Rescan the Vita theme folder and update the installed theme list.")); + + m_open_folder_button = new QPushButton(tr("Open Theme Folder"), buttons); + buttons_layout->addWidget(m_open_folder_button); + register_description( + m_open_folder_button, + tr("Open Theme Folder"), + tr("Open the selected theme's folder in the file manager.")); + + buttons_layout->addStretch(1); + + m_delete_button = new QPushButton(tr("Delete"), buttons); + buttons_layout->addWidget(m_delete_button); + register_description( + m_delete_button, + tr("Delete"), + tr("Remove the selected installed Vita theme from ux0/theme.")); + + m_apply_button = new QPushButton(tr("Apply"), buttons); + m_apply_button->setDefault(true); + buttons_layout->addWidget(m_apply_button); + register_description( + m_apply_button, + tr("Apply"), + tr("Generate and apply the emulator theme using the currently selected background, cycling options, and appearance settings.")); + + root_layout->addWidget(buttons); + + splitter->setStretchFactor(0, 0); + splitter->setStretchFactor(1, 1); + + connect(m_theme_list, &QListWidget::currentRowChanged, + this, &VitaThemesDialog::apply_theme_selection); + connect(m_refresh_button, &QPushButton::clicked, + this, &VitaThemesDialog::reload); + connect(m_apply_button, &QPushButton::clicked, + this, &VitaThemesDialog::apply_selected_theme); + connect(m_delete_button, &QPushButton::clicked, + this, &VitaThemesDialog::delete_selected_theme); + connect(m_open_folder_button, &QPushButton::clicked, + this, &VitaThemesDialog::open_selected_theme_folder); + connect(m_readability_slider, &QSlider::valueChanged, + this, [this] { + load_selection_into_ui(selection_from_ui()); + }); + connect(m_normalize_fonts_checkbox, &QCheckBox::toggled, this, [this](bool checked) { + Q_UNUSED(checked); + load_selection_into_ui(selection_from_ui()); + }); + connect(m_cycle_checkbox, &QCheckBox::toggled, this, [this](bool checked) { + Q_UNUSED(checked); + load_selection_into_ui(selection_from_ui()); + }); + connect(m_cycle_interval_spinbox, qOverload(&QSpinBox::valueChanged), this, [this](int value) { + Q_UNUSED(value); + load_selection_into_ui(selection_from_ui()); + }); + + update_readability_label(); + sync_cycle_controls(); + reset_description(); +} + +void VitaThemesDialog::restore_persistent_state() { + const QSize preferred_dialog_size(1120, 640); + + if (!m_gui_settings) { + resize(preferred_dialog_size); + return; + } + + if (!restoreGeometry(m_gui_settings->get_value(gui::vt_geometry).toByteArray())) + resize(preferred_dialog_size); + + const int capped_height = std::max(sizeHint().height(), minimumSizeHint().height()); + if (height() > capped_height) + resize(width(), capped_height); +} + +void VitaThemesDialog::save_persistent_state() const { + if (!m_gui_settings) + return; + + m_gui_settings->set_value(gui::vt_geometry, saveGeometry(), false); + m_gui_settings->sync(); +} + +void VitaThemesDialog::repopulate_theme_list(const QString &preferred_theme_id) { + m_themes = m_theme_manager.installed_vita_themes(); + + m_theme_list->blockSignals(true); + m_theme_list->clear(); + + int selected_row = -1; + for (int index = 0; index < static_cast(m_themes.size()); ++index) { + const auto &theme = m_themes[index]; + auto *item = new QListWidgetItem( + theme.provider.empty() + ? to_qstring(theme.title) + : tr("%1\n%2").arg(to_qstring(theme.title), to_qstring(theme.provider)), + m_theme_list); + item->setData(Qt::UserRole, to_qstring(theme.theme_id)); + + if (!preferred_theme_id.isEmpty() && preferred_theme_id == to_qstring(theme.theme_id)) + selected_row = index; + } + + m_theme_list->blockSignals(false); + + if (selected_row < 0 && !m_themes.empty()) + selected_row = 0; + + if (selected_row >= 0) + m_theme_list->setCurrentRow(selected_row); + else + apply_theme_selection(-1); +} + +void VitaThemesDialog::apply_theme_selection(const int row) { + if (row < 0 || row >= static_cast(m_themes.size())) { + clear_selection_state(); + load_theme_into_ui(nullptr); + sync_preview(); + sync_metadata(); + sync_cycle_controls(); + sync_actions(); + return; + } + + const auto &theme = m_themes[static_cast(row)]; + gui::VitaThemeSelection selection; + if (const auto applied_selection = m_theme_manager.applied_vita_theme_selection(); + applied_selection && applied_selection->theme_id == to_qstring(theme.theme_id)) { + selection = *applied_selection; + } else { + selection.theme_id = to_qstring(theme.theme_id); + } + + load_selection_into_ui(std::move(selection), std::nullopt, true, true); + reset_description(); +} + +void VitaThemesDialog::clear_selection_state() { + m_selected_background_id.clear(); + m_preview_background_id.clear(); + m_loaded_preview_background_id.clear(); + m_selected_cycle_background_ids.clear(); + m_preview_pixmap = {}; +} + +gui::VitaThemeSelection VitaThemesDialog::selection_from_ui() const { + gui::VitaThemeSelection selection; + if (const auto *theme = current_theme()) + selection.theme_id = to_qstring(theme->theme_id); + + selection.background_id = m_selected_background_id; + selection.readability = m_readability_slider ? m_readability_slider->value() : 55; + selection.normalize_font_colors = m_normalize_fonts_checkbox + ? m_normalize_fonts_checkbox->isChecked() + : true; + selection.cycle_enabled = m_cycle_checkbox && m_cycle_checkbox->isChecked(); + selection.cycle_interval_seconds = m_cycle_interval_spinbox + ? m_cycle_interval_spinbox->value() + : 15; + selection.cycle_background_ids = m_selected_cycle_background_ids; + return selection; +} + +void VitaThemesDialog::load_selection_into_ui( + gui::VitaThemeSelection selection, + const std::optional preview_background_id, + const bool follow_primary_preview, + const bool reload_theme_ui) { + const auto *theme = current_theme(); + if (!theme) { + clear_selection_state(); + load_theme_into_ui(nullptr); + sync_preview(); + sync_metadata(); + sync_cycle_controls(); + sync_actions(); + return; + } + + selection.theme_id = to_qstring(theme->theme_id); + const gui::VitaThemeSelection normalized_selection = gui::normalize_vita_theme_selection(*theme, std::move(selection)); + + { + const QSignalBlocker readability_blocker(m_readability_slider); + const QSignalBlocker normalize_fonts_blocker(m_normalize_fonts_checkbox); + const QSignalBlocker cycle_checkbox_blocker(m_cycle_checkbox); + const QSignalBlocker cycle_interval_blocker(m_cycle_interval_spinbox); + m_readability_slider->setValue(normalized_selection.readability); + m_normalize_fonts_checkbox->setChecked(normalized_selection.normalize_font_colors); + m_cycle_checkbox->setChecked(normalized_selection.cycle_enabled); + m_cycle_interval_spinbox->setValue(normalized_selection.cycle_interval_seconds); + } + + m_selected_background_id = normalized_selection.background_id; + m_selected_cycle_background_ids = normalized_selection.cycle_background_ids; + m_preview_background_id = follow_primary_preview + ? m_selected_background_id + : resolve_preview_background_id( + *theme, + preview_background_id ? *preview_background_id : m_preview_background_id); + + if (reload_theme_ui || m_loaded_theme_id != normalized_selection.theme_id) + load_theme_into_ui(theme); + + update_readability_label(); + sync_background_rows(); + sync_preview(); + sync_metadata(); + sync_cycle_controls(); + sync_actions(); +} + +void VitaThemesDialog::load_theme_into_ui(const gui::VitaThemeInfo *theme) { + if (!m_background_list) + return; + + const QSignalBlocker list_blocker(m_background_list); + m_background_rows.clear(); + m_background_list->clear(); + if (m_primary_background_group) { + delete m_primary_background_group; + m_primary_background_group = nullptr; + } + + m_loaded_theme_id = theme ? to_qstring(theme->theme_id) : QString(); + m_loaded_preview_background_id.clear(); + if (!theme) + return; + + m_primary_background_group = new QButtonGroup(this); + m_primary_background_group->setExclusive(true); + + for (const auto &background : theme->background_options) { + BackgroundRow row; + row.id = to_qstring(background.id); + + row.item = new QTreeWidgetItem(m_background_list); + row.item->setData(0, Qt::UserRole, row.id); + row.item->setText(0, to_qstring(background.label)); + row.item->setSizeHint(0, QSize(0, 34)); + + auto *primary_widget = new QWidget(m_background_list); + primary_widget->setProperty("themeRole", QStringLiteral("backgroundCell")); + auto *primary_layout = new QHBoxLayout(primary_widget); + primary_layout->setContentsMargins(6, 0, 6, 0); + primary_layout->setSpacing(0); + primary_layout->setAlignment(Qt::AlignCenter); + + row.primary_radio = new QRadioButton(primary_widget); + row.primary_radio->setProperty("themeRole", QStringLiteral("backgroundCellControl")); + m_primary_background_group->addButton(row.primary_radio); + connect(row.primary_radio, &QRadioButton::clicked, this, [this, id = row.id] { + set_selected_background(id); + }); + primary_layout->addWidget(row.primary_radio); + m_background_list->setItemWidget(row.item, 1, primary_widget); + + auto *cycle_widget = new QWidget(m_background_list); + cycle_widget->setProperty("themeRole", QStringLiteral("backgroundCell")); + auto *cycle_layout = new QHBoxLayout(cycle_widget); + cycle_layout->setContentsMargins(6, 0, 6, 0); + cycle_layout->setSpacing(0); + cycle_layout->setAlignment(Qt::AlignCenter); + + row.cycle_checkbox = new QCheckBox(cycle_widget); + row.cycle_checkbox->setProperty("themeRole", QStringLiteral("backgroundCellControl")); + connect(row.cycle_checkbox, &QCheckBox::toggled, this, [this, id = row.id](const bool checked) { + QStringList next_ids = m_selected_cycle_background_ids; + if (checked) { + if (!next_ids.contains(id)) + next_ids.append(id); + } else { + next_ids.removeAll(id); + } + set_selected_cycle_backgrounds(next_ids); + }); + cycle_layout->addWidget(row.cycle_checkbox); + m_background_list->setItemWidget(row.item, 2, cycle_widget); + + const QString description = tr("Set this as the primary background for the generated theme. %1 backgrounds can also be included in cycling.") + .arg(background_source_label(background)); + register_description(primary_widget, tr("Primary Background"), description); + register_description(row.primary_radio, tr("Primary Background"), description); + register_description( + cycle_widget, + tr("Include In Cycle"), + tr("Include this background when cycling is enabled. The primary background is always kept in the cycle order.")); + register_description( + row.cycle_checkbox, + tr("Include In Cycle"), + tr("Include this background when cycling is enabled. The primary background is always kept in the cycle order.")); + + m_background_rows.push_back(row); + } +} + +void VitaThemesDialog::sync_background_rows() { + if (!m_background_list) + return; + + const auto *theme = current_theme(); + const bool can_cycle = theme && theme->background_options.size() > 1; + const QString current_preview_id = theme + ? resolve_preview_background_id(*theme, m_preview_background_id) + : QString(); + QTreeWidgetItem *current_item = nullptr; + + const QSignalBlocker list_blocker(m_background_list); + for (auto &row : m_background_rows) { + if (row.primary_radio) { + const QSignalBlocker blocker(row.primary_radio); + row.primary_radio->setChecked(row.id == m_selected_background_id); + } + + if (row.cycle_checkbox) { + const QSignalBlocker blocker(row.cycle_checkbox); + row.cycle_checkbox->setChecked(m_selected_cycle_background_ids.contains(row.id)); + row.cycle_checkbox->setEnabled(can_cycle && row.id != m_selected_background_id); + } + + if (row.item && row.id == current_preview_id) + current_item = row.item; + } + + if (!current_item) { + for (const auto &row : m_background_rows) { + if (row.item && row.id == m_selected_background_id) { + current_item = row.item; + break; + } + } + } + + if (!current_item && !m_background_rows.empty()) + current_item = m_background_rows.front().item; + + if (m_background_list->currentItem() != current_item) + m_background_list->setCurrentItem(current_item); +} + +void VitaThemesDialog::set_selected_background(const QString &background_id) { + auto selection = selection_from_ui(); + selection.background_id = background_id; + load_selection_into_ui(std::move(selection), std::nullopt, true); +} + +void VitaThemesDialog::set_preview_background(const QString &background_id) { + load_selection_into_ui(selection_from_ui(), background_id); +} + +void VitaThemesDialog::set_selected_cycle_backgrounds(const QStringList &background_ids) { + auto selection = selection_from_ui(); + selection.cycle_background_ids = background_ids; + load_selection_into_ui(std::move(selection)); +} + +void VitaThemesDialog::sync_preview() { + auto *preview_label = dynamic_cast(m_preview); + const auto *background = current_preview_background(); + if (!background) { + m_loaded_preview_background_id.clear(); + if (preview_label) + preview_label->set_aspect_size(k_default_preview_aspect); + m_preview->setText(tr("No preview available")); + m_preview->setPixmap(QPixmap()); + m_preview_pixmap = {}; + return; + } + + const QString background_id = to_qstring(background->id); + if (m_loaded_preview_background_id == background_id && !m_preview_pixmap.isNull()) { + update_preview_pixmap(); + return; + } + + const QString qt_path = gui::utils::to_qt_path(background->asset_path); + QPixmap pixmap(qt_path); + if (pixmap.isNull()) { + m_loaded_preview_background_id.clear(); + if (preview_label) + preview_label->set_aspect_size(k_default_preview_aspect); + m_preview->setText(tr("Unable to load preview")); + m_preview->setPixmap(QPixmap()); + m_preview_pixmap = {}; + return; + } + + m_loaded_preview_background_id = background_id; + m_preview->setText(QString()); + if (preview_label) + preview_label->set_aspect_size(pixmap.size()); + m_preview_pixmap = pixmap; + update_preview_pixmap(); +} + +void VitaThemesDialog::sync_metadata() { + const auto *theme = current_theme(); + if (!theme) { + m_theme_title->setText(tr("No themes installed")); + m_theme_provider->setText(tr("Install or copy a Vita theme into ux0/theme to get started.")); + m_version_value->clear(); + m_theme_id_value->clear(); + m_size_value->clear(); + m_updated_value->clear(); + m_background_value->clear(); + m_cycle_value->clear(); + if (m_cycle_summary_value) + m_cycle_summary_value->clear(); + return; + } + + m_theme_title->setText(to_qstring(theme->title)); + m_theme_provider->setText(theme->provider.empty() ? tr("Unknown provider") : to_qstring(theme->provider)); + m_version_value->setText(theme->content_version.empty() ? tr("Unknown") : to_qstring(theme->content_version)); + m_theme_id_value->setText(to_qstring(theme->theme_id)); + m_size_value->setText(QLocale().formattedDataSize(static_cast(theme->installed_size))); + m_updated_value->setText(theme->updated_time == 0 + ? tr("Unknown") + : QLocale().toString(QDateTime::fromSecsSinceEpoch(static_cast(theme->updated_time)), QLocale::LongFormat)); + + if (const auto *background = current_background()) { + m_background_value->setText(tr("%1: %2") + .arg(background_source_label(*background), to_qstring(background->label))); + } else { + m_background_value->clear(); + } + + if (m_cycle_checkbox && m_cycle_checkbox->isChecked()) { + m_cycle_value->setText(tr("%1 backgrounds every %2 seconds") + .arg(m_selected_cycle_background_ids.size()) + .arg(m_cycle_interval_spinbox ? m_cycle_interval_spinbox->value() : 15)); + } else if (m_selected_cycle_background_ids.size() > 1) { + m_cycle_value->setText(tr("Off (%1 saved)").arg(m_selected_cycle_background_ids.size())); + } else { + m_cycle_value->setText(tr("Off")); + } + + update_package_thumbnail(); +} + +void VitaThemesDialog::sync_actions() { + const bool has_theme = current_theme() != nullptr; + const bool has_background = current_background() != nullptr; + const bool has_cycle_selection = !m_selected_cycle_background_ids.isEmpty(); + m_apply_button->setEnabled(has_theme && has_background && has_cycle_selection); + m_delete_button->setEnabled(has_theme); + m_open_folder_button->setEnabled(has_theme); +} + +void VitaThemesDialog::sync_cycle_controls() { + const auto *theme = current_theme(); + const int background_count = theme ? static_cast(theme->background_options.size()) : 0; + const bool can_cycle = background_count > 1; + bool cycle_state_changed = false; + + if (!can_cycle && m_cycle_checkbox && m_cycle_checkbox->isChecked()) { + const QSignalBlocker blocker(m_cycle_checkbox); + m_cycle_checkbox->setChecked(false); + cycle_state_changed = true; + } + + if (m_cycle_checkbox) + m_cycle_checkbox->setEnabled(can_cycle); + if (m_cycle_interval_spinbox) + m_cycle_interval_spinbox->setEnabled(can_cycle && m_cycle_checkbox && m_cycle_checkbox->isChecked()); + if (m_background_list) + m_background_list->setEnabled(background_count > 0); + if (m_cycle_summary_value) { + if (!theme) { + m_cycle_summary_value->clear(); + } else if (!can_cycle) { + m_cycle_summary_value->setText(tr("Single background")); + } else if (m_cycle_checkbox && m_cycle_checkbox->isChecked()) { + m_cycle_summary_value->setText(tr("%1 selected").arg(m_selected_cycle_background_ids.size())); + } else if (m_selected_cycle_background_ids.size() > 1) { + m_cycle_summary_value->setText(tr("%1 saved").arg(m_selected_cycle_background_ids.size())); + } else { + m_cycle_summary_value->setText(tr("Off")); + } + } + + if (cycle_state_changed) + sync_metadata(); +} + +void VitaThemesDialog::update_preview_pixmap() { + if (m_preview_pixmap.isNull()) { + m_preview->setPixmap(QPixmap()); + return; + } + + const QSize target_size = m_preview->size() - QSize(12, 12); + if (target_size.width() <= 0 || target_size.height() <= 0) + return; + + m_preview->setPixmap(m_preview_pixmap.scaled( + target_size, + Qt::KeepAspectRatio, + Qt::SmoothTransformation)); +} + +void VitaThemesDialog::update_package_thumbnail() { + const auto *theme = current_theme(); + if (!theme || theme->package_thumbnail_path.empty() || !fs::exists(theme->package_thumbnail_path)) { + m_package_thumbnail->setPixmap(QPixmap()); + m_package_thumbnail->setText(tr("No package thumbnail")); + return; + } + + const QPixmap pixmap(gui::utils::to_qt_path(theme->package_thumbnail_path)); + if (pixmap.isNull()) { + m_package_thumbnail->setPixmap(QPixmap()); + m_package_thumbnail->setText(tr("No package thumbnail")); + return; + } + + m_package_thumbnail->setText(QString()); + m_package_thumbnail->setPixmap(pixmap.scaled( + m_package_thumbnail->size() - QSize(12, 12), + Qt::KeepAspectRatio, + Qt::SmoothTransformation)); +} + +void VitaThemesDialog::update_readability_label() { + if (!m_readability_slider || !m_readability_value) + return; + + m_readability_value->setText(tr("%1%").arg(m_readability_slider->value())); +} + +void VitaThemesDialog::apply_selected_theme() { + const auto *theme = current_theme(); + const auto *background = current_background(); + if (!theme || !background) + return; + + const gui::VitaThemeSelection selection = selection_from_ui(); + + if (!m_theme_manager.apply_vita_theme_selection(selection, true)) { + QMessageBox::warning( + this, + tr("Apply Failed"), + tr("Vita3K could not generate or apply the selected theme stylesheet.")); + return; + } +} + +void VitaThemesDialog::delete_selected_theme() { + const auto *theme = current_theme(); + if (!theme) + return; + + const auto result = QMessageBox::question( + this, + tr("Delete Theme"), + tr("Delete \"%1\" from ux0/theme?").arg(to_qstring(theme->title)), + QMessageBox::Yes | QMessageBox::No, + QMessageBox::No); + if (result != QMessageBox::Yes) + return; + + const QString deleting_theme_id = to_qstring(theme->theme_id); + const auto applied_selection = m_theme_manager.applied_vita_theme_selection(); + const bool was_active = applied_selection && applied_selection->theme_id == deleting_theme_id; + boost::system::error_code error_code; + fs::remove_all(theme->installed_path, error_code); + if (error_code) { + QMessageBox::warning( + this, + tr("Delete Failed"), + tr("Vita3K could not remove the selected theme folder.")); + return; + } + + if (was_active && m_gui_settings) { + m_theme_manager.clear_applied_vita_theme(); + m_gui_settings->set_value(gui::m_currentStylesheet, gui::DarkStylesheet, false); + m_gui_settings->sync(); + m_theme_manager.apply_theme(gui::DarkStylesheet, true); + } + m_theme_manager.refresh_vita_theme_catalog(); + repopulate_theme_list(); +} + +void VitaThemesDialog::open_selected_theme_folder() const { + const auto *theme = current_theme(); + if (!theme) + return; + + gui::utils::open_dir(theme->installed_path); +} + +void VitaThemesDialog::register_description(QWidget *widget, const QString &title, const QString &description) { + if (!widget) + return; + + widget->setProperty("_desc_title", title); + widget->setProperty("_desc", description); + widget->installEventFilter(this); +} + +void VitaThemesDialog::set_description(const QString &title, const QString &text) { + if (!m_help_text) + return; + + m_help_text->setHtml(gui::utils::format_help_html(title, text)); +} + +void VitaThemesDialog::reset_description() { + set_description(tr("Vita Themes"), default_description()); +} + +QString VitaThemesDialog::default_description() const { + if (!current_theme()) { + return tr("Select an installed Vita theme to preview its backgrounds and generate a matching emulator theme."); + } + + return tr("Choose one primary background, decide which backgrounds should appear in the cycle, adjust readability options, then apply the generated theme."); +} + +QString VitaThemesDialog::resolve_preview_background_id( + const gui::VitaThemeInfo &theme, + const QString &background_id) const { + if (theme.background_options.empty()) + return {}; + + if (!background_id.isEmpty()) { + const auto matches = [&background_id](const gui::VitaThemeBackgroundOption &option) { + return background_id == to_qstring(option.id); + }; + if (std::find_if(theme.background_options.begin(), theme.background_options.end(), matches) != theme.background_options.end()) + return background_id; + } + + return m_selected_background_id.isEmpty() + ? to_qstring(theme.background_options.front().id) + : m_selected_background_id; +} + +const gui::VitaThemeInfo *VitaThemesDialog::current_theme() const { + const int row = m_theme_list ? m_theme_list->currentRow() : -1; + if (row < 0 || row >= static_cast(m_themes.size())) + return nullptr; + + return &m_themes[static_cast(row)]; +} + +const gui::VitaThemeBackgroundOption *VitaThemesDialog::current_background() const { + const auto *theme = current_theme(); + if (!theme) + return nullptr; + + for (const auto &background : theme->background_options) { + if (m_selected_background_id == to_qstring(background.id)) + return &background; + } + + return theme->background_options.empty() ? nullptr : &theme->background_options.front(); +} + +const gui::VitaThemeBackgroundOption *VitaThemesDialog::current_preview_background() const { + const auto *theme = current_theme(); + if (!theme) + return nullptr; + + for (const auto &background : theme->background_options) { + if (m_preview_background_id == to_qstring(background.id)) + return &background; + } + + return current_background(); +} diff --git a/vita3k/icons/check_dark.svg b/vita3k/icons/check_dark.svg new file mode 100644 index 000000000..2a1292f4d --- /dev/null +++ b/vita3k/icons/check_dark.svg @@ -0,0 +1,3 @@ + + + diff --git a/vita3k/icons/check_light.svg b/vita3k/icons/check_light.svg new file mode 100644 index 000000000..f12d5d72b --- /dev/null +++ b/vita3k/icons/check_light.svg @@ -0,0 +1,3 @@ + + + diff --git a/vita3k/icons/radio_dot_dark.svg b/vita3k/icons/radio_dot_dark.svg new file mode 100644 index 000000000..d0b49473f --- /dev/null +++ b/vita3k/icons/radio_dot_dark.svg @@ -0,0 +1,3 @@ + + + diff --git a/vita3k/icons/radio_dot_light.svg b/vita3k/icons/radio_dot_light.svg new file mode 100644 index 000000000..f774f241f --- /dev/null +++ b/vita3k/icons/radio_dot_light.svg @@ -0,0 +1,3 @@ + + + diff --git a/vita3k/interface.cpp b/vita3k/interface.cpp index f5a848a89..71f6e3ef7 100644 --- a/vita3k/interface.cpp +++ b/vita3k/interface.cpp @@ -44,6 +44,7 @@ #include #include #include +#include #include #include @@ -76,6 +77,18 @@ static const char *miniz_get_error(const ZipPtr &zip) { return mz_zip_get_error_string(mz_zip_get_last_error(zip.get())); } +static std::string fallback_theme_root_name(const std::string &content_path) { + std::string trimmed = content_path; + while (!trimmed.empty() && ((trimmed.back() == '/') || (trimmed.back() == '\\'))) + trimmed.pop_back(); + + if (trimmed.empty()) + return {}; + + const auto separator = trimmed.find_last_of("/\\"); + return (separator == std::string::npos) ? trimmed : trimmed.substr(separator + 1); +} + static bool is_nonpdrm(EmuEnvState &emuenv, const fs::path &output_path) { const auto app_license_path{ emuenv.pref_path / "ux0/license" / emuenv.app_info.app_title_id / fmt::format("{}.rif", emuenv.app_info.app_content_id) }; const auto is_patch_found_app_license = (emuenv.app_info.app_category == "gp") && fs::exists(app_license_path); @@ -120,18 +133,32 @@ static bool set_content_path(EmuEnvState &emuenv, const bool is_theme, fs::path return true; } -static void set_theme_name(EmuEnvState &emuenv, const vfs::FileBuffer &buffer) { +static void set_theme_name(EmuEnvState &emuenv, const vfs::FileBuffer &buffer, const std::string &fallback_id_hint = {}) { + std::string content_id; + std::string title; + pugi::xml_document doc; if (doc.load_buffer(buffer.data(), buffer.size())) { const auto info = doc.child("theme").child("InfomationProperty"); - const char *content_id = info.child("m_contentId").text().as_string(); - const char *title = info.child("m_title").child("m_default").text().as_string(); - if (content_id[0] != '\0') { - emuenv.app_info.app_content_id = content_id; - emuenv.app_info.app_title_id = content_id; - } - if (title[0] != '\0') - emuenv.app_info.app_title = title; + content_id = info.child("m_contentId").text().as_string(); + title = info.child("m_title").child("m_default").text().as_string(); + } else { + LOG_WARN("Unable to parse theme.xml metadata during install, falling back to folder/title-derived theme identity"); + } + + const std::string resolved_id = vita_theme_utils::resolve_theme_id( + content_id, + fallback_id_hint, + title, + buffer.empty() ? nullptr : buffer.data(), + buffer.size()); + emuenv.app_info.app_content_id = resolved_id; + emuenv.app_info.app_title_id = resolved_id; + + if (!title.empty()) { + emuenv.app_info.app_title = title; + } else if (emuenv.app_info.app_title.empty()) { + emuenv.app_info.app_title = resolved_id; } } @@ -141,6 +168,7 @@ static bool install_archive_content(EmuEnvState &emuenv, const ZipPtr &zip, cons vfs::FileBuffer buffer, theme; const auto is_theme = mz_zip_reader_extract_file_to_callback(zip.get(), (content_path + theme_path).c_str(), &write_to_buffer, &theme, 0); + const std::string theme_root_name = fallback_theme_root_name(content_path); auto output_path{ emuenv.pref_path / "ux0" }; if (mz_zip_reader_extract_file_to_callback(zip.get(), (content_path + sfo_path).c_str(), &write_to_buffer, &buffer, 0)) { @@ -148,7 +176,7 @@ static bool install_archive_content(EmuEnvState &emuenv, const ZipPtr &zip, cons if (!set_content_path(emuenv, is_theme, output_path)) return false; } else if (is_theme) { - set_theme_name(emuenv, theme); + set_theme_name(emuenv, theme, theme_root_name); output_path /= fs::path("theme") / emuenv.app_info.app_content_id; } else { LOG_CRITICAL("miniz error: {} extracting file: {}", miniz_get_error(zip), sfo_path); @@ -321,7 +349,7 @@ static bool install_content(EmuEnvState &emuenv, const fs::path &content_path) { fs::remove_all(dst_path); } else if (fs_utils::read_data(theme_path, buffer)) { - set_theme_name(emuenv, buffer); + set_theme_name(emuenv, buffer, fs_utils::path_to_utf8(content_path.filename())); dst_path /= fs::path("theme") / fs_utils::utf8_to_path(emuenv.app_info.app_title_id); } else { LOG_ERROR("Param.sfo file is missing in path", sfo_path); @@ -434,7 +462,7 @@ static ExitCode load_app_impl(SceUID &main_module_id, EmuEnvState &emuenv, const } else return FileNotFound; // Set self name from self path, can contain folder, get file name only - emuenv.self_name = fs::path(emuenv.self_path).filename().string(); + emuenv.self_name = fs_utils::path_to_utf8(fs::path(emuenv.self_path).filename()); // get list of preload modules SceUInt32 process_preload_disabled = 0; diff --git a/vita3k/resources.qrc b/vita3k/resources.qrc index c9ddcb577..2d53e8afd 100644 --- a/vita3k/resources.qrc +++ b/vita3k/resources.qrc @@ -7,10 +7,21 @@ icons/silver.png icons/gold.png icons/platinum.png + gui-qt/resources/themes/light/light.qss + icons/chevron_down_dark.svg + icons/chevron_up_dark.svg + gui-qt/resources/themes/dark/dark.qss + icons/chevron_down_light.svg + icons/chevron_up_light.svg + gui-qt/resources/themes/vita/generated.qss.in icons/configure.png icons/controllers.png icons/chevron_down_dark.svg icons/chevron_down_light.svg + icons/check_dark.svg + icons/check_light.svg + icons/radio_dot_dark.svg + icons/radio_dot_light.svg icons/cross_dark.svg icons/cross_light.svg icons/chevron_up_dark.svg diff --git a/vita3k/util/CMakeLists.txt b/vita3k/util/CMakeLists.txt index f1a6d9888..f2c617bf5 100644 --- a/vita3k/util/CMakeLists.txt +++ b/vita3k/util/CMakeLists.txt @@ -12,6 +12,7 @@ add_library( src/net_utils.cpp src/string_utils.cpp src/tracy.cpp + src/vita_theme_utils.cpp ) # vc_runtime_checker.cpp is directly added from the main CMakeList (for some reason adding it here doesn't work) diff --git a/vita3k/util/include/util/string_utils.h b/vita3k/util/include/util/string_utils.h index 8424f3d4d..d17891cd4 100644 --- a/vita3k/util/include/util/string_utils.h +++ b/vita3k/util/include/util/string_utils.h @@ -29,6 +29,7 @@ std::wstring utf_to_wide(const std::string &str); std::string wide_to_utf(const std::wstring &str); std::string utf16_to_utf8(const std::u16string &str); std::u16string utf8_to_utf16(const std::string &str); +std::string trim_copy(const std::string &str); std::string remove_special_chars(std::string str); void replace(std::string &str, const std::string &in, const std::string &out); std::vector string_to_byte_array(const std::string &string); diff --git a/vita3k/util/include/util/vita_theme_utils.h b/vita3k/util/include/util/vita_theme_utils.h new file mode 100644 index 000000000..160516668 --- /dev/null +++ b/vita3k/util/include/util/vita_theme_utils.h @@ -0,0 +1,34 @@ +// Vita3K emulator project +// Copyright (C) 2026 Vita3K team +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along +// with this program; if not, write to the Free Software Foundation, Inc., +// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +#pragma once + +#include +#include +#include + +namespace vita_theme_utils { + +std::string sanitize_theme_id(std::string value); +std::string resolve_theme_id( + const std::string &content_id, + const std::string &folder_name, + const std::string &title, + const std::uint8_t *fallback_hash_data = nullptr, + std::size_t fallback_hash_size = 0); + +} // namespace vita_theme_utils diff --git a/vita3k/util/src/string_utils.cpp b/vita3k/util/src/string_utils.cpp index c93dcfc0e..1e75a8a93 100644 --- a/vita3k/util/src/string_utils.cpp +++ b/vita3k/util/src/string_utils.cpp @@ -53,6 +53,20 @@ std::string wide_to_utf(const std::wstring &str) { return myconv.to_bytes(str); } +std::string trim_copy(const std::string &str) { + const auto begin = std::find_if_not(str.begin(), str.end(), [](const unsigned char ch) { + return std::isspace(ch) != 0; + }); + const auto end = std::find_if_not(str.rbegin(), str.rend(), [](const unsigned char ch) { + return std::isspace(ch) != 0; + }).base(); + + if (begin >= end) + return {}; + + return std::string(begin, end); +} + std::string remove_special_chars(std::string str) { for (char &c : str) { switch (c) { diff --git a/vita3k/util/src/vita_theme_utils.cpp b/vita3k/util/src/vita_theme_utils.cpp new file mode 100644 index 000000000..33811820d --- /dev/null +++ b/vita3k/util/src/vita_theme_utils.cpp @@ -0,0 +1,81 @@ +// Vita3K emulator project +// Copyright (C) 2026 Vita3K team +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along +// with this program; if not, write to the Free Software Foundation, Inc., +// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +#include + +#include + +#include + +#include + +namespace vita_theme_utils { + +std::string sanitize_theme_id(std::string value) { + value = string_utils::trim_copy(string_utils::remove_special_chars(std::move(value))); + + std::string sanitized; + sanitized.reserve(value.size()); + + bool last_was_separator = false; + for (const unsigned char raw_char : value) { + const char ch = static_cast(raw_char); + if (std::isalnum(raw_char) || (ch == '-') || (ch == '_')) { + sanitized.push_back(ch); + last_was_separator = false; + } else if (!last_was_separator) { + sanitized.push_back('_'); + last_was_separator = true; + } + } + + while (!sanitized.empty() && ((sanitized.front() == '_') || (sanitized.front() == '-'))) + sanitized.erase(sanitized.begin()); + while (!sanitized.empty() && ((sanitized.back() == '_') || (sanitized.back() == '-'))) + sanitized.pop_back(); + + return sanitized; +} + +std::string resolve_theme_id( + const std::string &content_id, + const std::string &folder_name, + const std::string &title, + const std::uint8_t *fallback_hash_data, + const std::size_t fallback_hash_size) { + if (const std::string trimmed_content_id = string_utils::trim_copy(content_id); !trimmed_content_id.empty()) + return trimmed_content_id; + + if (const std::string sanitized_folder = sanitize_theme_id(folder_name); !sanitized_folder.empty()) + return sanitized_folder; + + if (const std::string sanitized_title = sanitize_theme_id(title); !sanitized_title.empty()) + return sanitized_title; + + if (fallback_hash_data && fallback_hash_size > 0) { + uint32_t hash = 2166136261u; + for (std::size_t index = 0; index < fallback_hash_size; ++index) { + hash ^= fallback_hash_data[index]; + hash *= 16777619u; + } + return fmt::format("theme-{:08X}", hash); + } + + return "theme"; +} + +} // namespace vita_theme_utils