janela 0.2.0 → 0.3.0
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/README.md +66 -12
- package/bin/janela.mjs +351 -39
- package/package.json +1 -1
- package/runtime/janela.ts +170 -25
- package/shim/wvshim.cc +539 -63
- package/templates/index.html +17 -0
- package/templates/main.ts +32 -0
- package/templates/react/deps.json +4 -0
- package/templates/react/files/index.html +11 -0
- package/templates/react/files/janela.conf.json +10 -0
- package/templates/react/files/src/App.css +3 -0
- package/templates/react/files/src/App.jsx +38 -0
- package/templates/react/files/src/main.jsx +9 -0
- package/templates/react/files/src-host/main.ts +42 -0
- package/templates/react/files/vite.config.js +6 -0
- package/templates/solid/deps.json +4 -0
- package/templates/solid/files/index.html +11 -0
- package/templates/solid/files/janela.conf.json +10 -0
- package/templates/solid/files/src/App.css +3 -0
- package/templates/solid/files/src/App.jsx +35 -0
- package/templates/solid/files/src/main.jsx +4 -0
- package/templates/solid/files/src-host/main.ts +42 -0
- package/templates/solid/files/vite.config.js +6 -0
- package/templates/svelte/deps.json +7 -0
- package/templates/svelte/files/index.html +11 -0
- package/templates/svelte/files/janela.conf.json +10 -0
- package/templates/svelte/files/src/App.svelte +33 -0
- package/templates/svelte/files/src/main.js +4 -0
- package/templates/svelte/files/src-host/main.ts +42 -0
- package/templates/svelte/files/svelte.config.js +3 -0
- package/templates/svelte/files/vite.config.js +6 -0
- package/templates/vue/deps.json +4 -0
- package/templates/vue/files/index.html +11 -0
- package/templates/vue/files/janela.conf.json +10 -0
- package/templates/vue/files/src/App.vue +39 -0
- package/templates/vue/files/src/main.js +4 -0
- package/templates/vue/files/src-host/main.ts +42 -0
- package/templates/vue/files/vite.config.js +6 -0
package/shim/wvshim.cc
CHANGED
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
#include <atomic>
|
|
19
19
|
#include <chrono>
|
|
20
20
|
#include <cstdint>
|
|
21
|
+
#include <cstdio>
|
|
21
22
|
#include <cstring>
|
|
22
23
|
#include <filesystem>
|
|
23
24
|
#include <fstream>
|
|
@@ -27,6 +28,15 @@
|
|
|
27
28
|
#include <thread>
|
|
28
29
|
#include <vector>
|
|
29
30
|
|
|
31
|
+
// Native dialogs need each platform's own toolkit. webview.h has already
|
|
32
|
+
// pulled in the Cocoa bindings (via its own backend headers) and windows.h on
|
|
33
|
+
// Win32; these add only what it does not use itself.
|
|
34
|
+
#if defined(_WIN32)
|
|
35
|
+
#include <commdlg.h>
|
|
36
|
+
#elif !defined(__APPLE__)
|
|
37
|
+
#include <gtk/gtk.h>
|
|
38
|
+
#endif
|
|
39
|
+
|
|
30
40
|
namespace {
|
|
31
41
|
|
|
32
42
|
struct Bind {
|
|
@@ -79,54 +89,65 @@ std::string to_str(const uint8_t *p, size_t n) {
|
|
|
79
89
|
return std::string(reinterpret_cast<const char *>(p), n);
|
|
80
90
|
}
|
|
81
91
|
|
|
82
|
-
// ----
|
|
92
|
+
// ---- jobs ------------------------------------------------------------------
|
|
93
|
+
//
|
|
94
|
+
// A job is any unit of work whose answer cannot be produced during the FFI
|
|
95
|
+
// call that asks for it. TS starts one, gets an id back immediately, and polls
|
|
96
|
+
// wv_job_status() from its tick loop until the job is terminal.
|
|
83
97
|
//
|
|
84
|
-
//
|
|
85
|
-
//
|
|
86
|
-
//
|
|
87
|
-
//
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
98
|
+
// Two kinds use this pool, for opposite reasons:
|
|
99
|
+
// * file I/O — the blocking syscall must happen off the UI thread, so a
|
|
100
|
+
// worker thread does it. A worker touches only its own job and NEVER calls
|
|
101
|
+
// into TS: scriptc's runtime is not thread-safe, so the result is drained
|
|
102
|
+
// later, on the UI thread.
|
|
103
|
+
// * native dialogs — the modal must run ON the UI thread, but not while TS
|
|
104
|
+
// is on the stack (runModal/gtk_dialog_run spin a nested event loop, which
|
|
105
|
+
// would re-enter the tick handler underneath the invoke handler that asked
|
|
106
|
+
// for the dialog). So the job is posted with webview_dispatch and runs at
|
|
107
|
+
// the top of a later turn, with no TS frame beneath it.
|
|
108
|
+
|
|
109
|
+
const int32_t JOB_PENDING = 0;
|
|
110
|
+
const int32_t JOB_OK = 1;
|
|
111
|
+
const int32_t JOB_ERROR = 2;
|
|
112
|
+
|
|
113
|
+
struct Job {
|
|
114
|
+
// Written by the producer (worker thread, or the dispatched dialog) before
|
|
115
|
+
// `status` flips; read by the UI thread only after it observes a terminal
|
|
116
|
+
// status. The release/acquire pair on `status` is what publishes `data`, so
|
|
117
|
+
// no lock is needed for the payload itself.
|
|
118
|
+
std::atomic<int32_t> status{JOB_PENDING};
|
|
119
|
+
std::string data; // payload on success, the error message on failure
|
|
120
|
+
std::thread worker; // unused by dialog jobs, which run on the UI thread
|
|
100
121
|
bool used = false;
|
|
101
122
|
};
|
|
102
123
|
|
|
103
124
|
// Jobs are addressed by index and held behind unique_ptr so the vector may
|
|
104
125
|
// grow without invalidating a worker's pointer to its own job.
|
|
105
|
-
std::mutex
|
|
106
|
-
std::vector<std::unique_ptr<
|
|
126
|
+
std::mutex g_jobs_mu;
|
|
127
|
+
std::vector<std::unique_ptr<Job>> g_jobs;
|
|
107
128
|
|
|
108
|
-
|
|
109
|
-
std::lock_guard<std::mutex> lock(
|
|
110
|
-
if (id < 0 || static_cast<size_t>(id) >=
|
|
111
|
-
|
|
129
|
+
Job *job_at(int32_t id) {
|
|
130
|
+
std::lock_guard<std::mutex> lock(g_jobs_mu);
|
|
131
|
+
if (id < 0 || static_cast<size_t>(id) >= g_jobs.size()) return nullptr;
|
|
132
|
+
Job *j = g_jobs[id].get();
|
|
112
133
|
return j->used ? j : nullptr;
|
|
113
134
|
}
|
|
114
135
|
|
|
115
136
|
// Reuses a finished slot when one is free, so a long-running app that reads
|
|
116
137
|
// many files does not grow the table without bound.
|
|
117
|
-
int32_t
|
|
118
|
-
std::lock_guard<std::mutex> lock(
|
|
119
|
-
for (size_t i = 0; i <
|
|
120
|
-
if (
|
|
121
|
-
if (
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
138
|
+
int32_t new_job() {
|
|
139
|
+
std::lock_guard<std::mutex> lock(g_jobs_mu);
|
|
140
|
+
for (size_t i = 0; i < g_jobs.size(); i++) {
|
|
141
|
+
if (g_jobs[i]->used) continue;
|
|
142
|
+
if (g_jobs[i]->worker.joinable()) g_jobs[i]->worker.join();
|
|
143
|
+
g_jobs[i]->status.store(JOB_PENDING);
|
|
144
|
+
g_jobs[i]->data.clear();
|
|
145
|
+
g_jobs[i]->used = true;
|
|
125
146
|
return static_cast<int32_t>(i);
|
|
126
147
|
}
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
return static_cast<int32_t>(
|
|
148
|
+
g_jobs.push_back(std::unique_ptr<Job>(new Job()));
|
|
149
|
+
g_jobs.back()->used = true;
|
|
150
|
+
return static_cast<int32_t>(g_jobs.size() - 1);
|
|
130
151
|
}
|
|
131
152
|
|
|
132
153
|
// Node-shaped messages: janela apps already surface node:fs errors from
|
|
@@ -145,53 +166,421 @@ std::string fs_error_message(const std::string &path, const char *op) {
|
|
|
145
166
|
return "EIO: failed to " + std::string(op) + " '" + path + "'";
|
|
146
167
|
}
|
|
147
168
|
|
|
148
|
-
void
|
|
169
|
+
void job_finish(Job *j, int32_t status, std::string payload) {
|
|
149
170
|
j->data = std::move(payload);
|
|
150
171
|
j->status.store(status, std::memory_order_release);
|
|
151
172
|
}
|
|
152
173
|
|
|
153
|
-
void fs_read_worker(
|
|
174
|
+
void fs_read_worker(Job *j, std::string path) {
|
|
154
175
|
std::error_code ec;
|
|
155
176
|
if (std::filesystem::is_directory(path, ec)) {
|
|
156
|
-
|
|
177
|
+
job_finish(j, JOB_ERROR, fs_error_message(path, "read"));
|
|
157
178
|
return;
|
|
158
179
|
}
|
|
159
180
|
std::ifstream in(path, std::ios::binary);
|
|
160
181
|
if (!in) {
|
|
161
|
-
|
|
182
|
+
job_finish(j, JOB_ERROR, fs_error_message(path, "open"));
|
|
162
183
|
return;
|
|
163
184
|
}
|
|
164
185
|
std::string buf((std::istreambuf_iterator<char>(in)),
|
|
165
186
|
std::istreambuf_iterator<char>());
|
|
166
187
|
if (in.bad()) {
|
|
167
|
-
|
|
188
|
+
job_finish(j, JOB_ERROR, fs_error_message(path, "read"));
|
|
168
189
|
return;
|
|
169
190
|
}
|
|
170
|
-
|
|
191
|
+
job_finish(j, JOB_OK, std::move(buf));
|
|
171
192
|
}
|
|
172
193
|
|
|
173
|
-
void fs_write_worker(
|
|
194
|
+
void fs_write_worker(Job *j, std::string path, std::string data) {
|
|
174
195
|
std::ofstream out(path, std::ios::binary | std::ios::trunc);
|
|
175
196
|
if (!out) {
|
|
176
|
-
|
|
197
|
+
job_finish(j, JOB_ERROR, fs_error_message(path, "open"));
|
|
177
198
|
return;
|
|
178
199
|
}
|
|
179
200
|
out.write(data.data(), static_cast<std::streamsize>(data.size()));
|
|
180
201
|
out.flush();
|
|
181
202
|
if (!out) {
|
|
182
|
-
|
|
203
|
+
job_finish(j, JOB_ERROR, fs_error_message(path, "write"));
|
|
183
204
|
return;
|
|
184
205
|
}
|
|
185
|
-
|
|
206
|
+
job_finish(j, JOB_OK, std::string());
|
|
186
207
|
}
|
|
187
208
|
|
|
188
209
|
// Join every worker. Called at shutdown so no thread outlives the process's
|
|
189
210
|
// orderly exit (and so nothing writes into a job after main returns).
|
|
190
|
-
void
|
|
191
|
-
std::lock_guard<std::mutex> lock(
|
|
192
|
-
for (size_t i = 0; i <
|
|
193
|
-
if (
|
|
211
|
+
void jobs_join_all() {
|
|
212
|
+
std::lock_guard<std::mutex> lock(g_jobs_mu);
|
|
213
|
+
for (size_t i = 0; i < g_jobs.size(); i++) {
|
|
214
|
+
if (g_jobs[i]->worker.joinable()) g_jobs[i]->worker.join();
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// ---- native file dialogs ----------------------------------------------------
|
|
219
|
+
//
|
|
220
|
+
// Options arrive as plain FFI params rather than JSON so the shim needs no
|
|
221
|
+
// parser. `filters` is "Name|ext,ext|Name|ext" — the separators cannot appear
|
|
222
|
+
// in an extension, and a filter name containing one is the caller's problem.
|
|
223
|
+
// The answer is a JSON array of paths, or the literal `null` for a cancel.
|
|
224
|
+
|
|
225
|
+
const int32_t DLG_OPEN = 0;
|
|
226
|
+
const int32_t DLG_SAVE = 1;
|
|
227
|
+
const int32_t DLG_MULTIPLE = 1; // flags bit 0
|
|
228
|
+
const int32_t DLG_DIRECTORY = 2; // flags bit 1
|
|
229
|
+
|
|
230
|
+
struct DialogRequest {
|
|
231
|
+
int32_t app = -1;
|
|
232
|
+
int32_t job = -1;
|
|
233
|
+
int32_t kind = DLG_OPEN;
|
|
234
|
+
int32_t flags = 0;
|
|
235
|
+
std::string title;
|
|
236
|
+
std::string default_path;
|
|
237
|
+
std::string default_name;
|
|
238
|
+
std::string filters;
|
|
239
|
+
};
|
|
240
|
+
|
|
241
|
+
std::vector<std::string> split_on(const std::string &s, char sep) {
|
|
242
|
+
std::vector<std::string> out;
|
|
243
|
+
if (s.empty()) return out;
|
|
244
|
+
std::string cur;
|
|
245
|
+
for (size_t i = 0; i < s.size(); i++) {
|
|
246
|
+
if (s[i] == sep) {
|
|
247
|
+
out.push_back(cur);
|
|
248
|
+
cur.clear();
|
|
249
|
+
} else {
|
|
250
|
+
cur.push_back(s[i]);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
out.push_back(cur);
|
|
254
|
+
return out;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
struct Filter {
|
|
258
|
+
std::string name;
|
|
259
|
+
std::vector<std::string> extensions;
|
|
260
|
+
};
|
|
261
|
+
|
|
262
|
+
std::vector<Filter> parse_filters(const std::string &spec) {
|
|
263
|
+
std::vector<Filter> out;
|
|
264
|
+
std::vector<std::string> parts = split_on(spec, '|');
|
|
265
|
+
for (size_t i = 0; i + 1 < parts.size(); i += 2) {
|
|
266
|
+
Filter f;
|
|
267
|
+
f.name = parts[i];
|
|
268
|
+
f.extensions = split_on(parts[i + 1], ',');
|
|
269
|
+
if (!f.extensions.empty()) out.push_back(f);
|
|
270
|
+
}
|
|
271
|
+
return out;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// Paths are UTF-8 and JSON strings are UTF-8, so only the structural
|
|
275
|
+
// characters and C0 controls need escaping.
|
|
276
|
+
std::string json_escape(const std::string &s) {
|
|
277
|
+
std::string out;
|
|
278
|
+
out.reserve(s.size() + 2);
|
|
279
|
+
out.push_back('"');
|
|
280
|
+
for (size_t i = 0; i < s.size(); i++) {
|
|
281
|
+
unsigned char c = static_cast<unsigned char>(s[i]);
|
|
282
|
+
switch (c) {
|
|
283
|
+
case '"': out += "\\\""; break;
|
|
284
|
+
case '\\': out += "\\\\"; break;
|
|
285
|
+
case '\n': out += "\\n"; break;
|
|
286
|
+
case '\r': out += "\\r"; break;
|
|
287
|
+
case '\t': out += "\\t"; break;
|
|
288
|
+
default:
|
|
289
|
+
if (c < 0x20) {
|
|
290
|
+
char buf[7];
|
|
291
|
+
std::snprintf(buf, sizeof(buf), "\\u%04x", c);
|
|
292
|
+
out += buf;
|
|
293
|
+
} else {
|
|
294
|
+
out.push_back(static_cast<char>(c));
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
out.push_back('"');
|
|
299
|
+
return out;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
std::string json_array(const std::vector<std::string> &items) {
|
|
303
|
+
std::string out = "[";
|
|
304
|
+
for (size_t i = 0; i < items.size(); i++) {
|
|
305
|
+
if (i) out.push_back(',');
|
|
306
|
+
out += json_escape(items[i]);
|
|
307
|
+
}
|
|
308
|
+
out.push_back(']');
|
|
309
|
+
return out;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// Fills `out` and returns true when the user confirmed; returns false for a
|
|
313
|
+
// cancel. `error` is set only for a platform-level refusal.
|
|
314
|
+
bool run_file_dialog(const DialogRequest &req, std::vector<std::string> &out,
|
|
315
|
+
std::string &error);
|
|
316
|
+
|
|
317
|
+
#if defined(__APPLE__)
|
|
318
|
+
|
|
319
|
+
bool run_file_dialog(const DialogRequest &req, std::vector<std::string> &out,
|
|
320
|
+
std::string &error) {
|
|
321
|
+
(void)error;
|
|
322
|
+
using namespace webview::detail;
|
|
323
|
+
objc::autoreleasepool arp;
|
|
324
|
+
|
|
325
|
+
bool save = req.kind == DLG_SAVE;
|
|
326
|
+
id panel = save ? objc::msg_send<id>(objc::get_class("NSSavePanel"),
|
|
327
|
+
objc::selector("savePanel"))
|
|
328
|
+
: cocoa::NSOpenPanel_openPanel();
|
|
329
|
+
if (!panel) return false;
|
|
330
|
+
|
|
331
|
+
if (!req.title.empty()) {
|
|
332
|
+
objc::msg_send<void>(panel, objc::selector("setTitle:"),
|
|
333
|
+
cocoa::NSString_stringWithUTF8String(req.title));
|
|
334
|
+
}
|
|
335
|
+
if (!req.default_path.empty()) {
|
|
336
|
+
id url = objc::msg_send<id>(
|
|
337
|
+
objc::get_class("NSURL"), objc::selector("fileURLWithPath:"),
|
|
338
|
+
cocoa::NSString_stringWithUTF8String(req.default_path));
|
|
339
|
+
objc::msg_send<void>(panel, objc::selector("setDirectoryURL:"), url);
|
|
340
|
+
}
|
|
341
|
+
if (save && !req.default_name.empty()) {
|
|
342
|
+
objc::msg_send<void>(panel, objc::selector("setNameFieldStringValue:"),
|
|
343
|
+
cocoa::NSString_stringWithUTF8String(req.default_name));
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
std::vector<Filter> filters = parse_filters(req.filters);
|
|
347
|
+
if (!filters.empty()) {
|
|
348
|
+
// setAllowedFileTypes: is deprecated in favour of UTType on macOS 12+, but
|
|
349
|
+
// still honoured, and it takes plain extension strings — the UTType path
|
|
350
|
+
// would need a type lookup per extension for no gain here.
|
|
351
|
+
id types = objc::msg_send<id>(objc::get_class("NSMutableArray"),
|
|
352
|
+
objc::selector("array"));
|
|
353
|
+
for (size_t i = 0; i < filters.size(); i++) {
|
|
354
|
+
for (size_t j = 0; j < filters[i].extensions.size(); j++) {
|
|
355
|
+
const std::string &ext = filters[i].extensions[j];
|
|
356
|
+
if (ext.empty() || ext == "*") continue;
|
|
357
|
+
objc::msg_send<void>(types, objc::selector("addObject:"),
|
|
358
|
+
cocoa::NSString_stringWithUTF8String(ext));
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
if (objc::msg_send<NSUInteger>(types, objc::selector("count")) > 0) {
|
|
362
|
+
objc::msg_send<void>(panel, objc::selector("setAllowedFileTypes:"), types);
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
if (!save) {
|
|
367
|
+
bool want_dirs = (req.flags & DLG_DIRECTORY) != 0;
|
|
368
|
+
cocoa::NSOpenPanel_set_canChooseFiles(panel, !want_dirs);
|
|
369
|
+
cocoa::NSOpenPanel_set_canChooseDirectories(panel, want_dirs);
|
|
370
|
+
cocoa::NSOpenPanel_set_allowsMultipleSelection(
|
|
371
|
+
panel, (req.flags & DLG_MULTIPLE) != 0);
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
if (cocoa::NSSavePanel_runModal(panel) != cocoa::NSModalResponseOK) {
|
|
375
|
+
return false;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
auto path_of = [](id url) -> std::string {
|
|
379
|
+
id path = objc::msg_send<id>(url, objc::selector("path"));
|
|
380
|
+
const char *utf8 = cocoa::NSString_get_UTF8String(path);
|
|
381
|
+
return utf8 ? std::string(utf8) : std::string();
|
|
382
|
+
};
|
|
383
|
+
|
|
384
|
+
if (save) {
|
|
385
|
+
id url = objc::msg_send<id>(panel, objc::selector("URL"));
|
|
386
|
+
if (!url) return false;
|
|
387
|
+
out.push_back(path_of(url));
|
|
388
|
+
return true;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
id urls = cocoa::NSOpenPanel_get_URLs(panel);
|
|
392
|
+
NSUInteger n = objc::msg_send<NSUInteger>(urls, objc::selector("count"));
|
|
393
|
+
for (NSUInteger i = 0; i < n; i++) {
|
|
394
|
+
id url = objc::msg_send<id>(urls, objc::selector("objectAtIndex:"), i);
|
|
395
|
+
out.push_back(path_of(url));
|
|
396
|
+
}
|
|
397
|
+
return !out.empty();
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
#elif defined(_WIN32)
|
|
401
|
+
|
|
402
|
+
std::string from_wide(const wchar_t *w, int wlen) {
|
|
403
|
+
if (!w || wlen == 0) return std::string();
|
|
404
|
+
int n = WideCharToMultiByte(CP_UTF8, 0, w, wlen, nullptr, 0, nullptr, nullptr);
|
|
405
|
+
if (n <= 0) return std::string();
|
|
406
|
+
std::string out(static_cast<size_t>(n), '\0');
|
|
407
|
+
WideCharToMultiByte(CP_UTF8, 0, w, wlen, &out[0], n, nullptr, nullptr);
|
|
408
|
+
return out;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
std::wstring to_wide(const std::string &s) {
|
|
412
|
+
if (s.empty()) return std::wstring();
|
|
413
|
+
int n = MultiByteToWideChar(CP_UTF8, 0, s.data(),
|
|
414
|
+
static_cast<int>(s.size()), nullptr, 0);
|
|
415
|
+
if (n <= 0) return std::wstring();
|
|
416
|
+
std::wstring out(static_cast<size_t>(n), L'\0');
|
|
417
|
+
MultiByteToWideChar(CP_UTF8, 0, s.data(), static_cast<int>(s.size()), &out[0],
|
|
418
|
+
n);
|
|
419
|
+
return out;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
bool run_file_dialog(const DialogRequest &req, std::vector<std::string> &out,
|
|
423
|
+
std::string &error) {
|
|
424
|
+
if (req.flags & DLG_DIRECTORY) {
|
|
425
|
+
error = "ENOTSUP: directory selection is not implemented on Windows";
|
|
426
|
+
return false;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
// GetOpenFileNameW/GetSaveFileNameW render the modern common item dialog on
|
|
430
|
+
// Vista and later as long as no hook is installed, so this buys the current
|
|
431
|
+
// look without the COM ceremony of IFileDialog.
|
|
432
|
+
std::vector<Filter> filters = parse_filters(req.filters);
|
|
433
|
+
std::wstring filter_buf;
|
|
434
|
+
for (size_t i = 0; i < filters.size(); i++) {
|
|
435
|
+
std::string patterns;
|
|
436
|
+
for (size_t j = 0; j < filters[i].extensions.size(); j++) {
|
|
437
|
+
if (j) patterns += ";";
|
|
438
|
+
patterns += "*." + filters[i].extensions[j];
|
|
439
|
+
}
|
|
440
|
+
filter_buf += to_wide(filters[i].name + " (" + patterns + ")");
|
|
441
|
+
filter_buf.push_back(L'\0');
|
|
442
|
+
filter_buf += to_wide(patterns);
|
|
443
|
+
filter_buf.push_back(L'\0');
|
|
444
|
+
}
|
|
445
|
+
if (!filter_buf.empty()) filter_buf.push_back(L'\0');
|
|
446
|
+
|
|
447
|
+
// Multi-select returns "dir\0name\0name\0\0", so the buffer must hold more
|
|
448
|
+
// than one MAX_PATH.
|
|
449
|
+
std::vector<wchar_t> file(32768, L'\0');
|
|
450
|
+
std::wstring initial_name = to_wide(req.default_name);
|
|
451
|
+
if (!initial_name.empty() && initial_name.size() < file.size() - 1) {
|
|
452
|
+
std::memcpy(file.data(), initial_name.c_str(),
|
|
453
|
+
(initial_name.size() + 1) * sizeof(wchar_t));
|
|
454
|
+
}
|
|
455
|
+
std::wstring initial_dir = to_wide(req.default_path);
|
|
456
|
+
std::wstring title = to_wide(req.title);
|
|
457
|
+
|
|
458
|
+
OPENFILENAMEW ofn;
|
|
459
|
+
std::memset(&ofn, 0, sizeof(ofn));
|
|
460
|
+
ofn.lStructSize = sizeof(ofn);
|
|
461
|
+
ofn.hwndOwner = nullptr;
|
|
462
|
+
ofn.lpstrFilter = filter_buf.empty() ? nullptr : filter_buf.c_str();
|
|
463
|
+
ofn.lpstrFile = file.data();
|
|
464
|
+
ofn.nMaxFile = static_cast<DWORD>(file.size());
|
|
465
|
+
ofn.lpstrInitialDir = initial_dir.empty() ? nullptr : initial_dir.c_str();
|
|
466
|
+
ofn.lpstrTitle = title.empty() ? nullptr : title.c_str();
|
|
467
|
+
ofn.Flags = OFN_NOCHANGEDIR | OFN_EXPLORER;
|
|
468
|
+
|
|
469
|
+
if (req.kind == DLG_SAVE) {
|
|
470
|
+
ofn.Flags |= OFN_OVERWRITEPROMPT;
|
|
471
|
+
if (!GetSaveFileNameW(&ofn)) return false;
|
|
472
|
+
out.push_back(from_wide(ofn.lpstrFile, -1));
|
|
473
|
+
if (!out.back().empty()) out.back().pop_back(); // trailing NUL from -1
|
|
474
|
+
return true;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
ofn.Flags |= OFN_FILEMUSTEXIST | OFN_PATHMUSTEXIST;
|
|
478
|
+
if (req.flags & DLG_MULTIPLE) ofn.Flags |= OFN_ALLOWMULTISELECT;
|
|
479
|
+
if (!GetOpenFileNameW(&ofn)) return false;
|
|
480
|
+
|
|
481
|
+
// Single selection is one NUL-terminated path; multiple is a directory
|
|
482
|
+
// followed by bare names, all NUL-separated, ending in a double NUL.
|
|
483
|
+
const wchar_t *p = ofn.lpstrFile;
|
|
484
|
+
std::string first = from_wide(p, static_cast<int>(wcslen(p)));
|
|
485
|
+
p += wcslen(p) + 1;
|
|
486
|
+
if (*p == L'\0') {
|
|
487
|
+
out.push_back(first);
|
|
488
|
+
return true;
|
|
489
|
+
}
|
|
490
|
+
while (*p) {
|
|
491
|
+
std::string name = from_wide(p, static_cast<int>(wcslen(p)));
|
|
492
|
+
out.push_back(first + "\\" + name);
|
|
493
|
+
p += wcslen(p) + 1;
|
|
194
494
|
}
|
|
495
|
+
return !out.empty();
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
#else // GTK
|
|
499
|
+
|
|
500
|
+
bool run_file_dialog(const DialogRequest &req, std::vector<std::string> &out,
|
|
501
|
+
std::string &error) {
|
|
502
|
+
(void)error;
|
|
503
|
+
bool save = req.kind == DLG_SAVE;
|
|
504
|
+
bool want_dirs = (req.flags & DLG_DIRECTORY) != 0;
|
|
505
|
+
GtkFileChooserAction action =
|
|
506
|
+
save ? GTK_FILE_CHOOSER_ACTION_SAVE
|
|
507
|
+
: (want_dirs ? GTK_FILE_CHOOSER_ACTION_SELECT_FOLDER
|
|
508
|
+
: GTK_FILE_CHOOSER_ACTION_OPEN);
|
|
509
|
+
|
|
510
|
+
GtkWidget *dialog = gtk_file_chooser_dialog_new(
|
|
511
|
+
req.title.empty() ? (save ? "Save" : "Open") : req.title.c_str(), nullptr,
|
|
512
|
+
action, "_Cancel", GTK_RESPONSE_CANCEL,
|
|
513
|
+
save ? "_Save" : "_Open", GTK_RESPONSE_ACCEPT, nullptr);
|
|
514
|
+
if (!dialog) return false;
|
|
515
|
+
|
|
516
|
+
GtkFileChooser *chooser = GTK_FILE_CHOOSER(dialog);
|
|
517
|
+
if (!save && (req.flags & DLG_MULTIPLE)) {
|
|
518
|
+
gtk_file_chooser_set_select_multiple(chooser, TRUE);
|
|
519
|
+
}
|
|
520
|
+
if (save) {
|
|
521
|
+
gtk_file_chooser_set_do_overwrite_confirmation(chooser, TRUE);
|
|
522
|
+
if (!req.default_name.empty()) {
|
|
523
|
+
gtk_file_chooser_set_current_name(chooser, req.default_name.c_str());
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
if (!req.default_path.empty()) {
|
|
527
|
+
gtk_file_chooser_set_current_folder(chooser, req.default_path.c_str());
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
std::vector<Filter> filters = parse_filters(req.filters);
|
|
531
|
+
for (size_t i = 0; i < filters.size(); i++) {
|
|
532
|
+
GtkFileFilter *f = gtk_file_filter_new();
|
|
533
|
+
gtk_file_filter_set_name(f, filters[i].name.c_str());
|
|
534
|
+
for (size_t j = 0; j < filters[i].extensions.size(); j++) {
|
|
535
|
+
std::string pattern = "*." + filters[i].extensions[j];
|
|
536
|
+
gtk_file_filter_add_pattern(f, pattern.c_str());
|
|
537
|
+
}
|
|
538
|
+
gtk_file_chooser_add_filter(chooser, f);
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
bool ok = gtk_dialog_run(GTK_DIALOG(dialog)) == GTK_RESPONSE_ACCEPT;
|
|
542
|
+
if (ok) {
|
|
543
|
+
if (!save && (req.flags & DLG_MULTIPLE)) {
|
|
544
|
+
GSList *names = gtk_file_chooser_get_filenames(chooser);
|
|
545
|
+
for (GSList *it = names; it; it = it->next) {
|
|
546
|
+
char *path = static_cast<char *>(it->data);
|
|
547
|
+
if (path) {
|
|
548
|
+
out.push_back(path);
|
|
549
|
+
g_free(path);
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
g_slist_free(names);
|
|
553
|
+
} else {
|
|
554
|
+
char *path = gtk_file_chooser_get_filename(chooser);
|
|
555
|
+
if (path) {
|
|
556
|
+
out.push_back(path);
|
|
557
|
+
g_free(path);
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
gtk_widget_destroy(dialog);
|
|
562
|
+
// Let the destroy actually happen before the modal's caller resumes.
|
|
563
|
+
while (gtk_events_pending()) gtk_main_iteration();
|
|
564
|
+
return ok && !out.empty();
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
#endif
|
|
568
|
+
|
|
569
|
+
// Runs on the UI thread with no TS frame beneath it — see the note on the job
|
|
570
|
+
// pool above for why that matters.
|
|
571
|
+
void dialog_on_ui_thread(webview_t, void *arg) {
|
|
572
|
+
std::unique_ptr<DialogRequest> req(static_cast<DialogRequest *>(arg));
|
|
573
|
+
Job *j = job_at(req->job);
|
|
574
|
+
if (!j) return;
|
|
575
|
+
std::vector<std::string> picked;
|
|
576
|
+
std::string error;
|
|
577
|
+
bool ok = run_file_dialog(*req, picked, error);
|
|
578
|
+
if (!error.empty()) {
|
|
579
|
+
job_finish(j, JOB_ERROR, error);
|
|
580
|
+
return;
|
|
581
|
+
}
|
|
582
|
+
// A cancel is a successful call that answers `null`, not a failure.
|
|
583
|
+
job_finish(j, JOB_OK, ok ? json_array(picked) : "null");
|
|
195
584
|
}
|
|
196
585
|
|
|
197
586
|
// The single C trampoline registered with webview_bind. `arg` is the app
|
|
@@ -400,14 +789,14 @@ int32_t wv_tick_stop(int32_t h) {
|
|
|
400
789
|
// ---- async file I/O ---------------------------------------------------------
|
|
401
790
|
//
|
|
402
791
|
// wv_fs_read/wv_fs_write start a worker thread and return immediately with a
|
|
403
|
-
// job id. TS polls
|
|
792
|
+
// job id. TS polls wv_job_status() from its tick loop and drains the payload
|
|
404
793
|
// with wv_fs_byte() once the job is terminal. On failure the payload is the
|
|
405
794
|
// error message, so success and failure share one drain path.
|
|
406
795
|
|
|
407
796
|
int32_t wv_fs_read(int32_t h, const uint8_t *p, size_t n) {
|
|
408
797
|
if (!app_at(h)) return -1;
|
|
409
|
-
int32_t id =
|
|
410
|
-
|
|
798
|
+
int32_t id = new_job();
|
|
799
|
+
Job *j = job_at(id);
|
|
411
800
|
if (!j) return -1;
|
|
412
801
|
j->worker = std::thread(fs_read_worker, j, to_str(p, n));
|
|
413
802
|
return id;
|
|
@@ -416,17 +805,17 @@ int32_t wv_fs_read(int32_t h, const uint8_t *p, size_t n) {
|
|
|
416
805
|
int32_t wv_fs_write(int32_t h, const uint8_t *p, size_t n, const uint8_t *dp,
|
|
417
806
|
size_t dn) {
|
|
418
807
|
if (!app_at(h)) return -1;
|
|
419
|
-
int32_t id =
|
|
420
|
-
|
|
808
|
+
int32_t id = new_job();
|
|
809
|
+
Job *j = job_at(id);
|
|
421
810
|
if (!j) return -1;
|
|
422
811
|
j->worker = std::thread(fs_write_worker, j, to_str(p, n), to_str(dp, dn));
|
|
423
812
|
return id;
|
|
424
813
|
}
|
|
425
814
|
|
|
426
815
|
// 0 = still running, 1 = done, 2 = failed, -1 = no such job.
|
|
427
|
-
int32_t
|
|
816
|
+
int32_t wv_job_status(int32_t h, int32_t id) {
|
|
428
817
|
if (!app_at(h)) return -1;
|
|
429
|
-
|
|
818
|
+
Job *j = job_at(id);
|
|
430
819
|
if (!j) return -1;
|
|
431
820
|
return j->status.load(std::memory_order_acquire);
|
|
432
821
|
}
|
|
@@ -435,24 +824,24 @@ int32_t wv_fs_status(int32_t h, int32_t id) {
|
|
|
435
824
|
// lifetime:"call", so it runs synchronously here — on the UI thread, the only
|
|
436
825
|
// thread allowed to touch the scriptc runtime. The worker is already done by
|
|
437
826
|
// then (the caller has observed a terminal status), so `data` is stable.
|
|
438
|
-
int32_t
|
|
827
|
+
int32_t wv_job_take(int32_t h, int32_t id,
|
|
439
828
|
void (*sink)(const uint8_t *, size_t, void *), void *ctx) {
|
|
440
829
|
if (!app_at(h)) return -1;
|
|
441
|
-
|
|
830
|
+
Job *j = job_at(id);
|
|
442
831
|
if (!j || !sink) return -1;
|
|
443
|
-
if (j->status.load(std::memory_order_acquire) ==
|
|
832
|
+
if (j->status.load(std::memory_order_acquire) == JOB_PENDING) return -1;
|
|
444
833
|
sink(reinterpret_cast<const uint8_t *>(j->data.data()), j->data.size(), ctx);
|
|
445
834
|
return 0;
|
|
446
835
|
}
|
|
447
836
|
|
|
448
837
|
// Release the slot for reuse. Refuses while the worker is still running, so a
|
|
449
838
|
// job's buffer can never be recycled out from under its own thread.
|
|
450
|
-
int32_t
|
|
839
|
+
int32_t wv_job_free(int32_t h, int32_t id) {
|
|
451
840
|
if (!app_at(h)) return -1;
|
|
452
|
-
|
|
841
|
+
Job *j = job_at(id);
|
|
453
842
|
if (!j) return -1;
|
|
454
|
-
if (j->status.load(std::memory_order_acquire) ==
|
|
455
|
-
std::lock_guard<std::mutex> lock(
|
|
843
|
+
if (j->status.load(std::memory_order_acquire) == JOB_PENDING) return -1;
|
|
844
|
+
std::lock_guard<std::mutex> lock(g_jobs_mu);
|
|
456
845
|
if (j->worker.joinable()) j->worker.join();
|
|
457
846
|
j->data.clear();
|
|
458
847
|
j->data.shrink_to_fit();
|
|
@@ -460,6 +849,93 @@ int32_t wv_fs_free(int32_t h, int32_t id) {
|
|
|
460
849
|
return 0;
|
|
461
850
|
}
|
|
462
851
|
|
|
852
|
+
// ---- native dialogs ---------------------------------------------------------
|
|
853
|
+
|
|
854
|
+
// Start a file dialog. Returns a job id immediately; the modal itself runs on
|
|
855
|
+
// a later UI-thread turn, so this never blocks the invoke that asked for it.
|
|
856
|
+
// The finished payload is a JSON array of paths, or `null` for a cancel.
|
|
857
|
+
int32_t wv_dialog(int32_t h, int32_t kind, int32_t flags, const uint8_t *tp,
|
|
858
|
+
size_t tn, const uint8_t *pp, size_t pn, const uint8_t *np,
|
|
859
|
+
size_t nn, const uint8_t *fp, size_t fn) {
|
|
860
|
+
App *a = app_at(h);
|
|
861
|
+
if (!a) return -1;
|
|
862
|
+
int32_t id = new_job();
|
|
863
|
+
Job *j = job_at(id);
|
|
864
|
+
if (!j) return -1;
|
|
865
|
+
|
|
866
|
+
DialogRequest *req = new DialogRequest();
|
|
867
|
+
req->app = h;
|
|
868
|
+
req->job = id;
|
|
869
|
+
req->kind = kind;
|
|
870
|
+
req->flags = flags;
|
|
871
|
+
req->title = to_str(tp, tn);
|
|
872
|
+
req->default_path = to_str(pp, pn);
|
|
873
|
+
req->default_name = to_str(np, nn);
|
|
874
|
+
req->filters = to_str(fp, fn);
|
|
875
|
+
|
|
876
|
+
if (webview_dispatch(a->w, dialog_on_ui_thread, req) != WEBVIEW_ERROR_OK) {
|
|
877
|
+
delete req;
|
|
878
|
+
job_finish(j, JOB_ERROR, "EIO: could not post the dialog to the UI thread");
|
|
879
|
+
}
|
|
880
|
+
return id;
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
// ---- window control ---------------------------------------------------------
|
|
884
|
+
|
|
885
|
+
int32_t wv_set_fullscreen(int32_t h, int32_t on) {
|
|
886
|
+
App *a = app_at(h);
|
|
887
|
+
if (!a) return -1;
|
|
888
|
+
void *win = webview_get_window(a->w);
|
|
889
|
+
if (!win) return -1;
|
|
890
|
+
|
|
891
|
+
#if defined(__APPLE__)
|
|
892
|
+
using namespace webview::detail;
|
|
893
|
+
objc::autoreleasepool arp;
|
|
894
|
+
id window = static_cast<id>(win);
|
|
895
|
+
// NSWindowStyleMaskFullScreen. toggleFullScreen: only toggles, so read the
|
|
896
|
+
// current state first and leave it alone when it already matches.
|
|
897
|
+
const NSUInteger full = 1UL << 14;
|
|
898
|
+
NSUInteger mask = objc::msg_send<NSUInteger>(window, objc::selector("styleMask"));
|
|
899
|
+
bool is_full = (mask & full) != 0;
|
|
900
|
+
if (is_full != (on != 0)) {
|
|
901
|
+
objc::msg_send<void>(window, objc::selector("toggleFullScreen:"), nullptr);
|
|
902
|
+
}
|
|
903
|
+
return 0;
|
|
904
|
+
#elif defined(_WIN32)
|
|
905
|
+
HWND hwnd = static_cast<HWND>(win);
|
|
906
|
+
static WINDOWPLACEMENT saved = {sizeof(saved), 0, 0, {0, 0}, {0, 0}, {0, 0, 0, 0}};
|
|
907
|
+
LONG_PTR style = GetWindowLongPtrW(hwnd, GWL_STYLE);
|
|
908
|
+
if (on) {
|
|
909
|
+
MONITORINFO mi;
|
|
910
|
+
mi.cbSize = sizeof(mi);
|
|
911
|
+
if (!GetWindowPlacement(hwnd, &saved) ||
|
|
912
|
+
!GetMonitorInfoW(MonitorFromWindow(hwnd, MONITOR_DEFAULTTOPRIMARY), &mi)) {
|
|
913
|
+
return -1;
|
|
914
|
+
}
|
|
915
|
+
SetWindowLongPtrW(hwnd, GWL_STYLE, style & ~WS_OVERLAPPEDWINDOW);
|
|
916
|
+
SetWindowPos(hwnd, HWND_TOP, mi.rcMonitor.left, mi.rcMonitor.top,
|
|
917
|
+
mi.rcMonitor.right - mi.rcMonitor.left,
|
|
918
|
+
mi.rcMonitor.bottom - mi.rcMonitor.top,
|
|
919
|
+
SWP_NOOWNERZORDER | SWP_FRAMECHANGED);
|
|
920
|
+
} else {
|
|
921
|
+
SetWindowLongPtrW(hwnd, GWL_STYLE, style | WS_OVERLAPPEDWINDOW);
|
|
922
|
+
SetWindowPlacement(hwnd, &saved);
|
|
923
|
+
SetWindowPos(hwnd, nullptr, 0, 0, 0, 0,
|
|
924
|
+
SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER | SWP_NOOWNERZORDER |
|
|
925
|
+
SWP_FRAMECHANGED);
|
|
926
|
+
}
|
|
927
|
+
return 0;
|
|
928
|
+
#else
|
|
929
|
+
GtkWindow *window = GTK_WINDOW(win);
|
|
930
|
+
if (on) {
|
|
931
|
+
gtk_window_fullscreen(window);
|
|
932
|
+
} else {
|
|
933
|
+
gtk_window_unfullscreen(window);
|
|
934
|
+
}
|
|
935
|
+
return 0;
|
|
936
|
+
#endif
|
|
937
|
+
}
|
|
938
|
+
|
|
463
939
|
// Register the retained handler for page invokes. Valid until the app exits.
|
|
464
940
|
int32_t wv_on_invoke(int32_t h,
|
|
465
941
|
int32_t (*cb)(const uint8_t *, size_t, void *),
|
|
@@ -488,7 +964,7 @@ int32_t wv_run(int32_t h) {
|
|
|
488
964
|
// Nothing may call into TS once run() has returned.
|
|
489
965
|
a->ticking.store(false);
|
|
490
966
|
if (a->ticker.joinable()) a->ticker.join();
|
|
491
|
-
|
|
967
|
+
jobs_join_all(); // nor may an in-flight read outlive the app
|
|
492
968
|
a->on_invoke = nullptr;
|
|
493
969
|
a->on_tick = nullptr;
|
|
494
970
|
return rc;
|