diff --git a/src/ModMan/Web/Binds/DialogBinds.cpp b/src/ModMan/Web/Binds/DialogBinds.cpp new file mode 100644 index 00000000..3d72d9e3 --- /dev/null +++ b/src/ModMan/Web/Binds/DialogBinds.cpp @@ -0,0 +1,124 @@ +#include "DialogBinds.h" + +#include "Web/Platform/DialogHandler.h" +#include "Web/UiCommunication.h" + +#include "Json/JsonExtension.h" + +namespace +{ + class FileDialogFilterDto + { + public: + std::string name; + std::string filter; + }; + + NLOHMANN_DEFINE_TYPE_EXTENSION(FileDialogFilterDto, name, filter); + + class OpenFileDialogDto + { + public: + std::optional> filters; + }; + + NLOHMANN_DEFINE_TYPE_EXTENSION(OpenFileDialogDto, filters); + + class SaveFileDialogDto + { + public: + std::optional> filters; + }; + + NLOHMANN_DEFINE_TYPE_EXTENSION(SaveFileDialogDto, filters); + + void ReplyWithDialogResult(webview::webview& wv, + const std::string& id, + const ui::DialogCallbackResultType resultType, + const std::optional& result) + { + if (resultType == ui::DialogCallbackResultType::FAILED) + ui::PromiseReject(wv, id, result); + else + ui::PromiseResolve(wv, id, result); + } + + void OpenFileDialogBind(webview::webview& wv, const std::string& id, const std::optional& dto) + { + ui::OpenFileDialog dialog; + dialog.SetCallback( + [&wv, id](const ui::DialogCallbackResultType resultType, const std::optional& result) + { + ReplyWithDialogResult(wv, id, resultType, result); + }); + + if (dto && dto->filters) + { + for (auto& filter : *dto->filters) + { + dialog.AddFilter(filter.name, filter.filter); + } + } + + dialog.OpenAsync(); + } + + void SaveFileDialogBind(webview::webview& wv, const std::string& id, const std::optional& dto) + { + ui::SaveFileDialog dialog; + dialog.SetCallback( + [&wv, id](const ui::DialogCallbackResultType resultType, const std::optional& result) + { + ReplyWithDialogResult(wv, id, resultType, result); + }); + + if (dto && dto->filters) + { + for (auto& filter : *dto->filters) + { + dialog.AddFilter(filter.name, filter.filter); + } + } + + dialog.OpenAsync(); + } + + void FolderSelectDialogBind(webview::webview& wv, const std::string& id) + { + ui::FolderSelectDialog dialog; + dialog.SetCallback( + [&wv, id](const ui::DialogCallbackResultType resultType, const std::optional& result) + { + ReplyWithDialogResult(wv, id, resultType, result); + }); + + dialog.OpenAsync(); + } +} // namespace + +namespace ui +{ + void RegisterDialogHandlerBinds(webview::webview& wv) + { + BindAsync>(wv, + "openFileDialog", + [&wv](const std::string& id, const std::optional& dto) + { + OpenFileDialogBind(wv, id, dto); + }); + + BindAsync>(wv, + "saveFileDialog", + [&wv](const std::string& id, const std::optional& dto) + { + SaveFileDialogBind(wv, id, dto); + }); + + BindAsync(wv, + "folderSelectDialog", + [&wv](const std::string& id) + { + FolderSelectDialogBind(wv, id); + }); + } +} // namespace ui diff --git a/src/ModMan/Web/Binds/DialogBinds.h b/src/ModMan/Web/Binds/DialogBinds.h new file mode 100644 index 00000000..2d5a923e --- /dev/null +++ b/src/ModMan/Web/Binds/DialogBinds.h @@ -0,0 +1,8 @@ +#pragma once + +#include "Web/WebViewLib.h" + +namespace ui +{ + void RegisterDialogHandlerBinds(webview::webview& wv); +} diff --git a/src/ModMan/Web/Platform/AssetHandler.h b/src/ModMan/Web/Platform/AssetHandler.h new file mode 100644 index 00000000..ebdb1ebf --- /dev/null +++ b/src/ModMan/Web/Platform/AssetHandler.h @@ -0,0 +1,7 @@ +#pragma once + +#ifdef _WIN32 +#include "Windows/AssetHandlerWindows.h" +#elif defined(__linux__) +#include "Linux/AssetHandlerLinux.h" +#endif diff --git a/src/ModMan/Web/Platform/DialogHandler.cpp b/src/ModMan/Web/Platform/DialogHandler.cpp new file mode 100644 index 00000000..f93df3bc --- /dev/null +++ b/src/ModMan/Web/Platform/DialogHandler.cpp @@ -0,0 +1,24 @@ +#include "DialogHandler.h" + +namespace ui +{ + DialogWithCallback::DialogWithCallback() = default; + + void DialogWithCallback::SetCallback(callback_t callback) + { + m_callback = std::move(callback); + } + + FileDialogFilter::FileDialogFilter(std::string name, std::string filter) + : m_name(std::move(name)), + m_filter(std::move(filter)) + { + } + + FileDialog::FileDialog() = default; + + void FileDialog::AddFilter(std::string name, std::string filter) + { + m_filters.emplace_back(std::move(name), std::move(filter)); + } +} // namespace ui diff --git a/src/ModMan/Web/Platform/DialogHandler.h b/src/ModMan/Web/Platform/DialogHandler.h new file mode 100644 index 00000000..f793ccc2 --- /dev/null +++ b/src/ModMan/Web/Platform/DialogHandler.h @@ -0,0 +1,74 @@ +#pragma once + +#include +#include +#include +#include +#include + +namespace ui +{ + enum class DialogCallbackResultType : std::uint8_t + { + SUCCESSFUL, + CANCELLED, + FAILED + }; + + class DialogWithCallback + { + public: + using callback_t = std::function result)>; + + DialogWithCallback(); + + void SetCallback(callback_t callback); + + protected: + callback_t m_callback; + }; + + class FileDialogFilter + { + public: + FileDialogFilter(std::string name, std::string filter); + + std::string m_name; + std::string m_filter; + }; + + class FileDialog : public DialogWithCallback + { + public: + FileDialog(); + + void AddFilter(std::string name, std::string filter); + + protected: + std::vector m_filters; + }; + + class OpenFileDialog : public FileDialog + { + public: + OpenFileDialog(); + + void OpenAsync(); + }; + + class SaveFileDialog : public FileDialog + { + public: + SaveFileDialog(); + + void OpenAsync(); + }; + + class FolderSelectDialog : public DialogWithCallback + { + public: + FolderSelectDialog(); + + void OpenAsync(); + }; +} // namespace ui diff --git a/src/ModMan/Web/Gtk/AssetHandlerGtk.cpp b/src/ModMan/Web/Platform/Linux/AssetHandlerLinux.cpp similarity index 87% rename from src/ModMan/Web/Gtk/AssetHandlerGtk.cpp rename to src/ModMan/Web/Platform/Linux/AssetHandlerLinux.cpp index 3cec4a45..bb41be84 100644 --- a/src/ModMan/Web/Gtk/AssetHandlerGtk.cpp +++ b/src/ModMan/Web/Platform/Linux/AssetHandlerLinux.cpp @@ -1,4 +1,4 @@ -#include "AssetHandlerGtk.h" +#include "AssetHandlerLinux.h" #if defined(WEBVIEW_PLATFORM_LINUX) && defined(WEBVIEW_GTK) @@ -6,6 +6,9 @@ #include #include +#include + +using namespace PLATFORM_NAMESPACE_LINUX; namespace { @@ -34,9 +37,9 @@ namespace } } // namespace -namespace gtk +namespace PLATFORM_NAMESPACE_LINUX { - void InstallCustomProtocolHandler(webview::webview& wv) + void InstallAssetHandler(webview::webview& wv) { const auto widget = static_cast(wv.browser_controller().value()); const auto webView = WEBKIT_WEB_VIEW(widget); @@ -46,6 +49,6 @@ namespace gtk webkit_web_context_register_uri_scheme(context, "modman", ModManUriSchemeRequestCb, NULL, nullptr); } -} // namespace gtk +} // namespace PLATFORM_NAMESPACE_LINUX #endif diff --git a/src/ModMan/Web/Gtk/AssetHandlerGtk.h b/src/ModMan/Web/Platform/Linux/AssetHandlerLinux.h similarity index 54% rename from src/ModMan/Web/Gtk/AssetHandlerGtk.h rename to src/ModMan/Web/Platform/Linux/AssetHandlerLinux.h index 8a399dff..e8f49f2f 100644 --- a/src/ModMan/Web/Gtk/AssetHandlerGtk.h +++ b/src/ModMan/Web/Platform/Linux/AssetHandlerLinux.h @@ -1,16 +1,18 @@ #pragma once +#include "Web/Platform/Platform.h" + #include #if defined(WEBVIEW_PLATFORM_LINUX) && defined(WEBVIEW_GTK) #include "Web/WebViewLib.h" -namespace gtk +namespace PLATFORM_NAMESPACE_LINUX { constexpr auto URL_PREFIX = "modman://localhost/"; - void InstallCustomProtocolHandler(webview::webview& wv); -} // namespace gtk + void InstallAssetHandler(webview::webview& wv); +} // namespace PLATFORM_NAMESPACE_LINUX #endif diff --git a/src/ModMan/Web/Platform/Linux/DialogHandlerLinux.cpp b/src/ModMan/Web/Platform/Linux/DialogHandlerLinux.cpp new file mode 100644 index 00000000..16fcdc6e --- /dev/null +++ b/src/ModMan/Web/Platform/Linux/DialogHandlerLinux.cpp @@ -0,0 +1,183 @@ +#include "Web/Platform/DialogHandler.h" + +#ifdef __linux__ + +#include +#include +#include + +namespace +{ + enum class DialogOperation : std::uint8_t + { + OPEN_FILE, + SAVE_FILE, + SELECT_FOLDER + }; + + class DialogData + { + public: + DialogData(DialogOperation operation, ui::DialogWithCallback::callback_t callback) + : m_operation(operation), + m_callback(std::move(callback)) + { + } + + DialogOperation m_operation; + ui::DialogWithCallback::callback_t m_callback; + }; + + bool InitGtk() + { +#if GTK_MAJOR_VERSION >= 4 + return gtk_init_check(); +#else + return gtk_init_check(nullptr, nullptr); +#endif + } + + void OpenFileDialog() {} + + void SetFilters(GtkFileDialog* pDialog, const std::vector& filters) + { + if (filters.empty()) + return; + + auto* listStore = g_list_store_new(GTK_TYPE_FILE_FILTER); + + for (auto& filter : filters) + { + auto* fileFilter = gtk_file_filter_new(); + gtk_file_filter_set_name(fileFilter, filter.m_name.c_str()); + gtk_file_filter_add_pattern(fileFilter, filter.m_filter.c_str()); + g_list_store_append(listStore, fileFilter); + g_object_unref(fileFilter); + } + + auto* wildcardFilter = gtk_file_filter_new(); + gtk_file_filter_set_name(wildcardFilter, "All files"); + gtk_file_filter_add_pattern(wildcardFilter, "*.*"); + g_list_store_append(listStore, wildcardFilter); + g_object_unref(wildcardFilter); + + gtk_file_dialog_set_filters(pDialog, G_LIST_MODEL(listStore)); + g_object_unref(listStore); + } + + void OnDialogResult(GObject* source, GAsyncResult* asyncResult, gpointer userData) + { + auto* dialogData = reinterpret_cast(userData); + + GError* error = nullptr; + GFile* file = nullptr; + + if (dialogData->m_operation == DialogOperation::OPEN_FILE) + { + file = gtk_file_dialog_open_finish(GTK_FILE_DIALOG(source), asyncResult, &error); + } + else if (dialogData->m_operation == DialogOperation::SAVE_FILE) + { + file = gtk_file_dialog_save_finish(GTK_FILE_DIALOG(source), asyncResult, &error); + } + else if (dialogData->m_operation == DialogOperation::SELECT_FOLDER) + { + file = gtk_file_dialog_select_folder_finish(GTK_FILE_DIALOG(source), asyncResult, &error); + } + else + { + assert(false); + } + + std::optional result; + ui::DialogCallbackResultType resultType; + if (error) + { + if (error->code == GTK_DIALOG_ERROR_DISMISSED) + resultType = ui::DialogCallbackResultType::CANCELLED; + else + resultType = ui::DialogCallbackResultType::FAILED; + + g_error_free(error); + } + else + { + resultType = ui::DialogCallbackResultType::SUCCESSFUL; + result = std::string(g_file_get_path(file)); + } + + if (file) + { + g_object_unref(file); + } + + dialogData->m_callback(resultType, result); + + delete dialogData; + } + + std::optional ShowFileDialog() + { + std::optional result = std::nullopt; + + return result; + } +} // namespace + +namespace ui +{ +#if GTK_MAJOR_VERSION >= 4 + OpenFileDialog::OpenFileDialog() = default; + + void OpenFileDialog::OpenAsync() + { + if (!InitGtk()) + return m_callback(DialogCallbackResultType::FAILED, std::nullopt); + + auto* dialog = gtk_file_dialog_new(); + + SetFilters(dialog, m_filters); + + // Move data out of the dialog object since it may be destroyed + auto* dialogData = new DialogData(DialogOperation::OPEN_FILE, std::move(m_callback)); + gtk_file_dialog_open(dialog, nullptr, nullptr, OnDialogResult, dialogData); + + g_object_unref(dialog); + } + + SaveFileDialog::SaveFileDialog() = default; + + void SaveFileDialog::OpenAsync() + { + if (!InitGtk()) + return m_callback(DialogCallbackResultType::FAILED, std::nullopt); + + auto* dialog = gtk_file_dialog_new(); + + SetFilters(dialog, m_filters); + + // Move data out of the dialog object since it may be destroyed + auto* dialogData = new DialogData(DialogOperation::SAVE_FILE, std::move(m_callback)); + gtk_file_dialog_save(dialog, nullptr, nullptr, OnDialogResult, dialogData); + + g_object_unref(dialog); + } + + FolderSelectDialog::FolderSelectDialog() = default; + + void FolderSelectDialog::OpenAsync() + { + if (!InitGtk()) + return m_callback(DialogCallbackResultType::FAILED, std::nullopt); + + auto* dialog = gtk_file_dialog_new(); + + // Move data out of the dialog object since it may be destroyed + auto* dialogData = new DialogData(DialogOperation::SELECT_FOLDER, std::move(m_callback)); + gtk_file_dialog_select_folder(dialog, nullptr, nullptr, OnDialogResult, dialogData); + + g_object_unref(dialog); + } +#endif +} // namespace ui +#endif diff --git a/src/ModMan/Web/Platform/Platform.h b/src/ModMan/Web/Platform/Platform.h new file mode 100644 index 00000000..e531deca --- /dev/null +++ b/src/ModMan/Web/Platform/Platform.h @@ -0,0 +1,10 @@ +#pragma once + +#define PLATFORM_NAMESPACE_WINDOWS windows +#define PLATFORM_NAMESPACE_LINUX linux + +#ifdef _WIN32 +#define PLATFORM_NAMESPACE PLATFORM_NAMESPACE_WINDOWS +#elif defined(__linux__) +#define PLATFORM_NAMESPACE PLATFORM_NAMESPACE_LINUX +#endif diff --git a/src/ModMan/Web/Edge/AssetHandlerEdge.cpp b/src/ModMan/Web/Platform/Windows/AssetHandlerWindows.cpp similarity index 79% rename from src/ModMan/Web/Edge/AssetHandlerEdge.cpp rename to src/ModMan/Web/Platform/Windows/AssetHandlerWindows.cpp index a336e4a5..ba400bd0 100644 --- a/src/ModMan/Web/Edge/AssetHandlerEdge.cpp +++ b/src/ModMan/Web/Platform/Windows/AssetHandlerWindows.cpp @@ -1,7 +1,8 @@ -#include "AssetHandlerEdge.h" +#include "AssetHandlerWindows.h" #if defined(WEBVIEW_PLATFORM_WINDOWS) && defined(WEBVIEW_EDGE) +#include "PlatformUtilsWindows.h" #include "Web/UiAssets.h" #include @@ -11,40 +12,14 @@ #include #include +using namespace PLATFORM_NAMESPACE_WINDOWS; + namespace { constexpr auto LOCALHOST_PREFIX = "http://localhost:"; std::unordered_map assetLookup; - std::string WideStringToString(const std::wstring& wideString) - { - if (wideString.empty()) - return ""; - - const auto sizeNeeded = WideCharToMultiByte(CP_UTF8, 0, wideString.data(), static_cast(wideString.size()), nullptr, 0, nullptr, nullptr); - if (sizeNeeded <= 0) - throw std::runtime_error(std::format("WideCharToMultiByte() failed: {}", sizeNeeded)); - - std::string result(sizeNeeded, 0); - WideCharToMultiByte(CP_UTF8, 0, wideString.data(), static_cast(wideString.size()), result.data(), sizeNeeded, nullptr, nullptr); - return result; - } - - std::wstring StringToWideString(const std::string& string) - { - if (string.empty()) - return L""; - - const auto sizeNeeded = MultiByteToWideChar(CP_UTF8, 0, string.data(), static_cast(string.size()), nullptr, 0); - if (sizeNeeded <= 0) - throw std::runtime_error(std::format("MultiByteToWideChar() failed: {}", sizeNeeded)); - - std::wstring result(sizeNeeded, 0); - MultiByteToWideChar(CP_UTF8, 0, string.data(), static_cast(string.size()), result.data(), sizeNeeded); - return result; - } - std::wstring HeadersForAssetName(const std::string& assetName, const size_t contentLength) { std::wstringstream wss; @@ -99,9 +74,9 @@ namespace return S_OK; #endif - if (uri.starts_with(edge::URL_PREFIX)) + if (uri.starts_with(URL_PREFIX)) { - const auto asset = uri.substr(std::char_traits::length(edge::URL_PREFIX) - 1); + const auto asset = uri.substr(std::char_traits::length(URL_PREFIX) - 1); const auto foundUiFile = assetLookup.find(asset); if (foundUiFile != assetLookup.end()) @@ -142,9 +117,9 @@ namespace } } // namespace -namespace edge +namespace PLATFORM_NAMESPACE_WINDOWS { - void InstallCustomProtocolHandler(webview::webview& wv) + void InstallAssetHandler(webview::webview& wv) { assetLookup = ui::BuildUiFileLookup(); @@ -182,6 +157,6 @@ namespace edge std::cerr << "Failed to add resource requested filter\n"; } } -} // namespace edge +} // namespace PLATFORM_NAMESPACE_WINDOWS #endif diff --git a/src/ModMan/Web/Edge/AssetHandlerEdge.h b/src/ModMan/Web/Platform/Windows/AssetHandlerWindows.h similarity index 54% rename from src/ModMan/Web/Edge/AssetHandlerEdge.h rename to src/ModMan/Web/Platform/Windows/AssetHandlerWindows.h index fb01197b..b6475fd9 100644 --- a/src/ModMan/Web/Edge/AssetHandlerEdge.h +++ b/src/ModMan/Web/Platform/Windows/AssetHandlerWindows.h @@ -1,16 +1,18 @@ #pragma once +#include "Web/Platform/Platform.h" + #include #if defined(WEBVIEW_PLATFORM_WINDOWS) && defined(WEBVIEW_EDGE) #include "Web/WebViewLib.h" -namespace edge +namespace PLATFORM_NAMESPACE_WINDOWS { constexpr auto URL_PREFIX = "http://modman.local/"; - void InstallCustomProtocolHandler(webview::webview& wv); -} // namespace edge + void InstallAssetHandler(webview::webview& wv); +} // namespace PLATFORM_NAMESPACE_WINDOWS #endif diff --git a/src/ModMan/Web/Platform/Windows/DialogHandlerWindows.cpp b/src/ModMan/Web/Platform/Windows/DialogHandlerWindows.cpp new file mode 100644 index 00000000..68b34f23 --- /dev/null +++ b/src/ModMan/Web/Platform/Windows/DialogHandlerWindows.cpp @@ -0,0 +1,219 @@ +#include "Web/Platform/DialogHandler.h" + +#ifdef _WIN32 + +#include "PlatformUtilsWindows.h" + +#include +#include +#include + +using namespace PLATFORM_NAMESPACE_WINDOWS; + +namespace +{ + bool SetFilters(IFileDialog* pFileOpen, const std::vector& filters) + { + if (filters.empty()) + return true; + + const auto filterCount = filters.size(); + std::vector filterSpecs; + filterSpecs.reserve(filterCount + 1); + + std::vector filterStrings; + filterStrings.reserve(filterCount * 2); + + for (auto i = 0u; i < filterCount; i++) + { + const auto& filter = filters[i]; + COMDLG_FILTERSPEC filterSpec; + + const auto& wName = filterStrings.emplace_back(StringToWideString(filter.m_name)); + const auto& wSpec = filterStrings.emplace_back(StringToWideString(filter.m_filter)); + + filterSpec.pszName = wName.c_str(); + filterSpec.pszSpec = wSpec.c_str(); + + filterSpecs.emplace_back(filterSpec); + } + + COMDLG_FILTERSPEC wildCardSpec; + wildCardSpec.pszName = L"All files"; + wildCardSpec.pszSpec = L"*.*"; + filterSpecs.emplace_back(wildCardSpec); + + const auto result = pFileOpen->SetFileTypes(static_cast(filterCount + 1), filterSpecs.data()); + + return SUCCEEDED(result); + } + + ui::DialogCallbackResultType ShowFileDialog(IFileDialog* pFileDialog, std::optional& result) + { + auto resultType = ui::DialogCallbackResultType::FAILED; + + auto hr = pFileDialog->Show(nullptr); + if (SUCCEEDED(hr)) + { + IShellItem* pItem; + hr = pFileDialog->GetResult(&pItem); + if (SUCCEEDED(hr)) + { + PWSTR pszFilePath; + hr = pItem->GetDisplayName(SIGDN_FILESYSPATH, &pszFilePath); + + // Display the file name to the user. + if (SUCCEEDED(hr)) + { + result = WideStringToString(pszFilePath); + CoTaskMemFree(pszFilePath); + + resultType = ui::DialogCallbackResultType::SUCCESSFUL; + } + pItem->Release(); + } + } + else if (HRESULT_FROM_WIN32(ERROR_CANCELLED) == hr) + { + resultType = ui::DialogCallbackResultType::CANCELLED; + } + + return resultType; + } +} // namespace + +namespace ui +{ + OpenFileDialog::OpenFileDialog() = default; + + void OpenFileDialog::OpenAsync() + { + // Move data out of the dialog object since it may be destroyed + callback_t callback(std::move(m_callback)); + std::vector filters(std::move(m_filters)); + + // Windows dialogs are not asynchronous -> Spawn another thread + std::thread( + [callback, filters] + { + const auto initResult = CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED | COINIT_DISABLE_OLE1DDE); + if (!SUCCEEDED(initResult)) + { + callback(DialogCallbackResultType::FAILED, std::nullopt); + return; + } + + auto resultType = DialogCallbackResultType::FAILED; + std::optional result = std::nullopt; + IFileOpenDialog* pFileOpen; + + const auto hr = CoCreateInstance(CLSID_FileOpenDialog, nullptr, CLSCTX_ALL, IID_IFileOpenDialog, reinterpret_cast(&pFileOpen)); + + if (SUCCEEDED(hr) && SetFilters(pFileOpen, filters)) + { + resultType = ShowFileDialog(pFileOpen, result); + pFileOpen->Release(); + } + + CoUninitialize(); + + callback(resultType, result); + }) + .detach(); + } + + SaveFileDialog::SaveFileDialog() = default; + + void SaveFileDialog::OpenAsync() + { + // Move data out of the dialog object since it may be destroyed + callback_t callback(std::move(m_callback)); + std::vector filters(std::move(m_filters)); + + // Windows dialogs are not asynchronous -> Spawn another thread + std::thread( + [callback, filters] + { + const auto initResult = CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED | COINIT_DISABLE_OLE1DDE); + if (!SUCCEEDED(initResult)) + { + callback(DialogCallbackResultType::FAILED, std::nullopt); + return; + } + + auto resultType = DialogCallbackResultType::FAILED; + std::optional result = std::nullopt; + IFileSaveDialog* pFileSave; + + const auto hr = CoCreateInstance(CLSID_FileSaveDialog, nullptr, CLSCTX_ALL, IID_IFileSaveDialog, reinterpret_cast(&pFileSave)); + + if (SUCCEEDED(hr) && SetFilters(pFileSave, filters)) + { + resultType = ShowFileDialog(pFileSave, result); + pFileSave->Release(); + } + + CoUninitialize(); + + callback(resultType, result); + }) + .detach(); + } + + FolderSelectDialog::FolderSelectDialog() = default; + + void FolderSelectDialog::OpenAsync() + { + // Move data out of the dialog object since it may be destroyed + callback_t callback(std::move(m_callback)); + + // Windows dialogs are not asynchronous -> Spawn another thread + std::thread( + [callback] + { + const auto initResult = CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED | COINIT_DISABLE_OLE1DDE); + if (!SUCCEEDED(initResult)) + { + callback(DialogCallbackResultType::FAILED, std::nullopt); + return; + } + + std::optional result = std::nullopt; + IFileOpenDialog* pFileOpen; + + const auto hr = CoCreateInstance(CLSID_FileOpenDialog, nullptr, CLSCTX_ALL, IID_IFileOpenDialog, reinterpret_cast(&pFileOpen)); + if (!SUCCEEDED(hr)) + { + CoUninitialize(); + callback(DialogCallbackResultType::FAILED, std::nullopt); + return; + } + + DWORD dwOptions = 0; + if (!SUCCEEDED(pFileOpen->GetOptions(&dwOptions))) + { + pFileOpen->Release(); + CoUninitialize(); + callback(DialogCallbackResultType::FAILED, std::nullopt); + return; + } + + if (!SUCCEEDED(pFileOpen->SetOptions(dwOptions | FOS_PICKFOLDERS))) + { + pFileOpen->Release(); + CoUninitialize(); + callback(DialogCallbackResultType::FAILED, std::nullopt); + return; + } + + const auto resultType = ShowFileDialog(pFileOpen, result); + + pFileOpen->Release(); + CoUninitialize(); + + callback(resultType, result); + }) + .detach(); + } +} // namespace ui +#endif diff --git a/src/ModMan/Web/Platform/Windows/PlatformUtilsWindows.cpp b/src/ModMan/Web/Platform/Windows/PlatformUtilsWindows.cpp new file mode 100644 index 00000000..b9e419ad --- /dev/null +++ b/src/ModMan/Web/Platform/Windows/PlatformUtilsWindows.cpp @@ -0,0 +1,40 @@ +#include "PlatformUtilsWindows.h" + +#ifdef _WIN32 + +#include +#include +#include + +namespace PLATFORM_NAMESPACE_WINDOWS +{ + std::string WideStringToString(const std::wstring& wideString) + { + if (wideString.empty()) + return ""; + + const auto sizeNeeded = WideCharToMultiByte(CP_UTF8, 0, wideString.data(), static_cast(wideString.size()), nullptr, 0, nullptr, nullptr); + if (sizeNeeded <= 0) + throw std::runtime_error(std::format("WideCharToMultiByte() failed: {}", sizeNeeded)); + + std::string result(sizeNeeded, 0); + WideCharToMultiByte(CP_UTF8, 0, wideString.data(), static_cast(wideString.size()), result.data(), sizeNeeded, nullptr, nullptr); + return result; + } + + std::wstring StringToWideString(const std::string& string) + { + if (string.empty()) + return L""; + + const auto sizeNeeded = MultiByteToWideChar(CP_UTF8, 0, string.data(), static_cast(string.size()), nullptr, 0); + if (sizeNeeded <= 0) + throw std::runtime_error(std::format("MultiByteToWideChar() failed: {}", sizeNeeded)); + + std::wstring result(sizeNeeded, 0); + MultiByteToWideChar(CP_UTF8, 0, string.data(), static_cast(string.size()), result.data(), sizeNeeded); + return result; + } +} // namespace PLATFORM_NAMESPACE_WINDOWS + +#endif diff --git a/src/ModMan/Web/Platform/Windows/PlatformUtilsWindows.h b/src/ModMan/Web/Platform/Windows/PlatformUtilsWindows.h new file mode 100644 index 00000000..0edb4b86 --- /dev/null +++ b/src/ModMan/Web/Platform/Windows/PlatformUtilsWindows.h @@ -0,0 +1,16 @@ +#pragma once + +#ifdef _WIN32 + +#include "Web/Platform/Platform.h" + +#include +#include + +namespace PLATFORM_NAMESPACE_WINDOWS +{ + std::string WideStringToString(const std::wstring& wideString); + std::wstring StringToWideString(const std::string& string); +} // namespace PLATFORM_NAMESPACE_WINDOWS + +#endif diff --git a/src/ModMan/Web/UiCommunication.h b/src/ModMan/Web/UiCommunication.h index df0935ba..840d6bcf 100644 --- a/src/ModMan/Web/UiCommunication.h +++ b/src/ModMan/Web/UiCommunication.h @@ -33,7 +33,11 @@ namespace ui con::error("Webview params are not an array: {}", req); return ""; } - param = json.at(0).get(); + + if (json.empty()) + param = nlohmann::json().get(); + else + param = json.at(0).get(); } catch (const nlohmann::json::exception& e) { @@ -42,7 +46,6 @@ namespace ui } fn(std::move(param)); - return ""; }); } @@ -72,7 +75,11 @@ namespace ui con::error("Webview params are not an array: {}", req); return ""; } - param = json.at(0).get(); + + if (json.empty()) + param = nlohmann::json().get(); + else + param = json.at(0).get(); } catch (const nlohmann::json::exception& e) { @@ -81,7 +88,6 @@ namespace ui } auto result = fn(std::move(param)); - return nlohmann::json(result).dump(); }); } @@ -112,7 +118,11 @@ namespace ui con::error("Webview params are not an array: {}", req); return ""; } - param = json.at(0).get(); + + if (json.empty()) + param = nlohmann::json().get(); + else + param = json.at(0).get(); } catch (const nlohmann::json::exception& e) { @@ -121,6 +131,7 @@ namespace ui } fn(id, std::move(param)); + return ""; }, nullptr); } diff --git a/src/ModMan/main.cpp b/src/ModMan/main.cpp index 1929ef9c..a6afc7bb 100644 --- a/src/ModMan/main.cpp +++ b/src/ModMan/main.cpp @@ -1,6 +1,6 @@ #include "GitVersion.h" -#include "Web/Edge/AssetHandlerEdge.h" -#include "Web/Gtk/AssetHandlerGtk.h" +#include "Web/Binds/DialogBinds.h" +#include "Web/Platform/AssetHandler.h" #include "Web/UiCommunication.h" #include "Web/ViteAssets.h" #include "Web/WebViewLib.h" @@ -13,6 +13,7 @@ #include using namespace std::string_literals; +using namespace PLATFORM_NAMESPACE; namespace { @@ -55,6 +56,8 @@ namespace w.set_size(1280, 640, WEBVIEW_HINT_NONE); w.set_size(480, 320, WEBVIEW_HINT_MIN); + ui::RegisterDialogHandlerBinds(w); + // A binding that counts up or down and immediately returns the new value. ui::Bind(w, "greet", @@ -64,36 +67,12 @@ namespace return std::format("Hello from C++ {}!", name); }); - // A binding that counts up or down and immediately returns the new value. - ui::Bind(w, - "debug", - []() - { - con::info("Debug"); - }); - - // A binding that creates a new thread and returns the result at a later time. - ui::BindAsync(w, - "compute", - [&](const std::string& id) - { - // Create a thread and forget about it for the sake of simplicity. - std::thread( - [&, id] - { - // Simulate load. - std::this_thread::sleep_for(std::chrono::seconds(5)); - ui::PromiseResolve(w, id, 42); - }) - .detach(); - }); - #if defined(WEBVIEW_PLATFORM_WINDOWS) && defined(WEBVIEW_EDGE) - edge::InstallCustomProtocolHandler(w); - constexpr auto urlPrefix = edge::URL_PREFIX; + InstallAssetHandler(w); + constexpr auto urlPrefix = URL_PREFIX; #elif defined(WEBVIEW_PLATFORM_LINUX) && defined(WEBVIEW_GTK) - gtk::InstallCustomProtocolHandler(w); - constexpr auto urlPrefix = gtk::URL_PREFIX; + InstallAssetHandler(w); + constexpr auto urlPrefix = URL_PREFIX; #else #error Unsupported platform #endif diff --git a/src/ModManUi/src/App.vue b/src/ModManUi/src/App.vue index c906c7d3..1ff66ed6 100644 --- a/src/ModManUi/src/App.vue +++ b/src/ModManUi/src/App.vue @@ -4,6 +4,7 @@ import { webviewBinds, webviewAddEventListener, webviewRemoveEventListener } fro const greetMsg = ref(""); const lastPersonGreeted = ref(""); +const lastPath = ref(""); const name = ref(""); async function greet() { @@ -14,6 +15,11 @@ function onPersonGreeted(person: string) { lastPersonGreeted.value = person; } +async function onOpenFastfileClick() { + lastPath.value = + (await webviewBinds.openFileDialog({ filters: [{ name: "Fastfiles", filter: "*.ff" }] })) ?? ""; +} + webviewAddEventListener("greeting", onPersonGreeted); onUnmounted(() => webviewRemoveEventListener("greeting", onPersonGreeted)); @@ -30,6 +36,10 @@ onUnmounted(() => webviewRemoveEventListener("greeting", onPersonGreeted));

