basalt-win32 0.0.0-rc5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/cli/runWindows.js +35 -0
- package/cli/shared.js +75 -0
- package/native/CMakeLists.txt +398 -0
- package/native/tests/test_win32_accessibility.cpp +271 -0
- package/native/tests/test_win32_clipboard.cpp +121 -0
- package/native/tests/test_win32_focus.cpp +195 -0
- package/native/tests/test_win32_hittest.cpp +410 -0
- package/native/tests/test_win32_image.cpp +239 -0
- package/native/tests/test_win32_imageloader.cpp +208 -0
- package/native/tests/test_win32_input.cpp +361 -0
- package/native/tests/test_win32_key_events.cpp +84 -0
- package/native/tests/test_win32_mounting.cpp +410 -0
- package/native/tests/test_win32_paint.cpp +505 -0
- package/native/tests/test_win32_scrollview.cpp +385 -0
- package/native/tests/test_win32_text.cpp +258 -0
- package/native/tests/test_win32_textinput.cpp +609 -0
- package/native/tests/test_win32_titlebar.cpp +111 -0
- package/native/tests/test_win32_titlebar_actions.cpp +62 -0
- package/native/tests/test_win32_view.cpp +222 -0
- package/native/win32/ColorSchemeWin32.cpp +56 -0
- package/native/win32/ComponentRegistryWin32.cpp +124 -0
- package/native/win32/DirectWriteLayout.cpp +116 -0
- package/native/win32/DirectWriteLayout.h +36 -0
- package/native/win32/DirectWriteLayoutManager.cpp +124 -0
- package/native/win32/FontRegistryDirectWrite.cpp +117 -0
- package/native/win32/PlatformServicesWin32.cpp +643 -0
- package/native/win32/RnWin32Accessible.cpp +258 -0
- package/native/win32/RnWin32Accessible.h +91 -0
- package/native/win32/RnWin32Image.cpp +265 -0
- package/native/win32/RnWin32Image.h +93 -0
- package/native/win32/RnWin32TextLayout.cpp +310 -0
- package/native/win32/RnWin32TextLayout.h +160 -0
- package/native/win32/RnWin32View.cpp +1281 -0
- package/native/win32/RnWin32View.h +506 -0
- package/native/win32/Win32AnimationChoreographer.cpp +76 -0
- package/native/win32/Win32AnimationChoreographer.h +61 -0
- package/native/win32/Win32Clip.h +173 -0
- package/native/win32/Win32DropTarget.cpp +567 -0
- package/native/win32/Win32DropTarget.h +42 -0
- package/native/win32/Win32Focus.cpp +262 -0
- package/native/win32/Win32Focus.h +109 -0
- package/native/win32/Win32ImageLoader.cpp +167 -0
- package/native/win32/Win32ImageLoader.h +104 -0
- package/native/win32/Win32KeyEvents.cpp +126 -0
- package/native/win32/Win32KeyEvents.h +20 -0
- package/native/win32/Win32MenuBar.cpp +352 -0
- package/native/win32/Win32MenuBar.h +16 -0
- package/native/win32/Win32MountingManager.cpp +727 -0
- package/native/win32/Win32MountingManager.h +203 -0
- package/native/win32/Win32Packaging.cpp +161 -0
- package/native/win32/Win32Packaging.h +56 -0
- package/native/win32/Win32RunLoopObserver.cpp +48 -0
- package/native/win32/Win32RunLoopObserver.h +42 -0
- package/native/win32/Win32ScrollView.cpp +515 -0
- package/native/win32/Win32ScrollView.h +208 -0
- package/native/win32/Win32SkiaContext.cpp +221 -0
- package/native/win32/Win32SkiaContext.h +53 -0
- package/native/win32/Win32SkiaModule.cpp +80 -0
- package/native/win32/Win32SkiaModule.h +50 -0
- package/native/win32/Win32SkiaPeer.cpp +358 -0
- package/native/win32/Win32SkiaPeer.h +66 -0
- package/native/win32/Win32SkiaUnicode.cpp +42 -0
- package/native/win32/Win32Snapshot.cpp +201 -0
- package/native/win32/Win32Snapshot.h +66 -0
- package/native/win32/Win32Strings.h +54 -0
- package/native/win32/Win32TextInput.cpp +811 -0
- package/native/win32/Win32TextInput.h +232 -0
- package/native/win32/Win32TitleBar.cpp +562 -0
- package/native/win32/Win32TitleBar.h +146 -0
- package/native/win32/Win32TitleBarLayout.cpp +46 -0
- package/native/win32/Win32TitleBarLayout.h +58 -0
- package/native/win32/Win32TouchDispatcher.cpp +443 -0
- package/native/win32/Win32TouchDispatcher.h +153 -0
- package/native/win32/Win32UiThread.h +27 -0
- package/native/win32/Win32WindowControl.cpp +329 -0
- package/native/win32/Win32WindowModule.cpp +209 -0
- package/native/win32/Win32WindowModule.h +72 -0
- package/native/win32/demo_layout_win32.cpp +232 -0
- package/native/win32/main_win32.cpp +2481 -0
- package/native/win32/mount_harness_win32.cpp +316 -0
- package/package.json +26 -0
- package/react-native.config.js +22 -0
|
@@ -0,0 +1,643 @@
|
|
|
1
|
+
// Windows' half of core/PlatformServices.h: the clipboard, opening a URL, an
|
|
2
|
+
// alert, and getting work onto the UI thread.
|
|
3
|
+
//
|
|
4
|
+
// The last of those is the one the mounting manager depends on, and it is the
|
|
5
|
+
// only one with a real design decision in it. See postToUiThread below.
|
|
6
|
+
|
|
7
|
+
#include "PlatformServices.h"
|
|
8
|
+
#include "ShareFallback.h"
|
|
9
|
+
|
|
10
|
+
#include "Win32Strings.h"
|
|
11
|
+
#include "Win32UiThread.h"
|
|
12
|
+
|
|
13
|
+
#include <windows.h>
|
|
14
|
+
|
|
15
|
+
#include <shellapi.h>
|
|
16
|
+
#include <shlobj.h>
|
|
17
|
+
#include <shobjidl.h>
|
|
18
|
+
|
|
19
|
+
#include <mutex>
|
|
20
|
+
#include <string>
|
|
21
|
+
#include <thread>
|
|
22
|
+
#include <utility>
|
|
23
|
+
#include <vector>
|
|
24
|
+
|
|
25
|
+
namespace basalt {
|
|
26
|
+
|
|
27
|
+
using win32::narrow;
|
|
28
|
+
using win32::widen;
|
|
29
|
+
namespace {
|
|
30
|
+
|
|
31
|
+
// --- the UI thread -----------------------------------------------------------
|
|
32
|
+
//
|
|
33
|
+
// A message-only window, created on whichever thread calls `installUiThread`
|
|
34
|
+
// first -- which the host does before it starts React Native, and which the
|
|
35
|
+
// tests do not do at all.
|
|
36
|
+
//
|
|
37
|
+
// A message-only window (HWND_MESSAGE) rather than posting to the host's real
|
|
38
|
+
// window: it exists before any surface does, it cannot be destroyed by a user
|
|
39
|
+
// closing something, and it keeps this seam from needing to know what the host
|
|
40
|
+
// draws into. It is also the mechanism a Win32 program is expected to use --
|
|
41
|
+
// PostMessage is the only thread-safe way into a message loop.
|
|
42
|
+
//
|
|
43
|
+
// GTK does this with g_idle_add_full and macOS with dispatch_async_f. All three
|
|
44
|
+
// are the same promise: the work runs on the UI thread, and work posted earlier
|
|
45
|
+
// runs first. That ordering is not a nicety -- a command that arrived before
|
|
46
|
+
// the transaction creating the view it names would find nothing and be dropped.
|
|
47
|
+
|
|
48
|
+
constexpr UINT kRunWork = WM_APP + 1;
|
|
49
|
+
|
|
50
|
+
struct UiThread {
|
|
51
|
+
std::mutex mutex;
|
|
52
|
+
std::vector<std::function<void()>> pending;
|
|
53
|
+
HWND window = nullptr;
|
|
54
|
+
std::thread::id threadId{};
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
UiThread &uiThread() {
|
|
58
|
+
static UiThread instance;
|
|
59
|
+
return instance;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
void drainPendingWork() {
|
|
63
|
+
std::vector<std::function<void()>> work;
|
|
64
|
+
{
|
|
65
|
+
std::lock_guard<std::mutex> lock(uiThread().mutex);
|
|
66
|
+
work.swap(uiThread().pending);
|
|
67
|
+
}
|
|
68
|
+
for (auto &item : work) {
|
|
69
|
+
item();
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
LRESULT CALLBACK uiThreadProc(HWND hwnd, UINT message, WPARAM wparam, LPARAM lparam) {
|
|
74
|
+
if (message == kRunWork) {
|
|
75
|
+
drainPendingWork();
|
|
76
|
+
return 0;
|
|
77
|
+
}
|
|
78
|
+
return DefWindowProc(hwnd, message, wparam, lparam);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
} // namespace
|
|
82
|
+
|
|
83
|
+
// See Win32UiThread.h.
|
|
84
|
+
|
|
85
|
+
void installUiThread() {
|
|
86
|
+
if (uiThread().window != nullptr) {
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
static const ATOM registered = [] {
|
|
90
|
+
WNDCLASSEXW windowClass{};
|
|
91
|
+
windowClass.cbSize = sizeof(windowClass);
|
|
92
|
+
windowClass.lpfnWndProc = uiThreadProc;
|
|
93
|
+
windowClass.hInstance = GetModuleHandleW(nullptr);
|
|
94
|
+
windowClass.lpszClassName = L"BasaltUiThreadDispatch";
|
|
95
|
+
return RegisterClassExW(&windowClass);
|
|
96
|
+
}();
|
|
97
|
+
(void)registered;
|
|
98
|
+
|
|
99
|
+
uiThread().threadId = std::this_thread::get_id();
|
|
100
|
+
uiThread().window = CreateWindowExW(0,
|
|
101
|
+
L"BasaltUiThreadDispatch",
|
|
102
|
+
L"",
|
|
103
|
+
0,
|
|
104
|
+
0,
|
|
105
|
+
0,
|
|
106
|
+
0,
|
|
107
|
+
0,
|
|
108
|
+
HWND_MESSAGE,
|
|
109
|
+
nullptr,
|
|
110
|
+
GetModuleHandleW(nullptr),
|
|
111
|
+
nullptr);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
bool hasUiThread() {
|
|
115
|
+
return uiThread().window != nullptr;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
bool isUiThread() {
|
|
119
|
+
// With no UI thread installed there is only one thread that matters, so
|
|
120
|
+
// saying yes is both true and what keeps the tests from marshalling into a
|
|
121
|
+
// message loop nobody is pumping.
|
|
122
|
+
if (uiThread().window == nullptr) {
|
|
123
|
+
return true;
|
|
124
|
+
}
|
|
125
|
+
return std::this_thread::get_id() == uiThread().threadId;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
void postToUiThread(std::function<void()> work) {
|
|
129
|
+
if (!work) {
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// No host, so no message loop to post to: run it here. This is the harness
|
|
134
|
+
// and the test path, and it is safe precisely because there is no second
|
|
135
|
+
// thread in it. A real host installs the window first, after which even work
|
|
136
|
+
// posted *from* the UI thread is queued rather than run inline -- which is
|
|
137
|
+
// what preserves ordering against work already in the queue, and is the same
|
|
138
|
+
// rule the other two platforms follow.
|
|
139
|
+
if (uiThread().window == nullptr) {
|
|
140
|
+
work();
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
{
|
|
145
|
+
std::lock_guard<std::mutex> lock(uiThread().mutex);
|
|
146
|
+
uiThread().pending.push_back(std::move(work));
|
|
147
|
+
}
|
|
148
|
+
PostMessageW(uiThread().window, kRunWork, 0, 0);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
void postDelayed(double milliseconds, std::function<void()> work) {
|
|
152
|
+
if (!work) {
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
if (uiThread().window == nullptr) {
|
|
156
|
+
work();
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
// A timer queue rather than SetTimer: SetTimer's resolution is the message
|
|
160
|
+
// loop's and it coalesces, which is wrong for something a JavaScript timer is
|
|
161
|
+
// waiting on. The callback arrives on a pool thread, so it is bounced onto
|
|
162
|
+
// the UI thread rather than running there.
|
|
163
|
+
struct Delayed {
|
|
164
|
+
std::function<void()> work;
|
|
165
|
+
HANDLE timer = nullptr;
|
|
166
|
+
};
|
|
167
|
+
auto *delayed = new Delayed{std::move(work), nullptr};
|
|
168
|
+
const auto fire = [](PVOID parameter, BOOLEAN) {
|
|
169
|
+
auto *item = static_cast<Delayed *>(parameter);
|
|
170
|
+
postToUiThread([item] {
|
|
171
|
+
item->work();
|
|
172
|
+
if (item->timer != nullptr) {
|
|
173
|
+
DeleteTimerQueueTimer(nullptr, item->timer, nullptr);
|
|
174
|
+
}
|
|
175
|
+
delete item;
|
|
176
|
+
});
|
|
177
|
+
};
|
|
178
|
+
if (!CreateTimerQueueTimer(&delayed->timer,
|
|
179
|
+
nullptr,
|
|
180
|
+
fire,
|
|
181
|
+
delayed,
|
|
182
|
+
static_cast<DWORD>(milliseconds < 0 ? 0 : milliseconds),
|
|
183
|
+
0,
|
|
184
|
+
WT_EXECUTEONLYONCE)) {
|
|
185
|
+
// Could not schedule it; running late is better than never.
|
|
186
|
+
postToUiThread(std::move(delayed->work));
|
|
187
|
+
delete delayed;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// --- clipboard ---------------------------------------------------------------
|
|
192
|
+
|
|
193
|
+
namespace {
|
|
194
|
+
|
|
195
|
+
// The clipboard is one lock shared by every window on the desktop, and
|
|
196
|
+
// OpenClipboard does not wait for it: if a window anywhere has it open, the
|
|
197
|
+
// call fails at once with access denied. Clipboard listeners are windows, and
|
|
198
|
+
// each is told of every change and opens the clipboard to read it, so a read or
|
|
199
|
+
// a write that lands while one is reading is refused -- and before this, the
|
|
200
|
+
// refusal was silent: a write simply did not happen.
|
|
201
|
+
//
|
|
202
|
+
// Found as js/modules.js failing "clipboard takes unicode", its second write in
|
|
203
|
+
// a row, in 2 of 15 runs on the development machine, which compare_hosts.sh
|
|
204
|
+
// reported as Windows disagreeing with Linux. What was holding the clipboard
|
|
205
|
+
// was never caught: with this retry in place and logging on every refusal, 190
|
|
206
|
+
// further runs saw none at all. So the retry is not proven to be what fixed
|
|
207
|
+
// that; tests/test_win32_clipboard.cpp is what shows it is needed, by holding
|
|
208
|
+
// the clipboard from a window and failing without it.
|
|
209
|
+
//
|
|
210
|
+
// A few short tries is what everyone does, Chromium included. Sleep(5) sleeps
|
|
211
|
+
// a scheduler tick, about 15ms, so this is up to ~150ms on the UI thread --
|
|
212
|
+
// only when somebody else has the clipboard, and a listener holds it for a
|
|
213
|
+
// read rather than for long.
|
|
214
|
+
bool openClipboard() {
|
|
215
|
+
constexpr int kAttempts = 10;
|
|
216
|
+
constexpr DWORD kPauseMs = 5;
|
|
217
|
+
for (int attempt = 0; attempt < kAttempts; ++attempt) {
|
|
218
|
+
if (OpenClipboard(nullptr)) {
|
|
219
|
+
return true;
|
|
220
|
+
}
|
|
221
|
+
Sleep(kPauseMs);
|
|
222
|
+
}
|
|
223
|
+
return false;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
} // namespace
|
|
227
|
+
|
|
228
|
+
std::string clipboardText() {
|
|
229
|
+
if (!openClipboard()) {
|
|
230
|
+
return {};
|
|
231
|
+
}
|
|
232
|
+
std::string result;
|
|
233
|
+
// CF_UNICODETEXT rather than CF_TEXT: the ANSI variant loses anything outside
|
|
234
|
+
// the active code page, and a clipboard is exactly where that shows up.
|
|
235
|
+
if (const HANDLE handle = GetClipboardData(CF_UNICODETEXT)) {
|
|
236
|
+
if (const auto *text = static_cast<const wchar_t *>(GlobalLock(handle))) {
|
|
237
|
+
result = narrow(text);
|
|
238
|
+
GlobalUnlock(handle);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
CloseClipboard();
|
|
242
|
+
return result;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
void setClipboardText(const std::string &text) {
|
|
246
|
+
if (!openClipboard()) {
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
EmptyClipboard();
|
|
250
|
+
|
|
251
|
+
const std::wstring wide = widen(text);
|
|
252
|
+
const size_t bytes = (wide.size() + 1) * sizeof(wchar_t);
|
|
253
|
+
// GMEM_MOVEABLE, and ownership passes to the clipboard on success -- freeing
|
|
254
|
+
// it afterwards is a use-after-free that usually survives long enough to be
|
|
255
|
+
// mysterious.
|
|
256
|
+
if (const HGLOBAL handle = GlobalAlloc(GMEM_MOVEABLE, bytes)) {
|
|
257
|
+
if (auto *destination = static_cast<wchar_t *>(GlobalLock(handle))) {
|
|
258
|
+
memcpy(destination, wide.c_str(), bytes);
|
|
259
|
+
GlobalUnlock(handle);
|
|
260
|
+
if (SetClipboardData(CF_UNICODETEXT, handle) == nullptr) {
|
|
261
|
+
GlobalFree(handle);
|
|
262
|
+
}
|
|
263
|
+
} else {
|
|
264
|
+
GlobalFree(handle);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
CloseClipboard();
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// --- opening a URL -----------------------------------------------------------
|
|
271
|
+
|
|
272
|
+
bool canOpenUrl(const std::string &url) {
|
|
273
|
+
// Answered by asking the registry whether the scheme is registered, which is
|
|
274
|
+
// what "can open" means on Windows: a scheme is a key under HKCR with a
|
|
275
|
+
// URL Protocol value. Not by trying to open it, which would open it.
|
|
276
|
+
const size_t colon = url.find(':');
|
|
277
|
+
if (colon == std::string::npos || colon == 0) {
|
|
278
|
+
return false;
|
|
279
|
+
}
|
|
280
|
+
const std::wstring scheme = widen(url.substr(0, colon));
|
|
281
|
+
|
|
282
|
+
HKEY key = nullptr;
|
|
283
|
+
if (RegOpenKeyExW(HKEY_CLASSES_ROOT, scheme.c_str(), 0, KEY_QUERY_VALUE, &key) != ERROR_SUCCESS) {
|
|
284
|
+
return false;
|
|
285
|
+
}
|
|
286
|
+
const bool isProtocol =
|
|
287
|
+
RegQueryValueExW(key, L"URL Protocol", nullptr, nullptr, nullptr, nullptr) == ERROR_SUCCESS;
|
|
288
|
+
RegCloseKey(key);
|
|
289
|
+
return isProtocol;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
bool openUrl(const std::string &url) {
|
|
293
|
+
const std::wstring wide = widen(url);
|
|
294
|
+
// ShellExecute returns a fake HINSTANCE; anything above 32 is success. That
|
|
295
|
+
// is the documented contract, odd as it looks.
|
|
296
|
+
const auto result = reinterpret_cast<INT_PTR>(
|
|
297
|
+
ShellExecuteW(nullptr, L"open", wide.c_str(), nullptr, nullptr, SW_SHOWNORMAL));
|
|
298
|
+
return result > 32;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// --- sharing -----------------------------------------------------------------
|
|
302
|
+
|
|
303
|
+
// Windows does have a share UI -- the DataTransferManager's, the one the Share
|
|
304
|
+
// charm opens -- and reaching it means WinRT interop:
|
|
305
|
+
// `IDataTransferManagerInterop::ShowShareUIForWindow` against an HWND, plus the
|
|
306
|
+
// activation and the data-requested handler. That is worth doing and is not
|
|
307
|
+
// done here, because it cannot be tested from the machine this was written on
|
|
308
|
+
// and a share sheet that fails is worse than one that is plainly minimal. See
|
|
309
|
+
// docs/BACKLOG.md.
|
|
310
|
+
//
|
|
311
|
+
// Until then, the picker core/ShareFallback.h builds out of a clipboard and a
|
|
312
|
+
// mail client, which is what every desktop has. Nothing here beyond the call:
|
|
313
|
+
// it goes through showAlert, setClipboardText and openUrl, all of which are
|
|
314
|
+
// implemented above.
|
|
315
|
+
void shareContent(const ShareRequest &request, ShareCallback onDone) {
|
|
316
|
+
shareThroughFallbackPicker(request, std::move(onDone));
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// --- file dialogs -------------------------------------------------------------
|
|
320
|
+
|
|
321
|
+
namespace {
|
|
322
|
+
|
|
323
|
+
// The filters, in the shape IFileDialog takes: one COMDLG_FILTERSPEC per entry,
|
|
324
|
+
// with the extensions joined into a single semicolon-separated pattern. The
|
|
325
|
+
// strings have to outlive the call, which is why they are built into a vector
|
|
326
|
+
// that the caller keeps rather than into temporaries.
|
|
327
|
+
struct FilterStorage {
|
|
328
|
+
std::vector<std::wstring> names;
|
|
329
|
+
std::vector<std::wstring> patterns;
|
|
330
|
+
std::vector<COMDLG_FILTERSPEC> specs;
|
|
331
|
+
};
|
|
332
|
+
|
|
333
|
+
FilterStorage buildFilters(const std::vector<FileFilter> &filters) {
|
|
334
|
+
FilterStorage storage;
|
|
335
|
+
storage.names.reserve(filters.size());
|
|
336
|
+
storage.patterns.reserve(filters.size());
|
|
337
|
+
for (const FileFilter &filter : filters) {
|
|
338
|
+
std::wstring pattern;
|
|
339
|
+
for (const std::string &extension : filter.extensions) {
|
|
340
|
+
if (!pattern.empty()) {
|
|
341
|
+
pattern += L";";
|
|
342
|
+
}
|
|
343
|
+
pattern += L"*.";
|
|
344
|
+
pattern += widen(extension);
|
|
345
|
+
}
|
|
346
|
+
storage.names.push_back(widen(filter.name.empty() ? "Files" : filter.name));
|
|
347
|
+
storage.patterns.push_back(std::move(pattern));
|
|
348
|
+
}
|
|
349
|
+
// Built second, because a vector that grows moves its strings and the specs
|
|
350
|
+
// hold pointers into them.
|
|
351
|
+
storage.specs.reserve(storage.names.size());
|
|
352
|
+
for (size_t i = 0; i < storage.names.size(); i++) {
|
|
353
|
+
storage.specs.push_back(
|
|
354
|
+
COMDLG_FILTERSPEC{storage.names[i].c_str(), storage.patterns[i].c_str()});
|
|
355
|
+
}
|
|
356
|
+
return storage;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
std::string pathOf(IShellItem *item) {
|
|
360
|
+
PWSTR path = nullptr;
|
|
361
|
+
if (FAILED(item->GetDisplayName(SIGDN_FILESYSPATH, &path)) || path == nullptr) {
|
|
362
|
+
return {};
|
|
363
|
+
}
|
|
364
|
+
const std::string result = narrow(path);
|
|
365
|
+
CoTaskMemFree(path);
|
|
366
|
+
return result;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
} // namespace
|
|
370
|
+
|
|
371
|
+
void showFileDialog(const FileDialogRequest &request, FileDialogCallback onDone) {
|
|
372
|
+
// Onto the UI thread. IFileDialog::Show runs its own modal loop and must be
|
|
373
|
+
// called on a thread with an apartment, which is the one the host's window
|
|
374
|
+
// lives on -- and running it on the JavaScript thread would deadlock the
|
|
375
|
+
// runtime.
|
|
376
|
+
postToUiThread([request, onDone = std::move(onDone)] {
|
|
377
|
+
const bool saving = request.kind == FileDialogRequest::Kind::SaveFile;
|
|
378
|
+
|
|
379
|
+
IFileDialog *dialog = nullptr;
|
|
380
|
+
const HRESULT created = CoCreateInstance(saving ? CLSID_FileSaveDialog : CLSID_FileOpenDialog,
|
|
381
|
+
nullptr,
|
|
382
|
+
CLSCTX_INPROC_SERVER,
|
|
383
|
+
IID_IFileDialog,
|
|
384
|
+
(void **)&dialog);
|
|
385
|
+
if (FAILED(created) || dialog == nullptr) {
|
|
386
|
+
onDone(true, {});
|
|
387
|
+
return;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
DWORD options = 0;
|
|
391
|
+
dialog->GetOptions(&options);
|
|
392
|
+
options |= FOS_FORCEFILESYSTEM;
|
|
393
|
+
if (request.kind == FileDialogRequest::Kind::OpenFolder) {
|
|
394
|
+
options |= FOS_PICKFOLDERS;
|
|
395
|
+
}
|
|
396
|
+
if (request.kind == FileDialogRequest::Kind::OpenFile && request.multiple) {
|
|
397
|
+
options |= FOS_ALLOWMULTISELECT;
|
|
398
|
+
}
|
|
399
|
+
dialog->SetOptions(options);
|
|
400
|
+
|
|
401
|
+
if (!request.title.empty()) {
|
|
402
|
+
dialog->SetTitle(widen(request.title).c_str());
|
|
403
|
+
}
|
|
404
|
+
if (!request.confirmLabel.empty()) {
|
|
405
|
+
dialog->SetOkButtonLabel(widen(request.confirmLabel).c_str());
|
|
406
|
+
}
|
|
407
|
+
// A folder picker with file-type filters is a dialog that filters nothing
|
|
408
|
+
// and shows a combo box saying so.
|
|
409
|
+
FilterStorage filters;
|
|
410
|
+
if (request.kind != FileDialogRequest::Kind::OpenFolder && !request.filters.empty()) {
|
|
411
|
+
filters = buildFilters(request.filters);
|
|
412
|
+
if (!filters.specs.empty()) {
|
|
413
|
+
dialog->SetFileTypes(static_cast<UINT>(filters.specs.size()), filters.specs.data());
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
if (!request.defaultPath.empty()) {
|
|
417
|
+
const std::wstring wide = widen(request.defaultPath);
|
|
418
|
+
const size_t slash = wide.find_last_of(L"\\/");
|
|
419
|
+
const std::wstring directory = slash == std::wstring::npos ? L"" : wide.substr(0, slash);
|
|
420
|
+
const std::wstring name = slash == std::wstring::npos ? wide : wide.substr(slash + 1);
|
|
421
|
+
|
|
422
|
+
if (saving && !name.empty()) {
|
|
423
|
+
dialog->SetFileName(name.c_str());
|
|
424
|
+
}
|
|
425
|
+
const std::wstring folder = saving ? directory : wide;
|
|
426
|
+
if (!folder.empty()) {
|
|
427
|
+
IShellItem *item = nullptr;
|
|
428
|
+
if (SUCCEEDED(SHCreateItemFromParsingName(folder.c_str(), nullptr, IID_IShellItem,
|
|
429
|
+
(void **)&item)) &&
|
|
430
|
+
item != nullptr) {
|
|
431
|
+
dialog->SetFolder(item);
|
|
432
|
+
item->Release();
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
const HRESULT shown = dialog->Show(GetActiveWindow());
|
|
438
|
+
if (FAILED(shown)) {
|
|
439
|
+
// HRESULT_FROM_WIN32(ERROR_CANCELLED) is Cancel, and every other failure
|
|
440
|
+
// is reported the same way on purpose: an app can do nothing different
|
|
441
|
+
// with "the dialog would not open" than with "the person said no".
|
|
442
|
+
dialog->Release();
|
|
443
|
+
onDone(true, {});
|
|
444
|
+
return;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
std::vector<std::string> paths;
|
|
448
|
+
IFileOpenDialog *openDialog = nullptr;
|
|
449
|
+
if (!saving && SUCCEEDED(dialog->QueryInterface(IID_IFileOpenDialog, (void **)&openDialog)) &&
|
|
450
|
+
openDialog != nullptr) {
|
|
451
|
+
IShellItemArray *items = nullptr;
|
|
452
|
+
if (SUCCEEDED(openDialog->GetResults(&items)) && items != nullptr) {
|
|
453
|
+
DWORD count = 0;
|
|
454
|
+
items->GetCount(&count);
|
|
455
|
+
for (DWORD i = 0; i < count; i++) {
|
|
456
|
+
IShellItem *item = nullptr;
|
|
457
|
+
if (SUCCEEDED(items->GetItemAt(i, &item)) && item != nullptr) {
|
|
458
|
+
std::string path = pathOf(item);
|
|
459
|
+
if (!path.empty()) {
|
|
460
|
+
paths.push_back(std::move(path));
|
|
461
|
+
}
|
|
462
|
+
item->Release();
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
items->Release();
|
|
466
|
+
}
|
|
467
|
+
openDialog->Release();
|
|
468
|
+
} else {
|
|
469
|
+
IShellItem *item = nullptr;
|
|
470
|
+
if (SUCCEEDED(dialog->GetResult(&item)) && item != nullptr) {
|
|
471
|
+
std::string path = pathOf(item);
|
|
472
|
+
if (!path.empty()) {
|
|
473
|
+
paths.push_back(std::move(path));
|
|
474
|
+
}
|
|
475
|
+
item->Release();
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
dialog->Release();
|
|
480
|
+
onDone(paths.empty(), paths);
|
|
481
|
+
});
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
// --- menus --------------------------------------------------------------------
|
|
485
|
+
|
|
486
|
+
namespace {
|
|
487
|
+
|
|
488
|
+
// "Ctrl+R" as a menu label suffix. Windows draws an accelerator by convention
|
|
489
|
+
// rather than by property: the text after a tab in an item's string is
|
|
490
|
+
// right-aligned, and that is all an HMENU knows about shortcuts. Nothing here
|
|
491
|
+
// binds the key -- the host does.
|
|
492
|
+
std::wstring withShortcut(const std::string &label, const std::string &shortcut) {
|
|
493
|
+
std::wstring text = widen(label);
|
|
494
|
+
if (!shortcut.empty()) {
|
|
495
|
+
text += L"\t";
|
|
496
|
+
text += widen(shortcut);
|
|
497
|
+
}
|
|
498
|
+
return text;
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
} // namespace
|
|
502
|
+
|
|
503
|
+
void showMenu(const MenuRequest &request, MenuCallback onChosen) {
|
|
504
|
+
// Onto the UI thread. TrackPopupMenuEx runs its own message loop and must be
|
|
505
|
+
// called on the thread that owns the window -- and running it on the
|
|
506
|
+
// JavaScript thread would deadlock the runtime, which is the same reason
|
|
507
|
+
// showAlert and the GTK and AppKit menus marshal.
|
|
508
|
+
postToUiThread([request, onChosen = std::move(onChosen)] {
|
|
509
|
+
HWND window = GetActiveWindow();
|
|
510
|
+
if (window == nullptr) {
|
|
511
|
+
window = GetForegroundWindow();
|
|
512
|
+
}
|
|
513
|
+
if (window == nullptr) {
|
|
514
|
+
onChosen(-1);
|
|
515
|
+
return;
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
HMENU menu = CreatePopupMenu();
|
|
519
|
+
if (menu == nullptr) {
|
|
520
|
+
onChosen(-1);
|
|
521
|
+
return;
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
for (size_t i = 0; i < request.entries.size(); i++) {
|
|
525
|
+
const MenuEntry &entry = request.entries[i];
|
|
526
|
+
if (entry.isSeparator()) {
|
|
527
|
+
AppendMenuW(menu, MF_SEPARATOR, 0, nullptr);
|
|
528
|
+
continue;
|
|
529
|
+
}
|
|
530
|
+
// The command id is the index plus one: TrackPopupMenu returns zero for
|
|
531
|
+
// "nothing was chosen", so zero cannot also mean the first item.
|
|
532
|
+
const std::wstring text = withShortcut(entry.label, entry.shortcut);
|
|
533
|
+
AppendMenuW(menu,
|
|
534
|
+
MF_STRING | (entry.enabled ? MF_ENABLED : MF_GRAYED),
|
|
535
|
+
static_cast<UINT_PTR>(i + 1),
|
|
536
|
+
text.c_str());
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
POINT at;
|
|
540
|
+
if (request.x >= 0.0 && request.y >= 0.0) {
|
|
541
|
+
// The request is in the window's client coordinates, which is where every
|
|
542
|
+
// other coordinate in this host lives; TrackPopupMenuEx wants screen.
|
|
543
|
+
at.x = static_cast<LONG>(request.x);
|
|
544
|
+
at.y = static_cast<LONG>(request.y);
|
|
545
|
+
ClientToScreen(window, &at);
|
|
546
|
+
} else if (!GetCursorPos(&at)) {
|
|
547
|
+
at.x = 0;
|
|
548
|
+
at.y = 0;
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
// SetForegroundWindow before and the null PostMessage after are both from
|
|
552
|
+
// the documented TrackPopupMenu recipe: without them a menu opened from a
|
|
553
|
+
// window that is not foreground stays up after a click elsewhere, because
|
|
554
|
+
// it never receives the message that would dismiss it.
|
|
555
|
+
SetForegroundWindow(window);
|
|
556
|
+
const int chosen = TrackPopupMenuEx(menu,
|
|
557
|
+
TPM_LEFTALIGN | TPM_TOPALIGN | TPM_RETURNCMD | TPM_NONOTIFY,
|
|
558
|
+
at.x,
|
|
559
|
+
at.y,
|
|
560
|
+
window,
|
|
561
|
+
nullptr);
|
|
562
|
+
PostMessageW(window, WM_NULL, 0, 0);
|
|
563
|
+
DestroyMenu(menu);
|
|
564
|
+
|
|
565
|
+
onChosen(chosen > 0 ? chosen - 1 : -1);
|
|
566
|
+
});
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
// --- alerts ------------------------------------------------------------------
|
|
570
|
+
|
|
571
|
+
void showAlert(const AlertRequest &request, AlertCallback onButton) {
|
|
572
|
+
// MessageBox, which means up to three buttons and no text field. React
|
|
573
|
+
// Native's Alert allows more of both.
|
|
574
|
+
//
|
|
575
|
+
// TaskDialogIndirect would give arbitrary buttons and is the right answer;
|
|
576
|
+
// it needs a comctl32 v6 manifest, which is a host concern rather than a
|
|
577
|
+
// seam concern, so it waits for the host. A prompt needs a custom dialog on
|
|
578
|
+
// every platform -- GTK has the same gap, per docs/BACKLOG.md -- and is not
|
|
579
|
+
// faked here.
|
|
580
|
+
//
|
|
581
|
+
// Onto the UI thread, and back at once. PlatformServices.h says this must
|
|
582
|
+
// not block, and it did: MessageBoxW ran right here, on the
|
|
583
|
+
// JavaScript thread, so while an alert was up the runtime rendered nothing,
|
|
584
|
+
// and a window closed meanwhile never quit -- shutdown joins the JavaScript
|
|
585
|
+
// thread, which was sitting in a dialog nobody would answer. showMenu's
|
|
586
|
+
// comment below even says it marshals "for the same reason showAlert" does.
|
|
587
|
+
// It did not; now it does. The end-to-end suite never saw it because its
|
|
588
|
+
// alerts are answered by BASALT_TEST_DIALOG before this is reached; the
|
|
589
|
+
// cross-host comparison, which does not script dialogs, hung on js/share.js.
|
|
590
|
+
const std::wstring title = widen(request.title);
|
|
591
|
+
const std::wstring message = widen(request.message);
|
|
592
|
+
|
|
593
|
+
UINT type = MB_OK;
|
|
594
|
+
if (request.buttons.size() == 2) {
|
|
595
|
+
type = MB_OKCANCEL;
|
|
596
|
+
} else if (request.buttons.size() >= 3) {
|
|
597
|
+
type = MB_YESNOCANCEL;
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
const size_t buttonCount = request.buttons.size();
|
|
601
|
+
postToUiThread([title, message, type, buttonCount, onButton = std::move(onButton)] {
|
|
602
|
+
// Owned by the app's window rather than by nothing: modal to it, placed
|
|
603
|
+
// over it, and destroyed with it -- so a window that closes while an alert
|
|
604
|
+
// is up takes the alert along instead of leaving it on screen for a
|
|
605
|
+
// process that is trying to exit.
|
|
606
|
+
HWND owner = GetActiveWindow();
|
|
607
|
+
if (owner == nullptr) {
|
|
608
|
+
owner = GetForegroundWindow();
|
|
609
|
+
}
|
|
610
|
+
// Task-modal only when there is no window to own it: then it is the one
|
|
611
|
+
// way to keep the rest of this thread's windows from taking input.
|
|
612
|
+
const UINT modality = owner == nullptr ? MB_TASKMODAL : 0;
|
|
613
|
+
const int pressed = MessageBoxW(owner, message.c_str(), title.c_str(), type | modality);
|
|
614
|
+
// Zero is the dialog going away without an answer, which is what its
|
|
615
|
+
// owner being destroyed does. There is no button to report, and the app
|
|
616
|
+
// asking has already been told its window is closing.
|
|
617
|
+
if (!onButton || pressed == 0) {
|
|
618
|
+
return;
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
// Reported as an index into the buttons the app gave, which is what React
|
|
622
|
+
// Native's callback means, rather than as Windows' IDOK/IDCANCEL.
|
|
623
|
+
int index = 0;
|
|
624
|
+
switch (pressed) {
|
|
625
|
+
case IDOK:
|
|
626
|
+
case IDYES:
|
|
627
|
+
index = 0;
|
|
628
|
+
break;
|
|
629
|
+
case IDNO:
|
|
630
|
+
index = 1;
|
|
631
|
+
break;
|
|
632
|
+
case IDCANCEL:
|
|
633
|
+
index = buttonCount == 0 ? 0 : static_cast<int>(buttonCount) - 1;
|
|
634
|
+
break;
|
|
635
|
+
default:
|
|
636
|
+
index = 0;
|
|
637
|
+
break;
|
|
638
|
+
}
|
|
639
|
+
onButton(index, std::string{});
|
|
640
|
+
});
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
} // namespace basalt
|