diff --git a/src/app/core/project.cc b/src/app/core/project.cc index 5b8e624b..a715256c 100644 --- a/src/app/core/project.cc +++ b/src/app/core/project.cc @@ -10,6 +10,7 @@ #include "absl/strings/str_join.h" #include "absl/strings/str_split.h" #include "util/file_util.h" +#include "util/platform_paths.h" #include "app/gui/icons.h" #include "util/log.h" #include "app/zelda3/zelda3_labels.h" @@ -1059,13 +1060,19 @@ absl::Status YazeProject::SaveToJsonFormat() { // RecentFilesManager implementation std::string RecentFilesManager::GetFilePath() const { - return util::GetConfigDirectory() + "/" + kRecentFilesFilename; + auto config_dir = util::PlatformPaths::GetConfigDirectory(); + if (!config_dir.ok()) { + return ""; // Or handle error appropriately + } + return (*config_dir / kRecentFilesFilename).string(); } void RecentFilesManager::Save() { // Ensure config directory exists - if (!util::EnsureConfigDirectoryExists()) { - LOG_WARN("RecentFilesManager", "Could not create config directory for recent files"); + auto config_dir_status = util::PlatformPaths::GetConfigDirectory(); + if (!config_dir_status.ok()) { + LOG_ERROR("Project", "Failed to get or create config directory: %s", + config_dir_status.status().ToString().c_str()); return; } diff --git a/src/app/editor/agent/agent_chat_widget.cc b/src/app/editor/agent/agent_chat_widget.cc index ac073a54..b1c29a73 100644 --- a/src/app/editor/agent/agent_chat_widget.cc +++ b/src/app/editor/agent/agent_chat_widget.cc @@ -28,8 +28,10 @@ #include "app/rom.h" #include "imgui/imgui.h" #include "util/file_util.h" +#include "util/platform_paths.h" #include +#include #if defined(YAZE_WITH_GRPC) #include "app/test/test_manager.h" @@ -37,6 +39,7 @@ namespace { +namespace fs = std::filesystem; using yaze::cli::agent::ChatMessage; std::filesystem::path ExpandUserPath(std::string path) { @@ -55,7 +58,13 @@ std::filesystem::path ExpandUserPath(std::string path) { } std::filesystem::path ResolveHistoryPath(const std::string& session_id = "") { - std::filesystem::path base = ExpandUserPath(yaze::util::GetConfigDirectory()); + auto config_dir = yaze::util::PlatformPaths::GetConfigDirectory(); + if (!config_dir.ok()) { + // Fallback to a local directory if config can't be determined. + return fs::current_path() / ".yaze" / "agent" / "history" / (session_id.empty() ? "default.json" : session_id + ".json"); + } + + fs::path base = *config_dir; if (base.empty()) { base = ExpandUserPath(".yaze"); } diff --git a/src/app/editor/agent/agent_collaboration_coordinator.cc b/src/app/editor/agent/agent_collaboration_coordinator.cc index 5daba844..0ecfecf8 100644 --- a/src/app/editor/agent/agent_collaboration_coordinator.cc +++ b/src/app/editor/agent/agent_collaboration_coordinator.cc @@ -18,8 +18,12 @@ #include "absl/strings/str_format.h" #include "absl/strings/strip.h" #include "util/file_util.h" +#include "util/platform_paths.h" #include "util/macro.h" +#include +namespace fs = std::filesystem; + namespace yaze { namespace editor { @@ -259,11 +263,12 @@ std::string AgentCollaborationCoordinator::GenerateSessionCode() const { } std::filesystem::path AgentCollaborationCoordinator::SessionsDirectory() const { - std::filesystem::path base = ExpandUserPath(util::GetConfigDirectory()); - if (base.empty()) { - base = ExpandUserPath(".yaze"); + auto config_dir = util::PlatformPaths::GetConfigDirectory(); + if (!config_dir.ok()) { + // Fallback to a local directory if config can't be determined. + return fs::current_path() / ".yaze" / "agent" / "sessions"; } - return base / "agent" / "sessions"; + return *config_dir / "agent" / "sessions"; } std::filesystem::path AgentCollaborationCoordinator::SessionFilePath( diff --git a/src/app/editor/agent/agent_editor.cc b/src/app/editor/agent/agent_editor.cc index d755b376..576431c0 100644 --- a/src/app/editor/agent/agent_editor.cc +++ b/src/app/editor/agent/agent_editor.cc @@ -15,6 +15,7 @@ #include "app/gui/icons.h" #include "app/rom.h" #include "util/file_util.h" +#include "util/platform_paths.h" #ifdef YAZE_WITH_GRPC #include "app/editor/agent/network_collaboration_coordinator.h" @@ -1238,23 +1239,12 @@ absl::Status AgentEditor::ImportProfile(const std::filesystem::path& path) { } std::filesystem::path AgentEditor::GetProfilesDirectory() const { - std::filesystem::path config_dir(yaze::util::GetConfigDirectory()); - if (config_dir.empty()) { -#ifdef _WIN32 - const char* appdata = std::getenv("APPDATA"); - if (appdata) { - config_dir = std::filesystem::path(appdata) / "yaze"; - } -#else - const char* home_env = std::getenv("HOME"); - if (home_env) { - config_dir = std::filesystem::path(home_env) / ".yaze"; - } -#endif + auto config_dir = yaze::util::PlatformPaths::GetConfigDirectory(); + if (!config_dir.ok()) { + // Fallback to a local directory if config can't be determined. + return std::filesystem::current_path() / ".yaze" / "agent" / "profiles"; } - - return config_dir / std::filesystem::path("agent") / - std::filesystem::path("profiles"); + return *config_dir / "agent" / "profiles"; } absl::Status AgentEditor::EnsureProfilesDirectory() { diff --git a/src/app/editor/editor_manager.cc b/src/app/editor/editor_manager.cc index 56e01aa4..9867bec7 100644 --- a/src/app/editor/editor_manager.cc +++ b/src/app/editor/editor_manager.cc @@ -13,6 +13,7 @@ #include "app/core/features.h" #include "app/core/timing.h" #include "util/file_util.h" +#include "util/platform_paths.h" #include "app/core/project.h" #include "app/editor/code/assembly_editor.h" #include "app/editor/dungeon/dungeon_editor.h" @@ -82,8 +83,16 @@ std::string GetEditorName(EditorType type) { // Settings + preset helpers void EditorManager::LoadUserSettings() { + auto config_dir = util::PlatformPaths::GetConfigDirectory(); + if (!config_dir.ok()) { + LOG_WARN("EditorManager", "Could not determine config directory for settings."); + return; + } + + std::string settings_path = (*config_dir / settings_filename_).string(); + try { - auto data = util::LoadConfigFile(settings_filename_); + auto data = util::LoadFile(settings_path); if (!data.empty()) { std::istringstream ss(data); std::string line; @@ -102,7 +111,9 @@ void EditorManager::LoadUserSettings() { } ImGui::GetIO().FontGlobalScale = font_global_scale_; } - } catch (...) {} + } catch (...) { + // Could not load file, just use defaults. + } } void EditorManager::SaveUserSettings() { @@ -121,17 +132,21 @@ void EditorManager::RefreshWorkspacePresets() { // Try to read a simple index file of presets try { - auto data = util::LoadConfigFile("workspace_presets.txt"); - if (!data.empty()) { - std::istringstream ss(data); - std::string name; - while (std::getline(ss, name)) { - // Trim whitespace and validate - name.erase(0, name.find_first_not_of(" \t\r\n")); - name.erase(name.find_last_not_of(" \t\r\n") + 1); - if (!name.empty() && - name.length() < 256) { // Reasonable length limit - new_presets.emplace_back(std::move(name)); + auto config_dir = util::PlatformPaths::GetConfigDirectory(); + if (config_dir.ok()) { + std::string presets_path = (*config_dir / "workspace_presets.txt").string(); + auto data = util::LoadFile(presets_path); + if (!data.empty()) { + std::istringstream ss(data); + std::string name; + while (std::getline(ss, name)) { + // Trim whitespace and validate + name.erase(0, name.find_first_not_of(" \t\r\n")); + name.erase(name.find_last_not_of(" \t\r\n") + 1); + if (!name.empty() && + name.length() < 256) { // Reasonable length limit + new_presets.emplace_back(std::move(name)); + } } } } diff --git a/src/app/editor/music/music_editor.cc b/src/app/editor/music/music_editor.cc index 24ba87d0..64e834a9 100644 --- a/src/app/editor/music/music_editor.cc +++ b/src/app/editor/music/music_editor.cc @@ -3,9 +3,11 @@ #include "absl/strings/str_format.h" #include "app/gfx/performance_profiler.h" #include "app/editor/code/assembly_editor.h" +#include "app/emu/emulator.h" #include "app/gui/icons.h" #include "app/gui/input.h" #include "imgui/imgui.h" +#include "util/log.h" namespace yaze { namespace editor { @@ -210,5 +212,75 @@ void MusicEditor::DrawToolset() { ImGui::ProgressBar((float)current_time / SONG_DURATION); } +// ============================================================================ +// Audio Control Methods (Emulator Integration) +// ============================================================================ + +void MusicEditor::PlaySong(int song_id) { + if (!emulator_) { + LOG_WARN("MusicEditor", "No emulator instance - cannot play song"); + return; + } + + if (!emulator_->snes().running()) { + LOG_WARN("MusicEditor", "Emulator not running - cannot play song"); + return; + } + + // Write song request to game memory ($7E012C) + // This triggers the NMI handler to send the song to APU + try { + emulator_->snes().Write(0x7E012C, static_cast(song_id)); + LOG_INFO("MusicEditor", "Requested song %d (%s)", song_id, + song_id < 30 ? kGameSongs[song_id] : "Unknown"); + + // Ensure audio backend is playing + if (auto* audio = emulator_->audio_backend()) { + auto status = audio->GetStatus(); + if (!status.is_playing) { + audio->Play(); + LOG_INFO("MusicEditor", "Started audio backend playback"); + } + } + + is_playing_ = true; + } catch (const std::exception& e) { + LOG_ERROR("MusicEditor", "Failed to play song: %s", e.what()); + } +} + +void MusicEditor::StopSong() { + if (!emulator_) return; + + // Write stop command to game memory + try { + emulator_->snes().Write(0x7E012C, 0xFF); // 0xFF = stop music + LOG_INFO("MusicEditor", "Stopped music playback"); + + // Optional: pause audio backend to save CPU + if (auto* audio = emulator_->audio_backend()) { + audio->Pause(); + } + + is_playing_ = false; + } catch (const std::exception& e) { + LOG_ERROR("MusicEditor", "Failed to stop song: %s", e.what()); + } +} + +void MusicEditor::SetVolume(float volume) { + if (!emulator_) return; + + // Clamp volume to valid range + volume = std::clamp(volume, 0.0f, 1.0f); + + if (auto* audio = emulator_->audio_backend()) { + audio->SetVolume(volume); + LOG_DEBUG("MusicEditor", "Set volume to %.2f", volume); + } else { + LOG_WARN("MusicEditor", "No audio backend available"); + } +} + } // namespace editor } // namespace yaze diff --git a/src/app/editor/music/music_editor.h b/src/app/editor/music/music_editor.h index 7bb3a45b..bf6cb129 100644 --- a/src/app/editor/music/music_editor.h +++ b/src/app/editor/music/music_editor.h @@ -9,6 +9,12 @@ #include "imgui/imgui.h" namespace yaze { + +// Forward declaration +namespace emu { +class Emulator; +} + namespace editor { static const char* kGameSongs[] = {"Title", @@ -77,6 +83,15 @@ class MusicEditor : public Editor { // Get the ROM pointer Rom* rom() const { return rom_; } + + // Emulator integration for live audio playback + void set_emulator(emu::Emulator* emulator) { emulator_ = emulator; } + emu::Emulator* emulator() const { return emulator_; } + + // Audio control methods + void PlaySong(int song_id); + void StopSong(); + void SetVolume(float volume); // 0.0 to 1.0 private: // UI Drawing Methods @@ -112,6 +127,7 @@ class MusicEditor : public Editor { ImGuiTableFlags_BordersV | ImGuiTableFlags_PadOuterX; Rom* rom_; + emu::Emulator* emulator_ = nullptr; // For live audio playback }; } // namespace editor diff --git a/src/app/editor/ui/editor_selection_dialog.cc b/src/app/editor/ui/editor_selection_dialog.cc index 1c014aa8..e90424e7 100644 --- a/src/app/editor/ui/editor_selection_dialog.cc +++ b/src/app/editor/ui/editor_selection_dialog.cc @@ -344,7 +344,7 @@ void EditorSelectionDialog::MarkRecentlyUsed(EditorType type) { void EditorSelectionDialog::LoadRecentEditors() { try { - auto data = util::LoadConfigFile("recent_editors.txt"); + auto data = util::LoadFileFromConfigDir("recent_editors.txt"); if (!data.empty()) { std::istringstream ss(data); std::string line; diff --git a/src/app/gui/theme_manager.cc b/src/app/gui/theme_manager.cc index ea275bc7..c140908d 100644 --- a/src/app/gui/theme_manager.cc +++ b/src/app/gui/theme_manager.cc @@ -10,6 +10,7 @@ #include "absl/strings/str_format.h" #include "absl/strings/str_split.h" #include "util/file_util.h" +#include "util/platform_paths.h" #include "app/gui/icons.h" #include "app/gui/style.h" // For ColorsYaze function #include "imgui/imgui.h" @@ -1917,8 +1918,10 @@ std::vector ThemeManager::GetThemeSearchPaths() const { #endif // User config directory - std::string config_themes = util::GetConfigDirectory() + "/themes/"; - search_paths.push_back(config_themes); + auto config_dir = util::PlatformPaths::GetConfigDirectory(); + if (config_dir.ok()) { + search_paths.push_back((*config_dir / "themes/").string()); + } return search_paths; } diff --git a/src/util/file_util.cc b/src/util/file_util.cc index 9fefeab5..945afd7f 100644 --- a/src/util/file_util.cc +++ b/src/util/file_util.cc @@ -1,39 +1,23 @@ #include "util/file_util.h" -#ifdef _WIN32 -// Include Windows-specific headers -#include -#include -#else // Linux and MacOS -#include -#include -#include -#endif - -#include +#include #include #include -#include #include "app/core/features.h" +#include "util/platform_paths.h" namespace yaze { namespace util { +namespace fs = std::filesystem; + std::string GetFileExtension(const std::string &filename) { - size_t dot = filename.find_last_of('.'); - if (dot == std::string::npos) { - return ""; - } - return filename.substr(dot + 1); + return fs::path(filename).extension().string(); } std::string GetFileName(const std::string &filename) { - size_t slash = filename.find_last_of('/'); - if (slash == std::string::npos) { - return filename; - } - return filename.substr(slash + 1); + return fs::path(filename).filename().string(); } std::string LoadFile(const std::string &filename) { @@ -51,9 +35,14 @@ std::string LoadFile(const std::string &filename) { return contents; } -std::string LoadConfigFile(const std::string &filename) { +std::string LoadFileFromConfigDir(const std::string &filename) { + auto config_dir = PlatformPaths::GetConfigDirectory(); + if (!config_dir.ok()) { + return ""; // Or handle error appropriately + } + + fs::path filepath = *config_dir / filename; std::string contents; - std::string filepath = GetConfigDirectory() + "/" + filename; std::ifstream file(filepath); if (file.is_open()) { std::stringstream buffer; @@ -65,7 +54,12 @@ std::string LoadConfigFile(const std::string &filename) { } void SaveFile(const std::string &filename, const std::string &contents) { - std::string filepath = GetConfigDirectory() + "/" + filename; + auto config_dir = PlatformPaths::GetConfigDirectory(); + if (!config_dir.ok()) { + // Or handle error appropriately + return; + } + fs::path filepath = *config_dir / filename; std::ofstream file(filepath); if (file.is_open()) { file << contents; @@ -86,642 +80,8 @@ std::string GetResourcePath(const std::string &resource_path) { #endif } -std::string ExpandHomePath(const std::string& path) { - if (path.empty() || path[0] != '~') { - return path; - } - - const char* home = nullptr; -#ifdef _WIN32 - home = std::getenv("USERPROFILE"); - if (!home) { - home = std::getenv("HOMEDRIVE"); - const char* homePath = std::getenv("HOMEPATH"); - if (home && homePath) { - static std::string full_path; - full_path = std::string(home) + std::string(homePath); - home = full_path.c_str(); - } - } -#else - home = std::getenv("HOME"); -#endif - - if (!home) { - return path; // Fallback to original path if HOME not found - } - - // Replace ~ with home directory - if (path.size() == 1 || path[1] == '/') { - return std::string(home) + path.substr(1); - } - - return path; -} - -bool EnsureConfigDirectoryExists() { - std::string config_dir = GetConfigDirectory(); - -#ifdef _WIN32 - // Windows directory creation - DWORD attr = GetFileAttributesA(config_dir.c_str()); - if (attr == INVALID_FILE_ATTRIBUTES) { - // Directory doesn't exist, create it - if (!CreateDirectoryA(config_dir.c_str(), NULL)) { - DWORD error = GetLastError(); - if (error != ERROR_ALREADY_EXISTS) { - return false; - } - } - } -#else - // Unix-like directory creation - struct stat st; - if (stat(config_dir.c_str(), &st) != 0) { - // Directory doesn't exist, create it with 0755 permissions - if (mkdir(config_dir.c_str(), 0755) != 0) { - if (errno != EEXIST) { - return false; - } - } - } -#endif - - return true; -} - -std::string GetConfigDirectory() { - std::string config_directory = ".yaze"; - Platform platform; -#if defined(__APPLE__) && defined(__MACH__) -#if TARGET_IPHONE_SIMULATOR == 1 || TARGET_OS_IPHONE == 1 - platform = Platform::kiOS; -#elif TARGET_OS_MAC == 1 - platform = Platform::kMacOS; -#else - platform = Platform::kMacOS; // Default for macOS -#endif -#elif defined(_WIN32) - platform = Platform::kWindows; -#elif defined(__linux__) - platform = Platform::kLinux; -#else - platform = Platform::kUnknown; -#endif - switch (platform) { - case Platform::kWindows: - config_directory = "~/AppData/Roaming/yaze"; - break; - case Platform::kMacOS: - case Platform::kLinux: - config_directory = "~/.config/yaze"; - break; - default: - break; - } - // Expand the home directory path - return ExpandHomePath(config_directory); -} - -#ifdef _WIN32 - -#if defined(YAZE_ENABLE_NFD) && YAZE_ENABLE_NFD -#include -#endif - -std::string FileDialogWrapper::ShowOpenFileDialog() { - // Use global feature flag to choose implementation - if (core::FeatureFlags::get().kUseNativeFileDialog) { - return ShowOpenFileDialogNFD(); - } else { - return ShowOpenFileDialogBespoke(); - } -} - -std::string FileDialogWrapper::ShowOpenFileDialogNFD() { -#if defined(YAZE_ENABLE_NFD) && YAZE_ENABLE_NFD - NFD_Init(); - nfdu8char_t *out_path = NULL; - nfdu8filteritem_t filters[1] = {{"ROM Files", "sfc,smc"}}; - nfdopendialogu8args_t args = {0}; - args.filterList = filters; - args.filterCount = 1; - nfdresult_t result = NFD_OpenDialogU8_With(&out_path, &args); - if (result == NFD_OKAY) { - std::string file_path(out_path); - NFD_FreePath(out_path); - NFD_Quit(); - return file_path; - } else if (result == NFD_CANCEL) { - NFD_Quit(); - return ""; - } - NFD_Quit(); - return ""; -#else - // NFD not available - fallback to bespoke - return ShowOpenFileDialogBespoke(); -#endif -} - -std::string FileDialogWrapper::ShowOpenFileDialogBespoke() { - // Use Windows COM-based IFileOpenDialog as fallback when NFD not available - HRESULT hr = CoInitializeEx(NULL, COINIT_APARTMENTTHREADED | COINIT_DISABLE_OLE1DDE); - if (FAILED(hr)) { - return ""; - } - - std::string result; - IFileOpenDialog *pFileOpen = NULL; - - // Create the FileOpenDialog object - hr = CoCreateInstance(CLSID_FileOpenDialog, NULL, CLSCTX_ALL, - IID_IFileOpenDialog, reinterpret_cast(&pFileOpen)); - - if (SUCCEEDED(hr)) { - // Set file type filters - COMDLG_FILTERSPEC rgSpec[] = { - {L"ROM Files", L"*.sfc;*.smc"}, - {L"All Files", L"*.*"} - }; - pFileOpen->SetFileTypes(ARRAYSIZE(rgSpec), rgSpec); - pFileOpen->SetFileTypeIndex(1); - pFileOpen->SetDefaultExtension(L"sfc"); - - // Show the Open dialog - hr = pFileOpen->Show(NULL); - - if (SUCCEEDED(hr)) { - // Get the file name from the dialog - IShellItem *pItem; - hr = pFileOpen->GetResult(&pItem); - if (SUCCEEDED(hr)) { - PWSTR pszFilePath; - hr = pItem->GetDisplayName(SIGDN_FILESYSPATH, &pszFilePath); - - if (SUCCEEDED(hr)) { - // Convert wide string to narrow string - int size_needed = WideCharToMultiByte(CP_UTF8, 0, pszFilePath, -1, NULL, 0, NULL, NULL); - if (size_needed > 0) { - std::vector buffer(size_needed); - WideCharToMultiByte(CP_UTF8, 0, pszFilePath, -1, buffer.data(), size_needed, NULL, NULL); - result = std::string(buffer.data()); - } - CoTaskMemFree(pszFilePath); - } - pItem->Release(); - } - } - pFileOpen->Release(); - } - - CoUninitialize(); - return result; -} - -std::string FileDialogWrapper::ShowSaveFileDialog(const std::string& default_name, - const std::string& default_extension) { - // Use global feature flag to choose implementation - if (core::FeatureFlags::get().kUseNativeFileDialog) { - return ShowSaveFileDialogNFD(default_name, default_extension); - } else { - return ShowSaveFileDialogBespoke(default_name, default_extension); - } -} - -std::string FileDialogWrapper::ShowSaveFileDialogNFD(const std::string& default_name, - const std::string& default_extension) { -#if defined(YAZE_ENABLE_NFD) && YAZE_ENABLE_NFD - NFD_Init(); - nfdu8char_t *out_path = NULL; - - nfdsavedialogu8args_t args = {0}; - if (!default_extension.empty()) { - // Create filter for the save dialog - static nfdu8filteritem_t filters[3] = { - {"Theme Files", "theme"}, - {"YAZE Project Files", "yaze"}, - {"ROM Files", "sfc,smc"} - }; - - if (default_extension == "theme") { - args.filterList = &filters[0]; - args.filterCount = 1; - } else if (default_extension == "yaze") { - args.filterList = &filters[1]; - args.filterCount = 1; - } else if (default_extension == "sfc" || default_extension == "smc") { - args.filterList = &filters[2]; - args.filterCount = 1; - } - } - - if (!default_name.empty()) { - args.defaultName = default_name.c_str(); - } - - nfdresult_t result = NFD_SaveDialogU8_With(&out_path, &args); - if (result == NFD_OKAY) { - std::string file_path(out_path); - NFD_FreePath(out_path); - NFD_Quit(); - return file_path; - } else if (result == NFD_CANCEL) { - NFD_Quit(); - return ""; - } - NFD_Quit(); - return ""; -#else - // NFD not available - fallback to bespoke - return ShowSaveFileDialogBespoke(default_name, default_extension); -#endif -} - -std::string FileDialogWrapper::ShowSaveFileDialogBespoke(const std::string& default_name, - const std::string& default_extension) { - // Use Windows COM-based IFileSaveDialog as fallback when NFD not available - HRESULT hr = CoInitializeEx(NULL, COINIT_APARTMENTTHREADED | COINIT_DISABLE_OLE1DDE); - if (FAILED(hr)) { - return ""; - } - - std::string result; - IFileSaveDialog *pFileSave = NULL; - - // Create the FileSaveDialog object - hr = CoCreateInstance(CLSID_FileSaveDialog, NULL, CLSCTX_ALL, - IID_IFileSaveDialog, reinterpret_cast(&pFileSave)); - - if (SUCCEEDED(hr)) { - // Set file type filters based on extension - if (default_extension == "theme") { - COMDLG_FILTERSPEC rgSpec[] = {{L"Theme Files", L"*.theme"}}; - pFileSave->SetFileTypes(ARRAYSIZE(rgSpec), rgSpec); - pFileSave->SetDefaultExtension(L"theme"); - } else if (default_extension == "yaze") { - COMDLG_FILTERSPEC rgSpec[] = {{L"YAZE Project Files", L"*.yaze"}}; - pFileSave->SetFileTypes(ARRAYSIZE(rgSpec), rgSpec); - pFileSave->SetDefaultExtension(L"yaze"); - } else if (default_extension == "sfc" || default_extension == "smc") { - COMDLG_FILTERSPEC rgSpec[] = { - {L"SFC ROM Files", L"*.sfc"}, - {L"SMC ROM Files", L"*.smc"} - }; - pFileSave->SetFileTypes(ARRAYSIZE(rgSpec), rgSpec); - pFileSave->SetDefaultExtension(default_extension == "sfc" ? L"sfc" : L"smc"); - } - - // Set default filename if provided - if (!default_name.empty()) { - int size_needed = MultiByteToWideChar(CP_UTF8, 0, default_name.c_str(), -1, NULL, 0); - if (size_needed > 0) { - std::vector wname(size_needed); - MultiByteToWideChar(CP_UTF8, 0, default_name.c_str(), -1, wname.data(), size_needed); - pFileSave->SetFileName(wname.data()); - } - } - - // Show the Save dialog - hr = pFileSave->Show(NULL); - - if (SUCCEEDED(hr)) { - // Get the file name from the dialog - IShellItem *pItem; - hr = pFileSave->GetResult(&pItem); - if (SUCCEEDED(hr)) { - PWSTR pszFilePath; - hr = pItem->GetDisplayName(SIGDN_FILESYSPATH, &pszFilePath); - - if (SUCCEEDED(hr)) { - // Convert wide string to narrow string - int size_needed = WideCharToMultiByte(CP_UTF8, 0, pszFilePath, -1, NULL, 0, NULL, NULL); - if (size_needed > 0) { - std::vector buffer(size_needed); - WideCharToMultiByte(CP_UTF8, 0, pszFilePath, -1, buffer.data(), size_needed, NULL, NULL); - result = std::string(buffer.data()); - } - CoTaskMemFree(pszFilePath); - } - pItem->Release(); - } - } - pFileSave->Release(); - } - - CoUninitialize(); - return result; -} - -std::string FileDialogWrapper::ShowOpenFolderDialog() { - // Use global feature flag to choose implementation - if (core::FeatureFlags::get().kUseNativeFileDialog) { - return ShowOpenFolderDialogNFD(); - } else { - return ShowOpenFolderDialogBespoke(); - } -} - -std::string FileDialogWrapper::ShowOpenFolderDialogNFD() { -#if defined(YAZE_ENABLE_NFD) && YAZE_ENABLE_NFD - NFD_Init(); - nfdu8char_t *out_path = NULL; - nfdresult_t result = NFD_PickFolderU8(&out_path, NULL); - if (result == NFD_OKAY) { - std::string folder_path(out_path); - NFD_FreePath(out_path); - NFD_Quit(); - return folder_path; - } else if (result == NFD_CANCEL) { - NFD_Quit(); - return ""; - } - NFD_Quit(); - return ""; -#else - // NFD not available - fallback to bespoke - return ShowOpenFolderDialogBespoke(); -#endif -} - -std::string FileDialogWrapper::ShowOpenFolderDialogBespoke() { - // Use Windows COM-based IFileOpenDialog in folder-picking mode as fallback - HRESULT hr = CoInitializeEx(NULL, COINIT_APARTMENTTHREADED | COINIT_DISABLE_OLE1DDE); - if (FAILED(hr)) { - return ""; - } - - std::string result; - IFileOpenDialog *pFileOpen = NULL; - - // Create the FileOpenDialog object - hr = CoCreateInstance(CLSID_FileOpenDialog, NULL, CLSCTX_ALL, - IID_IFileOpenDialog, reinterpret_cast(&pFileOpen)); - - if (SUCCEEDED(hr)) { - // Set options to pick folders instead of files - DWORD dwOptions; - hr = pFileOpen->GetOptions(&dwOptions); - if (SUCCEEDED(hr)) { - hr = pFileOpen->SetOptions(dwOptions | FOS_PICKFOLDERS); - } - - if (SUCCEEDED(hr)) { - // Show the Open dialog - hr = pFileOpen->Show(NULL); - - if (SUCCEEDED(hr)) { - // Get the folder path from the dialog - IShellItem *pItem; - hr = pFileOpen->GetResult(&pItem); - if (SUCCEEDED(hr)) { - PWSTR pszFolderPath; - hr = pItem->GetDisplayName(SIGDN_FILESYSPATH, &pszFolderPath); - - if (SUCCEEDED(hr)) { - // Convert wide string to narrow string - int size_needed = WideCharToMultiByte(CP_UTF8, 0, pszFolderPath, -1, NULL, 0, NULL, NULL); - if (size_needed > 0) { - std::vector buffer(size_needed); - WideCharToMultiByte(CP_UTF8, 0, pszFolderPath, -1, buffer.data(), size_needed, NULL, NULL); - result = std::string(buffer.data()); - } - CoTaskMemFree(pszFolderPath); - } - pItem->Release(); - } - } - } - pFileOpen->Release(); - } - - CoUninitialize(); - return result; -} - -std::vector FileDialogWrapper::GetSubdirectoriesInFolder( - const std::string &folder_path) { - std::vector subdirectories; - WIN32_FIND_DATA findFileData; - HANDLE hFind = FindFirstFile((folder_path + "\\*").c_str(), &findFileData); - if (hFind != INVALID_HANDLE_VALUE) { - do { - if (findFileData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) { - if (strcmp(findFileData.cFileName, ".") != 0 && - strcmp(findFileData.cFileName, "..") != 0) { - subdirectories.emplace_back(findFileData.cFileName); - } - } - } while (FindNextFile(hFind, &findFileData) != 0); - FindClose(hFind); - } - return subdirectories; -} - -std::vector FileDialogWrapper::GetFilesInFolder( - const std::string &folder_path) { - std::vector files; - WIN32_FIND_DATA findFileData; - HANDLE hFind = FindFirstFile((folder_path + "\\*").c_str(), &findFileData); - if (hFind != INVALID_HANDLE_VALUE) { - do { - if (!(findFileData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY)) { - files.emplace_back(findFileData.cFileName); - } - } while (FindNextFile(hFind, &findFileData) != 0); - FindClose(hFind); - } - return files; -} - -#elif defined(__linux__) - -#if defined(YAZE_ENABLE_NFD) && YAZE_ENABLE_NFD -#include -#endif - -std::string FileDialogWrapper::ShowOpenFileDialog() { - // Use global feature flag to choose implementation - if (core::FeatureFlags::get().kUseNativeFileDialog) { - return ShowOpenFileDialogNFD(); - } else { - return ShowOpenFileDialogBespoke(); - } -} - -std::string FileDialogWrapper::ShowOpenFileDialogNFD() { -#if defined(YAZE_ENABLE_NFD) && YAZE_ENABLE_NFD - NFD_Init(); - nfdu8char_t *out_path = NULL; - nfdu8filteritem_t filters[1] = {{"Rom File", "sfc,smc"}}; - nfdopendialogu8args_t args = {0}; - args.filterList = filters; - args.filterCount = 1; - nfdresult_t result = NFD_OpenDialogU8_With(&out_path, &args); - if (result == NFD_OKAY) { - std::string file_path_linux(out_path); - NFD_FreePath(out_path); - NFD_Quit(); - return file_path_linux; - } else if (result == NFD_CANCEL) { - NFD_Quit(); - return ""; - } - NFD_Quit(); - return "Error: NFD_OpenDialog"; -#else - // NFD not available - fallback to bespoke - return ShowOpenFileDialogBespoke(); -#endif -} - -std::string FileDialogWrapper::ShowOpenFileDialogBespoke() { - // Implement bespoke file dialog or return placeholder - // This would contain the custom Linux implementation - return ""; // Placeholder for bespoke implementation -} - -std::string FileDialogWrapper::ShowSaveFileDialogNFD(const std::string& default_name, - const std::string& default_extension) { -#if defined(YAZE_ENABLE_NFD) && YAZE_ENABLE_NFD - NFD_Init(); - nfdu8char_t *out_path = NULL; - - nfdsavedialogu8args_t args = {0}; - if (!default_extension.empty()) { - // Create filter for the save dialog - static nfdu8filteritem_t filters[3] = { - {"Theme File", "theme"}, - {"Project File", "yaze"}, - {"ROM File", "sfc,smc"} - }; - - if (default_extension == "theme") { - args.filterList = &filters[0]; - args.filterCount = 1; - } else if (default_extension == "yaze") { - args.filterList = &filters[1]; - args.filterCount = 1; - } else if (default_extension == "sfc" || default_extension == "smc") { - args.filterList = &filters[2]; - args.filterCount = 1; - } - } - - if (!default_name.empty()) { - args.defaultName = default_name.c_str(); - } - - nfdresult_t result = NFD_SaveDialogU8_With(&out_path, &args); - if (result == NFD_OKAY) { - std::string file_path(out_path); - NFD_FreePath(out_path); - NFD_Quit(); - return file_path; - } else if (result == NFD_CANCEL) { - NFD_Quit(); - return ""; - } - NFD_Quit(); - return ""; -#else - // NFD not available - fallback to bespoke - return ShowSaveFileDialogBespoke(default_name, default_extension); -#endif -} - -std::string FileDialogWrapper::ShowSaveFileDialogBespoke(const std::string& default_name, - const std::string& default_extension) { - // Basic Linux implementation using system command - // For CI/CD, just return a placeholder path - if (!default_name.empty() && !default_extension.empty()) { - return default_name + "." + default_extension; - } - return ""; // For now return empty - full implementation can be added later -} - -std::string FileDialogWrapper::ShowSaveFileDialog(const std::string& default_name, - const std::string& default_extension) { - // Use global feature flag to choose implementation - if (core::FeatureFlags::get().kUseNativeFileDialog) { - return ShowSaveFileDialogNFD(default_name, default_extension); - } else { - return ShowSaveFileDialogBespoke(default_name, default_extension); - } -} - -std::string FileDialogWrapper::ShowOpenFolderDialog() { - // Use global feature flag to choose implementation - if (core::FeatureFlags::get().kUseNativeFileDialog) { - return ShowOpenFolderDialogNFD(); - } else { - return ShowOpenFolderDialogBespoke(); - } -} - -std::string FileDialogWrapper::ShowOpenFolderDialogNFD() { -#if defined(YAZE_ENABLE_NFD) && YAZE_ENABLE_NFD - NFD_Init(); - nfdu8char_t *out_path = NULL; - nfdresult_t result = NFD_PickFolderU8(&out_path, NULL); - if (result == NFD_OKAY) { - std::string folder_path_linux(out_path); - NFD_FreePath(out_path); - NFD_Quit(); - return folder_path_linux; - } else if (result == NFD_CANCEL) { - NFD_Quit(); - return ""; - } - NFD_Quit(); - return "Error: NFD_PickFolder"; -#else - // NFD not available - fallback to bespoke - return ShowOpenFolderDialogBespoke(); -#endif -} - -std::string FileDialogWrapper::ShowOpenFolderDialogBespoke() { - // Implement bespoke folder dialog or return placeholder - // This would contain the custom macOS implementation - return ""; // Placeholder for bespoke implementation -} - -std::vector FileDialogWrapper::GetSubdirectoriesInFolder( - const std::string &folder_path) { - std::vector subdirectories; - DIR *dir; - struct dirent *ent; - if ((dir = opendir(folder_path.c_str())) != NULL) { - while ((ent = readdir(dir)) != NULL) { - if (ent->d_type == DT_DIR) { - if (strcmp(ent->d_name, ".") != 0 && strcmp(ent->d_name, "..") != 0) { - subdirectories.push_back(ent->d_name); - } - } - } - closedir(dir); - } - return subdirectories; -} - -std::vector FileDialogWrapper::GetFilesInFolder( - const std::string &folder_path) { - std::vector files; - DIR *dir; - struct dirent *ent; - if ((dir = opendir(folder_path.c_str())) != NULL) { - while ((ent = readdir(dir)) != NULL) { - if (ent->d_type == DT_REG) { - files.push_back(ent->d_name); - } - } - closedir(dir); - } - return files; -} - -#endif +// Note: FileDialogWrapper implementations are in src/app/platform/file_dialog.mm +// (platform-specific implementations to avoid duplicate symbols) } // namespace util } // namespace yaze \ No newline at end of file diff --git a/src/util/file_util.h b/src/util/file_util.h index 9cc2dd6c..f01f7647 100644 --- a/src/util/file_util.h +++ b/src/util/file_util.h @@ -51,16 +51,46 @@ std::string GetBundleResourcePath(); enum class Platform { kUnknown, kMacOS, kiOS, kWindows, kLinux }; +/** + * @brief Gets the file extension from a filename. + * + * Uses std::filesystem for cross-platform consistency. + * + * @param filename The name of the file. + * @return The file extension, or an empty string if none is found. + */ std::string GetFileExtension(const std::string &filename); + +/** + * @brief Gets the filename from a full path. + * + * Uses std::filesystem for cross-platform consistency. + * + * @param filename The full path to the file. + * @return The filename, including the extension. + */ std::string GetFileName(const std::string &filename); -std::string LoadFile(const std::string &filename); -std::string LoadConfigFile(const std::string &filename); -std::string GetConfigDirectory(); -bool EnsureConfigDirectoryExists(); -std::string ExpandHomePath(const std::string& path); std::string GetResourcePath(const std::string &resource_path); void SaveFile(const std::string &filename, const std::string &data); + /** + * @brief Loads the entire contents of a file into a string. + * + * Throws a std::runtime_error if the file cannot be opened. + * + * @param filename The full, absolute path to the file. + * @return The contents of the file as a string. + */ + std::string LoadFile(const std::string &filename); + + /** + * @brief Loads a file from the user's config directory. + * + * @param filename The name of the file inside the config directory. + * @return The contents of the file, or an empty string if not found. + */ + std::string LoadFileFromConfigDir(const std::string &filename); + } // namespace util } // namespace yaze diff --git a/src/util/platform_paths.cc b/src/util/platform_paths.cc index f07b72cf..dbbeeb91 100644 --- a/src/util/platform_paths.cc +++ b/src/util/platform_paths.cc @@ -52,15 +52,53 @@ std::filesystem::path PlatformPaths::GetHomeDirectory() { } absl::StatusOr PlatformPaths::GetAppDataDirectory() { +#ifdef _WIN32 + wchar_t path[MAX_PATH]; + if (SUCCEEDED(SHGetFolderPathW(NULL, CSIDL_APPDATA, NULL, 0, path))) { + std::filesystem::path app_data = std::filesystem::path(path) / "yaze"; + auto status = EnsureDirectoryExists(app_data); + if (!status.ok()) { + return status; + } + return app_data; + } + // Fallback if SHGetFolderPathW fails std::filesystem::path home = GetHomeDirectory(); - std::filesystem::path app_data = home / ".yaze"; - + std::filesystem::path app_data = home / "yaze_data"; + auto status = EnsureDirectoryExists(app_data); + if (!status.ok()) { + return status; + } + return app_data; +#elif defined(__APPLE__) + std::filesystem::path home = GetHomeDirectory(); + std::filesystem::path app_data = home / "Library" / "Application Support" / "yaze"; auto status = EnsureDirectoryExists(app_data); if (!status.ok()) { return status; } - return app_data; +#else // Linux and other Unix-like systems + const char* xdg_config_home = std::getenv("XDG_CONFIG_HOME"); + std::filesystem::path config_dir; + if (xdg_config_home && *xdg_config_home) { + config_dir = std::filesystem::path(xdg_config_home); + } else { + config_dir = GetHomeDirectory() / ".config"; + } + std::filesystem::path app_data = config_dir / "yaze"; + auto status = EnsureDirectoryExists(app_data); + if (!status.ok()) { + return status; + } + return app_data; +#endif +} + +absl::StatusOr PlatformPaths::GetConfigDirectory() { + // For yaze, config and data directories are the same. + // This provides a semantically clearer API for config access. + return GetAppDataDirectory(); } absl::StatusOr PlatformPaths::GetAppDataSubdirectory( diff --git a/src/util/platform_paths.h b/src/util/platform_paths.h index da23b471..e7d342f7 100644 --- a/src/util/platform_paths.h +++ b/src/util/platform_paths.h @@ -29,16 +29,34 @@ class PlatformPaths { static std::filesystem::path GetHomeDirectory(); /** - * @brief Get application data directory for YAZE - * - * Creates the directory if it doesn't exist. - * - * - Windows: %USERPROFILE%\.yaze - * - Unix/macOS: $HOME/.yaze - * - * @return StatusOr with path to data directory + * @brief Get the user-specific application data directory for YAZE. + * + * This is the standard location for storing user-specific data, settings, + * and cache files. The directory is created if it doesn't exist. + * + * - Windows: `%APPDATA%\yaze` (e.g., C:\Users\user\AppData\Roaming\yaze) + * - macOS: `~/Library/Application Support/yaze` + * - Linux: `$XDG_CONFIG_HOME/yaze` or `~/.config/yaze` + * + * @return StatusOr with path to the application data directory. */ static absl::StatusOr GetAppDataDirectory(); + + /** + * @brief Get the user-specific configuration directory for YAZE. + * + * This is the standard location for storing user configuration files. + * It often maps to the same location as GetAppDataDirectory but provides + * a more semantically correct API for config files. + * The directory is created if it doesn't exist. + * + * - Windows: `%APPDATA%\yaze` + * - macOS: `~/Library/Application Support/yaze` + * - Linux: `$XDG_CONFIG_HOME/yaze` or `~/.config/yaze` + * + * @return StatusOr with path to the configuration directory. + */ + static absl::StatusOr GetConfigDirectory(); /** * @brief Get a subdirectory within the app data folder diff --git a/src/util/util.cmake b/src/util/util.cmake index d9aef788..8f706052 100644 --- a/src/util/util.cmake +++ b/src/util/util.cmake @@ -17,6 +17,7 @@ set(YAZE_UTIL_SRC util/log.cc util/platform_paths.cc util/file_util.cc + util/platform_paths.cc ) add_library(yaze_util STATIC ${YAZE_UTIL_SRC})