diff --git a/frontend/drivers/platform_uwp.c b/frontend/drivers/platform_uwp.c index dfbc113928..7c5205f72d 100644 --- a/frontend/drivers/platform_uwp.c +++ b/frontend/drivers/platform_uwp.c @@ -279,7 +279,7 @@ static int frontend_uwp_parse_drive_list(void *data, bool load_content) for (drive[0] = 'A'; drive[0] <= 'Z'; drive[0]++) { - if (path_is_valid(drive)) + if (uwp_drive_exists(drive)) { menu_entries_append_enum(list, drive, @@ -296,13 +296,22 @@ static int frontend_uwp_parse_drive_list(void *data, bool load_content) enum_idx, FILE_TYPE_DIRECTORY, 0, 0); - if (!have_any_drives && string_is_equal(uwp_device_family, "Windows.Desktop")) + if (!have_any_drives) { menu_entries_append_enum(list, - msg_hash_to_str(MENU_ENUM_LABEL_VALUE_FILE_BROWSER_OPEN_UWP_PERMISSIONS), - msg_hash_to_str(MENU_ENUM_LABEL_FILE_BROWSER_OPEN_UWP_PERMISSIONS), - MENU_ENUM_LABEL_FILE_BROWSER_OPEN_UWP_PERMISSIONS, + msg_hash_to_str(MENU_ENUM_LABEL_VALUE_FILE_BROWSER_OPEN_PICKER), + msg_hash_to_str(MENU_ENUM_LABEL_FILE_BROWSER_OPEN_PICKER), + MENU_ENUM_LABEL_FILE_BROWSER_OPEN_PICKER, MENU_SETTING_ACTION, 0, 0); + + if (string_is_equal(uwp_device_family, "Windows.Desktop")) + { + menu_entries_append_enum(list, + msg_hash_to_str(MENU_ENUM_LABEL_VALUE_FILE_BROWSER_OPEN_UWP_PERMISSIONS), + msg_hash_to_str(MENU_ENUM_LABEL_FILE_BROWSER_OPEN_UWP_PERMISSIONS), + MENU_ENUM_LABEL_FILE_BROWSER_OPEN_UWP_PERMISSIONS, + MENU_SETTING_ACTION, 0, 0); + } } free(home_dir); diff --git a/intl/msg_hash_us.c b/intl/msg_hash_us.c index b273e76090..6905970494 100644 --- a/intl/msg_hash_us.c +++ b/intl/msg_hash_us.c @@ -395,6 +395,11 @@ int menu_hash_get_help_us_enum(enum msg_hash_enums msg, char *s, size_t len) "Open Windows permission settings to enable \n" "the broadFileSystemAccess capability."); break; + case MENU_ENUM_LABEL_FILE_BROWSER_OPEN_PICKER: + snprintf(s, len, + "Open the system file picker to access \n" + "additional directories."); + break; case MENU_ENUM_LABEL_FILE_BROWSER_SHADER_PRESET: snprintf(s, len, "Shader preset file."); diff --git a/intl/msg_hash_us.h b/intl/msg_hash_us.h index 6b1323e3a8..0a39e2c700 100644 --- a/intl/msg_hash_us.h +++ b/intl/msg_hash_us.h @@ -2052,6 +2052,14 @@ MSG_HASH( MENU_ENUM_SUBLABEL_FILE_BROWSER_OPEN_UWP_PERMISSIONS, "Open Windows file access permissions settings" ) +MSG_HASH( + MENU_ENUM_LABEL_VALUE_FILE_BROWSER_OPEN_PICKER, + "Open..." +) +MSG_HASH( + MENU_ENUM_SUBLABEL_FILE_BROWSER_OPEN_PICKER, + "Open another directory using the system file picker" +) MSG_HASH( MENU_ENUM_LABEL_VALUE_PAUSE_LIBRETRO, "Pause when menu activated" diff --git a/libretro-common/vfs/vfs_implementation_uwp.cpp b/libretro-common/vfs/vfs_implementation_uwp.cpp index b1eee0cf0a..4a8b174c24 100644 --- a/libretro-common/vfs/vfs_implementation_uwp.cpp +++ b/libretro-common/vfs/vfs_implementation_uwp.cpp @@ -27,8 +27,10 @@ #include #include #include +#include #include +using namespace Windows::Foundation; using namespace Windows::Foundation::Collections; using namespace Windows::Storage; using namespace Windows::Storage::Streams; @@ -78,8 +80,13 @@ namespace finished = true; }); + /* Don't stall the UI thread - prevents a deadlock */ + Windows::UI::Core::CoreWindow^ corewindow = Windows::UI::Core::CoreWindow::GetForCurrentThread(); while (!finished) - Windows::UI::Core::CoreWindow::GetForCurrentThread()->Dispatcher->ProcessEvents(Windows::UI::Core::CoreProcessEventsOption::ProcessAllIfPresent); + { + if (corewindow) + corewindow->Dispatcher->ProcessEvents(Windows::UI::Core::CoreProcessEventsOption::ProcessAllIfPresent); + } if (exception != nullptr) throw exception; @@ -115,6 +122,146 @@ namespace } } +namespace +{ + /* Damn you, UWP, why no functions for that either */ + template + concurrency::task GetItemFromPathAsync(Platform::String^ path) + { + static_assert(false, "StorageFile and StorageFolder only"); + } + template<> + concurrency::task GetItemFromPathAsync(Platform::String^ path) + { + return concurrency::create_task(StorageFile::GetFileFromPathAsync(path)); + } + template<> + concurrency::task GetItemFromPathAsync(Platform::String^ path) + { + return concurrency::create_task(StorageFolder::GetFolderFromPathAsync(path)); + } + + template + concurrency::task GetItemInFolderFromPathAsync(StorageFolder^ folder, Platform::String^ path) + { + static_assert(false, "StorageFile and StorageFolder only"); + } + template<> + concurrency::task GetItemInFolderFromPathAsync(StorageFolder^ folder, Platform::String^ path) + { + if (path->IsEmpty()) + retro_assert(false); /* Attempt to read a folder as a file - this really should have been caught earlier */ + return concurrency::create_task(folder->GetFileAsync(path)); + } + template<> + concurrency::task GetItemInFolderFromPathAsync(StorageFolder^ folder, Platform::String^ path) + { + if (path->IsEmpty()) + return concurrency::create_task(concurrency::create_async([folder]() { return folder; })); + return concurrency::create_task(folder->GetFolderAsync(path)); + } +} + +namespace +{ + /* A list of all StorageFolder objects returned using from the file picker */ + Platform::Collections::Vector accessible_directories; + + concurrency::task TriggerPickerAddDialog() + { + auto folderPicker = ref new Windows::Storage::Pickers::FolderPicker(); + folderPicker->SuggestedStartLocation = Windows::Storage::Pickers::PickerLocationId::Desktop; + folderPicker->FileTypeFilter->Append("*"); + + return concurrency::create_task(folderPicker->PickSingleFolderAsync()).then([](StorageFolder^ folder) { + if (folder == nullptr) + throw ref new Platform::Exception(E_ABORT, L"Operation cancelled by user"); + + /* TODO: check for duplicates */ + accessible_directories.Append(folder); + return folder->Path; + }); + } + + template + concurrency::task LocateStorageItem(Platform::String^ path) + { + /* Look for a matching directory we can use */ + for each (StorageFolder^ folder in accessible_directories) + { + std::wstring folder_path = folder->Path->Data(); + /* Could be C:\ or C:\Users\somebody - remove the trailing slash to unify them */ + if (folder_path[folder_path.size() - 1] == '\\') + folder_path.erase(folder_path.size() - 1); + std::wstring file_path = path->Data(); + if (file_path.find(folder_path) == 0) + { + /* Found a match */ + file_path = file_path.length() > folder_path.length() ? file_path.substr(folder_path.length() + 1) : L""; + return concurrency::create_task(GetItemInFolderFromPathAsync(folder, ref new Platform::String(file_path.data()))); + } + } + + /* No matches - try accessing directly, and fallback to user prompt */ + return concurrency::create_task(GetItemFromPathAsync(path)).then([path](concurrency::task item) { + try + { + T^ storageItem = item.get(); + return concurrency::create_task(concurrency::create_async([storageItem]() { return storageItem; })); + } + catch (Platform::AccessDeniedException^ e) + { + Windows::UI::Popups::MessageDialog^ dialog = + ref new Windows::UI::Popups::MessageDialog("Path \"" + path + "\" is not currently accessible. Please open any containing directory to access it."); + dialog->Commands->Append(ref new Windows::UI::Popups::UICommand("Open file picker")); + dialog->Commands->Append(ref new Windows::UI::Popups::UICommand("Cancel")); + return concurrency::create_task(dialog->ShowAsync()).then([path](Windows::UI::Popups::IUICommand^ cmd) { + if (cmd->Label == "Open file picker") + { + return TriggerPickerAddDialog().then([path](Platform::String^ added_path) { + /* Retry */ + return LocateStorageItem(path); + }); + } + else + { + throw ref new Platform::Exception(E_ABORT, L"Operation cancelled by user"); + } + }); + } + }); + } + + IStorageItem^ LocateStorageFileOrFolder(Platform::String^ path) + { + if (!path || path->IsEmpty()) + return nullptr; + + if (*(path->End() - 1) == '\\') + { + /* Ends with a slash, so it's definitely a directory */ + return RunAsyncAndCatchErrors([&]() { + return concurrency::create_task(LocateStorageItem(path)); + }, nullptr); + } + else + { + /* No final slash - probably a file (since RetroArch usually slash-terminates dirs), but there is still a chance it's a directory */ + IStorageItem^ item; + item = RunAsyncAndCatchErrors([&]() { + return concurrency::create_task(LocateStorageItem(path)); + }, nullptr); + if (!item) + { + item = RunAsyncAndCatchErrors([&]() { + return concurrency::create_task(LocateStorageItem(path)); + }, nullptr); + } + return item; + } + } +} + #ifdef VFS_FRONTEND struct retro_vfs_file_handle #else @@ -160,7 +307,7 @@ libretro_vfs_implementation_file *retro_vfs_file_open_impl(const char *path, uns retro_assert(!dirpath_str->IsEmpty() && !filename_str->IsEmpty()); return RunAsyncAndCatchErrors([&]() { - return concurrency::create_task(StorageFolder::GetFolderFromPathAsync(dirpath_str)).then([&](StorageFolder^ dir) { + return concurrency::create_task(LocateStorageItem(dirpath_str)).then([&](StorageFolder^ dir) { if (mode == RETRO_VFS_FILE_ACCESS_READ) return dir->GetFileAsync(filename_str); else @@ -361,7 +508,7 @@ int retro_vfs_file_remove_impl(const char *path) free(path_wide); return RunAsyncAndCatchErrors([&]() { - return concurrency::create_task(StorageFile::GetFileFromPathAsync(path_str)).then([&](StorageFile^ file) { + return concurrency::create_task(LocateStorageItem(path_str)).then([&](StorageFile^ file) { return file->DeleteAsync(StorageDeleteOption::PermanentDelete); }).then([&]() { return 0; @@ -397,8 +544,8 @@ int retro_vfs_file_rename_impl(const char *old_path, const char *new_path) retro_assert(!old_path_str->IsEmpty() && !new_dir_path_str->IsEmpty() && !new_file_name_str->IsEmpty()); return RunAsyncAndCatchErrors([&]() { - concurrency::task old_file_task = concurrency::create_task(StorageFile::GetFileFromPathAsync(old_path_str)); - concurrency::task new_dir_task = concurrency::create_task(StorageFolder::GetFolderFromPathAsync(new_dir_path_str)); + concurrency::task old_file_task = concurrency::create_task(LocateStorageItem(old_path_str)); + concurrency::task new_dir_task = concurrency::create_task(LocateStorageItem(new_dir_path_str)); return concurrency::create_task([&] { /* Run these two tasks in parallel */ /* TODO: There may be some cleaner way to express this */ @@ -422,37 +569,6 @@ const char *retro_vfs_file_get_path_impl(libretro_vfs_implementation_file *strea return stream->orig_path; } - -/* This is ugly, but I can't figure out a better way and there may be no better way... */ -static IStorageItem^ GetFileOrFolderFromPath(Platform::String^ path) -{ - if (!path || path->IsEmpty()) - return nullptr; - - if (*(path->End() - 1) == '\\') - { - /* Ends with a slash, so it's definitely a directory */ - return RunAsyncAndCatchErrors([&]() { - return concurrency::create_task(StorageFolder::GetFolderFromPathAsync(path)); - }, nullptr); - } - else - { - /* No final slash - probably a file (since RetroArch usually slash-terminates dirs), but there is a chance it's a directory */ - IStorageItem^ item; - item = RunAsyncAndCatchErrors([&]() { - return concurrency::create_task(StorageFile::GetFileFromPathAsync(path)); - }, nullptr); - if (!item) - { - item = RunAsyncAndCatchErrors([&]() { - return concurrency::create_task(StorageFolder::GetFolderFromPathAsync(path)); - }, nullptr); - } - return item; - } -} - int retro_vfs_stat_impl(const char *path, int32_t *size) { if (!path || !*path) @@ -463,7 +579,7 @@ int retro_vfs_stat_impl(const char *path, int32_t *size) Platform::String^ path_str = ref new Platform::String(path_wide); free(path_wide); - IStorageItem^ item = GetFileOrFolderFromPath(path_str); + IStorageItem^ item = LocateStorageFileOrFolder(path_str); if (!item) return 0; @@ -507,7 +623,7 @@ int retro_vfs_mkdir_impl(const char *dir) free(dir_local); return RunAsyncAndCatchErrors([&]() { - return concurrency::create_task(StorageFolder::GetFolderFromPathAsync(parent_path_str)).then([&](StorageFolder^ parent) { + return concurrency::create_task(LocateStorageItem(parent_path_str)).then([&](StorageFolder^ parent) { return parent->CreateFolderAsync(dir_name_str); }).then([&](concurrency::task new_dir) { try @@ -553,7 +669,7 @@ libretro_vfs_implementation_dir *retro_vfs_opendir_impl(const char *name, bool i free(name_wide); rdir->directory = RunAsyncAndCatchErrors^>([&]() { - return concurrency::create_task(StorageFolder::GetFolderFromPathAsync(name_str)).then([&](StorageFolder^ folder) { + return concurrency::create_task(LocateStorageItem(name_str)).then([&](StorageFolder^ folder) { return folder->GetItemsAsync(); }); }, nullptr); @@ -615,3 +731,28 @@ bool uwp_is_path_accessible_using_standard_io(char *path) free(relative_path_abbrev); return result; } + +bool uwp_drive_exists(const char *path) +{ + if (!path || !*path) + return 0; + + wchar_t *path_wide = utf8_to_utf16_string_alloc(path); + Platform::String^ path_str = ref new Platform::String(path_wide); + free(path_wide); + + return RunAsyncAndCatchErrors([&]() { + return concurrency::create_task(StorageFolder::GetFolderFromPathAsync(path_str)).then([](StorageFolder^ properties) { + return true; + }); + }, false); +} + +char* uwp_trigger_picker(void) +{ + return RunAsyncAndCatchErrors([&]() { + return TriggerPickerAddDialog().then([](Platform::String^ path) { + return utf16_to_utf8_string_alloc(path->Data()); + }); + }, NULL); +} diff --git a/menu/cbs/menu_cbs_ok.c b/menu/cbs/menu_cbs_ok.c index 22a3358bc7..8fb64ca3fe 100644 --- a/menu/cbs/menu_cbs_ok.c +++ b/menu/cbs/menu_cbs_ok.c @@ -3980,6 +3980,28 @@ static int action_ok_open_uwp_permission_settings(const char *path, return 0; } +static int action_ok_open_picker(const char *path, + const char *label, unsigned type, size_t idx, size_t entry_idx) +{ + char* new_path; + int ret; +#ifdef __WINRT__ + new_path = uwp_trigger_picker(); + if (!new_path) + return 0; /* User aborted */ +#else + retro_assert(false); +#endif + + ret = generic_action_ok_displaylist_push(path, new_path, + msg_hash_to_str(MENU_ENUM_LABEL_FAVORITES), + type, idx, + entry_idx, ACTION_OK_DL_CONTENT_LIST); + + free(new_path); + return ret; +} + static int action_ok_shader_pass(const char *path, const char *label, unsigned type, size_t idx, size_t entry_idx) { @@ -5483,6 +5505,9 @@ static int menu_cbs_init_bind_ok_compare_label(menu_file_list_cbs_t *cbs, case MENU_ENUM_LABEL_FILE_BROWSER_OPEN_UWP_PERMISSIONS: BIND_ACTION_OK(cbs, action_ok_open_uwp_permission_settings); break; + case MENU_ENUM_LABEL_FILE_BROWSER_OPEN_PICKER: + BIND_ACTION_OK(cbs, action_ok_open_picker); + break; case MENU_ENUM_LABEL_RETRO_ACHIEVEMENTS_SETTINGS: BIND_ACTION_OK(cbs, action_ok_retro_achievements_list); break; diff --git a/menu/cbs/menu_cbs_sublabel.c b/menu/cbs/menu_cbs_sublabel.c index 8d0c9d76e7..520edfe59b 100644 --- a/menu/cbs/menu_cbs_sublabel.c +++ b/menu/cbs/menu_cbs_sublabel.c @@ -335,6 +335,7 @@ default_sublabel_macro(action_bind_sublabel_goto_music, default_sublabel_macro(action_bind_sublabel_goto_video, MENU_ENUM_SUBLABEL_GOTO_VIDEO) default_sublabel_macro(action_bind_sublabel_menu_filebrowser_settings, MENU_ENUM_SUBLABEL_MENU_FILE_BROWSER_SETTINGS) default_sublabel_macro(action_bind_sublabel_menu_filebrowser_open_uwp_permissions, MENU_ENUM_SUBLABEL_FILE_BROWSER_OPEN_UWP_PERMISSIONS) +default_sublabel_macro(action_bind_sublabel_menu_filebrowser_open_picker, MENU_ENUM_SUBLABEL_FILE_BROWSER_OPEN_PICKER) default_sublabel_macro(action_bind_sublabel_auto_remaps_enable, MENU_ENUM_SUBLABEL_AUTO_REMAPS_ENABLE) default_sublabel_macro(action_bind_sublabel_auto_overrides_enable, MENU_ENUM_SUBLABEL_AUTO_OVERRIDES_ENABLE) default_sublabel_macro(action_bind_sublabel_game_specific_options, MENU_ENUM_SUBLABEL_GAME_SPECIFIC_OPTIONS) @@ -1383,6 +1384,9 @@ int menu_cbs_init_bind_sublabel(menu_file_list_cbs_t *cbs, case MENU_ENUM_LABEL_FILE_BROWSER_OPEN_UWP_PERMISSIONS: BIND_ACTION_SUBLABEL(cbs, action_bind_sublabel_menu_filebrowser_open_uwp_permissions); break; + case MENU_ENUM_LABEL_FILE_BROWSER_OPEN_PICKER: + BIND_ACTION_SUBLABEL(cbs, action_bind_sublabel_menu_filebrowser_open_picker); + break; case MENU_ENUM_LABEL_ADD_TO_FAVORITES: BIND_ACTION_SUBLABEL(cbs, action_bind_sublabel_add_to_favorites); break; diff --git a/msg_hash.h b/msg_hash.h index 7f87dfba60..a73ada2a02 100644 --- a/msg_hash.h +++ b/msg_hash.h @@ -825,6 +825,7 @@ enum msg_hash_enums MENU_LABEL(PARENT_DIRECTORY), MENU_LABEL(FILE_BROWSER_OPEN_UWP_PERMISSIONS), + MENU_LABEL(FILE_BROWSER_OPEN_PICKER), MENU_ENUM_LABEL_CONTENT_ACTIONS, diff --git a/uwp/uwp_func.h b/uwp/uwp_func.h index d137909c04..a48382fbe6 100644 --- a/uwp/uwp_func.h +++ b/uwp/uwp_func.h @@ -28,6 +28,8 @@ extern char uwp_device_family[128]; void uwp_open_broadfilesystemaccess_settings(void); bool uwp_is_path_accessible_using_standard_io(char *path); +bool uwp_drive_exists(const char *path); +char* uwp_trigger_picker(void); void* uwp_get_corewindow(void);