MayaFlux 0.4.0
Digital-First Multimedia Processing Framework
Loading...
Searching...
No Matches
COMBackend.cpp
Go to the documentation of this file.
1#ifdef MAYAFLUX_PLATFORM_WINDOWS
2
3#include "COMBackend.hpp"
4
6
7#include <shobjidl_core.h>
8
9#ifndef WIN32_LEAN_AND_MEAN
10#define WIN32_LEAN_AND_MEAN
11#endif
12#ifndef NOMINMAX
13#define NOMINMAX
14#endif
15
16#ifdef ERROR
17#undef ERROR
18#endif // ERROR
19
20namespace MayaFlux::Core {
21
22namespace {
23
24 /**
25 * @brief Convert a UTF-8 std::string to a UTF-16 std::wstring.
26 */
27 std::wstring to_wide(const std::string& s)
28 {
29 if (s.empty()) {
30 return {};
31 }
32 const int len = MultiByteToWideChar(CP_UTF8, 0, s.c_str(), -1, nullptr, 0);
33 std::wstring result(len - 1, L'\0');
34 MultiByteToWideChar(CP_UTF8, 0, s.c_str(), -1, result.data(), len);
35 return result;
36 }
37
38 /**
39 * @brief Convert a UTF-16 std::wstring to a UTF-8 std::string.
40 */
41 std::string to_narrow(const std::wstring& ws)
42 {
43 if (ws.empty()) {
44 return {};
45 }
46 const int len = WideCharToMultiByte(CP_UTF8, 0, ws.c_str(), -1, nullptr, 0, nullptr, nullptr);
47 std::string result(len - 1, '\0');
48 WideCharToMultiByte(CP_UTF8, 0, ws.c_str(), -1, result.data(), len, nullptr, nullptr);
49 return result;
50 }
51
52 /**
53 * @brief Build a COMDLG_FILTERSPEC array from SystemFileFilter entries.
54 *
55 * Both the name and pattern wstrings must outlive the COMDLG_FILTERSPEC array,
56 * so they are returned as parallel vectors that the caller keeps alive.
57 */
58 void build_filter_specs(
59 const std::vector<SystemFileFilter>& filters,
60 std::vector<std::wstring>& names,
61 std::vector<std::wstring>& patterns,
62 std::vector<COMDLG_FILTERSPEC>& specs)
63 {
64 names.reserve(filters.size());
65 patterns.reserve(filters.size());
66 specs.reserve(filters.size());
67
68 for (const auto& f : filters) {
69 names.push_back(to_wide(f.name));
70
71 std::wstring pattern;
72 for (size_t i = 0; i < f.extensions.size(); ++i) {
73 if (i > 0) {
74 pattern += L';';
75 }
76 pattern += (f.extensions[i] == "*") ? L"*.*" : L"*." + to_wide(f.extensions[i]);
77 }
78 patterns.push_back(std::move(pattern));
79
80 specs.push_back({ names.back().c_str(), patterns.back().c_str() });
81 }
82 }
83
84 /**
85 * @brief Run one COM file dialog on the calling thread.
86 *
87 * CoInitializeEx / CoUninitialize are called here so the dispatch
88 * thread owns its own COM apartment. Fires @p callback before returning.
89 */
90 void run_dialog(
91 bool is_save,
92 FileDialogCallback callback,
93 const std::vector<SystemFileFilter>& filters,
94 const std::filesystem::path& start_dir,
95 const std::string& suggested_name)
96 {
97 HRESULT hr = CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED | COINIT_DISABLE_OLE1DDE);
98 if (FAILED(hr)) {
100 "COMBackend: CoInitializeEx failed: 0x{:08X}", static_cast<uint32_t>(hr));
101 callback(std::unexpected(SystemDialogError::BackendError));
102 return;
103 }
104
105 IFileDialog* dialog = nullptr;
106 const CLSID clsid = is_save ? CLSID_FileSaveDialog : CLSID_FileOpenDialog;
107 const IID iid = is_save ? IID_IFileSaveDialog : IID_IFileOpenDialog;
108
109 hr = CoCreateInstance(clsid, nullptr, CLSCTX_ALL, iid,
110 reinterpret_cast<void**>(&dialog));
111
112 if (FAILED(hr) || !dialog) {
114 "COMBackend: CoCreateInstance failed: 0x{:08X}", static_cast<uint32_t>(hr));
115 CoUninitialize();
116 callback(std::unexpected(SystemDialogError::BackendError));
117 return;
118 }
119
120 std::vector<std::wstring> names, pattern_strings;
121 std::vector<COMDLG_FILTERSPEC> specs;
122 build_filter_specs(filters, names, pattern_strings, specs);
123 if (!specs.empty()) {
124 dialog->SetFileTypes(static_cast<UINT>(specs.size()), specs.data());
125 }
126
127 if (is_save && !suggested_name.empty()) {
128 dialog->SetFileName(to_wide(suggested_name).c_str());
129 }
130
131 if (!start_dir.empty()) {
132 IShellItem* folder = nullptr;
133 hr = SHCreateItemFromParsingName(start_dir.wstring().c_str(), nullptr,
134 IID_IShellItem, reinterpret_cast<void**>(&folder));
135 if (SUCCEEDED(hr) && folder) {
136 dialog->SetFolder(folder);
137 folder->Release();
138 }
139 }
140
141 hr = dialog->Show(nullptr);
142
143 if (hr == HRESULT_FROM_WIN32(ERROR_CANCELLED)) {
144 dialog->Release();
145 CoUninitialize();
146 callback(std::unexpected(SystemDialogError::Cancelled));
147 return;
148 }
149
150 if (FAILED(hr)) {
152 "COMBackend: dialog Show failed: 0x{:08X}", static_cast<uint32_t>(hr));
153 dialog->Release();
154 CoUninitialize();
155 callback(std::unexpected(SystemDialogError::BackendError));
156 return;
157 }
158
159 IShellItem* result = nullptr;
160 hr = dialog->GetResult(&result);
161
162 if (FAILED(hr) || !result) {
163 dialog->Release();
164 CoUninitialize();
165 callback(std::unexpected(SystemDialogError::BackendError));
166 return;
167 }
168
169 PWSTR path_raw = nullptr;
170 hr = result->GetDisplayName(SIGDN_FILESYSPATH, &path_raw);
171 result->Release();
172 dialog->Release();
173
174 if (FAILED(hr) || !path_raw) {
175 CoUninitialize();
176 callback(std::unexpected(SystemDialogError::BackendError));
177 return;
178 }
179
180 std::filesystem::path chosen_path(path_raw);
181 CoTaskMemFree(path_raw);
182 CoUninitialize();
183
184 callback(std::move(chosen_path));
185 }
186
187} // namespace
188
189// =============================================================================
190// Lifecycle
191// =============================================================================
192
193bool COMBackend::initialize()
194{
195 if (m_initialized) {
196 return true;
197 }
198 m_initialized = true;
200 "COMBackend initialized");
201 return true;
202}
203
204void COMBackend::shutdown()
205{
206 if (!m_initialized) {
207 return;
208 }
209 m_initialized = false;
211 "COMBackend shutdown");
212}
213
214// =============================================================================
215// Dialog operations
216// =============================================================================
217
218void COMBackend::open_file(
219 FileDialogCallback callback,
220 std::vector<SystemFileFilter> filters,
221 std::filesystem::path start_dir)
222{
223 invoke_dialog(false, std::move(callback), filters, start_dir, {});
224}
225
226void COMBackend::save_file(
227 FileDialogCallback callback,
228 std::string suggested_name,
229 std::vector<SystemFileFilter> filters,
230 std::filesystem::path start_dir)
231{
232 invoke_dialog(true, std::move(callback), filters, start_dir, suggested_name);
233}
234
235void COMBackend::invoke_dialog(
236 bool is_save,
237 FileDialogCallback callback,
238 const std::vector<SystemFileFilter>& filters,
239 const std::filesystem::path& start_dir,
240 const std::string& suggested_name)
241{
242 if (!m_initialized) {
243 callback(std::unexpected(SystemDialogError::BackendError));
244 return;
245 }
246
247 std::thread(run_dialog, is_save, std::move(callback), filters, start_dir, suggested_name).detach();
248}
249
250} // namespace MayaFlux::Core
251
252#endif // MAYAFLUX_PLATFORM_WINDOWS
#define MF_INFO(comp, ctx,...)
#define MF_ERROR(comp, ctx,...)
@ BackendError
Platform backend failed to open or communicate.
@ Cancelled
User dismissed the dialog without completing it.
std::function< void(FileDialogResult)> FileDialogCallback
Callback type for all file dialog operations.
@ API
API calls from external code.
@ Core
Core engine, backend, subsystems.
std::shared_ptr< Vruta::Routine > pattern(std::function< std::any(uint64_t)> pattern_func, std::function< void(std::any)> callback, double interval_seconds, Vruta::ProcessingToken token)
Creates a generative algorithm that produces values based on a pattern function.
Definition Tasks.cpp:116