janela 0.17.0 → 0.18.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/bin/janela.mjs +24 -2
- package/bin/lib.mjs +32 -1
- package/package.json +1 -1
- package/runtime/janela.ts +186 -39
- package/runtime/types.ts +0 -50
- package/shim/wvshim.cc +105 -16
package/bin/janela.mjs
CHANGED
|
@@ -22,6 +22,7 @@ import { fileURLToPath } from "node:url";
|
|
|
22
22
|
import {
|
|
23
23
|
ANDROID_ABI, ANDROID_TARGET_SDK, androidConf, ffiManifest, iosConf,
|
|
24
24
|
installCommand, libraryProfile, mimeFor, NAME_RE, packageManager as pmFor,
|
|
25
|
+
shimCacheKey,
|
|
25
26
|
patchPeSubsystem, PeError, rewriteHostSpecifier, suggestName,
|
|
26
27
|
} from "./lib.mjs";
|
|
27
28
|
|
|
@@ -730,10 +731,29 @@ function buildShim(cacheDir) {
|
|
|
730
731
|
// part of an MSVC toolchain, and a lone object needs no archive index.
|
|
731
732
|
const obj = join(cacheDir, win ? "wvshim.obj" : "wvshim.o");
|
|
732
733
|
const lib = win ? obj : join(cacheDir, "libwvshim.a");
|
|
733
|
-
|
|
734
|
+
const inc = `-I${join(KIT, "vendor-webview", "core", "include")}`;
|
|
735
|
+
|
|
736
|
+
// Keyed on the shim's CONTENT, not its mtime.
|
|
737
|
+
//
|
|
738
|
+
// The mtime test this replaces was wrong in the one case that matters:
|
|
739
|
+
// upgrading janela. pnpm's store is content-addressed and hard-links files
|
|
740
|
+
// into node_modules with the STORE's mtime, which is older than a cached
|
|
741
|
+
// object built minutes earlier from the previous version — so the cache
|
|
742
|
+
// looked fresh and a stale archive was reused. The symptom is a link failure
|
|
743
|
+
// naming symbols the new shim exports and the old object does not
|
|
744
|
+
// ("Undefined symbols: _wv_set_menu, _wv_on_menu"), which reads as a broken
|
|
745
|
+
// install rather than a stale cache.
|
|
746
|
+
//
|
|
747
|
+
// The version is in the key too: a shim that happens to be byte-identical
|
|
748
|
+
// across versions still gets recompiled if anything else about the build
|
|
749
|
+
// moved, which is cheap insurance against the next variant of this.
|
|
750
|
+
const key = shimCacheKey(readFileSync(src), VERSION, process.platform, inc);
|
|
751
|
+
const keyFile = join(cacheDir, "wvshim.key");
|
|
752
|
+
if (existsSync(lib) && existsSync(keyFile) && readFileSync(keyFile, "utf8") === key) {
|
|
753
|
+
return lib;
|
|
754
|
+
}
|
|
734
755
|
|
|
735
756
|
console.log("janela: compiling webview shim");
|
|
736
|
-
const inc = `-I${join(KIT, "vendor-webview", "core", "include")}`;
|
|
737
757
|
if (win) {
|
|
738
758
|
console.log(`janela: building for ${winCcOrFail()}`);
|
|
739
759
|
run([
|
|
@@ -743,6 +763,7 @@ function buildShim(cacheDir) {
|
|
|
743
763
|
`-I${join(KIT, "vendor-webview", "compatibility", "mingw", "include")}`,
|
|
744
764
|
"-DWIN32_LEAN_AND_MEAN", "-D_WIN32_WINNT=0x0601",
|
|
745
765
|
]);
|
|
766
|
+
writeFileSync(keyFile, key);
|
|
746
767
|
return lib;
|
|
747
768
|
}
|
|
748
769
|
if (process.platform === "darwin") {
|
|
@@ -752,6 +773,7 @@ function buildShim(cacheDir) {
|
|
|
752
773
|
run(["g++", "-c", src, "-o", obj, "-std=c++17", "-O2", inc, ...cflags]);
|
|
753
774
|
}
|
|
754
775
|
run(["ar", "rcs", lib, obj]);
|
|
776
|
+
writeFileSync(keyFile, key);
|
|
755
777
|
return lib;
|
|
756
778
|
}
|
|
757
779
|
|
package/bin/lib.mjs
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
1
2
|
/**
|
|
2
3
|
* Pure helpers shared by the CLI and its tests.
|
|
3
4
|
*
|
|
@@ -230,11 +231,20 @@ export function ffiManifest(shimLib, { platform = process.platform, macSdkPath =
|
|
|
230
231
|
name: "wvOnMenu", symbol: "wv_on_menu",
|
|
231
232
|
params: [
|
|
232
233
|
"i32",
|
|
233
|
-
|
|
234
|
+
// The tag is the host's own registry index, so the click comes back as
|
|
235
|
+
// a number and TypeScript looks up the item's handler. Nothing is
|
|
236
|
+
// matched by string.
|
|
237
|
+
{ callback: { id: "menu", params: ["i32", { context: "menu" }], returns: "i32", lifetime: "retained" } },
|
|
234
238
|
{ context: "menu" },
|
|
235
239
|
],
|
|
236
240
|
returns: "i32",
|
|
237
241
|
},
|
|
242
|
+
{ name: "wvMenuSetEnabled", symbol: "wv_menu_set_enabled", params: ["i32", "i32", "i32"], returns: "i32" },
|
|
243
|
+
{ name: "wvMenuSetChecked", symbol: "wv_menu_set_checked", params: ["i32", "i32", "i32"], returns: "i32" },
|
|
244
|
+
{
|
|
245
|
+
name: "wvMenuSetLabel", symbol: "wv_menu_set_label",
|
|
246
|
+
params: ["i32", "i32", "string"], returns: "i32",
|
|
247
|
+
},
|
|
238
248
|
];
|
|
239
249
|
|
|
240
250
|
if (platform === "win32") {
|
|
@@ -384,3 +394,24 @@ export function installCommand(pm) {
|
|
|
384
394
|
if (pm === "npm") return ["npm", "install", "--no-fund", "--no-audit"];
|
|
385
395
|
return [pm, "install"];
|
|
386
396
|
}
|
|
397
|
+
|
|
398
|
+
/**
|
|
399
|
+
* The cache key for the compiled webview shim.
|
|
400
|
+
*
|
|
401
|
+
* Keyed on the shim's CONTENT, not its mtime. The mtime test this replaced was
|
|
402
|
+
* wrong in the one case that matters — upgrading janela: pnpm's store is
|
|
403
|
+
* content-addressed and hard-links files into node_modules carrying the
|
|
404
|
+
* STORE's mtime, which is older than an object compiled minutes earlier from
|
|
405
|
+
* the previous version. The cache then looked fresh, the stale archive was
|
|
406
|
+
* reused, and the link failed naming symbols the new shim exports and the old
|
|
407
|
+
* object does not.
|
|
408
|
+
*
|
|
409
|
+
* `version` is in the key so a shim that happens to be byte-identical across
|
|
410
|
+
* releases still recompiles when anything else about the build moved.
|
|
411
|
+
*/
|
|
412
|
+
export function shimCacheKey(source, version, platform, includeFlag) {
|
|
413
|
+
return createHash("sha256")
|
|
414
|
+
.update(source)
|
|
415
|
+
.update(`\u0000${version}\u0000${platform}\u0000${includeFlag}`)
|
|
416
|
+
.digest("hex");
|
|
417
|
+
}
|
package/package.json
CHANGED
package/runtime/janela.ts
CHANGED
|
@@ -43,7 +43,10 @@ declare function wvDialog(
|
|
|
43
43
|
): number;
|
|
44
44
|
declare function wvSetFullscreen(h: number, on: number): number;
|
|
45
45
|
declare function wvSetMenu(h: number, spec: string): number;
|
|
46
|
-
declare function wvOnMenu(h: number, cb: (
|
|
46
|
+
declare function wvOnMenu(h: number, cb: (tag: number) => number): number;
|
|
47
|
+
declare function wvMenuSetEnabled(h: number, tag: number, on: number): number;
|
|
48
|
+
declare function wvMenuSetChecked(h: number, tag: number, on: number): number;
|
|
49
|
+
declare function wvMenuSetLabel(h: number, tag: number, label: string): number;
|
|
47
50
|
|
|
48
51
|
const JOB_PENDING = 0;
|
|
49
52
|
const JOB_OK = 1;
|
|
@@ -92,7 +95,6 @@ export type {
|
|
|
92
95
|
DialogFilter,
|
|
93
96
|
Events,
|
|
94
97
|
FsCallback,
|
|
95
|
-
MenuEntry,
|
|
96
98
|
OpenDialogOptions,
|
|
97
99
|
SaveDialogOptions,
|
|
98
100
|
WindowConfig,
|
|
@@ -102,7 +104,6 @@ export type {
|
|
|
102
104
|
// A project's `import { defineCommands } from "janela/host"` is rewritten to
|
|
103
105
|
// this module by the CLI before scriptc sees it.
|
|
104
106
|
export { defineCommands, defineEvents } from "./types";
|
|
105
|
-
export { menuItem, menuSeparator, submenu } from "./types";
|
|
106
107
|
|
|
107
108
|
import type {
|
|
108
109
|
AsyncCommandHandler,
|
|
@@ -114,7 +115,6 @@ import type {
|
|
|
114
115
|
DialogFilter,
|
|
115
116
|
Events,
|
|
116
117
|
FsCallback,
|
|
117
|
-
MenuEntry,
|
|
118
118
|
OpenDialogOptions,
|
|
119
119
|
SaveDialogOptions,
|
|
120
120
|
WindowConfig,
|
|
@@ -161,12 +161,18 @@ const TIMER_JOBS = -1;
|
|
|
161
161
|
* interface (being signature-only) never is. A class receiver works even as a
|
|
162
162
|
* plain function parameter, which is what `setup(app)` is.
|
|
163
163
|
*/
|
|
164
|
-
// ----
|
|
164
|
+
// ---- menus -------------------------------------------------------------
|
|
165
165
|
//
|
|
166
|
-
//
|
|
167
|
-
//
|
|
168
|
-
//
|
|
169
|
-
//
|
|
166
|
+
// A menu item is an OBJECT that carries its own click handler, not a row with
|
|
167
|
+
// an id that something matches on later. muda — Tauri's menu crate — uses ids
|
|
168
|
+
// because Rust cannot attach a closure to an item across its global event
|
|
169
|
+
// channel, so it hands you `MenuId(String)` and you match on it; a typo there
|
|
170
|
+
// is silent. TypeScript has no such problem: the handler lives on the item, so
|
|
171
|
+
// there is no name to declare, to keep in sync, or to get wrong.
|
|
172
|
+
//
|
|
173
|
+
// The tag is an implementation detail the caller never sees: it indexes the
|
|
174
|
+
// handler registry here, is written into the wire format, comes back on a
|
|
175
|
+
// click, and is what setEnabled/setChecked/setLabel address.
|
|
170
176
|
|
|
171
177
|
// NSEventModifierFlags.
|
|
172
178
|
const MENU_SHIFT = 131072;
|
|
@@ -205,24 +211,116 @@ function menuSafe(v: string): string {
|
|
|
205
211
|
return v.split("\x1f").join(" ").split("\n").join(" ");
|
|
206
212
|
}
|
|
207
213
|
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
+
/** Submenus and separators have nothing to run. */
|
|
215
|
+
function noop(): void {}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* An entry in the application menu.
|
|
219
|
+
*
|
|
220
|
+
* Build one with `menuItem`, `menuSeparator` or `submenu` and keep the
|
|
221
|
+
* reference if you want to change it later — there is no lookup by name,
|
|
222
|
+
* because there are no names.
|
|
223
|
+
*/
|
|
224
|
+
export class MenuItem {
|
|
225
|
+
label: string;
|
|
226
|
+
accel: string;
|
|
227
|
+
separator: boolean;
|
|
228
|
+
items: MenuItem[];
|
|
229
|
+
onClick: () => void;
|
|
230
|
+
enabled = true;
|
|
231
|
+
// Data on the base so the flattener can read it without a type test; only
|
|
232
|
+
// CheckMenuItem exposes a way to change it.
|
|
233
|
+
checkable = false;
|
|
234
|
+
checked = false;
|
|
235
|
+
|
|
236
|
+
// Assigned when the item is mounted by setMenu; -1 while detached. The
|
|
237
|
+
// window handle is enough to reach the FFI — holding the app itself would
|
|
238
|
+
// drag its two type parameters in here for no benefit.
|
|
239
|
+
tag = -1;
|
|
240
|
+
ownerHandle = -1;
|
|
241
|
+
|
|
242
|
+
constructor(
|
|
243
|
+
label: string,
|
|
244
|
+
accel: string,
|
|
245
|
+
separator: boolean,
|
|
246
|
+
items: MenuItem[],
|
|
247
|
+
onClick: () => void,
|
|
248
|
+
) {
|
|
249
|
+
this.label = label;
|
|
250
|
+
this.accel = accel;
|
|
251
|
+
this.separator = separator;
|
|
252
|
+
this.items = items;
|
|
253
|
+
this.onClick = onClick;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Grey the item out, or bring it back.
|
|
258
|
+
*
|
|
259
|
+
* Applied immediately when the item is already in a menu, and remembered
|
|
260
|
+
* for the next `setMenu` when it is not — so state set before mounting is
|
|
261
|
+
* not lost.
|
|
262
|
+
*/
|
|
263
|
+
setEnabled(on: boolean): void {
|
|
264
|
+
this.enabled = on;
|
|
265
|
+
if (this.ownerHandle >= 0 && this.tag >= 0) {
|
|
266
|
+
wvMenuSetEnabled(this.ownerHandle, this.tag, on ? 1 : 0);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/** Change the text without rebuilding the menu. */
|
|
271
|
+
setLabel(label: string): void {
|
|
272
|
+
this.label = label;
|
|
273
|
+
if (this.ownerHandle >= 0 && this.tag >= 0) {
|
|
274
|
+
wvMenuSetLabel(this.ownerHandle, this.tag, label);
|
|
214
275
|
}
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* An item that can carry a tick.
|
|
281
|
+
*
|
|
282
|
+
* Separate from `MenuItem` because GTK decides this at construction: a tick
|
|
283
|
+
* needs `GtkCheckMenuItem`, a different widget, and an item cannot become one
|
|
284
|
+
* later. macOS and Windows would let any item show a check, but a method that
|
|
285
|
+
* works on two platforms and silently does nothing on the third is worse than
|
|
286
|
+
* one that is simply absent — so `setChecked` lives here, and calling it on a
|
|
287
|
+
* plain item is a compile error rather than a surprise on Linux.
|
|
288
|
+
*/
|
|
289
|
+
export class CheckMenuItem extends MenuItem {
|
|
290
|
+
constructor(label: string, accel: string, onClick: () => void) {
|
|
291
|
+
super(label, accel, false, [], onClick);
|
|
292
|
+
this.checkable = true;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/** Tick or untick the item. */
|
|
296
|
+
setChecked(on: boolean): void {
|
|
297
|
+
this.checked = on;
|
|
298
|
+
if (this.ownerHandle >= 0 && this.tag >= 0) {
|
|
299
|
+
wvMenuSetChecked(this.ownerHandle, this.tag, on ? 1 : 0);
|
|
221
300
|
}
|
|
222
|
-
rows.push("I\x1f" + menuSafe(e.id) + "\x1f" + label + "\x1f" + parseAccel(e.accel));
|
|
223
301
|
}
|
|
224
302
|
}
|
|
225
303
|
|
|
304
|
+
/** A clickable entry. `accel` is "" for no shortcut. */
|
|
305
|
+
export function menuItem(label: string, accel: string, onClick: () => void): MenuItem {
|
|
306
|
+
return new MenuItem(label, accel, false, [], onClick);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/** A clickable entry that carries a tick. `accel` is "" for no shortcut. */
|
|
310
|
+
export function menuCheckItem(label: string, accel: string, onClick: () => void): CheckMenuItem {
|
|
311
|
+
return new CheckMenuItem(label, accel, onClick);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/** A divider. */
|
|
315
|
+
export function menuSeparator(): MenuItem {
|
|
316
|
+
return new MenuItem("", "", true, [], noop);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/** A submenu holding other entries. Nestable. */
|
|
320
|
+
export function submenu(label: string, items: MenuItem[]): MenuItem {
|
|
321
|
+
return new MenuItem(label, "", false, items, noop);
|
|
322
|
+
}
|
|
323
|
+
|
|
226
324
|
export class JanelaAppImpl<
|
|
227
325
|
C extends CommandShapes = CommandShapes,
|
|
228
326
|
E = Record<string, unknown>,
|
|
@@ -231,6 +329,14 @@ export class JanelaAppImpl<
|
|
|
231
329
|
names: string[] = [];
|
|
232
330
|
handlers: CommandHandler[] = [];
|
|
233
331
|
|
|
332
|
+
// Menu items in tag order; the tag IS the index. Rebuilt by setMenu.
|
|
333
|
+
menuItems: MenuItem[] = [];
|
|
334
|
+
// The handlers, separately: scriptc does not support calling a closure held
|
|
335
|
+
// on an object property (SC1090), only one reached through an array — the
|
|
336
|
+
// same reason command handlers live in `handlers`.
|
|
337
|
+
menuHandlers: (() => void)[] = [];
|
|
338
|
+
menuBound = false;
|
|
339
|
+
|
|
234
340
|
// ---- scheduling ----------------------------------------------------------
|
|
235
341
|
// scriptc's event loop is parked for as long as the program sits inside the
|
|
236
342
|
// wvRun() FFI call, so setTimeout/await never fire while the window is open.
|
|
@@ -584,34 +690,75 @@ export class JanelaAppImpl<
|
|
|
584
690
|
* wholesale is what would. Calling it again replaces only what a previous
|
|
585
691
|
* call added, so the menu can shrink as well as grow.
|
|
586
692
|
*
|
|
587
|
-
*
|
|
693
|
+
* Each item carries its own handler; there are no ids to declare or match.
|
|
694
|
+
* Keep a reference to change an item later.
|
|
588
695
|
*
|
|
589
696
|
* ```ts
|
|
590
|
-
*
|
|
591
|
-
*
|
|
592
|
-
*
|
|
593
|
-
*
|
|
594
|
-
*
|
|
595
|
-
*
|
|
596
|
-
*
|
|
597
|
-
*
|
|
697
|
+
* const save = menuItem("Save", "CmdOrCtrl+S", () => write());
|
|
698
|
+
* app.setMenu([submenu("File", [
|
|
699
|
+
* menuItem("Open…", "CmdOrCtrl+O", () => open()),
|
|
700
|
+
* menuSeparator(),
|
|
701
|
+
* save,
|
|
702
|
+
* ])]);
|
|
703
|
+
*
|
|
704
|
+
* save.setEnabled(false); // later, without rebuilding
|
|
598
705
|
* ```
|
|
599
706
|
*
|
|
600
707
|
* Returns false where custom menus are not supported yet — everything but
|
|
601
708
|
* macOS. The standard menu is unaffected either way.
|
|
602
709
|
*/
|
|
603
|
-
setMenu(entries:
|
|
710
|
+
setMenu(entries: MenuItem[]): boolean {
|
|
711
|
+
// A rebuild invalidates every tag from the previous call, so the registry
|
|
712
|
+
// is rebuilt with it. Items carried over keep working because they are
|
|
713
|
+
// re-tagged here; items dropped from the tree go inert, which is what
|
|
714
|
+
// "removed from the menu" should mean.
|
|
715
|
+
for (let i = 0; i < this.menuItems.length; i++) {
|
|
716
|
+
this.menuItems[i].tag = -1;
|
|
717
|
+
this.menuItems[i].ownerHandle = -1;
|
|
718
|
+
}
|
|
719
|
+
this.menuItems = [];
|
|
720
|
+
this.menuHandlers = [];
|
|
721
|
+
|
|
604
722
|
const rows: string[] = [];
|
|
605
|
-
flattenMenu(entries, rows);
|
|
723
|
+
this.flattenMenu(entries, rows);
|
|
724
|
+
|
|
725
|
+
if (!this.menuBound) {
|
|
726
|
+
// Registered once: the tag comes back and the item's own closure runs.
|
|
727
|
+
wvOnMenu(this.handle, (tag) => {
|
|
728
|
+
if (tag >= 0 && tag < this.menuHandlers.length) this.menuHandlers[tag]();
|
|
729
|
+
return 0;
|
|
730
|
+
});
|
|
731
|
+
this.menuBound = true;
|
|
732
|
+
}
|
|
606
733
|
return wvSetMenu(this.handle, rows.join("\n")) === 0;
|
|
607
734
|
}
|
|
608
735
|
|
|
609
|
-
/**
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
736
|
+
/** Walks the tree, assigns each clickable item its registry tag. */
|
|
737
|
+
flattenMenu(entries: MenuItem[], rows: string[]): void {
|
|
738
|
+
for (let i = 0; i < entries.length; i++) {
|
|
739
|
+
const e = entries[i];
|
|
740
|
+
if (e.separator) {
|
|
741
|
+
rows.push("-");
|
|
742
|
+
continue;
|
|
743
|
+
}
|
|
744
|
+
const label = menuSafe(e.label);
|
|
745
|
+
if (e.items.length > 0) {
|
|
746
|
+
rows.push("S\x1f" + label);
|
|
747
|
+
this.flattenMenu(e.items, rows);
|
|
748
|
+
rows.push("E");
|
|
749
|
+
continue;
|
|
750
|
+
}
|
|
751
|
+
const tag = this.menuItems.length;
|
|
752
|
+
this.menuItems.push(e);
|
|
753
|
+
this.menuHandlers.push(e.onClick);
|
|
754
|
+
e.tag = tag;
|
|
755
|
+
e.ownerHandle = this.handle;
|
|
756
|
+
rows.push(
|
|
757
|
+
"I\x1f" + tag + "\x1f" + label + "\x1f" + parseAccel(e.accel) +
|
|
758
|
+
"\x1f" + (e.enabled ? "1" : "0") + "\x1f" + (e.checked ? "1" : "0") +
|
|
759
|
+
"\x1f" + (e.checkable ? "1" : "0"),
|
|
760
|
+
);
|
|
761
|
+
}
|
|
615
762
|
}
|
|
616
763
|
|
|
617
764
|
/**
|
package/runtime/types.ts
CHANGED
|
@@ -41,56 +41,6 @@ export type AsyncCommandHandler = (
|
|
|
41
41
|
*/
|
|
42
42
|
export type FsCallback = (err: string | null, text: string) => void;
|
|
43
43
|
|
|
44
|
-
/**
|
|
45
|
-
* One entry in the application menu.
|
|
46
|
-
*
|
|
47
|
-
* Every field is required rather than optional, and the entries are built with
|
|
48
|
-
* `submenu`, `menuItem` and `menuSeparator` rather than written as literals.
|
|
49
|
-
* That is a scriptc constraint made into a nicer API: a record whose fields are
|
|
50
|
-
* all optional infers as `{ label: string | undefined, … }`, and an array
|
|
51
|
-
* mixing a submenu, an item and a separator is a union that will not re-tag
|
|
52
|
-
* into it ("union types must match exactly", SC2003). The helpers each return
|
|
53
|
-
* the same total shape, so the array is homogeneous and the call site reads as
|
|
54
|
-
* a tree.
|
|
55
|
-
*
|
|
56
|
-
* ```ts
|
|
57
|
-
* app.setMenu([
|
|
58
|
-
* submenu("File", [
|
|
59
|
-
* menuItem("Open…", "open", "CmdOrCtrl+O"),
|
|
60
|
-
* menuSeparator(),
|
|
61
|
-
* ]),
|
|
62
|
-
* ]);
|
|
63
|
-
* ```
|
|
64
|
-
*/
|
|
65
|
-
export type MenuEntry = {
|
|
66
|
-
label: string;
|
|
67
|
-
/** Sent to `onMenu` when clicked. Empty for submenus and separators. */
|
|
68
|
-
id: string;
|
|
69
|
-
/**
|
|
70
|
-
* A shortcut like "CmdOrCtrl+O" or "CmdOrCtrl+Shift+S". Modifiers: Cmd,
|
|
71
|
-
* Ctrl, CmdOrCtrl, Alt/Option, Shift. Empty for none.
|
|
72
|
-
*/
|
|
73
|
-
accel: string;
|
|
74
|
-
separator: boolean;
|
|
75
|
-
/** Children. Empty for a leaf. */
|
|
76
|
-
items: MenuEntry[];
|
|
77
|
-
};
|
|
78
|
-
|
|
79
|
-
/** A clickable entry. `accel` is "" for no shortcut. */
|
|
80
|
-
export function menuItem(label: string, id: string, accel: string): MenuEntry {
|
|
81
|
-
return { label: label, id: id, accel: accel, separator: false, items: [] };
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
/** A divider. */
|
|
85
|
-
export function menuSeparator(): MenuEntry {
|
|
86
|
-
return { label: "", id: "", accel: "", separator: true, items: [] };
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
/** A submenu holding other entries. Nestable. */
|
|
90
|
-
export function submenu(label: string, items: MenuEntry[]): MenuEntry {
|
|
91
|
-
return { label: label, id: "", accel: "", separator: false, items: items };
|
|
92
|
-
}
|
|
93
|
-
/** A named group of extensions offered in a dialog's file-type popup. */
|
|
94
44
|
export interface DialogFilter {
|
|
95
45
|
name: string;
|
|
96
46
|
/** Bare extensions, no dot and no glob: ["png", "jpg"]. */
|
package/shim/wvshim.cc
CHANGED
|
@@ -75,12 +75,20 @@ struct App {
|
|
|
75
75
|
void *on_invoke_ctx = nullptr;
|
|
76
76
|
void (*on_timer)(int32_t, void *) = nullptr;
|
|
77
77
|
void *on_timer_ctx = nullptr;
|
|
78
|
-
|
|
78
|
+
// The tag identifies the item; the host owns the numbering and hands it
|
|
79
|
+
// down, because the click handler lives in TypeScript now.
|
|
80
|
+
int32_t (*on_menu)(int32_t, void *) = nullptr;
|
|
79
81
|
void *on_menu_ctx = nullptr;
|
|
80
82
|
|
|
81
|
-
//
|
|
82
|
-
//
|
|
83
|
-
|
|
83
|
+
// Live NSMenuItems, indexed by tag, so setEnabled/setChecked/setLabel can
|
|
84
|
+
// reach one without rebuilding the bar. Retained; released on the next
|
|
85
|
+
// setMenu. The menu bar is process-global on macOS, so only one app can own
|
|
86
|
+
// it — see g_menu_owner.
|
|
87
|
+
//
|
|
88
|
+
// `void *` rather than `id`: this struct is compiled on every platform and
|
|
89
|
+
// `id` is Objective-C, so naming it here breaks the Linux and Windows
|
|
90
|
+
// builds. The Apple code casts on the way in and out.
|
|
91
|
+
std::vector<void *> menu_items;
|
|
84
92
|
// How many submenus the standard bar installed, so setMenu appends after
|
|
85
93
|
// them and can remove only its own on a later call.
|
|
86
94
|
size_t std_menu_count = 0;
|
|
@@ -531,10 +539,7 @@ static int32_t g_menu_owner = -1;
|
|
|
531
539
|
static void menu_clicked(long tag) {
|
|
532
540
|
App *a = g_menu_owner >= 0 ? app_at(g_menu_owner) : nullptr;
|
|
533
541
|
if (!a || !a->on_menu) return;
|
|
534
|
-
|
|
535
|
-
const std::string &id = a->menu_ids[static_cast<size_t>(tag)];
|
|
536
|
-
a->on_menu(reinterpret_cast<const uint8_t *>(id.data()), id.size(),
|
|
537
|
-
a->on_menu_ctx);
|
|
542
|
+
a->on_menu(static_cast<int32_t>(tag), a->on_menu_ctx);
|
|
538
543
|
}
|
|
539
544
|
|
|
540
545
|
// One shared target for every custom item; the tag says which one fired.
|
|
@@ -563,6 +568,21 @@ static id menu_target() {
|
|
|
563
568
|
return instance;
|
|
564
569
|
}
|
|
565
570
|
|
|
571
|
+
// Retains the item under its tag so a later setEnabled/setChecked/setLabel can
|
|
572
|
+
// find it. The vector is sized to fit rather than pushed, because the host's
|
|
573
|
+
// tags are registry indices and need not arrive in order.
|
|
574
|
+
static void remember_item(App *a, long tag, id item) {
|
|
575
|
+
if (tag < 0) return;
|
|
576
|
+
size_t at = static_cast<size_t>(tag);
|
|
577
|
+
if (a->menu_items.size() <= at) a->menu_items.resize(at + 1, nullptr);
|
|
578
|
+
a->menu_items[at] = webview::detail::objc::retain(item);
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
static id item_at(App *a, int32_t tag) {
|
|
582
|
+
if (tag < 0 || static_cast<size_t>(tag) >= a->menu_items.size()) return nullptr;
|
|
583
|
+
return static_cast<id>(a->menu_items[static_cast<size_t>(tag)]);
|
|
584
|
+
}
|
|
585
|
+
|
|
566
586
|
// Appends the host's submenus to the standard menu bar rather than replacing
|
|
567
587
|
// it: a custom menu must not be able to cost the app Cmd+Q and Cmd+V, which is
|
|
568
588
|
// what replacing the bar wholesale would do.
|
|
@@ -583,7 +603,10 @@ static int32_t apply_custom_menu(App *a, const std::string &spec) {
|
|
|
583
603
|
objc::msg_send<void>(menubar, objc::selector("removeItemAtIndex:"),
|
|
584
604
|
static_cast<NSInteger>(n - 1));
|
|
585
605
|
}
|
|
586
|
-
a->
|
|
606
|
+
for (void *old_item : a->menu_items) {
|
|
607
|
+
if (old_item) objc::release(static_cast<id>(old_item));
|
|
608
|
+
}
|
|
609
|
+
a->menu_items.clear();
|
|
587
610
|
|
|
588
611
|
std::vector<id> stack;
|
|
589
612
|
stack.push_back(menubar);
|
|
@@ -605,6 +628,10 @@ static int32_t apply_custom_menu(App *a, const std::string &spec) {
|
|
|
605
628
|
cocoa::NSString_stringWithUTF8String(f[1]));
|
|
606
629
|
objc::msg_send<void>(holder, objc::selector("setTitle:"),
|
|
607
630
|
cocoa::NSString_stringWithUTF8String(f[1]));
|
|
631
|
+
// Without this AppKit decides each item's enabled state from the
|
|
632
|
+
// responder chain and setEnabled: is silently ignored — the item stays
|
|
633
|
+
// live however many times the host disables it.
|
|
634
|
+
objc::msg_send<void>(menu, objc::selector("setAutoenablesItems:"), false);
|
|
608
635
|
objc::msg_send<void>(holder, objc::selector("setSubmenu:"), menu);
|
|
609
636
|
objc::msg_send<void>(stack.back(), objc::selector("addItem:"), holder);
|
|
610
637
|
stack.push_back(menu);
|
|
@@ -617,9 +644,11 @@ static int32_t apply_custom_menu(App *a, const std::string &spec) {
|
|
|
617
644
|
objc::msg_send<id>(objc::get_class("NSMenuItem"),
|
|
618
645
|
objc::selector("separatorItem")));
|
|
619
646
|
}
|
|
620
|
-
} else if (kind == "I" && f.size() >=
|
|
621
|
-
|
|
622
|
-
|
|
647
|
+
} else if (kind == "I" && f.size() >= 8 && stack.size() > 1) {
|
|
648
|
+
// The host owns the tag: it indexes the handler registry in TypeScript,
|
|
649
|
+
// so the numbering has to come from there rather than from insertion
|
|
650
|
+
// order here.
|
|
651
|
+
long tag = strtol(f[1].c_str(), nullptr, 10);
|
|
623
652
|
id it = objc::msg_send<id>(
|
|
624
653
|
objc::msg_send<id>(objc::get_class("NSMenuItem"),
|
|
625
654
|
objc::selector("alloc")),
|
|
@@ -635,9 +664,17 @@ static int32_t apply_custom_menu(App *a, const std::string &spec) {
|
|
|
635
664
|
NSUInteger mods = static_cast<NSUInteger>(strtoul(f[4].c_str(), nullptr, 10));
|
|
636
665
|
objc::msg_send<void>(it, objc::selector("setKeyEquivalentModifierMask:"),
|
|
637
666
|
mods);
|
|
667
|
+
objc::msg_send<void>(it, objc::selector("setEnabled:"), f[5] == "1");
|
|
668
|
+
// f[7] says the item is checkable. AppKit lets any item carry a state,
|
|
669
|
+
// so it is unused here — it is on the wire for the GTK renderer, which
|
|
670
|
+
// has to choose GtkCheckMenuItem at construction.
|
|
671
|
+
objc::msg_send<void>(it, objc::selector("setState:"),
|
|
672
|
+
static_cast<NSInteger>(f[6] == "1" ? 1 : 0));
|
|
638
673
|
objc::msg_send<void>(stack.back(), objc::selector("addItem:"), it);
|
|
674
|
+
remember_item(a, tag, it);
|
|
639
675
|
}
|
|
640
676
|
}
|
|
677
|
+
|
|
641
678
|
g_menu_owner = -1;
|
|
642
679
|
for (int32_t i = 0; i < 8; i++) {
|
|
643
680
|
if (app_at(i) == a) { g_menu_owner = i; break; }
|
|
@@ -1044,7 +1081,7 @@ int32_t wv_create(int32_t debug) {
|
|
|
1044
1081
|
g_apps[i].on_timer_ctx = nullptr;
|
|
1045
1082
|
g_apps[i].on_menu = nullptr;
|
|
1046
1083
|
g_apps[i].on_menu_ctx = nullptr;
|
|
1047
|
-
g_apps[i].
|
|
1084
|
+
g_apps[i].menu_items.clear();
|
|
1048
1085
|
g_apps[i].req.clear();
|
|
1049
1086
|
g_apps[i].cur_id.clear();
|
|
1050
1087
|
g_apps[i].reply.clear();
|
|
@@ -1358,9 +1395,7 @@ int32_t wv_set_menu(int32_t h, const uint8_t *p, size_t n) {
|
|
|
1358
1395
|
}
|
|
1359
1396
|
|
|
1360
1397
|
// Retained, like wv_on_invoke: registered once and valid until the app exits.
|
|
1361
|
-
int32_t wv_on_menu(int32_t h,
|
|
1362
|
-
int32_t (*cb)(const uint8_t *, size_t, void *),
|
|
1363
|
-
void *ctx) {
|
|
1398
|
+
int32_t wv_on_menu(int32_t h, int32_t (*cb)(int32_t, void *), void *ctx) {
|
|
1364
1399
|
App *a = app_at(h);
|
|
1365
1400
|
if (!a) return -1;
|
|
1366
1401
|
a->on_menu = cb;
|
|
@@ -1368,6 +1403,60 @@ int32_t wv_on_menu(int32_t h,
|
|
|
1368
1403
|
return 0;
|
|
1369
1404
|
}
|
|
1370
1405
|
|
|
1406
|
+
// Change one live item without rebuilding the bar. The tag is the host's
|
|
1407
|
+
// registry index, handed down by wv_set_menu. -1 for an unknown tag rather
|
|
1408
|
+
// than silence, so a stale handle is visible instead of a no-op.
|
|
1409
|
+
int32_t wv_menu_set_enabled(int32_t h, int32_t tag, int32_t on) {
|
|
1410
|
+
App *a = app_at(h);
|
|
1411
|
+
if (!a) return -1;
|
|
1412
|
+
#if defined(__APPLE__)
|
|
1413
|
+
using namespace webview::detail;
|
|
1414
|
+
objc::autoreleasepool arp;
|
|
1415
|
+
id it = item_at(a, tag);
|
|
1416
|
+
if (!it) return -1;
|
|
1417
|
+
objc::msg_send<void>(it, objc::selector("setEnabled:"), on != 0);
|
|
1418
|
+
return 0;
|
|
1419
|
+
#else
|
|
1420
|
+
(void)tag; (void)on;
|
|
1421
|
+
return -1;
|
|
1422
|
+
#endif
|
|
1423
|
+
}
|
|
1424
|
+
|
|
1425
|
+
int32_t wv_menu_set_checked(int32_t h, int32_t tag, int32_t on) {
|
|
1426
|
+
App *a = app_at(h);
|
|
1427
|
+
if (!a) return -1;
|
|
1428
|
+
#if defined(__APPLE__)
|
|
1429
|
+
using namespace webview::detail;
|
|
1430
|
+
objc::autoreleasepool arp;
|
|
1431
|
+
id it = item_at(a, tag);
|
|
1432
|
+
if (!it) return -1;
|
|
1433
|
+
// NSControlStateValueOn / Off.
|
|
1434
|
+
objc::msg_send<void>(it, objc::selector("setState:"),
|
|
1435
|
+
static_cast<NSInteger>(on != 0 ? 1 : 0));
|
|
1436
|
+
return 0;
|
|
1437
|
+
#else
|
|
1438
|
+
(void)tag; (void)on;
|
|
1439
|
+
return -1;
|
|
1440
|
+
#endif
|
|
1441
|
+
}
|
|
1442
|
+
|
|
1443
|
+
int32_t wv_menu_set_label(int32_t h, int32_t tag, const uint8_t *p, size_t n) {
|
|
1444
|
+
App *a = app_at(h);
|
|
1445
|
+
if (!a) return -1;
|
|
1446
|
+
#if defined(__APPLE__)
|
|
1447
|
+
using namespace webview::detail;
|
|
1448
|
+
objc::autoreleasepool arp;
|
|
1449
|
+
id it = item_at(a, tag);
|
|
1450
|
+
if (!it) return -1;
|
|
1451
|
+
objc::msg_send<void>(it, objc::selector("setTitle:"),
|
|
1452
|
+
cocoa::NSString_stringWithUTF8String(to_str(p, n)));
|
|
1453
|
+
return 0;
|
|
1454
|
+
#else
|
|
1455
|
+
(void)tag; (void)p; (void)n;
|
|
1456
|
+
return -1;
|
|
1457
|
+
#endif
|
|
1458
|
+
}
|
|
1459
|
+
|
|
1371
1460
|
int32_t wv_set_fullscreen(int32_t h, int32_t on) {
|
|
1372
1461
|
App *a = app_at(h);
|
|
1373
1462
|
if (!a) return -1;
|