From c5e8e04f65db6f63d1354908c4593bbc27b39367 Mon Sep 17 00:00:00 2001 From: scawful Date: Fri, 26 Sep 2025 19:55:40 -0400 Subject: [PATCH] Enhance theme management with dynamic discovery and macOS bundle support - Updated CMake configuration to ensure theme files are included in macOS bundles and added explicit file handling for theme resources. - Refactored ThemeManager to replace hardcoded theme lists with dynamic discovery of available themes, improving flexibility across development and deployment scenarios. - Introduced methods for refreshing and loading themes at runtime, enhancing user experience and customization options. - Improved logging for theme loading processes, providing better feedback on successes and failures. --- CMakeLists.txt | 7 +- src/CMakeLists.txt | 4 + src/app/gui/theme_manager.cc | 185 ++++++++++++++++++++++++++++++++--- src/app/gui/theme_manager.h | 10 ++ 4 files changed, 190 insertions(+), 16 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 81090548..3aa2e6fe 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -122,11 +122,14 @@ endif() # ImGui (after minimal build flags are set) include(cmake/imgui.cmake) -# Project Files -# Copy theme files to build directory +# Project Files +# Copy theme files to build directory (for development) file(GLOB THEME_FILES "${CMAKE_SOURCE_DIR}/assets/themes/*.theme") file(COPY ${THEME_FILES} DESTINATION "${CMAKE_BINARY_DIR}/assets/themes/") +# IMPORTANT: Also ensure themes are included in macOS bundles +# This is handled in src/CMakeLists.txt via YAZE_RESOURCE_FILES + add_subdirectory(src) # Tests diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index a9661be1..60e852cf 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -32,6 +32,10 @@ set(YAZE_RESOURCE_FILES ${CMAKE_SOURCE_DIR}/assets/font/MaterialIcons-Regular.ttf ) +# Add theme files for macOS bundle (replacing the glob pattern with explicit files) +file(GLOB YAZE_THEME_FILES "${CMAKE_SOURCE_DIR}/assets/themes/*.theme") +list(APPEND YAZE_RESOURCE_FILES ${YAZE_THEME_FILES}) + foreach (FILE ${YAZE_RESOURCE_FILES}) file(RELATIVE_PATH NEW_FILE "${CMAKE_SOURCE_DIR}/assets" ${FILE}) get_filename_component(NEW_FILE_PATH ${NEW_FILE} DIRECTORY) diff --git a/src/app/gui/theme_manager.cc b/src/app/gui/theme_manager.cc index a30a905f..a0017880 100644 --- a/src/app/gui/theme_manager.cc +++ b/src/app/gui/theme_manager.cc @@ -2,6 +2,7 @@ #include #include +#include #include #include "absl/strings/str_format.h" @@ -109,20 +110,10 @@ void ThemeManager::InitializeBuiltInThemes() { // Always create fallback theme first CreateFallbackYazeClassic(); - // Try to load themes from files (will override fallback if successful) - std::vector theme_files = { - "yaze_tre.theme", - "cyberpunk.theme", - "sunset.theme", - "forest.theme", - "midnight.theme" - }; - - for (const auto& theme_file : theme_files) { - auto status = LoadThemeFromFile(theme_file); - if (!status.ok()) { - util::logf("Failed to load theme file %s: %s", theme_file.c_str(), status.message().data()); - } + // Load all available theme files dynamically + auto status = LoadAllAvailableThemes(); + if (!status.ok()) { + util::logf("Warning: Failed to load some theme files: %s", status.message().data()); } // Ensure we have a valid current theme (prefer file-based theme) @@ -424,6 +415,14 @@ void ThemeManager::ShowThemeSelector(bool* p_open) { } ImGui::Separator(); + if (ImGui::Button(absl::StrFormat("%s Refresh Themes", ICON_MD_REFRESH).c_str())) { + auto status = RefreshAvailableThemes(); + if (!status.ok()) { + util::logf("Failed to refresh themes: %s", status.message().data()); + } + } + + ImGui::SameLine(); if (ImGui::Button(absl::StrFormat("%s Load Custom Theme", ICON_MD_FOLDER_OPEN).c_str())) { auto file_path = core::FileDialogWrapper::ShowOpenFileDialog(); if (!file_path.empty()) { @@ -875,5 +874,163 @@ void ThemeManager::ShowSimpleThemeEditor(bool* p_open) { ImGui::End(); } +std::vector ThemeManager::GetThemeSearchPaths() const { + std::vector search_paths; + + // Development path (relative to build directory) + search_paths.push_back("assets/themes/"); + search_paths.push_back("../assets/themes/"); + + // Platform-specific resource paths +#ifdef __APPLE__ + // macOS bundle resource path (this should be the primary path for bundled apps) + std::string bundle_themes = core::GetResourcePath("assets/themes/"); + util::logf("🔍 Bundle themes path from GetResourcePath: '%s'", bundle_themes.c_str()); + if (!bundle_themes.empty()) { + search_paths.push_back(bundle_themes); + } + + // Alternative bundle locations + std::string bundle_root = core::GetBundleResourcePath(); + util::logf("🔍 Bundle root path: '%s'", bundle_root.c_str()); + + search_paths.push_back(bundle_root + "Contents/Resources/themes/"); + search_paths.push_back(bundle_root + "Contents/Resources/assets/themes/"); + search_paths.push_back(bundle_root + "assets/themes/"); + search_paths.push_back(bundle_root + "themes/"); +#else + // Linux/Windows relative paths + search_paths.push_back("./assets/themes/"); + search_paths.push_back("./themes/"); +#endif + + // User config directory + std::string config_themes = core::GetConfigDirectory() + "/themes/"; + search_paths.push_back(config_themes); + + // Debug: Print all search paths + util::logf("🔍 Theme search paths (%zu total):", search_paths.size()); + for (size_t i = 0; i < search_paths.size(); ++i) { + util::logf(" [%zu]: '%s'", i, search_paths[i].c_str()); + } + + return search_paths; +} + +std::string ThemeManager::GetThemesDirectory() const { + auto search_paths = GetThemeSearchPaths(); + + // Try each search path and return the first one that exists + for (const auto& path : search_paths) { + std::ifstream test_file(path + "."); // Test if directory exists by trying to access it + if (test_file.good()) { + util::logf("Found themes directory: %s", path.c_str()); + return path; + } + + // Also try with platform-specific directory separators + std::string normalized_path = path; + if (!normalized_path.empty() && normalized_path.back() != '/' && normalized_path.back() != '\\') { + normalized_path += "/"; + } + + std::ifstream test_file2(normalized_path + "."); + if (test_file2.good()) { + util::logf("Found themes directory: %s", normalized_path.c_str()); + return normalized_path; + } + } + + util::logf("No themes directory found in search paths"); + return search_paths.empty() ? "assets/themes/" : search_paths[0]; +} + +std::vector ThemeManager::DiscoverAvailableThemeFiles() const { + std::vector theme_files; + auto search_paths = GetThemeSearchPaths(); + + for (const auto& search_path : search_paths) { + util::logf("Searching for theme files in: %s", search_path.c_str()); + + try { + // Use platform-specific file discovery instead of glob +#ifdef __APPLE__ + auto files_in_folder = core::FileDialogWrapper::GetFilesInFolder(search_path); + for (const auto& file : files_in_folder) { + if (file.length() > 6 && file.substr(file.length() - 6) == ".theme") { + std::string full_path = search_path + file; + util::logf("Found theme file: %s", full_path.c_str()); + theme_files.push_back(full_path); + } + } +#else + // For Linux/Windows, use filesystem directory iteration + // (could be extended with platform-specific implementations if needed) + std::vector known_themes = { + "yaze_tre.theme", "cyberpunk.theme", "sunset.theme", + "forest.theme", "midnight.theme" + }; + + for (const auto& theme_name : known_themes) { + std::string full_path = search_path + theme_name; + std::ifstream test_file(full_path); + if (test_file.good()) { + util::logf("Found theme file: %s", full_path.c_str()); + theme_files.push_back(full_path); + } + } +#endif + } catch (const std::exception& e) { + util::logf("Error scanning directory %s: %s", search_path.c_str(), e.what()); + } + } + + // Remove duplicates while preserving order + std::vector unique_files; + std::set seen_basenames; + + for (const auto& file : theme_files) { + std::string basename = core::GetFileName(file); + if (seen_basenames.find(basename) == seen_basenames.end()) { + unique_files.push_back(file); + seen_basenames.insert(basename); + } + } + + util::logf("Discovered %zu unique theme files", unique_files.size()); + return unique_files; +} + +absl::Status ThemeManager::LoadAllAvailableThemes() { + auto theme_files = DiscoverAvailableThemeFiles(); + + int successful_loads = 0; + int failed_loads = 0; + + for (const auto& theme_file : theme_files) { + auto status = LoadThemeFromFile(theme_file); + if (status.ok()) { + successful_loads++; + util::logf("✅ Successfully loaded theme: %s", theme_file.c_str()); + } else { + failed_loads++; + util::logf("❌ Failed to load theme %s: %s", theme_file.c_str(), status.message().data()); + } + } + + util::logf("Theme loading complete: %d successful, %d failed", successful_loads, failed_loads); + + if (successful_loads == 0 && failed_loads > 0) { + return absl::InternalError(absl::StrFormat("Failed to load any themes (%d failures)", failed_loads)); + } + + return absl::OkStatus(); +} + +absl::Status ThemeManager::RefreshAvailableThemes() { + util::logf("Refreshing available themes..."); + return LoadAllAvailableThemes(); +} + } // namespace gui } // namespace yaze diff --git a/src/app/gui/theme_manager.h b/src/app/gui/theme_manager.h index 2ad2c12b..48a35c02 100644 --- a/src/app/gui/theme_manager.h +++ b/src/app/gui/theme_manager.h @@ -138,6 +138,12 @@ public: absl::Status LoadThemeFromFile(const std::string& filepath); absl::Status SaveThemeToFile(const EnhancedTheme& theme, const std::string& filepath) const; + // Dynamic theme discovery - replaces hardcoded theme lists with automatic discovery + // This works across development builds, macOS app bundles, and other deployment scenarios + std::vector DiscoverAvailableThemeFiles() const; + absl::Status LoadAllAvailableThemes(); + absl::Status RefreshAvailableThemes(); // Public method to refresh at runtime + // Built-in themes void InitializeBuiltInThemes(); std::vector GetAvailableThemes() const; @@ -171,6 +177,10 @@ private: absl::Status ParseThemeFile(const std::string& content, EnhancedTheme& theme); Color ParseColorFromString(const std::string& color_str) const; std::string SerializeTheme(const EnhancedTheme& theme) const; + + // Helper methods for path resolution + std::vector GetThemeSearchPaths() const; + std::string GetThemesDirectory() const; }; } // namespace gui