janela 0.16.1 → 0.17.1
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 +34 -0
- package/package.json +1 -1
- package/runtime/janela.ts +105 -0
- package/runtime/types.ts +49 -0
- package/shim/wvshim.cc +190 -1
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
|
*
|
|
@@ -223,6 +224,18 @@ export function ffiManifest(shimLib, { platform = process.platform, macSdkPath =
|
|
|
223
224
|
returns: "i32",
|
|
224
225
|
},
|
|
225
226
|
{ name: "wvSetFullscreen", symbol: "wv_set_fullscreen", params: ["i32", "i32"], returns: "i32" },
|
|
227
|
+
STR("wvSetMenu", "wv_set_menu"),
|
|
228
|
+
// Retained like wvOnInvoke: registered once, valid until the app exits.
|
|
229
|
+
// The clicked item's id rides in as a `string` param (format 3).
|
|
230
|
+
{
|
|
231
|
+
name: "wvOnMenu", symbol: "wv_on_menu",
|
|
232
|
+
params: [
|
|
233
|
+
"i32",
|
|
234
|
+
{ callback: { id: "menu", params: ["string", { context: "menu" }], returns: "i32", lifetime: "retained" } },
|
|
235
|
+
{ context: "menu" },
|
|
236
|
+
],
|
|
237
|
+
returns: "i32",
|
|
238
|
+
},
|
|
226
239
|
];
|
|
227
240
|
|
|
228
241
|
if (platform === "win32") {
|
|
@@ -372,3 +385,24 @@ export function installCommand(pm) {
|
|
|
372
385
|
if (pm === "npm") return ["npm", "install", "--no-fund", "--no-audit"];
|
|
373
386
|
return [pm, "install"];
|
|
374
387
|
}
|
|
388
|
+
|
|
389
|
+
/**
|
|
390
|
+
* The cache key for the compiled webview shim.
|
|
391
|
+
*
|
|
392
|
+
* Keyed on the shim's CONTENT, not its mtime. The mtime test this replaced was
|
|
393
|
+
* wrong in the one case that matters — upgrading janela: pnpm's store is
|
|
394
|
+
* content-addressed and hard-links files into node_modules carrying the
|
|
395
|
+
* STORE's mtime, which is older than an object compiled minutes earlier from
|
|
396
|
+
* the previous version. The cache then looked fresh, the stale archive was
|
|
397
|
+
* reused, and the link failed naming symbols the new shim exports and the old
|
|
398
|
+
* object does not.
|
|
399
|
+
*
|
|
400
|
+
* `version` is in the key so a shim that happens to be byte-identical across
|
|
401
|
+
* releases still recompiles when anything else about the build moved.
|
|
402
|
+
*/
|
|
403
|
+
export function shimCacheKey(source, version, platform, includeFlag) {
|
|
404
|
+
return createHash("sha256")
|
|
405
|
+
.update(source)
|
|
406
|
+
.update(`\u0000${version}\u0000${platform}\u0000${includeFlag}`)
|
|
407
|
+
.digest("hex");
|
|
408
|
+
}
|
package/package.json
CHANGED
package/runtime/janela.ts
CHANGED
|
@@ -42,6 +42,8 @@ declare function wvDialog(
|
|
|
42
42
|
filters: string,
|
|
43
43
|
): number;
|
|
44
44
|
declare function wvSetFullscreen(h: number, on: number): number;
|
|
45
|
+
declare function wvSetMenu(h: number, spec: string): number;
|
|
46
|
+
declare function wvOnMenu(h: number, cb: (id: string) => number): number;
|
|
45
47
|
|
|
46
48
|
const JOB_PENDING = 0;
|
|
47
49
|
const JOB_OK = 1;
|
|
@@ -90,6 +92,7 @@ export type {
|
|
|
90
92
|
DialogFilter,
|
|
91
93
|
Events,
|
|
92
94
|
FsCallback,
|
|
95
|
+
MenuEntry,
|
|
93
96
|
OpenDialogOptions,
|
|
94
97
|
SaveDialogOptions,
|
|
95
98
|
WindowConfig,
|
|
@@ -99,6 +102,7 @@ export type {
|
|
|
99
102
|
// A project's `import { defineCommands } from "janela/host"` is rewritten to
|
|
100
103
|
// this module by the CLI before scriptc sees it.
|
|
101
104
|
export { defineCommands, defineEvents } from "./types";
|
|
105
|
+
export { menuItem, menuSeparator, submenu } from "./types";
|
|
102
106
|
|
|
103
107
|
import type {
|
|
104
108
|
AsyncCommandHandler,
|
|
@@ -110,6 +114,7 @@ import type {
|
|
|
110
114
|
DialogFilter,
|
|
111
115
|
Events,
|
|
112
116
|
FsCallback,
|
|
117
|
+
MenuEntry,
|
|
113
118
|
OpenDialogOptions,
|
|
114
119
|
SaveDialogOptions,
|
|
115
120
|
WindowConfig,
|
|
@@ -156,6 +161,68 @@ const TIMER_JOBS = -1;
|
|
|
156
161
|
* interface (being signature-only) never is. A class receiver works even as a
|
|
157
162
|
* plain function parameter, which is what `setup(app)` is.
|
|
158
163
|
*/
|
|
164
|
+
// ---- menu flattening ---------------------------------------------------
|
|
165
|
+
//
|
|
166
|
+
// The shim renders menus but parses nothing structural: the tree is flattened
|
|
167
|
+
// here into one row per line with 0x1f between fields, which needs only a
|
|
168
|
+
// split on the native side. Keeping the parsing in TypeScript is the whole
|
|
169
|
+
// point — the native side stays a renderer.
|
|
170
|
+
|
|
171
|
+
// NSEventModifierFlags.
|
|
172
|
+
const MENU_SHIFT = 131072;
|
|
173
|
+
const MENU_CONTROL = 262144;
|
|
174
|
+
const MENU_OPTION = 524288;
|
|
175
|
+
const MENU_COMMAND = 1048576;
|
|
176
|
+
|
|
177
|
+
/** "CmdOrCtrl+Shift+O" -> "o<US>1179648". Unknown words are taken as the key. */
|
|
178
|
+
function parseAccel(accel: string): string {
|
|
179
|
+
const parts = accel.split("+");
|
|
180
|
+
let mods = 0;
|
|
181
|
+
let key = "";
|
|
182
|
+
for (let i = 0; i < parts.length; i++) {
|
|
183
|
+
const p = parts[i].toLowerCase();
|
|
184
|
+
if (p === "cmd" || p === "command" || p === "meta" || p === "super") {
|
|
185
|
+
mods = mods | MENU_COMMAND;
|
|
186
|
+
} else if (p === "cmdorctrl" || p === "commandorcontrol") {
|
|
187
|
+
// macOS is the only platform with custom menus so far, so this is
|
|
188
|
+
// Command here; the Windows and Linux renderers will read it as Control.
|
|
189
|
+
mods = mods | MENU_COMMAND;
|
|
190
|
+
} else if (p === "ctrl" || p === "control") {
|
|
191
|
+
mods = mods | MENU_CONTROL;
|
|
192
|
+
} else if (p === "alt" || p === "option") {
|
|
193
|
+
mods = mods | MENU_OPTION;
|
|
194
|
+
} else if (p === "shift") {
|
|
195
|
+
mods = mods | MENU_SHIFT;
|
|
196
|
+
} else if (p !== "") {
|
|
197
|
+
key = p;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
return key + "\x1f" + mods;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/** 0x1f and newlines are the wire format's own delimiters. */
|
|
204
|
+
function menuSafe(v: string): string {
|
|
205
|
+
return v.split("\x1f").join(" ").split("\n").join(" ");
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function flattenMenu(entries: MenuEntry[], rows: string[]): void {
|
|
209
|
+
for (let i = 0; i < entries.length; i++) {
|
|
210
|
+
const e = entries[i];
|
|
211
|
+
if (e.separator) {
|
|
212
|
+
rows.push("-");
|
|
213
|
+
continue;
|
|
214
|
+
}
|
|
215
|
+
const label = menuSafe(e.label);
|
|
216
|
+
if (e.items.length > 0) {
|
|
217
|
+
rows.push("S\x1f" + label);
|
|
218
|
+
flattenMenu(e.items, rows);
|
|
219
|
+
rows.push("E");
|
|
220
|
+
continue;
|
|
221
|
+
}
|
|
222
|
+
rows.push("I\x1f" + menuSafe(e.id) + "\x1f" + label + "\x1f" + parseAccel(e.accel));
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
159
226
|
export class JanelaAppImpl<
|
|
160
227
|
C extends CommandShapes = CommandShapes,
|
|
161
228
|
E = Record<string, unknown>,
|
|
@@ -509,6 +576,44 @@ export class JanelaAppImpl<
|
|
|
509
576
|
wvSetFullscreen(this.handle, on ? 1 : 0);
|
|
510
577
|
}
|
|
511
578
|
|
|
579
|
+
/**
|
|
580
|
+
* Put submenus in the application menu.
|
|
581
|
+
*
|
|
582
|
+
* They are ADDED to the standard ones rather than replacing them, so a
|
|
583
|
+
* custom menu can never cost the app Cmd+Q or Cmd+V — replacing the bar
|
|
584
|
+
* wholesale is what would. Calling it again replaces only what a previous
|
|
585
|
+
* call added, so the menu can shrink as well as grow.
|
|
586
|
+
*
|
|
587
|
+
* Clicks arrive on `onMenu` with the entry's `id`.
|
|
588
|
+
*
|
|
589
|
+
* ```ts
|
|
590
|
+
* app.setMenu([
|
|
591
|
+
* { label: "File", items: [
|
|
592
|
+
* { label: "Open…", id: "open", accel: "CmdOrCtrl+O" },
|
|
593
|
+
* { separator: true },
|
|
594
|
+
* { label: "Close", id: "close" },
|
|
595
|
+
* ]},
|
|
596
|
+
* ]);
|
|
597
|
+
* app.onMenu((id) => { if (id === "open") … });
|
|
598
|
+
* ```
|
|
599
|
+
*
|
|
600
|
+
* Returns false where custom menus are not supported yet — everything but
|
|
601
|
+
* macOS. The standard menu is unaffected either way.
|
|
602
|
+
*/
|
|
603
|
+
setMenu(entries: MenuEntry[]): boolean {
|
|
604
|
+
const rows: string[] = [];
|
|
605
|
+
flattenMenu(entries, rows);
|
|
606
|
+
return wvSetMenu(this.handle, rows.join("\n")) === 0;
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
/** Called with the `id` of the clicked menu entry. */
|
|
610
|
+
onMenu(cb: (id: string) => void): void {
|
|
611
|
+
wvOnMenu(this.handle, (id) => {
|
|
612
|
+
cb(id);
|
|
613
|
+
return 0;
|
|
614
|
+
});
|
|
615
|
+
}
|
|
616
|
+
|
|
512
617
|
/**
|
|
513
618
|
* Fire an event into the page; the payload is delivered as a value. Under a
|
|
514
619
|
* contract, the name must be declared and the payload must match its type.
|
package/runtime/types.ts
CHANGED
|
@@ -41,6 +41,55 @@ 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
|
+
}
|
|
44
93
|
/** A named group of extensions offered in a dialog's file-type popup. */
|
|
45
94
|
export interface DialogFilter {
|
|
46
95
|
name: string;
|
package/shim/wvshim.cc
CHANGED
|
@@ -75,6 +75,15 @@ 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
|
+
int32_t (*on_menu)(const uint8_t *, size_t, void *) = nullptr;
|
|
79
|
+
void *on_menu_ctx = nullptr;
|
|
80
|
+
|
|
81
|
+
// Custom menu item ids, indexed by the NSMenuItem's tag. The menu bar is
|
|
82
|
+
// process-global on macOS, so only one app can own it — see g_menu_owner.
|
|
83
|
+
std::vector<std::string> menu_ids;
|
|
84
|
+
// How many submenus the standard bar installed, so setMenu appends after
|
|
85
|
+
// them and can remove only its own on a later call.
|
|
86
|
+
size_t std_menu_count = 0;
|
|
78
87
|
|
|
79
88
|
// Staging for the in-flight request.
|
|
80
89
|
std::string req; // JSON args array from JS
|
|
@@ -489,6 +498,153 @@ static void install_main_menu() {
|
|
|
489
498
|
objc::msg_send<void>(app, objc::selector("setWindowsMenu:"), window);
|
|
490
499
|
}
|
|
491
500
|
|
|
501
|
+
// How many submenus the standard bar has; custom ones are appended after these
|
|
502
|
+
// and only those are removed on a later setMenu.
|
|
503
|
+
static size_t standard_menu_count() {
|
|
504
|
+
using namespace webview::detail;
|
|
505
|
+
objc::autoreleasepool arp;
|
|
506
|
+
id app = objc::msg_send<id>(objc::get_class("NSApplication"),
|
|
507
|
+
objc::selector("sharedApplication"));
|
|
508
|
+
id menubar = app ? objc::msg_send<id>(app, objc::selector("mainMenu")) : nullptr;
|
|
509
|
+
if (!menubar) return 0;
|
|
510
|
+
return static_cast<size_t>(
|
|
511
|
+
objc::msg_send<NSUInteger>(menubar, objc::selector("numberOfItems")));
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
// ---- custom menus ------------------------------------------------------
|
|
515
|
+
//
|
|
516
|
+
// The host describes its menus declaratively in TypeScript and this renders
|
|
517
|
+
// them. Nothing here parses JSON: the runtime flattens the tree into one row
|
|
518
|
+
// per line, fields separated by 0x1f, which needs a split and nothing more.
|
|
519
|
+
//
|
|
520
|
+
// S<US>Label open a submenu
|
|
521
|
+
// E close it
|
|
522
|
+
// I<US>tag<US>Label<US>key<US>mods an item
|
|
523
|
+
// - a separator
|
|
524
|
+
//
|
|
525
|
+
// A click sends the item's tag back, which indexes App::menu_ids, and the id
|
|
526
|
+
// string goes up to TypeScript on the retained on_menu callback — the same
|
|
527
|
+
// shape as on_invoke. The menu bar is process-global on macOS, so exactly one
|
|
528
|
+
// app owns it at a time.
|
|
529
|
+
static int32_t g_menu_owner = -1;
|
|
530
|
+
|
|
531
|
+
static void menu_clicked(long tag) {
|
|
532
|
+
App *a = g_menu_owner >= 0 ? app_at(g_menu_owner) : nullptr;
|
|
533
|
+
if (!a || !a->on_menu) return;
|
|
534
|
+
if (tag < 0 || static_cast<size_t>(tag) >= a->menu_ids.size()) return;
|
|
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);
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
// One shared target for every custom item; the tag says which one fired.
|
|
541
|
+
static id menu_target() {
|
|
542
|
+
static id instance = nullptr;
|
|
543
|
+
if (instance) return instance;
|
|
544
|
+
constexpr auto class_name = "JanelaMenuTarget";
|
|
545
|
+
// Registering the same class twice crashes, and this runs once per process
|
|
546
|
+
// anyway — the lookup keeps a reload honest.
|
|
547
|
+
Class cls = objc_lookUpClass(class_name);
|
|
548
|
+
if (!cls) {
|
|
549
|
+
cls = objc_allocateClassPair(
|
|
550
|
+
webview::detail::objc::get_class("NSObject"), class_name, 0);
|
|
551
|
+
class_addMethod(cls, webview::detail::objc::selector("janelaMenuAction:"),
|
|
552
|
+
(IMP)(+[](id, SEL, id sender) {
|
|
553
|
+
menu_clicked(webview::detail::objc::msg_send<long>(
|
|
554
|
+
sender, webview::detail::objc::selector("tag")));
|
|
555
|
+
}),
|
|
556
|
+
"v@:@");
|
|
557
|
+
objc_registerClassPair(cls);
|
|
558
|
+
}
|
|
559
|
+
instance = webview::detail::objc::msg_send<id>(
|
|
560
|
+
webview::detail::objc::msg_send<id>((id)cls,
|
|
561
|
+
webview::detail::objc::selector("alloc")),
|
|
562
|
+
webview::detail::objc::selector("init"));
|
|
563
|
+
return instance;
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
// Appends the host's submenus to the standard menu bar rather than replacing
|
|
567
|
+
// it: a custom menu must not be able to cost the app Cmd+Q and Cmd+V, which is
|
|
568
|
+
// what replacing the bar wholesale would do.
|
|
569
|
+
static int32_t apply_custom_menu(App *a, const std::string &spec) {
|
|
570
|
+
using namespace webview::detail;
|
|
571
|
+
objc::autoreleasepool arp;
|
|
572
|
+
|
|
573
|
+
id app = objc::msg_send<id>(objc::get_class("NSApplication"),
|
|
574
|
+
objc::selector("sharedApplication"));
|
|
575
|
+
id menubar = objc::msg_send<id>(app, objc::selector("mainMenu"));
|
|
576
|
+
if (!menubar) return -1;
|
|
577
|
+
|
|
578
|
+
// Drop whatever a previous call added, so setMenu is idempotent and can
|
|
579
|
+
// shrink the bar as well as grow it.
|
|
580
|
+
for (size_t n = objc::msg_send<NSUInteger>(menubar,
|
|
581
|
+
objc::selector("numberOfItems"));
|
|
582
|
+
n > a->std_menu_count; n--) {
|
|
583
|
+
objc::msg_send<void>(menubar, objc::selector("removeItemAtIndex:"),
|
|
584
|
+
static_cast<NSInteger>(n - 1));
|
|
585
|
+
}
|
|
586
|
+
a->menu_ids.clear();
|
|
587
|
+
|
|
588
|
+
std::vector<id> stack;
|
|
589
|
+
stack.push_back(menubar);
|
|
590
|
+
|
|
591
|
+
for (const std::string &line : split_on(spec, '\n')) {
|
|
592
|
+
if (line.empty()) continue;
|
|
593
|
+
std::vector<std::string> f = split_on(line, '\x1f');
|
|
594
|
+
const std::string &kind = f[0];
|
|
595
|
+
|
|
596
|
+
if (kind == "S" && f.size() >= 2) {
|
|
597
|
+
id holder = objc::msg_send<id>(
|
|
598
|
+
objc::msg_send<id>(objc::get_class("NSMenuItem"),
|
|
599
|
+
objc::selector("alloc")),
|
|
600
|
+
objc::selector("init"));
|
|
601
|
+
id menu = objc::msg_send<id>(
|
|
602
|
+
objc::msg_send<id>(objc::get_class("NSMenu"),
|
|
603
|
+
objc::selector("alloc")),
|
|
604
|
+
objc::selector("initWithTitle:"),
|
|
605
|
+
cocoa::NSString_stringWithUTF8String(f[1]));
|
|
606
|
+
objc::msg_send<void>(holder, objc::selector("setTitle:"),
|
|
607
|
+
cocoa::NSString_stringWithUTF8String(f[1]));
|
|
608
|
+
objc::msg_send<void>(holder, objc::selector("setSubmenu:"), menu);
|
|
609
|
+
objc::msg_send<void>(stack.back(), objc::selector("addItem:"), holder);
|
|
610
|
+
stack.push_back(menu);
|
|
611
|
+
} else if (kind == "E") {
|
|
612
|
+
if (stack.size() > 1) stack.pop_back();
|
|
613
|
+
} else if (kind == "-") {
|
|
614
|
+
if (stack.size() > 1) {
|
|
615
|
+
objc::msg_send<void>(
|
|
616
|
+
stack.back(), objc::selector("addItem:"),
|
|
617
|
+
objc::msg_send<id>(objc::get_class("NSMenuItem"),
|
|
618
|
+
objc::selector("separatorItem")));
|
|
619
|
+
}
|
|
620
|
+
} else if (kind == "I" && f.size() >= 5 && stack.size() > 1) {
|
|
621
|
+
a->menu_ids.push_back(f[1]);
|
|
622
|
+
long tag = static_cast<long>(a->menu_ids.size()) - 1;
|
|
623
|
+
id it = objc::msg_send<id>(
|
|
624
|
+
objc::msg_send<id>(objc::get_class("NSMenuItem"),
|
|
625
|
+
objc::selector("alloc")),
|
|
626
|
+
objc::selector("initWithTitle:action:keyEquivalent:"),
|
|
627
|
+
cocoa::NSString_stringWithUTF8String(f[2]),
|
|
628
|
+
objc::selector("janelaMenuAction:"),
|
|
629
|
+
cocoa::NSString_stringWithUTF8String(f[3]));
|
|
630
|
+
objc::msg_send<void>(it, objc::selector("setTarget:"), menu_target());
|
|
631
|
+
objc::msg_send<void>(it, objc::selector("setTag:"), tag);
|
|
632
|
+
// Always set the mask, including 0: AppKit's default for a key
|
|
633
|
+
// equivalent is Command, so leaving it alone would turn an accelerator
|
|
634
|
+
// with no modifiers into a Command shortcut.
|
|
635
|
+
NSUInteger mods = static_cast<NSUInteger>(strtoul(f[4].c_str(), nullptr, 10));
|
|
636
|
+
objc::msg_send<void>(it, objc::selector("setKeyEquivalentModifierMask:"),
|
|
637
|
+
mods);
|
|
638
|
+
objc::msg_send<void>(stack.back(), objc::selector("addItem:"), it);
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
g_menu_owner = -1;
|
|
642
|
+
for (int32_t i = 0; i < 8; i++) {
|
|
643
|
+
if (app_at(i) == a) { g_menu_owner = i; break; }
|
|
644
|
+
}
|
|
645
|
+
return 0;
|
|
646
|
+
}
|
|
647
|
+
|
|
492
648
|
bool run_file_dialog(const DialogRequest &req, std::vector<std::string> &out,
|
|
493
649
|
std::string &error) {
|
|
494
650
|
(void)error;
|
|
@@ -745,6 +901,14 @@ bool run_file_dialog(const DialogRequest &req, std::vector<std::string> &out,
|
|
|
745
901
|
// the editing keys are handled inside WebView2 and WebKitGTK. A menu here
|
|
746
902
|
// would be a feature, not a fix.
|
|
747
903
|
static void install_main_menu() {}
|
|
904
|
+
static size_t standard_menu_count() { return 0; }
|
|
905
|
+
|
|
906
|
+
// Custom menus are macOS-only for now. Two things have to be solved first:
|
|
907
|
+
// webview.h's win32 loop never calls TranslateAcceleratorW, without which
|
|
908
|
+
// accelerators do not fire, and the GTK backend keeps both GTK 3 and GTK 4
|
|
909
|
+
// alive where GTK 4 removed GtkMenuBar. Reporting -1 lets the host say
|
|
910
|
+
// "unsupported here" rather than pretend it worked.
|
|
911
|
+
static int32_t apply_custom_menu(App *, const std::string &) { return -1; }
|
|
748
912
|
#endif
|
|
749
913
|
|
|
750
914
|
// Runs on the UI thread with no TS frame beneath it — see the note on the job
|
|
@@ -868,8 +1032,9 @@ int32_t wv_create(int32_t debug) {
|
|
|
868
1032
|
webview_t w = webview_create(debug, nullptr);
|
|
869
1033
|
if (!w) return -1;
|
|
870
1034
|
// After webview_create, which is what brings NSApplication into being.
|
|
871
|
-
//
|
|
1035
|
+
// Both are no-ops off macOS.
|
|
872
1036
|
install_main_menu();
|
|
1037
|
+
g_apps[i].std_menu_count = standard_menu_count();
|
|
873
1038
|
// Field-wise reset: App holds a thread and atomics, so it is not
|
|
874
1039
|
// copy-assignable from a temporary.
|
|
875
1040
|
g_apps[i].binds.clear();
|
|
@@ -877,6 +1042,9 @@ int32_t wv_create(int32_t debug) {
|
|
|
877
1042
|
g_apps[i].on_invoke_ctx = nullptr;
|
|
878
1043
|
g_apps[i].on_timer = nullptr;
|
|
879
1044
|
g_apps[i].on_timer_ctx = nullptr;
|
|
1045
|
+
g_apps[i].on_menu = nullptr;
|
|
1046
|
+
g_apps[i].on_menu_ctx = nullptr;
|
|
1047
|
+
g_apps[i].menu_ids.clear();
|
|
880
1048
|
g_apps[i].req.clear();
|
|
881
1049
|
g_apps[i].cur_id.clear();
|
|
882
1050
|
g_apps[i].reply.clear();
|
|
@@ -1179,6 +1347,27 @@ int32_t wv_dialog(int32_t h, int32_t kind, int32_t flags, const uint8_t *tp,
|
|
|
1179
1347
|
|
|
1180
1348
|
// ---- window control ---------------------------------------------------------
|
|
1181
1349
|
|
|
1350
|
+
// Renders the host's declarative menu. The spec is one row per line with 0x1f
|
|
1351
|
+
// between fields; the runtime flattens the tree, so nothing here parses JSON.
|
|
1352
|
+
// Returns -1 where custom menus are not supported yet (everything but macOS),
|
|
1353
|
+
// which the host reports rather than swallowing.
|
|
1354
|
+
int32_t wv_set_menu(int32_t h, const uint8_t *p, size_t n) {
|
|
1355
|
+
App *a = app_at(h);
|
|
1356
|
+
if (!a) return -1;
|
|
1357
|
+
return apply_custom_menu(a, to_str(p, n));
|
|
1358
|
+
}
|
|
1359
|
+
|
|
1360
|
+
// 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) {
|
|
1364
|
+
App *a = app_at(h);
|
|
1365
|
+
if (!a) return -1;
|
|
1366
|
+
a->on_menu = cb;
|
|
1367
|
+
a->on_menu_ctx = ctx;
|
|
1368
|
+
return 0;
|
|
1369
|
+
}
|
|
1370
|
+
|
|
1182
1371
|
int32_t wv_set_fullscreen(int32_t h, int32_t on) {
|
|
1183
1372
|
App *a = app_at(h);
|
|
1184
1373
|
if (!a) return -1;
|