diff --git a/src/ModMan/Web/Binds/DialogBinds.cpp b/src/ModMan/Web/Binds/DialogBinds.cpp new file mode 100644 index 00000000..091a9dea --- /dev/null +++ b/src/ModMan/Web/Binds/DialogBinds.cpp @@ -0,0 +1,123 @@ +#include "DialogBinds.h" + +#include "Web/Platform/DialogHandler.h" +#include "Web/UiCommunication.h" + +#include "Json/JsonExtension.h" + +using namespace PLATFORM_NAMESPACE; + +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 DialogCallbackResultType resultType, const std::optional& result) + { + if (resultType == 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) + { + OpenFileDialog dialog; + dialog.SetCallback( + [&wv, id](const 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) + { + SaveFileDialog dialog; + dialog.SetCallback( + [&wv, id](const 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) + { + FolderSelectDialog dialog; + dialog.SetCallback( + [&wv, id](const 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.h b/src/ModMan/Web/Platform/DialogHandler.h new file mode 100644 index 00000000..09ed1eb8 --- /dev/null +++ b/src/ModMan/Web/Platform/DialogHandler.h @@ -0,0 +1,7 @@ +#pragma once + +#ifdef _WIN32 +#include "Windows/DialogHandlerWindows.h" +#elif defined(__linux__) +#include "Linux/DialogHandlerLinux.h" +#endif 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..292d5e2e --- /dev/null +++ b/src/ModMan/Web/Platform/Linux/DialogHandlerLinux.cpp @@ -0,0 +1,92 @@ +#include "DialogHandlerLinux.h" + +#ifdef __linux__ + +#include + +using namespace PLATFORM_NAMESPACE_LINUX; + +namespace +{ + bool InitGtk() + { +#if GTK_MAJOR_VERSION >= 4 + return gtk_init_check(); +#else + return gtk_init_check(nullptr, nullptr); +#endif + } + + void OpenFileDialog() + { +#ifdef GDK_AVAILABLE_IN_4_10 + auto* dialog = gtk_file_dialog_new(); + + gtk_file_dialog_open(dialog, nullptr, nullptr, [](GObject *source, + GAsyncResult *result, + gpointer user_data + ) -> void { + + }, nullptr + ); + + g_object_unref(dialog); +#endif + } + + bool SetFilters(void* pFileOpen, const std::vector& filters) + { + if (filters.empty()) + return true; + + return false; + } + + std::optional ShowFileDialog() + { + std::optional result = std::nullopt; + + return result; + } +} // namespace + +namespace PLATFORM_NAMESPACE_LINUX +{ + 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)); + } + + OpenFileDialog::OpenFileDialog() = default; + + std::optional OpenFileDialog::Open() const + { + if (!InitGtk()) + return std::nullopt; + + return std::nullopt; + } + + SaveFileDialog::SaveFileDialog() = default; + + std::optional SaveFileDialog::Open() const + { + return std::nullopt; + } + + FolderSelectDialog::FolderSelectDialog() = default; + + std::optional FolderSelectDialog::Open() const + { + return std::nullopt; + } +} // namespace PLATFORM_NAMESPACE_LINUX +#endif diff --git a/src/ModMan/Web/Platform/Linux/DialogHandlerLinux.h b/src/ModMan/Web/Platform/Linux/DialogHandlerLinux.h new file mode 100644 index 00000000..cc7e75b6 --- /dev/null +++ b/src/ModMan/Web/Platform/Linux/DialogHandlerLinux.h @@ -0,0 +1,70 @@ +#pragma once + +#ifdef __linux__ + +#include "Web/Platform/Platform.h" + +#include +#include +#include +#include + +namespace PLATFORM_NAMESPACE_LINUX +{ + class FileDialogFilter + { + public: + FileDialogFilter(std::string name, std::string filter); + + std::string m_name; + std::string m_filter; + }; + + class DialogWithCallback + { + public: + DialogWithCallback(); + + void SetCallback(std::function result)> callback); + + protected: + std::function result)> m_callback; + }; + + 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() const; + }; + + class SaveFileDialog : public FileDialog + { + public: + SaveFileDialog(); + + void OpenAsync() const; + }; + + class FolderSelectDialog : public DialogWithCallback + { + public: + FolderSelectDialog(); + + void OpenAsync() const; + }; +} // namespace PLATFORM_NAMESPACE_LINUX + +#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..68885db2 --- /dev/null +++ b/src/ModMan/Web/Platform/Windows/DialogHandlerWindows.cpp @@ -0,0 +1,239 @@ +#include "DialogHandlerWindows.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"*.*"; + wildCardSpec.pszSpec = L"*.*"; + filterSpecs.emplace_back(wildCardSpec); + + const auto result = pFileOpen->SetFileTypes(static_cast(filterCount + 1), filterSpecs.data()); + + return SUCCEEDED(result); + } + + DialogCallbackResultType ShowFileDialog(IFileDialog* pFileDialog, std::optional& result) + { + DialogCallbackResultType resultType = 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 = SUCCESSFUL; + } + pItem->Release(); + } + } + else if (HRESULT_FROM_WIN32(ERROR_CANCELLED) == hr) + { + resultType = CANCELLED; + } + + return resultType; + } +} // namespace + +namespace PLATFORM_NAMESPACE_WINDOWS +{ + 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)); + } + + 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(FAILED, std::nullopt); + return; + } + + DialogCallbackResultType resultType = 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(FAILED, std::nullopt); + return; + } + + DialogCallbackResultType resultType = 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(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(FAILED, std::nullopt); + return; + } + + DWORD dwOptions = 0; + if (!SUCCEEDED(pFileOpen->GetOptions(&dwOptions))) + { + pFileOpen->Release(); + CoUninitialize(); + callback(FAILED, std::nullopt); + return; + } + + if (!SUCCEEDED(pFileOpen->SetOptions(dwOptions | FOS_PICKFOLDERS))) + { + pFileOpen->Release(); + CoUninitialize(); + callback(FAILED, std::nullopt); + return; + } + + const auto resultType = ShowFileDialog(pFileOpen, result); + + pFileOpen->Release(); + CoUninitialize(); + + callback(resultType, result); + }) + .detach(); + } +} // namespace PLATFORM_NAMESPACE_WINDOWS +#endif diff --git a/src/ModMan/Web/Platform/Windows/DialogHandlerWindows.h b/src/ModMan/Web/Platform/Windows/DialogHandlerWindows.h new file mode 100644 index 00000000..0b60db85 --- /dev/null +++ b/src/ModMan/Web/Platform/Windows/DialogHandlerWindows.h @@ -0,0 +1,80 @@ +#pragma once + +#ifdef _WIN32 + +#include "Web/Platform/Platform.h" + +#include +#include +#include +#include +#include + +namespace PLATFORM_NAMESPACE_WINDOWS +{ + enum 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 PLATFORM_NAMESPACE_WINDOWS + +#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..4b5497a0 --- /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..f5f7c2c3 100644 --- a/src/ModMan/Web/UiCommunication.h +++ b/src/ModMan/Web/UiCommunication.h @@ -42,7 +42,6 @@ namespace ui } fn(std::move(param)); - return ""; }); } @@ -81,7 +80,6 @@ namespace ui } auto result = fn(std::move(param)); - return nlohmann::json(result).dump(); }); } @@ -121,6 +119,7 @@ namespace ui } fn(id, std::move(param)); + return ""; }, nullptr); } diff --git a/src/ModMan/main.cpp b/src/ModMan/main.cpp index 1929ef9c..736ae360 100644 --- a/src/ModMan/main.cpp +++ b/src/ModMan/main.cpp @@ -1,7 +1,7 @@ #include "GitVersion.h" -#include "Web/Edge/AssetHandlerEdge.h" -#include "Web/Gtk/AssetHandlerGtk.h" +#include "Web/Platform/AssetHandler.h" #include "Web/UiCommunication.h" +#include "Web/Binds/DialogBinds.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")