janela 0.18.1 → 0.19.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/lib.mjs +3 -0
- package/package.json +1 -1
- package/runtime/ios.ts +128 -0
- package/runtime/janela.ts +108 -32
- package/shim/wvshim.cc +1294 -235
- package/templates/main.ts +19 -0
- package/templates/react/files/src-host/main.ts +19 -0
- package/templates/solid/files/src-host/main.ts +19 -0
- package/templates/svelte/files/src-host/main.ts +19 -0
- package/templates/vue/files/src-host/main.ts +19 -0
package/shim/wvshim.cc
CHANGED
|
@@ -22,10 +22,12 @@
|
|
|
22
22
|
|
|
23
23
|
#include <algorithm>
|
|
24
24
|
#include <atomic>
|
|
25
|
+
#include <cctype>
|
|
25
26
|
#include <chrono>
|
|
26
27
|
#include <condition_variable>
|
|
27
28
|
#include <cstdint>
|
|
28
29
|
#include <cstdio>
|
|
30
|
+
#include <cstdlib>
|
|
29
31
|
#include <cstring>
|
|
30
32
|
#include <filesystem>
|
|
31
33
|
#include <fstream>
|
|
@@ -40,8 +42,25 @@
|
|
|
40
42
|
// Win32; these add only what it does not use itself.
|
|
41
43
|
#if defined(_WIN32)
|
|
42
44
|
#include <commdlg.h>
|
|
45
|
+
// ShellAboutW: the standard Windows About box, which is what `predefined.about`
|
|
46
|
+
// should show here for the same reason macOS shows NSApplication's.
|
|
47
|
+
#include <shellapi.h>
|
|
43
48
|
#elif !defined(__APPLE__)
|
|
44
49
|
#include <gtk/gtk.h>
|
|
50
|
+
// The editing actions (Copy, Paste, Undo) are WebKit's, not GTK's, and this is
|
|
51
|
+
// the only header that declares them. It costs nothing to include: the build
|
|
52
|
+
// already compiles and links against WebKitGTK for the backend itself.
|
|
53
|
+
//
|
|
54
|
+
// Which header depends on the GTK version, because webkit2gtk-4.1 IS the GTK 3
|
|
55
|
+
// build — the GTK 4 one is a different API version under a different name. The
|
|
56
|
+
// build pins gtk+-3.0 today; this split mirrors the vendored backend's own so
|
|
57
|
+
// that a GTK 4 attempt fails in the renderer, where the reason is written
|
|
58
|
+
// down, rather than on an include line.
|
|
59
|
+
#if GTK_MAJOR_VERSION >= 4
|
|
60
|
+
#include <webkit/webkit.h>
|
|
61
|
+
#else
|
|
62
|
+
#include <webkit2/webkit2.h>
|
|
63
|
+
#endif
|
|
45
64
|
#endif
|
|
46
65
|
|
|
47
66
|
namespace {
|
|
@@ -89,9 +108,10 @@ struct App {
|
|
|
89
108
|
// `id` is Objective-C, so naming it here breaks the Linux and Windows
|
|
90
109
|
// builds. The Apple code casts on the way in and out.
|
|
91
110
|
std::vector<void *> menu_items;
|
|
92
|
-
//
|
|
93
|
-
//
|
|
94
|
-
|
|
111
|
+
// Whether the host ever called setMenu. If it never does, wv_run installs
|
|
112
|
+
// the whole standard bar, so an app that says nothing about menus still gets
|
|
113
|
+
// Cmd+Q and Cmd+V.
|
|
114
|
+
bool menu_set = false;
|
|
95
115
|
|
|
96
116
|
// Staging for the in-flight request.
|
|
97
117
|
std::string req; // JSON args array from JS
|
|
@@ -373,6 +393,54 @@ std::string json_array(const std::vector<std::string> &items) {
|
|
|
373
393
|
bool run_file_dialog(const DialogRequest &req, std::vector<std::string> &out,
|
|
374
394
|
std::string &error);
|
|
375
395
|
|
|
396
|
+
// ---- menus: the part that is the same everywhere ---------------------------
|
|
397
|
+
//
|
|
398
|
+
// Three renderers sit below this: AppKit, Win32 and GTK. They agree on the
|
|
399
|
+
// wire format (see the note above apply_custom_menu) and on who owns a click.
|
|
400
|
+
//
|
|
401
|
+
// Ownership is a real question because the callback is per-app while the menu
|
|
402
|
+
// is not: on macOS the menu bar is process-global, on Windows and Linux it
|
|
403
|
+
// belongs to a window. The rule that works for both is the same one — the
|
|
404
|
+
// last app to call setMenu owns the clicks — so it lives here rather than
|
|
405
|
+
// three times below.
|
|
406
|
+
static int32_t g_menu_owner = -1;
|
|
407
|
+
|
|
408
|
+
static void menu_clicked(long tag) {
|
|
409
|
+
App *a = g_menu_owner >= 0 ? app_at(g_menu_owner) : nullptr;
|
|
410
|
+
if (!a || !a->on_menu) return;
|
|
411
|
+
a->on_menu(static_cast<int32_t>(tag), a->on_menu_ctx);
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
static void claim_menu_owner(App *a) {
|
|
415
|
+
g_menu_owner = -1;
|
|
416
|
+
for (int32_t i = 0; i < 8; i++) {
|
|
417
|
+
if (app_at(i) == a) { g_menu_owner = i; break; }
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
// The app a handle-free call acts on.
|
|
422
|
+
//
|
|
423
|
+
// wv_perform_action takes no handle because on macOS it needs none: the
|
|
424
|
+
// responder chain and the key window are process state. Win32 and GTK do need
|
|
425
|
+
// a window, and `predefined.copy()` is imported as a bare function with no app
|
|
426
|
+
// in scope, so the shim resolves it here. janela opens one window, which is
|
|
427
|
+
// what makes "the app" unambiguous rather than a guess; the menu owner is
|
|
428
|
+
// preferred so a two-window future picks the one the user is driving.
|
|
429
|
+
// Fullscreen is both a window API (app.setFullscreen) and a menu action
|
|
430
|
+
// (predefined.fullscreen), and the action has to toggle, so it needs to read
|
|
431
|
+
// the state as well as set it. Declared here, defined by each renderer.
|
|
432
|
+
static int32_t platform_set_fullscreen(void *win, bool on);
|
|
433
|
+
static bool platform_is_fullscreen(void *win);
|
|
434
|
+
|
|
435
|
+
static App *current_app() {
|
|
436
|
+
App *a = g_menu_owner >= 0 ? app_at(g_menu_owner) : nullptr;
|
|
437
|
+
if (a) return a;
|
|
438
|
+
for (int32_t i = 0; i < 8; i++) {
|
|
439
|
+
if (g_apps[i].used) return &g_apps[i];
|
|
440
|
+
}
|
|
441
|
+
return nullptr;
|
|
442
|
+
}
|
|
443
|
+
|
|
376
444
|
#if defined(__APPLE__)
|
|
377
445
|
|
|
378
446
|
// A standard macOS main menu.
|
|
@@ -392,34 +460,21 @@ bool run_file_dialog(const DialogRequest &req, std::vector<std::string> &out,
|
|
|
392
460
|
// chain, so there is nothing to call back into TypeScript for and no new FFI
|
|
393
461
|
// surface. WKWebView answers the editing ones itself. Custom menus — a
|
|
394
462
|
// declarative table passed down from the host — are a separate feature.
|
|
395
|
-
static
|
|
463
|
+
static NSUInteger cocoa_mods(unsigned long symbolic);
|
|
464
|
+
static void add_app_submenu(id menubar, const std::string &name);
|
|
465
|
+
static void add_standard_editing_menus(id menubar);
|
|
466
|
+
|
|
467
|
+
// The application submenu, which AppKit requires be FIRST: it turns the main
|
|
468
|
+
// menu's first item into the app menu whatever that item is titled. A host
|
|
469
|
+
// menu starting with "File" would otherwise be rendered as the app menu and
|
|
470
|
+
// labelled with the bundle name, so this always goes in front of whatever the
|
|
471
|
+
// host asked for.
|
|
472
|
+
static id build_menubar_with_app_menu() {
|
|
396
473
|
using namespace webview::detail;
|
|
397
|
-
objc::autoreleasepool arp;
|
|
398
|
-
|
|
399
474
|
id app = objc::msg_send<id>(objc::get_class("NSApplication"),
|
|
400
475
|
objc::selector("sharedApplication"));
|
|
401
|
-
if (!app) return;
|
|
402
|
-
// Idempotent: an embedder that already installed a menu keeps it.
|
|
403
|
-
if (objc::msg_send<id>(app, objc::selector("mainMenu"))) return;
|
|
476
|
+
if (!app) return nullptr;
|
|
404
477
|
|
|
405
|
-
// NSEventModifierFlags. The default for a key equivalent is Command alone,
|
|
406
|
-
// so only the combinations need naming.
|
|
407
|
-
const NSUInteger kShift = 1UL << 17;
|
|
408
|
-
const NSUInteger kControl = 1UL << 18;
|
|
409
|
-
const NSUInteger kOption = 1UL << 19;
|
|
410
|
-
const NSUInteger kCommand = 1UL << 20;
|
|
411
|
-
|
|
412
|
-
auto str = [](const std::string &v) {
|
|
413
|
-
return cocoa::NSString_stringWithUTF8String(v);
|
|
414
|
-
};
|
|
415
|
-
auto alloc_init = [](const char *cls) {
|
|
416
|
-
return objc::msg_send<id>(
|
|
417
|
-
objc::msg_send<id>(objc::get_class(cls), objc::selector("alloc")),
|
|
418
|
-
objc::selector("init"));
|
|
419
|
-
};
|
|
420
|
-
|
|
421
|
-
// AppKit labels the first submenu from the bundle, not from this title, but
|
|
422
|
-
// "About <name>" and "Quit <name>" are ours to spell.
|
|
423
478
|
id process = objc::msg_send<id>(objc::get_class("NSProcessInfo"),
|
|
424
479
|
objc::selector("processInfo"));
|
|
425
480
|
const char *raw =
|
|
@@ -427,96 +482,128 @@ static void install_main_menu() {
|
|
|
427
482
|
objc::msg_send<id>(process, objc::selector("processName")),
|
|
428
483
|
objc::selector("UTF8String"))
|
|
429
484
|
: nullptr;
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
id menu = objc::msg_send<id>(
|
|
437
|
-
objc::msg_send<id>(objc::get_class("NSMenu"), objc::selector("alloc")),
|
|
438
|
-
objc::selector("initWithTitle:"), str(title));
|
|
439
|
-
objc::msg_send<void>(holder, objc::selector("setSubmenu:"), menu);
|
|
440
|
-
objc::msg_send<void>(menubar, objc::selector("addItem:"), holder);
|
|
441
|
-
return menu;
|
|
442
|
-
};
|
|
485
|
+
id menubar = objc::msg_send<id>(
|
|
486
|
+
objc::msg_send<id>(objc::get_class("NSMenu"), objc::selector("alloc")),
|
|
487
|
+
objc::selector("init"));
|
|
488
|
+
add_app_submenu(menubar, raw ? raw : "App");
|
|
489
|
+
return menubar;
|
|
490
|
+
}
|
|
443
491
|
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
sel ? objc::selector(sel) : nullptr, str(key));
|
|
451
|
-
if (mods) {
|
|
452
|
-
objc::msg_send<void>(
|
|
453
|
-
it, objc::selector("setKeyEquivalentModifierMask:"), mods);
|
|
454
|
-
}
|
|
455
|
-
objc::msg_send<void>(menu, objc::selector("addItem:"), it);
|
|
456
|
-
};
|
|
457
|
-
auto separator = [&](id menu) {
|
|
458
|
-
objc::msg_send<void>(menu, objc::selector("addItem:"),
|
|
459
|
-
objc::msg_send<id>(objc::get_class("NSMenuItem"),
|
|
460
|
-
objc::selector("separatorItem")));
|
|
461
|
-
};
|
|
492
|
+
// The whole standard bar. Installed only when the host never calls setMenu —
|
|
493
|
+
// a scratch app still gets Cmd+Q and Cmd+V, while an app that sets its own
|
|
494
|
+
// menu owns the bar beyond the app submenu.
|
|
495
|
+
static void install_main_menu() {
|
|
496
|
+
using namespace webview::detail;
|
|
497
|
+
objc::autoreleasepool arp;
|
|
462
498
|
|
|
463
|
-
id
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
item(app_menu, "Hide Others", "hideOtherApplications:", "h",
|
|
468
|
-
kOption | kCommand);
|
|
469
|
-
item(app_menu, "Show All", "unhideAllApplications:", "", 0);
|
|
470
|
-
separator(app_menu);
|
|
471
|
-
// performClose:, not terminate: — measured, not assumed. Both quit and
|
|
472
|
-
// both exit 0, but terminate: exits the process itself, so the host never
|
|
473
|
-
// returns from wv_run and anything after app.run() is skipped;
|
|
474
|
-
// performClose: goes through the same window-close path the red button
|
|
475
|
-
// uses, the run loop unwinds, and the host prints its own "run returned 0".
|
|
476
|
-
// One shutdown path instead of two. muda picks terminate: because Tauri's
|
|
477
|
-
// app logic is native and multi-window; janela is single-window, so closing
|
|
478
|
-
// the window IS quitting. If janela ever grows multiple windows this has to
|
|
479
|
-
// become a real Quit that closes all of them.
|
|
480
|
-
//
|
|
481
|
-
// (An earlier note here claimed performClose: did not fire at all. That was
|
|
482
|
-
// a bad test: posted key events go to the FRONTMOST app, and the app was not
|
|
483
|
-
// active. With it activated first, both actions work.)
|
|
484
|
-
item(app_menu, "Quit " + name, "performClose:", "q", 0);
|
|
485
|
-
|
|
486
|
-
id edit = submenu("Edit");
|
|
487
|
-
item(edit, "Undo", "undo:", "z", 0);
|
|
488
|
-
item(edit, "Redo", "redo:", "z", kShift | kCommand);
|
|
489
|
-
separator(edit);
|
|
490
|
-
item(edit, "Cut", "cut:", "x", 0);
|
|
491
|
-
item(edit, "Copy", "copy:", "c", 0);
|
|
492
|
-
item(edit, "Paste", "paste:", "v", 0);
|
|
493
|
-
item(edit, "Select All", "selectAll:", "a", 0);
|
|
494
|
-
|
|
495
|
-
id view = submenu("View");
|
|
496
|
-
item(view, "Enter Full Screen", "toggleFullScreen:", "f",
|
|
497
|
-
kControl | kCommand);
|
|
498
|
-
|
|
499
|
-
id window = submenu("Window");
|
|
500
|
-
item(window, "Minimize", "performMiniaturize:", "m", 0);
|
|
501
|
-
item(window, "Close", "performClose:", "w", 0);
|
|
499
|
+
id app = objc::msg_send<id>(objc::get_class("NSApplication"),
|
|
500
|
+
objc::selector("sharedApplication"));
|
|
501
|
+
if (!app) return;
|
|
502
|
+
if (objc::msg_send<id>(app, objc::selector("mainMenu"))) return;
|
|
502
503
|
|
|
504
|
+
id menubar = build_menubar_with_app_menu();
|
|
505
|
+
if (!menubar) return;
|
|
506
|
+
add_standard_editing_menus(menubar);
|
|
503
507
|
objc::msg_send<void>(app, objc::selector("setMainMenu:"), menubar);
|
|
504
|
-
// Lets AppKit add the standard Window-menu bookkeeping (window list,
|
|
505
|
-
// Bring All to Front).
|
|
506
|
-
objc::msg_send<void>(app, objc::selector("setWindowsMenu:"), window);
|
|
507
508
|
}
|
|
508
509
|
|
|
509
|
-
//
|
|
510
|
-
|
|
511
|
-
|
|
510
|
+
// Shared by both paths: a submenu of items wired to standard selectors.
|
|
511
|
+
static void menu_add(id menu, const std::string &title, const char *sel,
|
|
512
|
+
const std::string &key, unsigned long symbolic) {
|
|
512
513
|
using namespace webview::detail;
|
|
513
|
-
objc::
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
514
|
+
id it = objc::msg_send<id>(
|
|
515
|
+
objc::msg_send<id>(objc::get_class("NSMenuItem"), objc::selector("alloc")),
|
|
516
|
+
objc::selector("initWithTitle:action:keyEquivalent:"),
|
|
517
|
+
cocoa::NSString_stringWithUTF8String(title),
|
|
518
|
+
sel ? objc::selector(sel) : nullptr,
|
|
519
|
+
cocoa::NSString_stringWithUTF8String(key));
|
|
520
|
+
objc::msg_send<void>(it, objc::selector("setKeyEquivalentModifierMask:"),
|
|
521
|
+
cocoa_mods(symbolic));
|
|
522
|
+
objc::msg_send<void>(menu, objc::selector("addItem:"), it);
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
static void menu_add_separator(id menu) {
|
|
526
|
+
using namespace webview::detail;
|
|
527
|
+
objc::msg_send<void>(menu, objc::selector("addItem:"),
|
|
528
|
+
objc::msg_send<id>(objc::get_class("NSMenuItem"),
|
|
529
|
+
objc::selector("separatorItem")));
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
static id menu_add_submenu(id menubar, const std::string &title) {
|
|
533
|
+
using namespace webview::detail;
|
|
534
|
+
id holder = objc::msg_send<id>(
|
|
535
|
+
objc::msg_send<id>(objc::get_class("NSMenuItem"), objc::selector("alloc")),
|
|
536
|
+
objc::selector("init"));
|
|
537
|
+
id menu = objc::msg_send<id>(
|
|
538
|
+
objc::msg_send<id>(objc::get_class("NSMenu"), objc::selector("alloc")),
|
|
539
|
+
objc::selector("initWithTitle:"),
|
|
540
|
+
cocoa::NSString_stringWithUTF8String(title));
|
|
541
|
+
objc::msg_send<void>(holder, objc::selector("setTitle:"),
|
|
542
|
+
cocoa::NSString_stringWithUTF8String(title));
|
|
543
|
+
objc::msg_send<void>(holder, objc::selector("setSubmenu:"), menu);
|
|
544
|
+
objc::msg_send<void>(menubar, objc::selector("addItem:"), holder);
|
|
545
|
+
return menu;
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
static void add_app_submenu(id menubar, const std::string &name) {
|
|
549
|
+
id m = menu_add_submenu(menubar, name);
|
|
550
|
+
menu_add(m, "About " + name, "orderFrontStandardAboutPanel:", "", 0);
|
|
551
|
+
menu_add_separator(m);
|
|
552
|
+
menu_add(m, "Hide " + name, "hide:", "h", 1);
|
|
553
|
+
menu_add(m, "Hide Others", "hideOtherApplications:", "h", 1 | 4);
|
|
554
|
+
menu_add(m, "Show All", "unhideAllApplications:", "", 0);
|
|
555
|
+
menu_add_separator(m);
|
|
556
|
+
// performClose:, not terminate: — measured: terminate: exits the process
|
|
557
|
+
// itself so the host never returns from wv_run, while performClose: unwinds
|
|
558
|
+
// through the window-close path the red button uses.
|
|
559
|
+
menu_add(m, "Quit " + name, "performClose:", "q", 1);
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
// True if the bar already has a submenu with this title — the host declaring
|
|
563
|
+
// its own "Edit" must not get two.
|
|
564
|
+
static bool menubar_has(id menubar, const std::string &title) {
|
|
565
|
+
using namespace webview::detail;
|
|
566
|
+
NSUInteger n = objc::msg_send<NSUInteger>(menubar,
|
|
567
|
+
objc::selector("numberOfItems"));
|
|
568
|
+
for (NSUInteger i = 0; i < n; i++) {
|
|
569
|
+
id item = objc::msg_send<id>(menubar, objc::selector("itemAtIndex:"), i);
|
|
570
|
+
const char *got = cocoa::NSString_get_UTF8String(
|
|
571
|
+
objc::msg_send<id>(item, objc::selector("title")));
|
|
572
|
+
if (got && title == got) return true;
|
|
573
|
+
}
|
|
574
|
+
return false;
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
// Each of the three is filled in only if the host has not declared one under
|
|
578
|
+
// the same title. Guarded SEPARATELY, and not with an early return: a host
|
|
579
|
+
// that declares its own Edit would otherwise silently lose View and Window
|
|
580
|
+
// too — measured, before this was three checks instead of one.
|
|
581
|
+
static void add_standard_editing_menus(id menubar) {
|
|
582
|
+
using namespace webview::detail;
|
|
583
|
+
if (!menubar_has(menubar, "Edit")) {
|
|
584
|
+
id edit = menu_add_submenu(menubar, "Edit");
|
|
585
|
+
menu_add(edit, "Undo", "undo:", "z", 1);
|
|
586
|
+
menu_add(edit, "Redo", "redo:", "z", 1 | 2);
|
|
587
|
+
menu_add_separator(edit);
|
|
588
|
+
menu_add(edit, "Cut", "cut:", "x", 1);
|
|
589
|
+
menu_add(edit, "Copy", "copy:", "c", 1);
|
|
590
|
+
menu_add(edit, "Paste", "paste:", "v", 1);
|
|
591
|
+
menu_add(edit, "Select All", "selectAll:", "a", 1);
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
if (!menubar_has(menubar, "View")) {
|
|
595
|
+
id view = menu_add_submenu(menubar, "View");
|
|
596
|
+
menu_add(view, "Enter Full Screen", "toggleFullScreen:", "f", 1 | 8);
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
if (!menubar_has(menubar, "Window")) {
|
|
600
|
+
id window = menu_add_submenu(menubar, "Window");
|
|
601
|
+
menu_add(window, "Minimize", "performMiniaturize:", "m", 1);
|
|
602
|
+
menu_add(window, "Close", "performClose:", "w", 1);
|
|
603
|
+
id app = objc::msg_send<id>(objc::get_class("NSApplication"),
|
|
604
|
+
objc::selector("sharedApplication"));
|
|
605
|
+
objc::msg_send<void>(app, objc::selector("setWindowsMenu:"), window);
|
|
606
|
+
}
|
|
520
607
|
}
|
|
521
608
|
|
|
522
609
|
// ---- custom menus ------------------------------------------------------
|
|
@@ -527,20 +614,12 @@ static size_t standard_menu_count() {
|
|
|
527
614
|
//
|
|
528
615
|
// S<US>Label open a submenu
|
|
529
616
|
// E close it
|
|
530
|
-
// I<US>tag<US>Label<US>key<US>mods
|
|
617
|
+
// I<US>tag<US>Label<US>key<US>mods<US>enabled<US>checked<US>checkable
|
|
531
618
|
// - a separator
|
|
532
619
|
//
|
|
533
|
-
// A click sends the item's tag back, which indexes
|
|
534
|
-
//
|
|
535
|
-
// shape as on_invoke.
|
|
536
|
-
// app owns it at a time.
|
|
537
|
-
static int32_t g_menu_owner = -1;
|
|
538
|
-
|
|
539
|
-
static void menu_clicked(long tag) {
|
|
540
|
-
App *a = g_menu_owner >= 0 ? app_at(g_menu_owner) : nullptr;
|
|
541
|
-
if (!a || !a->on_menu) return;
|
|
542
|
-
a->on_menu(static_cast<int32_t>(tag), a->on_menu_ctx);
|
|
543
|
-
}
|
|
620
|
+
// A click sends the item's tag back, which indexes the host's handler
|
|
621
|
+
// registry, and the tag goes up to TypeScript on the retained on_menu
|
|
622
|
+
// callback — the same shape as on_invoke.
|
|
544
623
|
|
|
545
624
|
// One shared target for every custom item; the tag says which one fired.
|
|
546
625
|
static id menu_target() {
|
|
@@ -568,6 +647,103 @@ static id menu_target() {
|
|
|
568
647
|
return instance;
|
|
569
648
|
}
|
|
570
649
|
|
|
650
|
+
// The platform's own menu items, by the host's action name.
|
|
651
|
+
//
|
|
652
|
+
// These carry a SELECTOR rather than a callback: Paste has to travel up the
|
|
653
|
+
// responder chain for WKWebView to handle it, so the behaviour is AppKit's and
|
|
654
|
+
// the click never reaches TypeScript. An unknown action renders nothing rather
|
|
655
|
+
// than a dead item.
|
|
656
|
+
struct Predefined {
|
|
657
|
+
const char *action;
|
|
658
|
+
const char *title;
|
|
659
|
+
const char *selector;
|
|
660
|
+
const char *key;
|
|
661
|
+
unsigned long mods; // symbolic, as the host sends them
|
|
662
|
+
};
|
|
663
|
+
|
|
664
|
+
static const Predefined *predefined_for(const std::string &action) {
|
|
665
|
+
static const Predefined table[] = {
|
|
666
|
+
{"about", "About", "orderFrontStandardAboutPanel:", "", 0},
|
|
667
|
+
{"services", "Services", nullptr, "", 0},
|
|
668
|
+
{"hide", "Hide", "hide:", "h", 1},
|
|
669
|
+
{"hideOthers", "Hide Others", "hideOtherApplications:", "h", 1 | 4},
|
|
670
|
+
{"showAll", "Show All", "unhideAllApplications:", "", 0},
|
|
671
|
+
// performClose:, not terminate: — see the note on the standard menu.
|
|
672
|
+
{"quit", "Quit", "performClose:", "q", 1},
|
|
673
|
+
{"undo", "Undo", "undo:", "z", 1},
|
|
674
|
+
{"redo", "Redo", "redo:", "z", 1 | 2},
|
|
675
|
+
{"cut", "Cut", "cut:", "x", 1},
|
|
676
|
+
{"copy", "Copy", "copy:", "c", 1},
|
|
677
|
+
{"paste", "Paste", "paste:", "v", 1},
|
|
678
|
+
{"selectAll", "Select All", "selectAll:", "a", 1},
|
|
679
|
+
{"minimize", "Minimize", "performMiniaturize:", "m", 1},
|
|
680
|
+
{"zoom", "Zoom", "performZoom:", "", 0},
|
|
681
|
+
{"fullscreen", "Enter Full Screen", "toggleFullScreen:", "f", 1 | 8},
|
|
682
|
+
{"closeWindow", "Close Window", "performClose:", "w", 1},
|
|
683
|
+
};
|
|
684
|
+
for (const Predefined &p : table) {
|
|
685
|
+
if (action == p.action) return &p;
|
|
686
|
+
}
|
|
687
|
+
return nullptr;
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
// Perform a platform action right now, from wherever the host calls it.
|
|
691
|
+
//
|
|
692
|
+
// The interesting case is copy/paste/undo: those are not "do this" but a
|
|
693
|
+
// SELECTOR sent up the responder chain, and by the time a TypeScript handler
|
|
694
|
+
// runs the menu click has already been delivered to us. sendAction:to:nil
|
|
695
|
+
// walks the chain at this moment instead, which is what lets it still reach
|
|
696
|
+
// the WKWebView's editing context.
|
|
697
|
+
static int32_t perform_action(const std::string &action) {
|
|
698
|
+
using namespace webview::detail;
|
|
699
|
+
objc::autoreleasepool arp;
|
|
700
|
+
const Predefined *pd = predefined_for(action);
|
|
701
|
+
if (!pd || !pd->selector) return -1;
|
|
702
|
+
id app = objc::msg_send<id>(objc::get_class("NSApplication"),
|
|
703
|
+
objc::selector("sharedApplication"));
|
|
704
|
+
if (!app) return -1;
|
|
705
|
+
|
|
706
|
+
// to:nil walks the responder chain from the key window's first responder,
|
|
707
|
+
// which is how this reaches the WKWebView's editing context.
|
|
708
|
+
if (objc::msg_send<bool>(app, objc::selector("sendAction:to:from:"),
|
|
709
|
+
objc::selector(pd->selector), nullptr, app)) {
|
|
710
|
+
return 0;
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
// Falls back to addressing the first responder directly. Observed once, on
|
|
714
|
+
// the very first launch: the window had not become key yet, so the chain
|
|
715
|
+
// walk above found nobody. Three runs in a row succeed on the first path
|
|
716
|
+
// once the app is settled, so this is a belt for the startup edge.
|
|
717
|
+
id key_window = objc::msg_send<id>(app, objc::selector("keyWindow"));
|
|
718
|
+
id responder = key_window ? objc::msg_send<id>(
|
|
719
|
+
key_window, objc::selector("firstResponder"))
|
|
720
|
+
: nullptr;
|
|
721
|
+
if (responder &&
|
|
722
|
+
objc::msg_send<bool>(app, objc::selector("sendAction:to:from:"),
|
|
723
|
+
objc::selector(pd->selector), responder, app)) {
|
|
724
|
+
return 0;
|
|
725
|
+
}
|
|
726
|
+
return -1;
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
|
|
730
|
+
// The host sends modifiers symbolically; this is the AppKit half of the map.
|
|
731
|
+
// MOD_PRIMARY is Command here — on Windows and Linux the same bit becomes
|
|
732
|
+
// Control, which is the whole point of sending intent rather than constants.
|
|
733
|
+
static NSUInteger cocoa_mods(unsigned long symbolic) {
|
|
734
|
+
const NSUInteger kShift = 1UL << 17;
|
|
735
|
+
const NSUInteger kControl = 1UL << 18;
|
|
736
|
+
const NSUInteger kOption = 1UL << 19;
|
|
737
|
+
const NSUInteger kCommand = 1UL << 20;
|
|
738
|
+
NSUInteger out = 0;
|
|
739
|
+
if (symbolic & 1) out |= kCommand; // primary
|
|
740
|
+
if (symbolic & 2) out |= kShift;
|
|
741
|
+
if (symbolic & 4) out |= kOption;
|
|
742
|
+
if (symbolic & 8) out |= kControl;
|
|
743
|
+
if (symbolic & 16) out |= kCommand;
|
|
744
|
+
return out;
|
|
745
|
+
}
|
|
746
|
+
|
|
571
747
|
// Retains the item under its tag so a later setEnabled/setChecked/setLabel can
|
|
572
748
|
// find it. The vector is sized to fit rather than pushed, because the host's
|
|
573
749
|
// tags are registry indices and need not arrive in order.
|
|
@@ -592,17 +768,18 @@ static int32_t apply_custom_menu(App *a, const std::string &spec) {
|
|
|
592
768
|
|
|
593
769
|
id app = objc::msg_send<id>(objc::get_class("NSApplication"),
|
|
594
770
|
objc::selector("sharedApplication"));
|
|
595
|
-
|
|
771
|
+
if (!app) return -1;
|
|
772
|
+
|
|
773
|
+
// A fresh bar every time: the host owns what it declared, so setMenu
|
|
774
|
+
// replaces rather than appends and the menu can shrink as well as grow.
|
|
775
|
+
//
|
|
776
|
+
// The application submenu is prepended regardless, because AppKit turns the
|
|
777
|
+
// main menu's FIRST item into the app menu whatever it is titled — a host
|
|
778
|
+
// menu starting with "File" would otherwise be rendered as the app menu and
|
|
779
|
+
// labelled with the bundle name. It is also the floor under Cmd+Q.
|
|
780
|
+
id menubar = build_menubar_with_app_menu();
|
|
596
781
|
if (!menubar) return -1;
|
|
597
782
|
|
|
598
|
-
// Drop whatever a previous call added, so setMenu is idempotent and can
|
|
599
|
-
// shrink the bar as well as grow it.
|
|
600
|
-
for (size_t n = objc::msg_send<NSUInteger>(menubar,
|
|
601
|
-
objc::selector("numberOfItems"));
|
|
602
|
-
n > a->std_menu_count; n--) {
|
|
603
|
-
objc::msg_send<void>(menubar, objc::selector("removeItemAtIndex:"),
|
|
604
|
-
static_cast<NSInteger>(n - 1));
|
|
605
|
-
}
|
|
606
783
|
for (void *old_item : a->menu_items) {
|
|
607
784
|
if (old_item) objc::release(static_cast<id>(old_item));
|
|
608
785
|
}
|
|
@@ -661,9 +838,8 @@ static int32_t apply_custom_menu(App *a, const std::string &spec) {
|
|
|
661
838
|
// Always set the mask, including 0: AppKit's default for a key
|
|
662
839
|
// equivalent is Command, so leaving it alone would turn an accelerator
|
|
663
840
|
// with no modifiers into a Command shortcut.
|
|
664
|
-
NSUInteger mods = static_cast<NSUInteger>(strtoul(f[4].c_str(), nullptr, 10));
|
|
665
841
|
objc::msg_send<void>(it, objc::selector("setKeyEquivalentModifierMask:"),
|
|
666
|
-
|
|
842
|
+
cocoa_mods(strtoul(f[4].c_str(), nullptr, 10)));
|
|
667
843
|
objc::msg_send<void>(it, objc::selector("setEnabled:"), f[5] == "1");
|
|
668
844
|
// f[7] says the item is checkable. AppKit lets any item carry a state,
|
|
669
845
|
// so it is unused here — it is on the wire for the GTK renderer, which
|
|
@@ -675,10 +851,50 @@ static int32_t apply_custom_menu(App *a, const std::string &spec) {
|
|
|
675
851
|
}
|
|
676
852
|
}
|
|
677
853
|
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
854
|
+
// The floor goes back on AFTER the host's menus, which also puts them in
|
|
855
|
+
// macOS's conventional order: App, File, …, Edit, View, Window. Without this
|
|
856
|
+
// any setMenu at all would cost the app ⌘C and ⌘V — the exact bug the
|
|
857
|
+
// standard bar exists to prevent, reintroduced by the feature that was
|
|
858
|
+
// supposed to build on it. A host that declares its own Edit, View or Window
|
|
859
|
+
// keeps it; only the missing ones are filled in.
|
|
860
|
+
add_standard_editing_menus(menubar);
|
|
861
|
+
|
|
862
|
+
objc::msg_send<void>(app, objc::selector("setMainMenu:"), menubar);
|
|
863
|
+
a->menu_set = true;
|
|
864
|
+
claim_menu_owner(a);
|
|
865
|
+
return 0;
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
// The live-item setters. Each takes the host's tag, which is a registry index
|
|
869
|
+
// in TypeScript, and -1 for an unknown one rather than silence — a stale
|
|
870
|
+
// handle should be visible, not a no-op.
|
|
871
|
+
static int32_t menu_set_enabled(App *a, int32_t tag, int32_t on) {
|
|
872
|
+
using namespace webview::detail;
|
|
873
|
+
objc::autoreleasepool arp;
|
|
874
|
+
id it = item_at(a, tag);
|
|
875
|
+
if (!it) return -1;
|
|
876
|
+
objc::msg_send<void>(it, objc::selector("setEnabled:"), on != 0);
|
|
877
|
+
return 0;
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
static int32_t menu_set_checked(App *a, int32_t tag, int32_t on) {
|
|
881
|
+
using namespace webview::detail;
|
|
882
|
+
objc::autoreleasepool arp;
|
|
883
|
+
id it = item_at(a, tag);
|
|
884
|
+
if (!it) return -1;
|
|
885
|
+
// NSControlStateValueOn / Off.
|
|
886
|
+
objc::msg_send<void>(it, objc::selector("setState:"),
|
|
887
|
+
static_cast<NSInteger>(on != 0 ? 1 : 0));
|
|
888
|
+
return 0;
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
static int32_t menu_set_label(App *a, int32_t tag, const std::string &label) {
|
|
892
|
+
using namespace webview::detail;
|
|
893
|
+
objc::autoreleasepool arp;
|
|
894
|
+
id it = item_at(a, tag);
|
|
895
|
+
if (!it) return -1;
|
|
896
|
+
objc::msg_send<void>(it, objc::selector("setTitle:"),
|
|
897
|
+
cocoa::NSString_stringWithUTF8String(label));
|
|
682
898
|
return 0;
|
|
683
899
|
}
|
|
684
900
|
|
|
@@ -763,6 +979,41 @@ bool run_file_dialog(const DialogRequest &req, std::vector<std::string> &out,
|
|
|
763
979
|
return !out.empty();
|
|
764
980
|
}
|
|
765
981
|
|
|
982
|
+
|
|
983
|
+
// Releases the retained items. The menu bar itself belongs to NSApplication,
|
|
984
|
+
// which replaces it wholesale on the next setMenu.
|
|
985
|
+
static void menu_teardown(App *a) {
|
|
986
|
+
using namespace webview::detail;
|
|
987
|
+
for (void *it : a->menu_items) {
|
|
988
|
+
if (it) objc::release(static_cast<id>(it));
|
|
989
|
+
}
|
|
990
|
+
a->menu_items.clear();
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
// AppKit parents the webview eagerly, so nothing here has to wait for run.
|
|
994
|
+
static void menu_realize(App *) {}
|
|
995
|
+
|
|
996
|
+
static bool platform_is_fullscreen(void *win) {
|
|
997
|
+
using namespace webview::detail;
|
|
998
|
+
objc::autoreleasepool arp;
|
|
999
|
+
// NSWindowStyleMaskFullScreen.
|
|
1000
|
+
const NSUInteger full = 1UL << 14;
|
|
1001
|
+
NSUInteger mask = objc::msg_send<NSUInteger>(static_cast<id>(win),
|
|
1002
|
+
objc::selector("styleMask"));
|
|
1003
|
+
return (mask & full) != 0;
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
static int32_t platform_set_fullscreen(void *win, bool on) {
|
|
1007
|
+
using namespace webview::detail;
|
|
1008
|
+
objc::autoreleasepool arp;
|
|
1009
|
+
// toggleFullScreen: only toggles, so read the current state first and leave
|
|
1010
|
+
// it alone when it already matches.
|
|
1011
|
+
if (platform_is_fullscreen(win) != on) {
|
|
1012
|
+
objc::msg_send<void>(static_cast<id>(win),
|
|
1013
|
+
objc::selector("toggleFullScreen:"), nullptr);
|
|
1014
|
+
}
|
|
1015
|
+
return 0;
|
|
1016
|
+
}
|
|
766
1017
|
#elif defined(_WIN32)
|
|
767
1018
|
|
|
768
1019
|
std::string from_wide(const wchar_t *w, int wlen) {
|
|
@@ -932,20 +1183,889 @@ bool run_file_dialog(const DialogRequest &req, std::vector<std::string> &out,
|
|
|
932
1183
|
|
|
933
1184
|
#endif
|
|
934
1185
|
|
|
935
|
-
#if
|
|
936
|
-
//
|
|
937
|
-
//
|
|
938
|
-
|
|
939
|
-
|
|
1186
|
+
#if defined(__APPLE__)
|
|
1187
|
+
// The AppKit renderer is further up, beside the standard menu bar it shares
|
|
1188
|
+
// its helpers with. This chain covers the other two.
|
|
1189
|
+
#elif defined(_WIN32)
|
|
1190
|
+
|
|
1191
|
+
// ---- Win32 menus -----------------------------------------------------------
|
|
1192
|
+
//
|
|
1193
|
+
// There is no standard bar to install here. Alt+F4 is a window-manager
|
|
1194
|
+
// message the win32 backend already answers as WM_CLOSE, and Ctrl+C/V/Z/A are
|
|
1195
|
+
// handled inside WebView2 — so unlike macOS, an app that says nothing about
|
|
1196
|
+
// menus loses nothing by having none. A menu on Windows is a feature the app
|
|
1197
|
+
// asked for, not a floor under the keyboard.
|
|
940
1198
|
static void install_main_menu() {}
|
|
941
|
-
static size_t standard_menu_count() { return 0; }
|
|
942
1199
|
|
|
943
|
-
//
|
|
944
|
-
//
|
|
945
|
-
//
|
|
946
|
-
|
|
947
|
-
|
|
1200
|
+
// The window that owns the menu bar, and the wndproc that was there before we
|
|
1201
|
+
// subclassed it. Process-wide because janela opens one window; the menu bar
|
|
1202
|
+
// itself is per-window, so a second one would need these per App.
|
|
1203
|
+
static HWND g_menu_hwnd = nullptr;
|
|
1204
|
+
static WNDPROC g_prev_wndproc = nullptr;
|
|
1205
|
+
static HACCEL g_accel = nullptr;
|
|
1206
|
+
static HHOOK g_accel_hook = nullptr;
|
|
1207
|
+
static HMENU g_menu_bar = nullptr;
|
|
1208
|
+
static bool g_win_fullscreen = false;
|
|
1209
|
+
|
|
1210
|
+
// WM_COMMAND's id is 16 bits, and Windows reserves the low range for its own
|
|
1211
|
+
// controls (IDOK is 1), so item ids start well clear of it.
|
|
1212
|
+
static const UINT kIdItemBase = 0x4000;
|
|
1213
|
+
|
|
1214
|
+
static std::wstring widen(const std::string &s) {
|
|
1215
|
+
if (s.empty()) return std::wstring();
|
|
1216
|
+
int n = MultiByteToWideChar(CP_UTF8, 0, s.c_str(), static_cast<int>(s.size()),
|
|
1217
|
+
nullptr, 0);
|
|
1218
|
+
if (n <= 0) return std::wstring();
|
|
1219
|
+
std::wstring out(static_cast<size_t>(n), L'\0');
|
|
1220
|
+
MultiByteToWideChar(CP_UTF8, 0, s.c_str(), static_cast<int>(s.size()),
|
|
1221
|
+
&out[0], n);
|
|
1222
|
+
return out;
|
|
1223
|
+
}
|
|
1224
|
+
|
|
1225
|
+
// A literal ampersand in a menu label is a mnemonic marker, so a label like
|
|
1226
|
+
// "Cut && Paste" has to be escaped or Windows eats the character and
|
|
1227
|
+
// underlines the P.
|
|
1228
|
+
static std::string escape_mnemonics(const std::string &s) {
|
|
1229
|
+
std::string out;
|
|
1230
|
+
for (char c : s) {
|
|
1231
|
+
out.push_back(c);
|
|
1232
|
+
if (c == '&') out.push_back('&');
|
|
1233
|
+
}
|
|
1234
|
+
return out;
|
|
1235
|
+
}
|
|
1236
|
+
|
|
1237
|
+
// The host sends a lowercased key name. A single character goes through the
|
|
1238
|
+
// keyboard layout; everything else is a named key, which is the only way F5 or
|
|
1239
|
+
// Delete can be an accelerator at all. `shown` is what the menu prints, which
|
|
1240
|
+
// is not always the name that was sent.
|
|
1241
|
+
static bool vk_for_key(const std::string &key, WORD &vk, std::string &shown) {
|
|
1242
|
+
if (key.empty()) return false;
|
|
1243
|
+
if (key.size() == 1) {
|
|
1244
|
+
SHORT s = VkKeyScanW(static_cast<wchar_t>(
|
|
1245
|
+
static_cast<unsigned char>(std::toupper(
|
|
1246
|
+
static_cast<unsigned char>(key[0])))));
|
|
1247
|
+
if (s == -1) return false;
|
|
1248
|
+
vk = static_cast<WORD>(s & 0xFF);
|
|
1249
|
+
shown = std::string(1, static_cast<char>(std::toupper(
|
|
1250
|
+
static_cast<unsigned char>(key[0]))));
|
|
1251
|
+
return true;
|
|
1252
|
+
}
|
|
1253
|
+
if (key[0] == 'f' && key.size() <= 3) {
|
|
1254
|
+
int n = atoi(key.c_str() + 1);
|
|
1255
|
+
if (n >= 1 && n <= 24) {
|
|
1256
|
+
vk = static_cast<WORD>(VK_F1 + n - 1);
|
|
1257
|
+
shown = "F" + std::to_string(n);
|
|
1258
|
+
return true;
|
|
1259
|
+
}
|
|
1260
|
+
}
|
|
1261
|
+
struct Named { const char *name; WORD vk; const char *shown; };
|
|
1262
|
+
static const Named table[] = {
|
|
1263
|
+
{"enter", VK_RETURN, "Enter"}, {"return", VK_RETURN, "Enter"},
|
|
1264
|
+
{"escape", VK_ESCAPE, "Esc"}, {"esc", VK_ESCAPE, "Esc"},
|
|
1265
|
+
{"space", VK_SPACE, "Space"}, {"tab", VK_TAB, "Tab"},
|
|
1266
|
+
{"backspace", VK_BACK, "Backspace"},
|
|
1267
|
+
{"delete", VK_DELETE, "Del"}, {"del", VK_DELETE, "Del"},
|
|
1268
|
+
{"insert", VK_INSERT, "Ins"}, {"up", VK_UP, "Up"},
|
|
1269
|
+
{"down", VK_DOWN, "Down"}, {"left", VK_LEFT, "Left"},
|
|
1270
|
+
{"right", VK_RIGHT, "Right"}, {"home", VK_HOME, "Home"},
|
|
1271
|
+
{"end", VK_END, "End"}, {"pageup", VK_PRIOR, "PgUp"},
|
|
1272
|
+
{"pagedown", VK_NEXT, "PgDn"},
|
|
1273
|
+
};
|
|
1274
|
+
for (const Named &n : table) {
|
|
1275
|
+
if (key == n.name) {
|
|
1276
|
+
vk = n.vk;
|
|
1277
|
+
shown = n.shown;
|
|
1278
|
+
return true;
|
|
1279
|
+
}
|
|
1280
|
+
}
|
|
1281
|
+
return false;
|
|
1282
|
+
}
|
|
1283
|
+
|
|
1284
|
+
// What the menu prints to the right of the label. MOD_PRIMARY is Control here
|
|
1285
|
+
// — the same symbolic bit AppKit renders as Command, which is the whole point
|
|
1286
|
+
// of the host sending intent rather than platform constants.
|
|
1287
|
+
static std::string accel_text(const std::string &shown, unsigned long mods) {
|
|
1288
|
+
std::string out;
|
|
1289
|
+
if (mods & (1 | 8)) out += "Ctrl+";
|
|
1290
|
+
if (mods & 4) out += "Alt+";
|
|
1291
|
+
if (mods & 2) out += "Shift+";
|
|
1292
|
+
return out.empty() && shown.empty() ? std::string() : out + shown;
|
|
1293
|
+
}
|
|
1294
|
+
|
|
1295
|
+
// ---- the editing commands, through the DevTools protocol -------------------
|
|
1296
|
+
//
|
|
1297
|
+
// WebView2 runs the page out of process and exposes no copy/paste entry point,
|
|
1298
|
+
// and a webview blocks document.execCommand("paste"). What it DOES expose is
|
|
1299
|
+
// the DevTools protocol, and CDP's Input.dispatchKeyEvent takes a `commands`
|
|
1300
|
+
// array: Blink's own editor commands, the same ones a real Ctrl+V runs,
|
|
1301
|
+
// executed in the browser process where the clipboard actually lives.
|
|
1302
|
+
//
|
|
1303
|
+
// Measured against Chromium (which is what WebView2 is), with a sentinel in
|
|
1304
|
+
// the system clipboard before and after: copy writes it, paste reads it, cut
|
|
1305
|
+
// does both. Blink refuses the clipboard half when the page is not focused,
|
|
1306
|
+
// which is why this dispatches at the webview rather than asking the page.
|
|
1307
|
+
//
|
|
1308
|
+
// undo, redo and selectAll would also work through plain ExecuteScript —
|
|
1309
|
+
// document.execCommand allows those without a user gesture, measured — but
|
|
1310
|
+
// routing all six the same way leaves one mechanism to be wrong instead of two.
|
|
1311
|
+
struct CdpDone final
|
|
1312
|
+
: public ICoreWebView2CallDevToolsProtocolMethodCompletedHandler {
|
|
1313
|
+
// The reply is of no interest; the call is fire-and-forget. A static
|
|
1314
|
+
// instance with inert refcounting is why there is no lifetime to manage.
|
|
1315
|
+
// The two IIDs come from different places, and each is the only one that
|
|
1316
|
+
// links. IID_IUnknown lives in libuuid — pulling a whole library into every
|
|
1317
|
+
// Windows build for one GUID is a worse trade than __uuidof, which mingw
|
|
1318
|
+
// declares and the compiler folds in place. WebView2's own IIDs go the other
|
|
1319
|
+
// way: the SDK header defines them `selectany`, but attaches no
|
|
1320
|
+
// __declspec(uuid), so __uuidof on one is an undefined symbol. Both mistakes
|
|
1321
|
+
// are LINK errors, which -fsyntax-only does not catch — see the win32 link
|
|
1322
|
+
// check in the cross-compile notes.
|
|
1323
|
+
HRESULT STDMETHODCALLTYPE QueryInterface(REFIID riid, void **ppv) override {
|
|
1324
|
+
if (!ppv) return E_POINTER;
|
|
1325
|
+
if (riid == __uuidof(IUnknown) ||
|
|
1326
|
+
riid == IID_ICoreWebView2CallDevToolsProtocolMethodCompletedHandler) {
|
|
1327
|
+
*ppv = static_cast<ICoreWebView2CallDevToolsProtocolMethodCompletedHandler *>(this);
|
|
1328
|
+
return S_OK;
|
|
1329
|
+
}
|
|
1330
|
+
*ppv = nullptr;
|
|
1331
|
+
return E_NOINTERFACE;
|
|
1332
|
+
}
|
|
1333
|
+
ULONG STDMETHODCALLTYPE AddRef() override { return 2; }
|
|
1334
|
+
ULONG STDMETHODCALLTYPE Release() override { return 1; }
|
|
1335
|
+
HRESULT STDMETHODCALLTYPE Invoke(HRESULT, LPCWSTR) override { return S_OK; }
|
|
1336
|
+
};
|
|
1337
|
+
|
|
1338
|
+
static ICoreWebView2 *core_webview(App *a) {
|
|
1339
|
+
if (!a || !a->w) return nullptr;
|
|
1340
|
+
void *h = webview_get_native_handle(
|
|
1341
|
+
a->w, WEBVIEW_NATIVE_HANDLE_KIND_BROWSER_CONTROLLER);
|
|
1342
|
+
if (!h) return nullptr;
|
|
1343
|
+
auto *controller = static_cast<ICoreWebView2Controller *>(h);
|
|
1344
|
+
ICoreWebView2 *core = nullptr;
|
|
1345
|
+
if (FAILED(controller->get_CoreWebView2(&core))) return nullptr;
|
|
1346
|
+
return core; // borrowed: the controller owns it for the app's lifetime
|
|
1347
|
+
}
|
|
1348
|
+
|
|
1349
|
+
// The key is the one the command's real shortcut uses, because `commands` is
|
|
1350
|
+
// "the editing commands associated with THIS key event" — Blink runs them in
|
|
1351
|
+
// place of the key's default handling, so a mismatched key would be a lie in
|
|
1352
|
+
// anything that observes the event.
|
|
1353
|
+
struct EditCommand {
|
|
1354
|
+
const char *action;
|
|
1355
|
+
const char *command; // Blink's name, which is not always ours
|
|
1356
|
+
int vk;
|
|
1357
|
+
const char *key;
|
|
1358
|
+
const char *code;
|
|
1359
|
+
bool shift;
|
|
1360
|
+
};
|
|
1361
|
+
|
|
1362
|
+
static int32_t cdp_edit(App *a, const EditCommand &e) {
|
|
1363
|
+
ICoreWebView2 *core = core_webview(a);
|
|
1364
|
+
if (!core) return -1;
|
|
1365
|
+
char json[320];
|
|
1366
|
+
// modifiers is a bitmask: 2 is Ctrl, 8 is Shift.
|
|
1367
|
+
snprintf(json, sizeof(json),
|
|
1368
|
+
"{\"type\":\"rawKeyDown\",\"modifiers\":%d,"
|
|
1369
|
+
"\"windowsVirtualKeyCode\":%d,\"nativeVirtualKeyCode\":%d,"
|
|
1370
|
+
"\"key\":\"%s\",\"code\":\"%s\",\"commands\":[\"%s\"]}",
|
|
1371
|
+
e.shift ? 2 | 8 : 2, e.vk, e.vk, e.key, e.code, e.command);
|
|
1372
|
+
static CdpDone done;
|
|
1373
|
+
return SUCCEEDED(core->CallDevToolsProtocolMethod(
|
|
1374
|
+
L"Input.dispatchKeyEvent", widen(json).c_str(), &done))
|
|
1375
|
+
? 0
|
|
1376
|
+
: -1;
|
|
1377
|
+
}
|
|
1378
|
+
|
|
1379
|
+
// The process name, which is what macOS's application menu shows and so what
|
|
1380
|
+
// About should say here too. Windows has no NSProcessInfo; the executable's
|
|
1381
|
+
// basename without its extension is the same thing by another route.
|
|
1382
|
+
static std::wstring process_name() {
|
|
1383
|
+
wchar_t path[MAX_PATH];
|
|
1384
|
+
DWORD n = GetModuleFileNameW(nullptr, path, MAX_PATH);
|
|
1385
|
+
if (n == 0 || n >= MAX_PATH) return L"App";
|
|
1386
|
+
std::wstring s(path, n);
|
|
1387
|
+
size_t slash = s.find_last_of(L"\\/");
|
|
1388
|
+
if (slash != std::wstring::npos) s = s.substr(slash + 1);
|
|
1389
|
+
size_t dot = s.find_last_of(L'.');
|
|
1390
|
+
if (dot != std::wstring::npos && dot > 0) s = s.substr(0, dot);
|
|
1391
|
+
return s.empty() ? L"App" : s;
|
|
1392
|
+
}
|
|
1393
|
+
|
|
1394
|
+
// ShellAboutW spins its own message loop, so it must not run with a TypeScript
|
|
1395
|
+
// frame beneath it — the menu handler that asked for it is still on the stack.
|
|
1396
|
+
// Posting it to a later UI turn is the same rule the file dialogs follow.
|
|
1397
|
+
static void about_on_ui_thread(webview_t w, void *) {
|
|
1398
|
+
const std::wstring name = process_name();
|
|
1399
|
+
ShellAboutW(static_cast<HWND>(webview_get_window(w)), name.c_str(), L"",
|
|
1400
|
+
nullptr);
|
|
1401
|
+
}
|
|
1402
|
+
|
|
1403
|
+
static int32_t perform_action(const std::string &action) {
|
|
1404
|
+
App *a = current_app();
|
|
1405
|
+
HWND hwnd = a && a->w ? static_cast<HWND>(webview_get_window(a->w))
|
|
1406
|
+
: g_menu_hwnd;
|
|
1407
|
+
if (!hwnd) return -1;
|
|
1408
|
+
// performClose:'s counterpart: WM_CLOSE unwinds through the same path the
|
|
1409
|
+
// title bar's X uses, so the host returns from wv_run instead of the process
|
|
1410
|
+
// vanishing under it.
|
|
1411
|
+
if (action == "quit" || action == "closeWindow") {
|
|
1412
|
+
PostMessageW(hwnd, WM_CLOSE, 0, 0);
|
|
1413
|
+
return 0;
|
|
1414
|
+
}
|
|
1415
|
+
if (action == "minimize") {
|
|
1416
|
+
ShowWindow(hwnd, SW_MINIMIZE);
|
|
1417
|
+
return 0;
|
|
1418
|
+
}
|
|
1419
|
+
if (action == "zoom") {
|
|
1420
|
+
ShowWindow(hwnd, IsZoomed(hwnd) ? SW_RESTORE : SW_MAXIMIZE);
|
|
1421
|
+
return 0;
|
|
1422
|
+
}
|
|
1423
|
+
if (action == "fullscreen") {
|
|
1424
|
+
platform_set_fullscreen(hwnd, !g_win_fullscreen);
|
|
1425
|
+
return 0;
|
|
1426
|
+
}
|
|
1427
|
+
if (action == "about") {
|
|
1428
|
+
return webview_dispatch(a->w, about_on_ui_thread, nullptr) == WEBVIEW_ERROR_OK
|
|
1429
|
+
? 0
|
|
1430
|
+
: -1;
|
|
1431
|
+
}
|
|
1432
|
+
|
|
1433
|
+
static const EditCommand edits[] = {
|
|
1434
|
+
{"undo", "undo", 'Z', "z", "KeyZ", false},
|
|
1435
|
+
// Windows redo is Ctrl+Y where macOS is Shift+Cmd+Z. Blink's command
|
|
1436
|
+
// name is the same either way; only the key event differs.
|
|
1437
|
+
{"redo", "redo", 'Y', "y", "KeyY", false},
|
|
1438
|
+
{"cut", "cut", 'X', "x", "KeyX", false},
|
|
1439
|
+
{"copy", "copy", 'C', "c", "KeyC", false},
|
|
1440
|
+
{"paste", "paste", 'V', "v", "KeyV", false},
|
|
1441
|
+
{"selectAll", "selectAll", 'A', "a", "KeyA", false},
|
|
1442
|
+
};
|
|
1443
|
+
for (const EditCommand &e : edits) {
|
|
1444
|
+
if (action == e.action) return cdp_edit(a, e);
|
|
1445
|
+
}
|
|
1446
|
+
|
|
1447
|
+
// hide, hideOthers and showAll are what is left, and they are not a missing
|
|
1448
|
+
// API but a missing CONCEPT. macOS hide is application state — windows
|
|
1449
|
+
// vanish, the app stays in the Dock, one click brings it back. The nearest
|
|
1450
|
+
// Windows call, ShowWindow(SW_HIDE), takes the window out of the taskbar and
|
|
1451
|
+
// Alt+Tab with no way back short of a tray icon; hideOthers and showAll
|
|
1452
|
+
// reach into other applications. False beats a dead menu entry.
|
|
1453
|
+
return -1;
|
|
1454
|
+
}
|
|
1455
|
+
|
|
1456
|
+
// A menu click and an accelerator both arrive as WM_COMMAND on the window that
|
|
1457
|
+
// owns the menu; the high word says which (0 for a menu, 1 for an accelerator)
|
|
1458
|
+
// and both are ours. Everything else goes back to webview.h's own wndproc —
|
|
1459
|
+
// this subclass adds a case, it does not replace the window's behaviour.
|
|
1460
|
+
static LRESULT CALLBACK menu_wndproc(HWND hwnd, UINT msg, WPARAM wp,
|
|
1461
|
+
LPARAM lp) {
|
|
1462
|
+
if (msg == WM_COMMAND && lp == 0 && (HIWORD(wp) == 0 || HIWORD(wp) == 1)) {
|
|
1463
|
+
UINT id = LOWORD(wp);
|
|
1464
|
+
if (id >= kIdItemBase) {
|
|
1465
|
+
menu_clicked(static_cast<long>(id - kIdItemBase));
|
|
1466
|
+
return 0;
|
|
1467
|
+
}
|
|
1468
|
+
}
|
|
1469
|
+
return g_prev_wndproc
|
|
1470
|
+
? CallWindowProcW(g_prev_wndproc, hwnd, msg, wp, lp)
|
|
1471
|
+
: DefWindowProcW(hwnd, msg, wp, lp);
|
|
1472
|
+
}
|
|
1473
|
+
|
|
1474
|
+
// Accelerators need TranslateAcceleratorW, and webview.h's loop never calls it
|
|
1475
|
+
// (win32_edge.hh, run_impl: GetMessageW / TranslateMessage / DispatchMessageW
|
|
1476
|
+
// and nothing else). We do not own that loop — but a WH_GETMESSAGE hook runs
|
|
1477
|
+
// INSIDE its GetMessageW, which is the same point in the cycle. Rewriting a
|
|
1478
|
+
// consumed message to WM_NULL is how the key stops here instead of also
|
|
1479
|
+
// reaching the page, which is what a real accelerator does.
|
|
1480
|
+
static LRESULT CALLBACK accel_hook(int code, WPARAM wp, LPARAM lp) {
|
|
1481
|
+
if (code == HC_ACTION && wp == PM_REMOVE && g_accel && g_menu_hwnd) {
|
|
1482
|
+
MSG *msg = reinterpret_cast<MSG *>(lp);
|
|
1483
|
+
if (msg && msg->message >= WM_KEYFIRST && msg->message <= WM_KEYLAST &&
|
|
1484
|
+
(msg->hwnd == g_menu_hwnd || IsChild(g_menu_hwnd, msg->hwnd))) {
|
|
1485
|
+
if (TranslateAcceleratorW(g_menu_hwnd, g_accel, msg)) {
|
|
1486
|
+
msg->message = WM_NULL;
|
|
1487
|
+
msg->wParam = 0;
|
|
1488
|
+
msg->lParam = 0;
|
|
1489
|
+
}
|
|
1490
|
+
}
|
|
1491
|
+
}
|
|
1492
|
+
return CallNextHookEx(nullptr, code, wp, lp);
|
|
1493
|
+
}
|
|
1494
|
+
|
|
1495
|
+
static int32_t apply_custom_menu(App *a, const std::string &spec) {
|
|
1496
|
+
HWND hwnd = a->w ? static_cast<HWND>(webview_get_window(a->w)) : nullptr;
|
|
1497
|
+
if (!hwnd) return -1;
|
|
1498
|
+
|
|
1499
|
+
// A fresh bar every time: the host owns what it declared, so setMenu
|
|
1500
|
+
// replaces rather than appends and the menu can shrink as well as grow.
|
|
1501
|
+
HMENU bar = CreateMenu();
|
|
1502
|
+
if (!bar) return -1;
|
|
1503
|
+
|
|
1504
|
+
a->menu_items.clear();
|
|
1505
|
+
std::vector<ACCEL> accels;
|
|
1506
|
+
|
|
1507
|
+
// The bar itself plus one popup per open submenu. Popups are appended to
|
|
1508
|
+
// their parent as they are created, so destroying the bar destroys them all.
|
|
1509
|
+
std::vector<HMENU> stack;
|
|
1510
|
+
stack.push_back(bar);
|
|
1511
|
+
|
|
1512
|
+
for (const std::string &line : split_on(spec, '\n')) {
|
|
1513
|
+
if (line.empty()) continue;
|
|
1514
|
+
std::vector<std::string> f = split_on(line, '\x1f');
|
|
1515
|
+
const std::string &kind = f[0];
|
|
1516
|
+
|
|
1517
|
+
if (kind == "S" && f.size() >= 2) {
|
|
1518
|
+
HMENU popup = CreatePopupMenu();
|
|
1519
|
+
if (!popup) continue;
|
|
1520
|
+
AppendMenuW(stack.back(), MF_POPUP,
|
|
1521
|
+
reinterpret_cast<UINT_PTR>(popup),
|
|
1522
|
+
widen(escape_mnemonics(f[1])).c_str());
|
|
1523
|
+
stack.push_back(popup);
|
|
1524
|
+
} else if (kind == "E") {
|
|
1525
|
+
if (stack.size() > 1) stack.pop_back();
|
|
1526
|
+
} else if (kind == "-") {
|
|
1527
|
+
if (stack.size() > 1) {
|
|
1528
|
+
AppendMenuW(stack.back(), MF_SEPARATOR, 0, nullptr);
|
|
1529
|
+
}
|
|
1530
|
+
} else if (kind == "I" && f.size() >= 8 && stack.size() > 1) {
|
|
1531
|
+
// The host owns the tag: it indexes the handler registry in TypeScript,
|
|
1532
|
+
// so the numbering comes from there rather than from insertion order.
|
|
1533
|
+
long tag = strtol(f[1].c_str(), nullptr, 10);
|
|
1534
|
+
UINT id = kIdItemBase + static_cast<UINT>(tag);
|
|
1535
|
+
WORD vk = 0;
|
|
1536
|
+
std::string shown;
|
|
1537
|
+
std::string label = escape_mnemonics(f[2]);
|
|
1538
|
+
unsigned long mods = strtoul(f[4].c_str(), nullptr, 10);
|
|
1539
|
+
if (!f[3].empty() && vk_for_key(f[3], vk, shown)) {
|
|
1540
|
+
label += "\t" + accel_text(shown, mods);
|
|
1541
|
+
ACCEL ac{};
|
|
1542
|
+
ac.fVirt = FVIRTKEY;
|
|
1543
|
+
if (mods & (1 | 8)) ac.fVirt |= FCONTROL;
|
|
1544
|
+
if (mods & 2) ac.fVirt |= FSHIFT;
|
|
1545
|
+
if (mods & 4) ac.fVirt |= FALT;
|
|
1546
|
+
ac.key = vk;
|
|
1547
|
+
ac.cmd = static_cast<WORD>(id);
|
|
1548
|
+
accels.push_back(ac);
|
|
1549
|
+
}
|
|
1550
|
+
UINT flags = MF_STRING;
|
|
1551
|
+
if (f[5] != "1") flags |= MF_GRAYED;
|
|
1552
|
+
if (f[6] == "1") flags |= MF_CHECKED;
|
|
1553
|
+
AppendMenuW(stack.back(), flags, id, widen(label).c_str());
|
|
1554
|
+
// The owning popup, not the bar: EnableMenuItem searches submenus by
|
|
1555
|
+
// command but SetMenuItemInfoW does not, and setLabel needs the latter.
|
|
1556
|
+
if (tag >= 0) {
|
|
1557
|
+
size_t at = static_cast<size_t>(tag);
|
|
1558
|
+
if (a->menu_items.size() <= at) a->menu_items.resize(at + 1, nullptr);
|
|
1559
|
+
a->menu_items[at] = stack.back();
|
|
1560
|
+
}
|
|
1561
|
+
}
|
|
1562
|
+
}
|
|
1563
|
+
|
|
1564
|
+
if (!SetMenu(hwnd, bar)) {
|
|
1565
|
+
DestroyMenu(bar);
|
|
1566
|
+
return -1;
|
|
1567
|
+
}
|
|
1568
|
+
// Destroying the previous bar only after the new one is installed: it is
|
|
1569
|
+
// still the window's menu until SetMenu returns.
|
|
1570
|
+
if (g_menu_bar && g_menu_bar != bar) DestroyMenu(g_menu_bar);
|
|
1571
|
+
g_menu_bar = bar;
|
|
1572
|
+
DrawMenuBar(hwnd);
|
|
1573
|
+
// A menu bar takes a row out of the client area, and webview.h sizes its
|
|
1574
|
+
// widget only from WM_SIZE — which SetMenu does not send. Without this the
|
|
1575
|
+
// webview keeps its old height and the bottom of the page is clipped off
|
|
1576
|
+
// the window. SWP_FRAMECHANGED forces the WM_NCCALCSIZE/WM_SIZE pair.
|
|
1577
|
+
SetWindowPos(hwnd, nullptr, 0, 0, 0, 0,
|
|
1578
|
+
SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER | SWP_NOOWNERZORDER |
|
|
1579
|
+
SWP_FRAMECHANGED);
|
|
1580
|
+
|
|
1581
|
+
if (g_accel) {
|
|
1582
|
+
DestroyAcceleratorTable(g_accel);
|
|
1583
|
+
g_accel = nullptr;
|
|
1584
|
+
}
|
|
1585
|
+
if (!accels.empty()) {
|
|
1586
|
+
g_accel = CreateAcceleratorTableW(accels.data(),
|
|
1587
|
+
static_cast<int>(accels.size()));
|
|
1588
|
+
}
|
|
1589
|
+
|
|
1590
|
+
if (g_menu_hwnd != hwnd) {
|
|
1591
|
+
g_prev_wndproc = reinterpret_cast<WNDPROC>(SetWindowLongPtrW(
|
|
1592
|
+
hwnd, GWLP_WNDPROC, reinterpret_cast<LONG_PTR>(menu_wndproc)));
|
|
1593
|
+
g_menu_hwnd = hwnd;
|
|
1594
|
+
}
|
|
1595
|
+
if (!g_accel_hook) {
|
|
1596
|
+
g_accel_hook = SetWindowsHookExW(WH_GETMESSAGE, accel_hook, nullptr,
|
|
1597
|
+
GetCurrentThreadId());
|
|
1598
|
+
}
|
|
1599
|
+
|
|
1600
|
+
a->menu_set = true;
|
|
1601
|
+
claim_menu_owner(a);
|
|
1602
|
+
return 0;
|
|
1603
|
+
}
|
|
1604
|
+
|
|
1605
|
+
static HMENU owner_menu(App *a, int32_t tag) {
|
|
1606
|
+
if (tag < 0 || static_cast<size_t>(tag) >= a->menu_items.size()) return nullptr;
|
|
1607
|
+
return static_cast<HMENU>(a->menu_items[static_cast<size_t>(tag)]);
|
|
1608
|
+
}
|
|
1609
|
+
|
|
1610
|
+
static int32_t menu_set_enabled(App *a, int32_t tag, int32_t on) {
|
|
1611
|
+
HMENU m = owner_menu(a, tag);
|
|
1612
|
+
if (!m || !g_menu_hwnd) return -1;
|
|
1613
|
+
if (EnableMenuItem(m, kIdItemBase + static_cast<UINT>(tag),
|
|
1614
|
+
MF_BYCOMMAND | (on ? MF_ENABLED : MF_GRAYED)) == -1) {
|
|
1615
|
+
return -1;
|
|
1616
|
+
}
|
|
1617
|
+
DrawMenuBar(g_menu_hwnd);
|
|
1618
|
+
return 0;
|
|
1619
|
+
}
|
|
1620
|
+
|
|
1621
|
+
static int32_t menu_set_checked(App *a, int32_t tag, int32_t on) {
|
|
1622
|
+
HMENU m = owner_menu(a, tag);
|
|
1623
|
+
if (!m) return -1;
|
|
1624
|
+
return CheckMenuItem(m, kIdItemBase + static_cast<UINT>(tag),
|
|
1625
|
+
MF_BYCOMMAND | (on ? MF_CHECKED : MF_UNCHECKED)) ==
|
|
1626
|
+
static_cast<DWORD>(-1)
|
|
1627
|
+
? -1
|
|
1628
|
+
: 0;
|
|
1629
|
+
}
|
|
1630
|
+
|
|
1631
|
+
static int32_t menu_set_label(App *a, int32_t tag, const std::string &label) {
|
|
1632
|
+
HMENU m = owner_menu(a, tag);
|
|
1633
|
+
if (!m || !g_menu_hwnd) return -1;
|
|
1634
|
+
// The accelerator text lives after a tab in the same string, so replacing
|
|
1635
|
+
// the label has to keep whatever was there or the shortcut stops being
|
|
1636
|
+
// printed while it goes on working.
|
|
1637
|
+
wchar_t existing[512];
|
|
1638
|
+
std::wstring text = widen(escape_mnemonics(label));
|
|
1639
|
+
int n = GetMenuStringW(m, kIdItemBase + static_cast<UINT>(tag), existing,
|
|
1640
|
+
512, MF_BYCOMMAND);
|
|
1641
|
+
if (n > 0) {
|
|
1642
|
+
std::wstring was(existing, static_cast<size_t>(n));
|
|
1643
|
+
size_t tab = was.find(L'\t');
|
|
1644
|
+
if (tab != std::wstring::npos) text += was.substr(tab);
|
|
1645
|
+
}
|
|
1646
|
+
MENUITEMINFOW mii{};
|
|
1647
|
+
mii.cbSize = sizeof(mii);
|
|
1648
|
+
mii.fMask = MIIM_STRING;
|
|
1649
|
+
mii.dwTypeData = const_cast<wchar_t *>(text.c_str());
|
|
1650
|
+
if (!SetMenuItemInfoW(m, kIdItemBase + static_cast<UINT>(tag), FALSE, &mii)) {
|
|
1651
|
+
return -1;
|
|
1652
|
+
}
|
|
1653
|
+
DrawMenuBar(g_menu_hwnd);
|
|
1654
|
+
return 0;
|
|
1655
|
+
}
|
|
1656
|
+
|
|
1657
|
+
static void menu_teardown(App *) {
|
|
1658
|
+
if (g_accel_hook) {
|
|
1659
|
+
UnhookWindowsHookEx(g_accel_hook);
|
|
1660
|
+
g_accel_hook = nullptr;
|
|
1661
|
+
}
|
|
1662
|
+
if (g_accel) {
|
|
1663
|
+
DestroyAcceleratorTable(g_accel);
|
|
1664
|
+
g_accel = nullptr;
|
|
1665
|
+
}
|
|
1666
|
+
// The menu bar is destroyed with the window; g_menu_bar is only cleared so
|
|
1667
|
+
// a later setMenu does not free a handle Windows already reclaimed.
|
|
1668
|
+
g_menu_bar = nullptr;
|
|
1669
|
+
g_menu_hwnd = nullptr;
|
|
1670
|
+
g_prev_wndproc = nullptr;
|
|
1671
|
+
}
|
|
1672
|
+
|
|
1673
|
+
static void menu_realize(App *) {}
|
|
1674
|
+
|
|
1675
|
+
|
|
1676
|
+
static bool platform_is_fullscreen(void *) { return g_win_fullscreen; }
|
|
1677
|
+
|
|
1678
|
+
static int32_t platform_set_fullscreen(void *win, bool on) {
|
|
1679
|
+
HWND hwnd = static_cast<HWND>(win);
|
|
1680
|
+
static WINDOWPLACEMENT saved = {sizeof(saved), 0, 0, {0, 0}, {0, 0}, {0, 0, 0, 0}};
|
|
1681
|
+
LONG_PTR style = GetWindowLongPtrW(hwnd, GWL_STYLE);
|
|
1682
|
+
if (on) {
|
|
1683
|
+
MONITORINFO mi;
|
|
1684
|
+
mi.cbSize = sizeof(mi);
|
|
1685
|
+
if (!GetWindowPlacement(hwnd, &saved) ||
|
|
1686
|
+
!GetMonitorInfoW(MonitorFromWindow(hwnd, MONITOR_DEFAULTTOPRIMARY), &mi)) {
|
|
1687
|
+
return -1;
|
|
1688
|
+
}
|
|
1689
|
+
SetWindowLongPtrW(hwnd, GWL_STYLE, style & ~WS_OVERLAPPEDWINDOW);
|
|
1690
|
+
SetWindowPos(hwnd, HWND_TOP, mi.rcMonitor.left, mi.rcMonitor.top,
|
|
1691
|
+
mi.rcMonitor.right - mi.rcMonitor.left,
|
|
1692
|
+
mi.rcMonitor.bottom - mi.rcMonitor.top,
|
|
1693
|
+
SWP_NOOWNERZORDER | SWP_FRAMECHANGED);
|
|
1694
|
+
} else {
|
|
1695
|
+
SetWindowLongPtrW(hwnd, GWL_STYLE, style | WS_OVERLAPPEDWINDOW);
|
|
1696
|
+
SetWindowPlacement(hwnd, &saved);
|
|
1697
|
+
SetWindowPos(hwnd, nullptr, 0, 0, 0, 0,
|
|
1698
|
+
SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER | SWP_NOOWNERZORDER |
|
|
1699
|
+
SWP_FRAMECHANGED);
|
|
1700
|
+
}
|
|
1701
|
+
g_win_fullscreen = on;
|
|
1702
|
+
return 0;
|
|
1703
|
+
}
|
|
1704
|
+
|
|
1705
|
+
#else // GTK
|
|
1706
|
+
|
|
1707
|
+
// ---- GTK menus -------------------------------------------------------------
|
|
1708
|
+
//
|
|
1709
|
+
// As on Windows there is no standard bar to install: WebKitGTK handles the
|
|
1710
|
+
// editing keys itself, and closing the window is the window manager's job. A
|
|
1711
|
+
// menu here is a feature the app asked for.
|
|
1712
|
+
static void install_main_menu() {}
|
|
1713
|
+
|
|
1714
|
+
#if GTK_MAJOR_VERSION >= 4
|
|
1715
|
+
// GTK 4 removed GtkMenuBar outright — menus there are GMenuModel, a different
|
|
1716
|
+
// model with a different lifetime. The build pins gtk+-3.0 (bin/janela.mjs),
|
|
1717
|
+
// so this is a compile-out rather than a guess at code nobody has run.
|
|
948
1718
|
static int32_t apply_custom_menu(App *, const std::string &) { return -1; }
|
|
1719
|
+
static int32_t menu_set_enabled(App *, int32_t, int32_t) { return -1; }
|
|
1720
|
+
static int32_t menu_set_checked(App *, int32_t, int32_t) { return -1; }
|
|
1721
|
+
static int32_t menu_set_label(App *, int32_t, const std::string &) { return -1; }
|
|
1722
|
+
static int32_t perform_action(const std::string &) { return -1; }
|
|
1723
|
+
static void menu_teardown(App *) {}
|
|
1724
|
+
static void menu_realize(App *) {}
|
|
1725
|
+
#else
|
|
1726
|
+
|
|
1727
|
+
static GtkAccelGroup *g_accel_group = nullptr;
|
|
1728
|
+
// Set when the webview was not parented yet at setMenu time — see attach_menubar.
|
|
1729
|
+
static std::string g_pending_spec;
|
|
1730
|
+
static bool g_menu_pending = false;
|
|
1731
|
+
|
|
1732
|
+
static WebKitWebView *webkit_of(App *a) {
|
|
1733
|
+
if (!a || !a->w) return nullptr;
|
|
1734
|
+
void *h = webview_get_native_handle(
|
|
1735
|
+
a->w, WEBVIEW_NATIVE_HANDLE_KIND_BROWSER_CONTROLLER);
|
|
1736
|
+
return h ? WEBKIT_WEB_VIEW(h) : nullptr;
|
|
1737
|
+
}
|
|
1738
|
+
|
|
1739
|
+
static int32_t perform_action(const std::string &action) {
|
|
1740
|
+
App *a = current_app();
|
|
1741
|
+
if (!a || !a->w) return -1;
|
|
1742
|
+
GtkWindow *window = GTK_WINDOW(webview_get_window(a->w));
|
|
1743
|
+
if (!window) return -1;
|
|
1744
|
+
|
|
1745
|
+
// WebKitGTK does expose the editing commands, unlike WebView2 — which is
|
|
1746
|
+
// why Copy and Paste are real menu items on Linux and refusals on Windows.
|
|
1747
|
+
// The names are the literal strings behind the WEBKIT_EDITING_COMMAND_*
|
|
1748
|
+
// macros, spelled out so a webkit2gtk without the newer macro still builds.
|
|
1749
|
+
static const struct { const char *action; const char *command; } edits[] = {
|
|
1750
|
+
{"cut", "Cut"}, {"copy", "Copy"}, {"paste", "Paste"},
|
|
1751
|
+
{"undo", "Undo"}, {"redo", "Redo"}, {"selectAll", "SelectAll"},
|
|
1752
|
+
};
|
|
1753
|
+
for (const auto &e : edits) {
|
|
1754
|
+
if (action != e.action) continue;
|
|
1755
|
+
WebKitWebView *wv = webkit_of(a);
|
|
1756
|
+
if (!wv) return -1;
|
|
1757
|
+
webkit_web_view_execute_editing_command(wv, e.command);
|
|
1758
|
+
return 0;
|
|
1759
|
+
}
|
|
1760
|
+
|
|
1761
|
+
// gtk_window_close, not gtk_main_quit: it unwinds through the same path the
|
|
1762
|
+
// title bar's close button uses, so the host returns from wv_run.
|
|
1763
|
+
if (action == "quit" || action == "closeWindow") {
|
|
1764
|
+
gtk_window_close(window);
|
|
1765
|
+
return 0;
|
|
1766
|
+
}
|
|
1767
|
+
if (action == "minimize") {
|
|
1768
|
+
gtk_window_iconify(window);
|
|
1769
|
+
return 0;
|
|
1770
|
+
}
|
|
1771
|
+
if (action == "zoom") {
|
|
1772
|
+
if (gtk_window_is_maximized(window)) {
|
|
1773
|
+
gtk_window_unmaximize(window);
|
|
1774
|
+
} else {
|
|
1775
|
+
gtk_window_maximize(window);
|
|
1776
|
+
}
|
|
1777
|
+
return 0;
|
|
1778
|
+
}
|
|
1779
|
+
if (action == "fullscreen") {
|
|
1780
|
+
platform_set_fullscreen(window, !platform_is_fullscreen(window));
|
|
1781
|
+
return 0;
|
|
1782
|
+
}
|
|
1783
|
+
if (action == "about") {
|
|
1784
|
+
// GTK's own About dialog, which is what a Linux user expects here for the
|
|
1785
|
+
// same reason macOS shows NSApplication's. Unlike Windows' ShellAboutW it
|
|
1786
|
+
// spins no nested loop — it presents and returns — so it is safe to call
|
|
1787
|
+
// with the menu handler's TypeScript frame still beneath us.
|
|
1788
|
+
//
|
|
1789
|
+
// The program name is what macOS's application menu shows, by the nearest
|
|
1790
|
+
// route Linux has: GTK sets it from argv[0] during init.
|
|
1791
|
+
const char *name = g_get_prgname();
|
|
1792
|
+
gtk_show_about_dialog(window, "program-name", name ? name : "App", NULL);
|
|
1793
|
+
return 0;
|
|
1794
|
+
}
|
|
1795
|
+
|
|
1796
|
+
// hide, hideOthers and showAll are what is left, and they are not a missing
|
|
1797
|
+
// API but a missing CONCEPT. macOS hide is application state — windows
|
|
1798
|
+
// vanish, the app stays in the Dock, one click brings it back. Hiding a GTK
|
|
1799
|
+
// window instead takes it out of the taskbar with no way back short of a
|
|
1800
|
+
// tray icon, and there is no desktop-wide "hide others" to call: that is the
|
|
1801
|
+
// window manager's business, and on Wayland not even that. False beats a
|
|
1802
|
+
// dead menu entry.
|
|
1803
|
+
return -1;
|
|
1804
|
+
}
|
|
1805
|
+
|
|
1806
|
+
static void on_item_activate(GtkMenuItem *, gpointer data) {
|
|
1807
|
+
menu_clicked(static_cast<long>(reinterpret_cast<intptr_t>(data)));
|
|
1808
|
+
}
|
|
1809
|
+
|
|
1810
|
+
// Turns the host's symbolic modifiers and key name into something
|
|
1811
|
+
// gtk_accelerator_parse understands. <Primary> is GTK's own name for "Control
|
|
1812
|
+
// here, Command on a Mac", which is exactly MOD_PRIMARY.
|
|
1813
|
+
static bool gtk_accel_for(const std::string &key, unsigned long mods,
|
|
1814
|
+
guint &out_key, GdkModifierType &out_mods) {
|
|
1815
|
+
if (key.empty()) return false;
|
|
1816
|
+
std::string spec;
|
|
1817
|
+
if (mods & 1) spec += "<Primary>";
|
|
1818
|
+
if (mods & 8) spec += "<Control>";
|
|
1819
|
+
if (mods & 4) spec += "<Alt>";
|
|
1820
|
+
if (mods & 2) spec += "<Shift>";
|
|
1821
|
+
if (mods & 16) spec += "<Super>"; // Cmd, explicitly; Super is the nearest thing
|
|
1822
|
+
|
|
1823
|
+
// GDK key names are case-sensitive ("F5", "Return"), and the host lowercases
|
|
1824
|
+
// everything. Single characters are already right; named keys need their own
|
|
1825
|
+
// spelling, and "Page_Up" is not a capitalisation of "pageup".
|
|
1826
|
+
static const struct { const char *name; const char *gdk; } named[] = {
|
|
1827
|
+
{"enter", "Return"}, {"return", "Return"},
|
|
1828
|
+
{"escape", "Escape"}, {"esc", "Escape"},
|
|
1829
|
+
{"space", "space"}, {"tab", "Tab"},
|
|
1830
|
+
{"backspace", "BackSpace"},
|
|
1831
|
+
{"delete", "Delete"}, {"del", "Delete"},
|
|
1832
|
+
{"insert", "Insert"}, {"up", "Up"},
|
|
1833
|
+
{"down", "Down"}, {"left", "Left"},
|
|
1834
|
+
{"right", "Right"}, {"home", "Home"},
|
|
1835
|
+
{"end", "End"}, {"pageup", "Page_Up"},
|
|
1836
|
+
{"pagedown", "Page_Down"},
|
|
1837
|
+
};
|
|
1838
|
+
std::string name = key;
|
|
1839
|
+
if (key.size() > 1) {
|
|
1840
|
+
bool found = false;
|
|
1841
|
+
for (const auto &n : named) {
|
|
1842
|
+
if (key == n.name) { name = n.gdk; found = true; break; }
|
|
1843
|
+
}
|
|
1844
|
+
if (!found) {
|
|
1845
|
+
// F1-F24 and anything else that is just the lowercase of its GDK name.
|
|
1846
|
+
name[0] = static_cast<char>(std::toupper(
|
|
1847
|
+
static_cast<unsigned char>(name[0])));
|
|
1848
|
+
}
|
|
1849
|
+
}
|
|
1850
|
+
spec += name;
|
|
1851
|
+
out_key = 0;
|
|
1852
|
+
out_mods = static_cast<GdkModifierType>(0);
|
|
1853
|
+
gtk_accelerator_parse(spec.c_str(), &out_key, &out_mods);
|
|
1854
|
+
return out_key != 0;
|
|
1855
|
+
}
|
|
1856
|
+
|
|
1857
|
+
// Puts the bar above the webview.
|
|
1858
|
+
//
|
|
1859
|
+
// The GTK backend does gtk_container_add(window, webview) — a GtkWindow is a
|
|
1860
|
+
// GtkBin and holds exactly one child — so a menu bar means re-parenting the
|
|
1861
|
+
// webview into a box first. That is done once and remembered on the box, so a
|
|
1862
|
+
// later setMenu swaps the bar rather than nesting another box.
|
|
1863
|
+
//
|
|
1864
|
+
// It can also arrive too early: the backend parents the webview lazily, in
|
|
1865
|
+
// window_show(), which runs on the first webview_set_size. Adding our box to
|
|
1866
|
+
// an empty window then would leave window_show() adding the webview to a
|
|
1867
|
+
// GtkBin that is already full — a GTK warning and a blank window. So an
|
|
1868
|
+
// unparented window means "not yet", and wv_run retries once setup is done.
|
|
1869
|
+
static bool attach_menubar(App *a, GtkWidget *bar) {
|
|
1870
|
+
GtkWidget *window = GTK_WIDGET(webview_get_window(a->w));
|
|
1871
|
+
if (!window) return false;
|
|
1872
|
+
GtkWidget *child = gtk_bin_get_child(GTK_BIN(window));
|
|
1873
|
+
if (!child) return false;
|
|
1874
|
+
|
|
1875
|
+
GtkWidget *box = nullptr;
|
|
1876
|
+
if (GTK_IS_BOX(child) &&
|
|
1877
|
+
g_object_get_data(G_OBJECT(child), "janela-menu-box")) {
|
|
1878
|
+
box = child;
|
|
1879
|
+
GtkWidget *old = GTK_WIDGET(
|
|
1880
|
+
g_object_get_data(G_OBJECT(box), "janela-menubar"));
|
|
1881
|
+
if (old) gtk_container_remove(GTK_CONTAINER(box), old);
|
|
1882
|
+
} else {
|
|
1883
|
+
g_object_ref(child);
|
|
1884
|
+
gtk_container_remove(GTK_CONTAINER(window), child);
|
|
1885
|
+
box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
|
|
1886
|
+
g_object_set_data(G_OBJECT(box), "janela-menu-box", box);
|
|
1887
|
+
gtk_box_pack_end(GTK_BOX(box), child, TRUE, TRUE, 0);
|
|
1888
|
+
g_object_unref(child);
|
|
1889
|
+
gtk_container_add(GTK_CONTAINER(window), box);
|
|
1890
|
+
gtk_widget_show(box);
|
|
1891
|
+
}
|
|
1892
|
+
gtk_box_pack_start(GTK_BOX(box), bar, FALSE, FALSE, 0);
|
|
1893
|
+
gtk_box_reorder_child(GTK_BOX(box), bar, 0);
|
|
1894
|
+
g_object_set_data(G_OBJECT(box), "janela-menubar", bar);
|
|
1895
|
+
gtk_widget_show_all(bar);
|
|
1896
|
+
|
|
1897
|
+
if (!g_accel_group) return true;
|
|
1898
|
+
GtkWindow *w = GTK_WINDOW(window);
|
|
1899
|
+
if (!g_object_get_data(G_OBJECT(window), "janela-accels")) {
|
|
1900
|
+
gtk_window_add_accel_group(w, g_accel_group);
|
|
1901
|
+
g_object_set_data(G_OBJECT(window), "janela-accels", g_accel_group);
|
|
1902
|
+
}
|
|
1903
|
+
return true;
|
|
1904
|
+
}
|
|
1905
|
+
|
|
1906
|
+
static int32_t apply_custom_menu(App *a, const std::string &spec) {
|
|
1907
|
+
if (!a->w) return -1;
|
|
1908
|
+
|
|
1909
|
+
// One accel group for the window's lifetime: gtk_window_add_accel_group is
|
|
1910
|
+
// not idempotent, so rebuilding the menu re-uses the group and only the
|
|
1911
|
+
// items inside it change.
|
|
1912
|
+
if (!g_accel_group) g_accel_group = gtk_accel_group_new();
|
|
1913
|
+
|
|
1914
|
+
GtkWidget *bar = gtk_menu_bar_new();
|
|
1915
|
+
a->menu_items.clear();
|
|
1916
|
+
|
|
1917
|
+
std::vector<GtkWidget *> stack; // containers: the bar, then each open menu
|
|
1918
|
+
stack.push_back(bar);
|
|
1919
|
+
|
|
1920
|
+
for (const std::string &line : split_on(spec, '\n')) {
|
|
1921
|
+
if (line.empty()) continue;
|
|
1922
|
+
std::vector<std::string> f = split_on(line, '\x1f');
|
|
1923
|
+
const std::string &kind = f[0];
|
|
1924
|
+
|
|
1925
|
+
if (kind == "S" && f.size() >= 2) {
|
|
1926
|
+
GtkWidget *holder = gtk_menu_item_new_with_label(f[1].c_str());
|
|
1927
|
+
GtkWidget *menu = gtk_menu_new();
|
|
1928
|
+
gtk_menu_item_set_submenu(GTK_MENU_ITEM(holder), menu);
|
|
1929
|
+
gtk_menu_shell_append(GTK_MENU_SHELL(stack.back()), holder);
|
|
1930
|
+
stack.push_back(menu);
|
|
1931
|
+
} else if (kind == "E") {
|
|
1932
|
+
if (stack.size() > 1) stack.pop_back();
|
|
1933
|
+
} else if (kind == "-") {
|
|
1934
|
+
if (stack.size() > 1) {
|
|
1935
|
+
gtk_menu_shell_append(GTK_MENU_SHELL(stack.back()),
|
|
1936
|
+
gtk_separator_menu_item_new());
|
|
1937
|
+
}
|
|
1938
|
+
} else if (kind == "I" && f.size() >= 8 && stack.size() > 1) {
|
|
1939
|
+
long tag = strtol(f[1].c_str(), nullptr, 10);
|
|
1940
|
+
// f[7] is why the wire carries "checkable" separately from "checked":
|
|
1941
|
+
// GTK decides this at construction — a tick needs GtkCheckMenuItem, a
|
|
1942
|
+
// different widget — where AppKit lets any item carry a state.
|
|
1943
|
+
const bool checkable = f[7] == "1";
|
|
1944
|
+
GtkWidget *it = checkable
|
|
1945
|
+
? gtk_check_menu_item_new_with_label(f[2].c_str())
|
|
1946
|
+
: gtk_menu_item_new_with_label(f[2].c_str());
|
|
1947
|
+
if (checkable) {
|
|
1948
|
+
gtk_check_menu_item_set_active(GTK_CHECK_MENU_ITEM(it), f[6] == "1");
|
|
1949
|
+
}
|
|
1950
|
+
gtk_widget_set_sensitive(it, f[5] == "1");
|
|
1951
|
+
g_signal_connect(it, "activate", G_CALLBACK(on_item_activate),
|
|
1952
|
+
reinterpret_cast<gpointer>(
|
|
1953
|
+
static_cast<intptr_t>(tag)));
|
|
1954
|
+
guint akey = 0;
|
|
1955
|
+
GdkModifierType amods = static_cast<GdkModifierType>(0);
|
|
1956
|
+
if (gtk_accel_for(f[3], strtoul(f[4].c_str(), nullptr, 10), akey, amods)) {
|
|
1957
|
+
gtk_widget_add_accelerator(it, "activate", g_accel_group, akey, amods,
|
|
1958
|
+
GTK_ACCEL_VISIBLE);
|
|
1959
|
+
}
|
|
1960
|
+
gtk_menu_shell_append(GTK_MENU_SHELL(stack.back()), it);
|
|
1961
|
+
if (tag >= 0) {
|
|
1962
|
+
size_t at = static_cast<size_t>(tag);
|
|
1963
|
+
if (a->menu_items.size() <= at) a->menu_items.resize(at + 1, nullptr);
|
|
1964
|
+
a->menu_items[at] = g_object_ref_sink(it);
|
|
1965
|
+
}
|
|
1966
|
+
}
|
|
1967
|
+
}
|
|
1968
|
+
|
|
1969
|
+
a->menu_set = true;
|
|
1970
|
+
claim_menu_owner(a);
|
|
1971
|
+
|
|
1972
|
+
if (!attach_menubar(a, bar)) {
|
|
1973
|
+
// Too early. Hold the spec and rebuild at run, when the webview is
|
|
1974
|
+
// certain to be parented; the widgets built here are dropped rather than
|
|
1975
|
+
// kept, because their accelerators would otherwise fire twice.
|
|
1976
|
+
g_object_ref_sink(bar);
|
|
1977
|
+
g_object_unref(bar);
|
|
1978
|
+
for (void *w : a->menu_items) {
|
|
1979
|
+
if (w) g_object_unref(G_OBJECT(w));
|
|
1980
|
+
}
|
|
1981
|
+
a->menu_items.clear();
|
|
1982
|
+
g_pending_spec = spec;
|
|
1983
|
+
g_menu_pending = true;
|
|
1984
|
+
}
|
|
1985
|
+
return 0;
|
|
1986
|
+
}
|
|
1987
|
+
|
|
1988
|
+
// The retry the note on attach_menubar describes. Called from wv_run, after
|
|
1989
|
+
// setup has had its chance to size the window.
|
|
1990
|
+
static void menu_realize(App *a) {
|
|
1991
|
+
if (!g_menu_pending) return;
|
|
1992
|
+
g_menu_pending = false;
|
|
1993
|
+
const std::string spec = g_pending_spec;
|
|
1994
|
+
g_pending_spec.clear();
|
|
1995
|
+
apply_custom_menu(a, spec);
|
|
1996
|
+
}
|
|
1997
|
+
|
|
1998
|
+
static GtkWidget *item_at(App *a, int32_t tag) {
|
|
1999
|
+
if (tag < 0 || static_cast<size_t>(tag) >= a->menu_items.size()) return nullptr;
|
|
2000
|
+
return GTK_WIDGET(a->menu_items[static_cast<size_t>(tag)]);
|
|
2001
|
+
}
|
|
2002
|
+
|
|
2003
|
+
static int32_t menu_set_enabled(App *a, int32_t tag, int32_t on) {
|
|
2004
|
+
GtkWidget *it = item_at(a, tag);
|
|
2005
|
+
if (!it) return -1;
|
|
2006
|
+
gtk_widget_set_sensitive(it, on != 0);
|
|
2007
|
+
return 0;
|
|
2008
|
+
}
|
|
2009
|
+
|
|
2010
|
+
static int32_t menu_set_checked(App *a, int32_t tag, int32_t on) {
|
|
2011
|
+
GtkWidget *it = item_at(a, tag);
|
|
2012
|
+
if (!it || !GTK_IS_CHECK_MENU_ITEM(it)) return -1;
|
|
2013
|
+
// set_active emits "activate", which would run the item's own click handler
|
|
2014
|
+
// as a side effect of the host setting the tick. Blocking the handler for
|
|
2015
|
+
// the duration is the difference between reflecting state and inventing a
|
|
2016
|
+
// click.
|
|
2017
|
+
g_signal_handlers_block_matched(G_OBJECT(it), G_SIGNAL_MATCH_FUNC, 0, 0,
|
|
2018
|
+
nullptr,
|
|
2019
|
+
reinterpret_cast<gpointer>(on_item_activate),
|
|
2020
|
+
nullptr);
|
|
2021
|
+
gtk_check_menu_item_set_active(GTK_CHECK_MENU_ITEM(it), on != 0);
|
|
2022
|
+
g_signal_handlers_unblock_matched(
|
|
2023
|
+
G_OBJECT(it), G_SIGNAL_MATCH_FUNC, 0, 0, nullptr,
|
|
2024
|
+
reinterpret_cast<gpointer>(on_item_activate), nullptr);
|
|
2025
|
+
return 0;
|
|
2026
|
+
}
|
|
2027
|
+
|
|
2028
|
+
static int32_t menu_set_label(App *a, int32_t tag, const std::string &label) {
|
|
2029
|
+
GtkWidget *it = item_at(a, tag);
|
|
2030
|
+
if (!it) return -1;
|
|
2031
|
+
gtk_menu_item_set_label(GTK_MENU_ITEM(it), label.c_str());
|
|
2032
|
+
return 0;
|
|
2033
|
+
}
|
|
2034
|
+
|
|
2035
|
+
static void menu_teardown(App *a) {
|
|
2036
|
+
for (void *w : a->menu_items) {
|
|
2037
|
+
if (w) g_object_unref(G_OBJECT(w));
|
|
2038
|
+
}
|
|
2039
|
+
a->menu_items.clear();
|
|
2040
|
+
g_menu_pending = false;
|
|
2041
|
+
g_pending_spec.clear();
|
|
2042
|
+
}
|
|
2043
|
+
|
|
2044
|
+
#endif // GTK_MAJOR_VERSION >= 4
|
|
2045
|
+
|
|
2046
|
+
// Asks the window manager rather than remembering what we last asked for: a
|
|
2047
|
+
// GTK window can leave fullscreen without us (the WM's own keybinding), and a
|
|
2048
|
+
// toggle that trusts a stale flag then does the wrong thing.
|
|
2049
|
+
static bool platform_is_fullscreen(void *win) {
|
|
2050
|
+
#if GTK_MAJOR_VERSION >= 4
|
|
2051
|
+
return gtk_window_is_fullscreen(GTK_WINDOW(win));
|
|
2052
|
+
#else
|
|
2053
|
+
GdkWindow *gdk = gtk_widget_get_window(GTK_WIDGET(win));
|
|
2054
|
+
if (!gdk) return false;
|
|
2055
|
+
return (gdk_window_get_state(gdk) & GDK_WINDOW_STATE_FULLSCREEN) != 0;
|
|
2056
|
+
#endif
|
|
2057
|
+
}
|
|
2058
|
+
|
|
2059
|
+
static int32_t platform_set_fullscreen(void *win, bool on) {
|
|
2060
|
+
GtkWindow *window = GTK_WINDOW(win);
|
|
2061
|
+
if (on) {
|
|
2062
|
+
gtk_window_fullscreen(window);
|
|
2063
|
+
} else {
|
|
2064
|
+
gtk_window_unfullscreen(window);
|
|
2065
|
+
}
|
|
2066
|
+
return 0;
|
|
2067
|
+
}
|
|
2068
|
+
|
|
949
2069
|
#endif
|
|
950
2070
|
|
|
951
2071
|
// Runs on the UI thread with no TS frame beneath it — see the note on the job
|
|
@@ -1068,10 +2188,9 @@ int32_t wv_create(int32_t debug) {
|
|
|
1068
2188
|
if (g_apps[i].used) continue;
|
|
1069
2189
|
webview_t w = webview_create(debug, nullptr);
|
|
1070
2190
|
if (!w) return -1;
|
|
1071
|
-
//
|
|
1072
|
-
//
|
|
1073
|
-
|
|
1074
|
-
g_apps[i].std_menu_count = standard_menu_count();
|
|
2191
|
+
// The menu is not installed here: an app that calls setMenu owns the bar,
|
|
2192
|
+
// and one that never does gets the standard one at wv_run. Deciding at
|
|
2193
|
+
// create time would mean building a bar only to throw it away.
|
|
1075
2194
|
// Field-wise reset: App holds a thread and atomics, so it is not
|
|
1076
2195
|
// copy-assignable from a temporary.
|
|
1077
2196
|
g_apps[i].binds.clear();
|
|
@@ -1082,6 +2201,7 @@ int32_t wv_create(int32_t debug) {
|
|
|
1082
2201
|
g_apps[i].on_menu = nullptr;
|
|
1083
2202
|
g_apps[i].on_menu_ctx = nullptr;
|
|
1084
2203
|
g_apps[i].menu_items.clear();
|
|
2204
|
+
g_apps[i].menu_set = false;
|
|
1085
2205
|
g_apps[i].req.clear();
|
|
1086
2206
|
g_apps[i].cur_id.clear();
|
|
1087
2207
|
g_apps[i].reply.clear();
|
|
@@ -1409,52 +2529,26 @@ int32_t wv_on_menu(int32_t h, int32_t (*cb)(int32_t, void *), void *ctx) {
|
|
|
1409
2529
|
int32_t wv_menu_set_enabled(int32_t h, int32_t tag, int32_t on) {
|
|
1410
2530
|
App *a = app_at(h);
|
|
1411
2531
|
if (!a) return -1;
|
|
1412
|
-
|
|
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
|
|
2532
|
+
return menu_set_enabled(a, tag, on);
|
|
1423
2533
|
}
|
|
1424
2534
|
|
|
1425
2535
|
int32_t wv_menu_set_checked(int32_t h, int32_t tag, int32_t on) {
|
|
1426
2536
|
App *a = app_at(h);
|
|
1427
2537
|
if (!a) return -1;
|
|
1428
|
-
|
|
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
|
|
2538
|
+
return menu_set_checked(a, tag, on);
|
|
1441
2539
|
}
|
|
1442
2540
|
|
|
1443
2541
|
int32_t wv_menu_set_label(int32_t h, int32_t tag, const uint8_t *p, size_t n) {
|
|
1444
2542
|
App *a = app_at(h);
|
|
1445
2543
|
if (!a) return -1;
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
return
|
|
1454
|
-
#else
|
|
1455
|
-
(void)tag; (void)p; (void)n;
|
|
1456
|
-
return -1;
|
|
1457
|
-
#endif
|
|
2544
|
+
return menu_set_label(a, tag, to_str(p, n));
|
|
2545
|
+
}
|
|
2546
|
+
|
|
2547
|
+
// Run a platform action now — copy, paste, close, quit. Takes no handle
|
|
2548
|
+
// because on macOS it needs none: the responder chain and the key window are
|
|
2549
|
+
// process state. The other two resolve the window through current_app().
|
|
2550
|
+
int32_t wv_perform_action(const uint8_t *p, size_t n) {
|
|
2551
|
+
return perform_action(to_str(p, n));
|
|
1458
2552
|
}
|
|
1459
2553
|
|
|
1460
2554
|
int32_t wv_set_fullscreen(int32_t h, int32_t on) {
|
|
@@ -1462,53 +2556,7 @@ int32_t wv_set_fullscreen(int32_t h, int32_t on) {
|
|
|
1462
2556
|
if (!a) return -1;
|
|
1463
2557
|
void *win = webview_get_window(a->w);
|
|
1464
2558
|
if (!win) return -1;
|
|
1465
|
-
|
|
1466
|
-
#if defined(__APPLE__)
|
|
1467
|
-
using namespace webview::detail;
|
|
1468
|
-
objc::autoreleasepool arp;
|
|
1469
|
-
id window = static_cast<id>(win);
|
|
1470
|
-
// NSWindowStyleMaskFullScreen. toggleFullScreen: only toggles, so read the
|
|
1471
|
-
// current state first and leave it alone when it already matches.
|
|
1472
|
-
const NSUInteger full = 1UL << 14;
|
|
1473
|
-
NSUInteger mask = objc::msg_send<NSUInteger>(window, objc::selector("styleMask"));
|
|
1474
|
-
bool is_full = (mask & full) != 0;
|
|
1475
|
-
if (is_full != (on != 0)) {
|
|
1476
|
-
objc::msg_send<void>(window, objc::selector("toggleFullScreen:"), nullptr);
|
|
1477
|
-
}
|
|
1478
|
-
return 0;
|
|
1479
|
-
#elif defined(_WIN32)
|
|
1480
|
-
HWND hwnd = static_cast<HWND>(win);
|
|
1481
|
-
static WINDOWPLACEMENT saved = {sizeof(saved), 0, 0, {0, 0}, {0, 0}, {0, 0, 0, 0}};
|
|
1482
|
-
LONG_PTR style = GetWindowLongPtrW(hwnd, GWL_STYLE);
|
|
1483
|
-
if (on) {
|
|
1484
|
-
MONITORINFO mi;
|
|
1485
|
-
mi.cbSize = sizeof(mi);
|
|
1486
|
-
if (!GetWindowPlacement(hwnd, &saved) ||
|
|
1487
|
-
!GetMonitorInfoW(MonitorFromWindow(hwnd, MONITOR_DEFAULTTOPRIMARY), &mi)) {
|
|
1488
|
-
return -1;
|
|
1489
|
-
}
|
|
1490
|
-
SetWindowLongPtrW(hwnd, GWL_STYLE, style & ~WS_OVERLAPPEDWINDOW);
|
|
1491
|
-
SetWindowPos(hwnd, HWND_TOP, mi.rcMonitor.left, mi.rcMonitor.top,
|
|
1492
|
-
mi.rcMonitor.right - mi.rcMonitor.left,
|
|
1493
|
-
mi.rcMonitor.bottom - mi.rcMonitor.top,
|
|
1494
|
-
SWP_NOOWNERZORDER | SWP_FRAMECHANGED);
|
|
1495
|
-
} else {
|
|
1496
|
-
SetWindowLongPtrW(hwnd, GWL_STYLE, style | WS_OVERLAPPEDWINDOW);
|
|
1497
|
-
SetWindowPlacement(hwnd, &saved);
|
|
1498
|
-
SetWindowPos(hwnd, nullptr, 0, 0, 0, 0,
|
|
1499
|
-
SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER | SWP_NOOWNERZORDER |
|
|
1500
|
-
SWP_FRAMECHANGED);
|
|
1501
|
-
}
|
|
1502
|
-
return 0;
|
|
1503
|
-
#else
|
|
1504
|
-
GtkWindow *window = GTK_WINDOW(win);
|
|
1505
|
-
if (on) {
|
|
1506
|
-
gtk_window_fullscreen(window);
|
|
1507
|
-
} else {
|
|
1508
|
-
gtk_window_unfullscreen(window);
|
|
1509
|
-
}
|
|
1510
|
-
return 0;
|
|
1511
|
-
#endif
|
|
2559
|
+
return platform_set_fullscreen(win, on != 0);
|
|
1512
2560
|
}
|
|
1513
2561
|
|
|
1514
2562
|
// Register the retained handler for page invokes. Valid until the app exits.
|
|
@@ -1536,6 +2584,16 @@ int32_t wv_on_timer(int32_t h, void (*cb)(int32_t, void *), void *ctx) {
|
|
|
1536
2584
|
int32_t wv_run(int32_t h) {
|
|
1537
2585
|
App *a = app_at(h);
|
|
1538
2586
|
if (!a) return -1;
|
|
2587
|
+
// The floor: an app that never set a menu still gets Cmd+Q and Cmd+V. By
|
|
2588
|
+
// now setup() has run, so this is the first moment it is knowable. A no-op
|
|
2589
|
+
// off macOS, where those keys need no menu.
|
|
2590
|
+
if (!a->menu_set) {
|
|
2591
|
+
install_main_menu();
|
|
2592
|
+
} else {
|
|
2593
|
+
// A renderer that could not attach during setup gets its second chance
|
|
2594
|
+
// here — see attach_menubar in the GTK section. A no-op elsewhere.
|
|
2595
|
+
menu_realize(a);
|
|
2596
|
+
}
|
|
1539
2597
|
int rc = webview_run(a->w);
|
|
1540
2598
|
// Nothing may call into TS once run() has returned.
|
|
1541
2599
|
stop_scheduler(a);
|
|
@@ -1555,6 +2613,7 @@ int32_t wv_destroy(int32_t h) {
|
|
|
1555
2613
|
App *a = app_at(h);
|
|
1556
2614
|
if (!a) return -1;
|
|
1557
2615
|
stop_scheduler(a);
|
|
2616
|
+
menu_teardown(a);
|
|
1558
2617
|
webview_destroy(a->w);
|
|
1559
2618
|
a->used = false;
|
|
1560
2619
|
a->w = nullptr;
|