From 5bbeaed3a344b7b2a7b60d90e86bd4664b689e19 Mon Sep 17 00:00:00 2001 From: Jan Laupetin Date: Thu, 9 Oct 2025 17:35:55 +0100 Subject: [PATCH 1/5] feat: add dialog handler for ModMan --- src/ModMan/Web/Binds/DialogBinds.cpp | 123 +++++++++ src/ModMan/Web/Binds/DialogBinds.h | 8 + src/ModMan/Web/Platform/AssetHandler.h | 7 + src/ModMan/Web/Platform/DialogHandler.h | 7 + .../Linux/AssetHandlerLinux.cpp} | 11 +- .../Linux/AssetHandlerLinux.h} | 8 +- .../Web/Platform/Linux/DialogHandlerLinux.cpp | 92 +++++++ .../Web/Platform/Linux/DialogHandlerLinux.h | 70 +++++ src/ModMan/Web/Platform/Platform.h | 10 + .../Windows/AssetHandlerWindows.cpp} | 43 +--- .../Windows/AssetHandlerWindows.h} | 8 +- .../Platform/Windows/DialogHandlerWindows.cpp | 239 ++++++++++++++++++ .../Platform/Windows/DialogHandlerWindows.h | 80 ++++++ .../Platform/Windows/PlatformUtilsWindows.cpp | 40 +++ .../Platform/Windows/PlatformUtilsWindows.h | 16 ++ src/ModMan/Web/UiCommunication.h | 3 +- src/ModMan/main.cpp | 39 +-- src/ModManUi/src/App.vue | 10 + src/ModManUi/src/native/DialogBinds.ts | 18 ++ .../src/{native.ts => native/index.ts} | 9 +- src/{ObjCommon => Utils}/Json/JsonExtension.h | 7 + thirdparty/json.lua | 3 +- 22 files changed, 771 insertions(+), 80 deletions(-) create mode 100644 src/ModMan/Web/Binds/DialogBinds.cpp create mode 100644 src/ModMan/Web/Binds/DialogBinds.h create mode 100644 src/ModMan/Web/Platform/AssetHandler.h create mode 100644 src/ModMan/Web/Platform/DialogHandler.h rename src/ModMan/Web/{Gtk/AssetHandlerGtk.cpp => Platform/Linux/AssetHandlerLinux.cpp} (87%) rename src/ModMan/Web/{Gtk/AssetHandlerGtk.h => Platform/Linux/AssetHandlerLinux.h} (54%) create mode 100644 src/ModMan/Web/Platform/Linux/DialogHandlerLinux.cpp create mode 100644 src/ModMan/Web/Platform/Linux/DialogHandlerLinux.h create mode 100644 src/ModMan/Web/Platform/Platform.h rename src/ModMan/Web/{Edge/AssetHandlerEdge.cpp => Platform/Windows/AssetHandlerWindows.cpp} (79%) rename src/ModMan/Web/{Edge/AssetHandlerEdge.h => Platform/Windows/AssetHandlerWindows.h} (54%) create mode 100644 src/ModMan/Web/Platform/Windows/DialogHandlerWindows.cpp create mode 100644 src/ModMan/Web/Platform/Windows/DialogHandlerWindows.h create mode 100644 src/ModMan/Web/Platform/Windows/PlatformUtilsWindows.cpp create mode 100644 src/ModMan/Web/Platform/Windows/PlatformUtilsWindows.h create mode 100644 src/ModManUi/src/native/DialogBinds.ts rename src/ModManUi/src/{native.ts => native/index.ts} (84%) rename src/{ObjCommon => Utils}/Json/JsonExtension.h (97%) 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") From 210941991e573bb326e163920656cdb839ae3c7f Mon Sep 17 00:00:00 2001 From: Jan Laupetin Date: Thu, 9 Oct 2025 23:25:04 +0200 Subject: [PATCH 2/5] chore: implement dialog handler for linux --- src/ModMan/Web/Binds/DialogBinds.cpp | 21 ++- src/ModMan/Web/Platform/DialogHandler.cpp | 24 +++ src/ModMan/Web/Platform/DialogHandler.h | 77 +++++++- .../Web/Platform/Linux/DialogHandlerLinux.cpp | 173 +++++++++++++----- .../Web/Platform/Linux/DialogHandlerLinux.h | 70 ------- .../Platform/Windows/DialogHandlerWindows.cpp | 52 ++---- .../Platform/Windows/DialogHandlerWindows.h | 80 -------- 7 files changed, 252 insertions(+), 245 deletions(-) create mode 100644 src/ModMan/Web/Platform/DialogHandler.cpp delete mode 100644 src/ModMan/Web/Platform/Linux/DialogHandlerLinux.h delete mode 100644 src/ModMan/Web/Platform/Windows/DialogHandlerWindows.h diff --git a/src/ModMan/Web/Binds/DialogBinds.cpp b/src/ModMan/Web/Binds/DialogBinds.cpp index 091a9dea..3d72d9e3 100644 --- a/src/ModMan/Web/Binds/DialogBinds.cpp +++ b/src/ModMan/Web/Binds/DialogBinds.cpp @@ -5,8 +5,6 @@ #include "Json/JsonExtension.h" -using namespace PLATFORM_NAMESPACE; - namespace { class FileDialogFilterDto @@ -34,9 +32,12 @@ namespace NLOHMANN_DEFINE_TYPE_EXTENSION(SaveFileDialogDto, filters); - void ReplyWithDialogResult(webview::webview& wv, const std::string& id, const DialogCallbackResultType resultType, const std::optional& result) + void ReplyWithDialogResult(webview::webview& wv, + const std::string& id, + const ui::DialogCallbackResultType resultType, + const std::optional& result) { - if (resultType == FAILED) + if (resultType == ui::DialogCallbackResultType::FAILED) ui::PromiseReject(wv, id, result); else ui::PromiseResolve(wv, id, result); @@ -44,9 +45,9 @@ namespace void OpenFileDialogBind(webview::webview& wv, const std::string& id, const std::optional& dto) { - OpenFileDialog dialog; + ui::OpenFileDialog dialog; dialog.SetCallback( - [&wv, id](const DialogCallbackResultType resultType, const std::optional& result) + [&wv, id](const ui::DialogCallbackResultType resultType, const std::optional& result) { ReplyWithDialogResult(wv, id, resultType, result); }); @@ -64,9 +65,9 @@ namespace void SaveFileDialogBind(webview::webview& wv, const std::string& id, const std::optional& dto) { - SaveFileDialog dialog; + ui::SaveFileDialog dialog; dialog.SetCallback( - [&wv, id](const DialogCallbackResultType resultType, const std::optional& result) + [&wv, id](const ui::DialogCallbackResultType resultType, const std::optional& result) { ReplyWithDialogResult(wv, id, resultType, result); }); @@ -84,9 +85,9 @@ namespace void FolderSelectDialogBind(webview::webview& wv, const std::string& id) { - FolderSelectDialog dialog; + ui::FolderSelectDialog dialog; dialog.SetCallback( - [&wv, id](const DialogCallbackResultType resultType, const std::optional& result) + [&wv, id](const ui::DialogCallbackResultType resultType, const std::optional& result) { ReplyWithDialogResult(wv, id, resultType, result); }); 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 index 09ed1eb8..f793ccc2 100644 --- a/src/ModMan/Web/Platform/DialogHandler.h +++ b/src/ModMan/Web/Platform/DialogHandler.h @@ -1,7 +1,74 @@ #pragma once -#ifdef _WIN32 -#include "Windows/DialogHandlerWindows.h" -#elif defined(__linux__) -#include "Linux/DialogHandlerLinux.h" -#endif +#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/Platform/Linux/DialogHandlerLinux.cpp b/src/ModMan/Web/Platform/Linux/DialogHandlerLinux.cpp index 292d5e2e..50b14b46 100644 --- a/src/ModMan/Web/Platform/Linux/DialogHandlerLinux.cpp +++ b/src/ModMan/Web/Platform/Linux/DialogHandlerLinux.cpp @@ -1,13 +1,33 @@ -#include "DialogHandlerLinux.h" +#include "Web/Platform/DialogHandler.h" #ifdef __linux__ +#include +#include #include -using namespace PLATFORM_NAMESPACE_LINUX; - 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 @@ -17,29 +37,77 @@ namespace #endif } - void OpenFileDialog() - { -#ifdef GDK_AVAILABLE_IN_4_10 - auto* dialog = gtk_file_dialog_new(); + void OpenFileDialog() {} - 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) + void SetFilters(GtkFileDialog* pDialog, const std::vector& filters) { if (filters.empty()) - return true; + return; - return false; + 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); + } + + 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() @@ -50,43 +118,60 @@ namespace } } // namespace -namespace PLATFORM_NAMESPACE_LINUX +namespace ui { - 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)); - } - +#if GTK_MAJOR_VERSION >= 4 OpenFileDialog::OpenFileDialog() = default; - std::optional OpenFileDialog::Open() const + void OpenFileDialog::OpenAsync() { if (!InitGtk()) - return std::nullopt; + return m_callback(DialogCallbackResultType::FAILED, std::nullopt); - return 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; - std::optional SaveFileDialog::Open() const + void SaveFileDialog::OpenAsync() { - return std::nullopt; + 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; - std::optional FolderSelectDialog::Open() const + void FolderSelectDialog::OpenAsync() { - return std::nullopt; + 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); } -} // namespace PLATFORM_NAMESPACE_LINUX +#endif +} // namespace ui #endif diff --git a/src/ModMan/Web/Platform/Linux/DialogHandlerLinux.h b/src/ModMan/Web/Platform/Linux/DialogHandlerLinux.h deleted file mode 100644 index cc7e75b6..00000000 --- a/src/ModMan/Web/Platform/Linux/DialogHandlerLinux.h +++ /dev/null @@ -1,70 +0,0 @@ -#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/Windows/DialogHandlerWindows.cpp b/src/ModMan/Web/Platform/Windows/DialogHandlerWindows.cpp index 68885db2..6a1766bf 100644 --- a/src/ModMan/Web/Platform/Windows/DialogHandlerWindows.cpp +++ b/src/ModMan/Web/Platform/Windows/DialogHandlerWindows.cpp @@ -1,4 +1,4 @@ -#include "DialogHandlerWindows.h" +#include "Web/Platform/DialogHandler.h" #ifdef _WIN32 @@ -12,7 +12,7 @@ using namespace PLATFORM_NAMESPACE_WINDOWS; namespace { - bool SetFilters(IFileDialog* pFileOpen, const std::vector& filters) + bool SetFilters(IFileDialog* pFileOpen, const std::vector& filters) { if (filters.empty()) return true; @@ -48,9 +48,9 @@ namespace return SUCCEEDED(result); } - DialogCallbackResultType ShowFileDialog(IFileDialog* pFileDialog, std::optional& result) + ui::DialogCallbackResultType ShowFileDialog(IFileDialog* pFileDialog, std::optional& result) { - DialogCallbackResultType resultType = FAILED; + auto resultType = ui::DialogCallbackResultType::FAILED; auto hr = pFileDialog->Show(nullptr); if (SUCCEEDED(hr)) @@ -68,42 +68,22 @@ namespace result = WideStringToString(pszFilePath); CoTaskMemFree(pszFilePath); - resultType = SUCCESSFUL; + resultType = ui::DialogCallbackResultType::SUCCESSFUL; } pItem->Release(); } } else if (HRESULT_FROM_WIN32(ERROR_CANCELLED) == hr) { - resultType = CANCELLED; + resultType = ui::DialogCallbackResultType::CANCELLED; } return resultType; } } // namespace -namespace PLATFORM_NAMESPACE_WINDOWS +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)); - } - OpenFileDialog::OpenFileDialog() = default; void OpenFileDialog::OpenAsync() @@ -119,11 +99,11 @@ namespace PLATFORM_NAMESPACE_WINDOWS const auto initResult = CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED | COINIT_DISABLE_OLE1DDE); if (!SUCCEEDED(initResult)) { - callback(FAILED, std::nullopt); + callback(DialogCallbackResultType::FAILED, std::nullopt); return; } - DialogCallbackResultType resultType = FAILED; + auto resultType = DialogCallbackResultType::FAILED; std::optional result = std::nullopt; IFileOpenDialog* pFileOpen; @@ -157,11 +137,11 @@ namespace PLATFORM_NAMESPACE_WINDOWS const auto initResult = CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED | COINIT_DISABLE_OLE1DDE); if (!SUCCEEDED(initResult)) { - callback(FAILED, std::nullopt); + callback(DialogCallbackResultType::FAILED, std::nullopt); return; } - DialogCallbackResultType resultType = FAILED; + auto resultType = DialogCallbackResultType::FAILED; std::optional result = std::nullopt; IFileSaveDialog* pFileSave; @@ -194,7 +174,7 @@ namespace PLATFORM_NAMESPACE_WINDOWS const auto initResult = CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED | COINIT_DISABLE_OLE1DDE); if (!SUCCEEDED(initResult)) { - callback(FAILED, std::nullopt); + callback(DialogCallbackResultType::FAILED, std::nullopt); return; } @@ -205,7 +185,7 @@ namespace PLATFORM_NAMESPACE_WINDOWS if (!SUCCEEDED(hr)) { CoUninitialize(); - callback(FAILED, std::nullopt); + callback(DialogCallbackResultType::FAILED, std::nullopt); return; } @@ -214,7 +194,7 @@ namespace PLATFORM_NAMESPACE_WINDOWS { pFileOpen->Release(); CoUninitialize(); - callback(FAILED, std::nullopt); + callback(DialogCallbackResultType::FAILED, std::nullopt); return; } @@ -222,7 +202,7 @@ namespace PLATFORM_NAMESPACE_WINDOWS { pFileOpen->Release(); CoUninitialize(); - callback(FAILED, std::nullopt); + callback(DialogCallbackResultType::FAILED, std::nullopt); return; } @@ -235,5 +215,5 @@ namespace PLATFORM_NAMESPACE_WINDOWS }) .detach(); } -} // namespace PLATFORM_NAMESPACE_WINDOWS +} // namespace ui #endif diff --git a/src/ModMan/Web/Platform/Windows/DialogHandlerWindows.h b/src/ModMan/Web/Platform/Windows/DialogHandlerWindows.h deleted file mode 100644 index 0b60db85..00000000 --- a/src/ModMan/Web/Platform/Windows/DialogHandlerWindows.h +++ /dev/null @@ -1,80 +0,0 @@ -#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 From 8e9358b07f6fe9b290f27f8961b5ba8778de7cfb Mon Sep 17 00:00:00 2001 From: Jan Laupetin Date: Thu, 9 Oct 2025 22:42:43 +0100 Subject: [PATCH 3/5] fix: make ui binds work without args --- src/ModMan/Web/UiCommunication.h | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/src/ModMan/Web/UiCommunication.h b/src/ModMan/Web/UiCommunication.h index f5f7c2c3..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) { @@ -71,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) { @@ -110,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) { From eb1262510e1dff633522dfd16ec2765a364e286a Mon Sep 17 00:00:00 2001 From: Jan Laupetin Date: Thu, 9 Oct 2025 23:46:26 +0200 Subject: [PATCH 4/5] chore: add wildcard dialog selection for linux --- src/ModMan/Web/Platform/Linux/DialogHandlerLinux.cpp | 6 ++++++ src/ModMan/Web/Platform/Windows/DialogHandlerWindows.cpp | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/ModMan/Web/Platform/Linux/DialogHandlerLinux.cpp b/src/ModMan/Web/Platform/Linux/DialogHandlerLinux.cpp index 50b14b46..16fcdc6e 100644 --- a/src/ModMan/Web/Platform/Linux/DialogHandlerLinux.cpp +++ b/src/ModMan/Web/Platform/Linux/DialogHandlerLinux.cpp @@ -55,6 +55,12 @@ namespace 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); } diff --git a/src/ModMan/Web/Platform/Windows/DialogHandlerWindows.cpp b/src/ModMan/Web/Platform/Windows/DialogHandlerWindows.cpp index 6a1766bf..68b34f23 100644 --- a/src/ModMan/Web/Platform/Windows/DialogHandlerWindows.cpp +++ b/src/ModMan/Web/Platform/Windows/DialogHandlerWindows.cpp @@ -39,7 +39,7 @@ namespace } COMDLG_FILTERSPEC wildCardSpec; - wildCardSpec.pszName = L"*.*"; + wildCardSpec.pszName = L"All files"; wildCardSpec.pszSpec = L"*.*"; filterSpecs.emplace_back(wildCardSpec); From 2099e99d5e3d216c4214014280e2ad40b8233b76 Mon Sep 17 00:00:00 2001 From: Jan Laupetin Date: Thu, 9 Oct 2025 23:54:40 +0200 Subject: [PATCH 5/5] chore: fix formatting --- src/ModMan/Web/Platform/Windows/PlatformUtilsWindows.cpp | 2 +- src/ModMan/main.cpp | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/ModMan/Web/Platform/Windows/PlatformUtilsWindows.cpp b/src/ModMan/Web/Platform/Windows/PlatformUtilsWindows.cpp index 4b5497a0..b9e419ad 100644 --- a/src/ModMan/Web/Platform/Windows/PlatformUtilsWindows.cpp +++ b/src/ModMan/Web/Platform/Windows/PlatformUtilsWindows.cpp @@ -2,9 +2,9 @@ #ifdef _WIN32 +#include #include #include -#include namespace PLATFORM_NAMESPACE_WINDOWS { diff --git a/src/ModMan/main.cpp b/src/ModMan/main.cpp index 736ae360..a6afc7bb 100644 --- a/src/ModMan/main.cpp +++ b/src/ModMan/main.cpp @@ -1,7 +1,7 @@ #include "GitVersion.h" +#include "Web/Binds/DialogBinds.h" #include "Web/Platform/AssetHandler.h" #include "Web/UiCommunication.h" -#include "Web/Binds/DialogBinds.h" #include "Web/ViteAssets.h" #include "Web/WebViewLib.h"