diff --git a/distribution/changelog.txt b/distribution/changelog.txt index d547b05b37..5be0aa2d96 100644 --- a/distribution/changelog.txt +++ b/distribution/changelog.txt @@ -7,6 +7,7 @@ - Fix: [#18134] Underground on-ride photo section partially clips through adjacent terrain edge. - Fix: [#18257] Guests ‘waiting’ on extended railway crossings. - Improved: [#18192, #18214] Tycoon Park has been added Extras tab, Competition scenarios have received their own section. +- Improved: [#18250] Added modern style file and folder pickers on Windows. - Change: [#18230] Make the large flat to steep pieces available on the corkscrew roller coaster without cheats. 0.4.2 (2022-10-05) diff --git a/src/openrct2-ui/UiContext.Win32.cpp b/src/openrct2-ui/UiContext.Win32.cpp index 740820d57c..0738961ba3 100644 --- a/src/openrct2-ui/UiContext.Win32.cpp +++ b/src/openrct2-ui/UiContext.Win32.cpp @@ -13,7 +13,6 @@ // clang-format off # include # include -# include // clang-format on # undef CreateWindow @@ -27,30 +26,38 @@ # include # include # include -# include -# include +# include +# include // Native resource IDs # include "../../resources/resource.h" -static std::wstring SHGetPathFromIDListLongPath(LPCITEMIDLIST pidl) +using namespace Microsoft::WRL; + +class CCoInitialize { - // Limit path length to 32K - std::wstring pszPath(std::numeric_limits().max(), 0); - auto result = SHGetPathFromIDListEx(pidl, pszPath.data(), static_cast(pszPath.size()), GPFIDL_DEFAULT); - if (result) +public: + CCoInitialize(DWORD dwCoInit) + : m_hr(CoInitializeEx(nullptr, dwCoInit)) { - // Truncate at first null terminator - auto length = pszPath.find(L'\0'); - if (length != std::wstring::npos) - { - pszPath.resize(length); - pszPath.shrink_to_fit(); - } - return pszPath; } - return std::wstring(); -} + + ~CCoInitialize() + { + if (SUCCEEDED(m_hr)) + { + CoUninitialize(); + } + } + + operator bool() const + { + return SUCCEEDED(m_hr); + } + +private: + HRESULT m_hr; +}; namespace OpenRCT2::Ui { @@ -116,94 +123,87 @@ namespace OpenRCT2::Ui ShellExecuteW(NULL, L"open", urlW.c_str(), NULL, NULL, SW_SHOWNORMAL); } - std::string ShowFileDialog(SDL_Window* window, const FileDialogDesc& desc) override + std::string ShowFileDialogInternal(SDL_Window* window, const FileDialogDesc& desc, bool isFolder) { - std::wstring wcFilename = String::ToWideChar(desc.DefaultFilename); - wcFilename.resize(std::max(wcFilename.size(), MAX_PATH)); - - std::wstring wcTitle = String::ToWideChar(desc.Title); - std::wstring wcInitialDirectory = String::ToWideChar(desc.InitialDirectory); - std::wstring wcFilters = GetFilterString(desc.Filters); - - // Set open file name options - OPENFILENAMEW openFileName = {}; - openFileName.lStructSize = sizeof(OPENFILENAMEW); - openFileName.lpstrTitle = wcTitle.c_str(); - openFileName.lpstrInitialDir = wcInitialDirectory.c_str(); - openFileName.lpstrFilter = wcFilters.c_str(); - openFileName.lpstrFile = &wcFilename[0]; - openFileName.nMaxFile = static_cast(wcFilename.size()); - - // Open dialog - BOOL dialogResult = FALSE; - DWORD commonFlags = OFN_EXPLORER | OFN_PATHMUSTEXIST | OFN_HIDEREADONLY | OFN_NOCHANGEDIR; - if (desc.Type == FileDialogType::Open) - { - openFileName.Flags = commonFlags | OFN_NONETWORKBUTTON | OFN_FILEMUSTEXIST; - dialogResult = GetOpenFileNameW(&openFileName); - } - else if (desc.Type == FileDialogType::Save) - { - openFileName.Flags = commonFlags | OFN_CREATEPROMPT | OFN_OVERWRITEPROMPT; - dialogResult = GetSaveFileNameW(&openFileName); - } - std::string resultFilename; - if (dialogResult) + + CCoInitialize coInitialize(COINIT_APARTMENTTHREADED); + if (coInitialize) { - resultFilename = String::ToUtf8(openFileName.lpstrFile); - - // If there is no extension, append the pattern - std::string resultExtension = Path::GetExtension(resultFilename); - if (resultExtension.empty()) + CLSID dialogId = CLSID_FileOpenDialog; + DWORD flagsToSet = FOS_FORCEFILESYSTEM; + if (desc.Type == FileDialogType::Save) { - int32_t filterIndex = openFileName.nFilterIndex - 1; + dialogId = CLSID_FileSaveDialog; + flagsToSet |= FOS_OVERWRITEPROMPT | FOS_CREATEPROMPT | FOS_STRICTFILETYPES; + } + if (isFolder) + { + flagsToSet |= FOS_PICKFOLDERS; + } - assert(filterIndex >= 0); - assert(filterIndex < static_cast(desc.Filters.size())); - - std::string pattern = desc.Filters[filterIndex].Pattern; - std::string patternExtension = Path::GetExtension(pattern); - if (!patternExtension.empty()) + ComPtr fileDialog; + if (SUCCEEDED( + CoCreateInstance(dialogId, nullptr, CLSCTX_INPROC_SERVER, IID_PPV_ARGS(fileDialog.GetAddressOf())))) + { + DWORD flags; + if (SUCCEEDED(fileDialog->GetOptions(&flags)) && SUCCEEDED(fileDialog->SetOptions(flags | flagsToSet))) { - resultFilename += patternExtension; + fileDialog->SetTitle(String::ToWideChar(desc.Title).c_str()); + fileDialog->SetFileName(String::ToWideChar(Path::GetFileName(desc.DefaultFilename)).c_str()); + + // Set default directory (optional, don't fail the operation if it fails to set) + ComPtr defaultDirectory; + if (SUCCEEDED(SHCreateItemFromParsingName( + String::ToWideChar(desc.InitialDirectory).c_str(), nullptr, + IID_PPV_ARGS(defaultDirectory.GetAddressOf())))) + { + fileDialog->SetFolder(defaultDirectory.Get()); + } + + // Opt-in to automatic extensions, this will ensure extension of the selected file matches the filter + // Setting it to an empty string so "All Files" does not get anything appended + fileDialog->SetDefaultExtension(L""); + + // Filters need an "auxillary" storage for wide strings + std::vector filtersStorage; + auto filters = GetFilters(desc.Filters, filtersStorage); + + bool filtersSet = true; + if (!filters.empty()) + { + filtersSet = SUCCEEDED(fileDialog->SetFileTypes(static_cast(filters.size()), filters.data())); + } + + if (filtersSet && SUCCEEDED(fileDialog->Show(nullptr))) + { + ComPtr resultItem; + if (SUCCEEDED(fileDialog->GetResult(resultItem.GetAddressOf()))) + { + PWSTR filePath = nullptr; + if (SUCCEEDED(resultItem->GetDisplayName(SIGDN_FILESYSPATH, &filePath))) + { + resultFilename = String::ToUtf8(filePath); + CoTaskMemFree(filePath); + } + } + } } } } return resultFilename; } + std::string ShowFileDialog(SDL_Window* window, const FileDialogDesc& desc) override + { + return ShowFileDialogInternal(window, desc, false); + } + std::string ShowDirectoryDialog(SDL_Window* window, const std::string& title) override { - std::string result; - - // Initialize COM - if (SUCCEEDED(CoInitializeEx(0, COINIT_APARTMENTTHREADED))) - { - std::wstring titleW = String::ToWideChar(title); - BROWSEINFOW bi = {}; - bi.lpszTitle = titleW.c_str(); - bi.ulFlags = BIF_RETURNONLYFSDIRS | BIF_NEWDIALOGSTYLE | BIF_NONEWFOLDERBUTTON; - - LPITEMIDLIST pidl = SHBrowseForFolderW(&bi); - if (pidl != nullptr) - { - result = String::ToUtf8(SHGetPathFromIDListLongPath(pidl)); - } - CoTaskMemFree(pidl); - - CoUninitialize(); - } - else - { - log_error("Error opening directory browse window"); - } - - // SHBrowseForFolderW might minimize the main window, - // so make sure that it's visible again. - ShowWindow(GetHWND(window), SW_RESTORE); - - return result; + FileDialogDesc desc; + desc.Title = title; + return ShowFileDialogInternal(window, desc, true); } bool HasFilePicker() const override @@ -230,14 +230,24 @@ namespace OpenRCT2::Ui return result; } - static std::wstring GetFilterString(const std::vector& filters) + static std::vector GetFilters( + const std::vector& filters, std::vector& outFiltersStorage) { - std::wstringstream filtersb; + std::vector result; for (const auto& filter : filters) { - filtersb << String::ToWideChar(filter.Name) << '\0' << String::ToWideChar(filter.Pattern) << '\0'; + outFiltersStorage.emplace_back(String::ToWideChar(filter.Name)); + outFiltersStorage.emplace_back(String::ToWideChar(filter.Pattern)); } - return filtersb.str(); + + for (auto it = outFiltersStorage.begin(); it != outFiltersStorage.end();) + { + const wchar_t* Name = (*it++).c_str(); + const wchar_t* Pattern = (*it++).c_str(); + result.push_back({ Name, Pattern }); + } + + return result; } };