{{ greetMsg }}

The last person greeted is: {{ lastPersonGreeted }}

+

+ + The last path: {{ lastPath }} +

diff --git a/src/ModManUi/src/native/DialogBinds.ts b/src/ModManUi/src/native/DialogBinds.ts new file mode 100644 index 00000000..8c7e66b7 --- /dev/null +++ b/src/ModManUi/src/native/DialogBinds.ts @@ -0,0 +1,18 @@ +export interface FileDialogFilterDto { + name: string; + filter: string; +} + +export interface OpenFileDialogDto { + filters?: FileDialogFilterDto[]; +} + +export interface SaveFileDialogDto { + filters?: FileDialogFilterDto[]; +} + +export interface DialogBinds { + openFileDialog(options?: OpenFileDialogDto): string | null; + saveFileDialog(options?: SaveFileDialogDto): string | null; + folderSelectDialog(): string | null; +} diff --git a/src/ModManUi/src/native.ts b/src/ModManUi/src/native/index.ts similarity index 84% rename from src/ModManUi/src/native.ts rename to src/ModManUi/src/native/index.ts index d2cc9a66..ae61f41f 100644 --- a/src/ModManUi/src/native.ts +++ b/src/ModManUi/src/native/index.ts @@ -1,6 +1,9 @@ -export interface NativeMethods { - greet: (name: string) => Promise; -} +import type { DialogBinds } from "./DialogBinds"; + + +export type NativeMethods = { + greet(name: string): Promise; +} & DialogBinds; interface NativeEventMap { greeting: string; diff --git a/src/ObjCommon/Json/JsonExtension.h b/src/Utils/Json/JsonExtension.h similarity index 97% rename from src/ObjCommon/Json/JsonExtension.h rename to src/Utils/Json/JsonExtension.h index 897dc4fe..7f2b761b 100644 --- a/src/ObjCommon/Json/JsonExtension.h +++ b/src/Utils/Json/JsonExtension.h @@ -3,7 +3,12 @@ // Credits to // https://www.kdab.com/jsonify-with-nlohmann-json/ +#ifdef HAS_NLOHMANN_JSON + +#pragma warning(push, 0) #include +#pragma warning(pop) + #include // partial specialization (full specialization works too) @@ -56,3 +61,5 @@ namespace nlohmann { \ NLOHMANN_JSON_EXPAND(NLOHMANN_JSON_PASTE(EXTEND_JSON_FROM, __VA_ARGS__)) \ } + +#endif diff --git a/thirdparty/json.lua b/thirdparty/json.lua index ddca7815..c24b2e6c 100644 --- a/thirdparty/json.lua +++ b/thirdparty/json.lua @@ -3,7 +3,8 @@ json = {} function json:include(includes) if includes:handle(self:name()) then defines { - "JSON_DIAGNOSTICS=1" + "JSON_DIAGNOSTICS=1", + "HAS_NLOHMANN_JSON" } includedirs { path.join(ThirdPartyFolder(), "json", "single_include")