mirror of https://github.com/OpenRCT2/OpenRCT2.git
Windows: Use a modern file/folder picker
This commit is contained in:
parent
def6b705cf
commit
9ea6351708
|
@ -7,6 +7,7 @@
|
||||||
- Fix: [#18134] Underground on-ride photo section partially clips through adjacent terrain edge.
|
- Fix: [#18134] Underground on-ride photo section partially clips through adjacent terrain edge.
|
||||||
- Fix: [#18257] Guests ‘waiting’ on extended railway crossings.
|
- 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: [#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.
|
- Change: [#18230] Make the large flat to steep pieces available on the corkscrew roller coaster without cheats.
|
||||||
|
|
||||||
0.4.2 (2022-10-05)
|
0.4.2 (2022-10-05)
|
||||||
|
|
|
@ -13,7 +13,6 @@
|
||||||
// clang-format off
|
// clang-format off
|
||||||
# include <windows.h>
|
# include <windows.h>
|
||||||
# include <shellapi.h>
|
# include <shellapi.h>
|
||||||
# include <commdlg.h>
|
|
||||||
// clang-format on
|
// clang-format on
|
||||||
# undef CreateWindow
|
# undef CreateWindow
|
||||||
|
|
||||||
|
@ -27,31 +26,39 @@
|
||||||
# include <openrct2/core/Path.hpp>
|
# include <openrct2/core/Path.hpp>
|
||||||
# include <openrct2/core/String.hpp>
|
# include <openrct2/core/String.hpp>
|
||||||
# include <openrct2/ui/UiContext.h>
|
# include <openrct2/ui/UiContext.h>
|
||||||
# include <shlobj.h>
|
# include <shobjidl.h>
|
||||||
# include <sstream>
|
# include <wrl/client.h>
|
||||||
|
|
||||||
// Native resource IDs
|
// Native resource IDs
|
||||||
# include "../../resources/resource.h"
|
# include "../../resources/resource.h"
|
||||||
|
|
||||||
static std::wstring SHGetPathFromIDListLongPath(LPCITEMIDLIST pidl)
|
using namespace Microsoft::WRL;
|
||||||
|
|
||||||
|
class CCoInitialize
|
||||||
{
|
{
|
||||||
// Limit path length to 32K
|
public:
|
||||||
std::wstring pszPath(std::numeric_limits<int16_t>().max(), 0);
|
CCoInitialize(DWORD dwCoInit)
|
||||||
auto result = SHGetPathFromIDListEx(pidl, pszPath.data(), static_cast<DWORD>(pszPath.size()), GPFIDL_DEFAULT);
|
: m_hr(CoInitializeEx(nullptr, dwCoInit))
|
||||||
if (result)
|
|
||||||
{
|
{
|
||||||
// Truncate at first null terminator
|
}
|
||||||
auto length = pszPath.find(L'\0');
|
|
||||||
if (length != std::wstring::npos)
|
~CCoInitialize()
|
||||||
{
|
{
|
||||||
pszPath.resize(length);
|
if (SUCCEEDED(m_hr))
|
||||||
pszPath.shrink_to_fit();
|
{
|
||||||
|
CoUninitialize();
|
||||||
}
|
}
|
||||||
return pszPath;
|
|
||||||
}
|
}
|
||||||
return std::wstring();
|
|
||||||
|
operator bool() const
|
||||||
|
{
|
||||||
|
return SUCCEEDED(m_hr);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
HRESULT m_hr;
|
||||||
|
};
|
||||||
|
|
||||||
namespace OpenRCT2::Ui
|
namespace OpenRCT2::Ui
|
||||||
{
|
{
|
||||||
class Win32Context : public IPlatformUiContext
|
class Win32Context : public IPlatformUiContext
|
||||||
|
@ -116,94 +123,87 @@ namespace OpenRCT2::Ui
|
||||||
ShellExecuteW(NULL, L"open", urlW.c_str(), NULL, NULL, SW_SHOWNORMAL);
|
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<size_t>(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<DWORD>(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;
|
std::string resultFilename;
|
||||||
if (dialogResult)
|
|
||||||
{
|
|
||||||
resultFilename = String::ToUtf8(openFileName.lpstrFile);
|
|
||||||
|
|
||||||
// If there is no extension, append the pattern
|
CCoInitialize coInitialize(COINIT_APARTMENTTHREADED);
|
||||||
std::string resultExtension = Path::GetExtension(resultFilename);
|
if (coInitialize)
|
||||||
if (resultExtension.empty())
|
|
||||||
{
|
{
|
||||||
int32_t filterIndex = openFileName.nFilterIndex - 1;
|
CLSID dialogId = CLSID_FileOpenDialog;
|
||||||
|
DWORD flagsToSet = FOS_FORCEFILESYSTEM;
|
||||||
assert(filterIndex >= 0);
|
if (desc.Type == FileDialogType::Save)
|
||||||
assert(filterIndex < static_cast<int32_t>(desc.Filters.size()));
|
|
||||||
|
|
||||||
std::string pattern = desc.Filters[filterIndex].Pattern;
|
|
||||||
std::string patternExtension = Path::GetExtension(pattern);
|
|
||||||
if (!patternExtension.empty())
|
|
||||||
{
|
{
|
||||||
resultFilename += patternExtension;
|
dialogId = CLSID_FileSaveDialog;
|
||||||
|
flagsToSet |= FOS_OVERWRITEPROMPT | FOS_CREATEPROMPT | FOS_STRICTFILETYPES;
|
||||||
|
}
|
||||||
|
if (isFolder)
|
||||||
|
{
|
||||||
|
flagsToSet |= FOS_PICKFOLDERS;
|
||||||
|
}
|
||||||
|
|
||||||
|
ComPtr<IFileDialog> 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)))
|
||||||
|
{
|
||||||
|
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<IShellItem> 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<std::wstring> filtersStorage;
|
||||||
|
auto filters = GetFilters(desc.Filters, filtersStorage);
|
||||||
|
|
||||||
|
bool filtersSet = true;
|
||||||
|
if (!filters.empty())
|
||||||
|
{
|
||||||
|
filtersSet = SUCCEEDED(fileDialog->SetFileTypes(static_cast<UINT>(filters.size()), filters.data()));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filtersSet && SUCCEEDED(fileDialog->Show(nullptr)))
|
||||||
|
{
|
||||||
|
ComPtr<IShellItem> 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;
|
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 ShowDirectoryDialog(SDL_Window* window, const std::string& title) override
|
||||||
{
|
{
|
||||||
std::string result;
|
FileDialogDesc desc;
|
||||||
|
desc.Title = title;
|
||||||
// Initialize COM
|
return ShowFileDialogInternal(window, desc, true);
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bool HasFilePicker() const override
|
bool HasFilePicker() const override
|
||||||
|
@ -230,14 +230,24 @@ namespace OpenRCT2::Ui
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
static std::wstring GetFilterString(const std::vector<FileDialogDesc::Filter>& filters)
|
static std::vector<COMDLG_FILTERSPEC> GetFilters(
|
||||||
|
const std::vector<FileDialogDesc::Filter>& filters, std::vector<std::wstring>& outFiltersStorage)
|
||||||
{
|
{
|
||||||
std::wstringstream filtersb;
|
std::vector<COMDLG_FILTERSPEC> result;
|
||||||
for (const auto& filter : filters)
|
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;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue