@crossbind/extension-menu 0.0.0-bootstrap.0 → 2.0.0-beta.69

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.
@@ -0,0 +1,320 @@
1
+ // The page's menus on Linux: a GTK menu bar above the webview.
2
+ #include "menu.h"
3
+
4
+ #include "crossbind/app.h"
5
+
6
+ #include <gtk/gtk.h>
7
+ #include <unistd.h>
8
+ #include <webkit2/webkit2.h>
9
+
10
+ #include <climits>
11
+ #include <map>
12
+ #include <memory>
13
+ #include <stdexcept>
14
+
15
+ namespace crossbind::menu {
16
+ namespace {
17
+
18
+ // Each window's bar, by the window's id, and the accelerators that come with it.
19
+ struct Bar {
20
+ GtkWidget *bar = nullptr;
21
+ GtkAccelGroup *accelerators = nullptr;
22
+ };
23
+ std::map<uint64_t, Bar> bars;
24
+ // The webviews' last button press, which a popup opens from, as Wayland asks of one, and when it came: a press too old
25
+ // for a grab, as a contextmenu of the keyboard follows none, gives way to none.
26
+ GdkEvent *lastPress = nullptr;
27
+ gint64 lastPressTime = 0;
28
+ constexpr gint64 FRESH_PRESS_US = 1000000;
29
+ // Whether a popup of the page's shows, beside which the page opens none.
30
+ bool isPopupOpen = false;
31
+
32
+ // What a click of an item does: one of the page's tells its menu's owner its id, a standard one does what its role says,
33
+ // to the window whose menus it is in.
34
+ struct Action {
35
+ uint64_t window;
36
+ std::string id;
37
+ std::string role;
38
+ MenuClicked clicked;
39
+ };
40
+
41
+ // A standard item's label and accelerator, as GTK apps have them.
42
+ struct Role {
43
+ const char *label;
44
+ guint key;
45
+ GdkModifierType modifiers;
46
+ };
47
+
48
+ constexpr auto CONTROL = GDK_CONTROL_MASK;
49
+ const std::map<std::string, Role> ROLES{
50
+ {"undo", {"Undo", GDK_KEY_z, CONTROL}},
51
+ {"redo", {"Redo", GDK_KEY_z, static_cast<GdkModifierType>(CONTROL | GDK_SHIFT_MASK)}},
52
+ {"cut", {"Cut", GDK_KEY_x, CONTROL}},
53
+ {"copy", {"Copy", GDK_KEY_c, CONTROL}},
54
+ {"paste", {"Paste", GDK_KEY_v, CONTROL}},
55
+ {"selectAll", {"Select All", GDK_KEY_a, CONTROL}},
56
+ {"minimize", {"Minimize", 0, static_cast<GdkModifierType>(0)}},
57
+ {"close", {"Close", GDK_KEY_w, CONTROL}},
58
+ {"quit", {"Quit", GDK_KEY_q, CONTROL}},
59
+ };
60
+
61
+ const std::map<std::string, const char *> EDITING_COMMANDS{
62
+ {"undo", WEBKIT_EDITING_COMMAND_UNDO}, {"redo", WEBKIT_EDITING_COMMAND_REDO},
63
+ {"cut", WEBKIT_EDITING_COMMAND_CUT}, {"copy", WEBKIT_EDITING_COMMAND_COPY},
64
+ {"paste", WEBKIT_EDITING_COMMAND_PASTE}, {"selectAll", WEBKIT_EDITING_COMMAND_SELECT_ALL},
65
+ };
66
+
67
+ const std::map<std::string, guint> NAMED_KEYS{
68
+ {"Enter", GDK_KEY_Return}, {"Escape", GDK_KEY_Escape}, {"Tab", GDK_KEY_Tab}, {"Space", GDK_KEY_space},
69
+ {"Backspace", GDK_KEY_BackSpace}, {"Delete", GDK_KEY_Delete}, {"Up", GDK_KEY_Up}, {"Down", GDK_KEY_Down},
70
+ {"Left", GDK_KEY_Left}, {"Right", GDK_KEY_Right}, {"Home", GDK_KEY_Home}, {"End", GDK_KEY_End},
71
+ {"PageUp", GDK_KEY_Page_Up}, {"PageDown", GDK_KEY_Page_Down}, {"Plus", GDK_KEY_plus}, {"Minus", GDK_KEY_minus},
72
+ {"Comma", GDK_KEY_comma}, {"Period", GDK_KEY_period},
73
+ };
74
+
75
+ guint keyOf(const Accelerator &accelerator) {
76
+ const std::string &key = accelerator.key;
77
+ if (accelerator.function) return GDK_KEY_F1 + static_cast<guint>(accelerator.function) - 1;
78
+ if (key.size() == 1) return gdk_unicode_to_keyval(static_cast<guint32>(g_ascii_tolower(key[0])));
79
+ const auto named = NAMED_KEYS.find(key);
80
+ if (named == NAMED_KEYS.end()) throw std::invalid_argument("crossbind: no accelerator has the key " + key);
81
+ return named->second;
82
+ }
83
+
84
+ void runRole(uint64_t window, const std::string &role) {
85
+ if (role == "quit") {
86
+ app::requestQuit();
87
+ return;
88
+ }
89
+ const std::optional<Window> open = windowOf(window);
90
+ if (!open) return;
91
+ if (const auto command = EDITING_COMMANDS.find(role); command != EDITING_COMMANDS.end()) {
92
+ webkit_web_view_execute_editing_command(WEBKIT_WEB_VIEW(open->webView()), command->second);
93
+ } else if (role == "minimize") {
94
+ gtk_window_iconify(GTK_WINDOW(open->native()));
95
+ } else {
96
+ gtk_window_close(GTK_WINDOW(open->native()));
97
+ }
98
+ }
99
+
100
+ void activated(GtkMenuItem *item, gpointer data) {
101
+ const Action &action = *static_cast<Action *>(data);
102
+ if (!action.role.empty()) {
103
+ runRole(action.window, action.role);
104
+ return;
105
+ }
106
+ std::optional<bool> checked;
107
+ if (GTK_IS_CHECK_MENU_ITEM(item)) checked = gtk_check_menu_item_get_active(GTK_CHECK_MENU_ITEM(item)) != FALSE;
108
+ if (action.clicked && !action.id.empty()) action.clicked(action.window, action.id, checked);
109
+ }
110
+
111
+ void connect(GtkWidget *item, Action action) {
112
+ g_signal_connect_data(item, "activate", G_CALLBACK(activated), new Action(std::move(action)),
113
+ [](gpointer data, GClosure *) { delete static_cast<Action *>(data); }, static_cast<GConnectFlags>(0));
114
+ }
115
+
116
+ void addItem(GtkMenuShell *shell, const MenuItem &page, uint64_t window, GtkAccelGroup *accelerators, const MenuClicked &clicked);
117
+
118
+ // Each item goes in its shell before what may throw, so the bar destroys it with itself.
119
+ void addMenu(GtkMenuShell *shell, const std::string &label, const std::vector<MenuItem> &items, uint64_t window, GtkAccelGroup *accelerators,
120
+ const MenuClicked &clicked) {
121
+ GtkWidget *holder = gtk_menu_item_new_with_label(label.c_str());
122
+ GtkWidget *menu = gtk_menu_new();
123
+ gtk_menu_item_set_submenu(GTK_MENU_ITEM(holder), menu);
124
+ gtk_menu_shell_append(shell, holder);
125
+ for (const auto &child : items) addItem(GTK_MENU_SHELL(menu), child, window, accelerators, clicked);
126
+ }
127
+
128
+ void addItem(GtkMenuShell *shell, const MenuItem &page, uint64_t window, GtkAccelGroup *accelerators, const MenuClicked &clicked) {
129
+ if (page.isSeparator) {
130
+ gtk_menu_shell_append(shell, gtk_separator_menu_item_new());
131
+ return;
132
+ }
133
+ if (page.isSubmenu) {
134
+ addMenu(shell, page.label, page.items, window, accelerators, clicked);
135
+ return;
136
+ }
137
+ GtkWidget *made = nullptr;
138
+ if (!page.role.empty()) {
139
+ const auto role = ROLES.find(page.role);
140
+ if (role == ROLES.end()) throw std::invalid_argument("crossbind: no menu item has the role " + page.role);
141
+ made = gtk_menu_item_new_with_label(page.label.empty() ? role->second.label : page.label.c_str());
142
+ if (!page.accelerator && role->second.key) {
143
+ gtk_widget_add_accelerator(made, "activate", accelerators, role->second.key, role->second.modifiers, GTK_ACCEL_VISIBLE);
144
+ }
145
+ connect(made, Action{window, "", page.role, nullptr});
146
+ } else {
147
+ made = page.checked ? gtk_check_menu_item_new_with_label(page.label.c_str()) : gtk_menu_item_new_with_label(page.label.c_str());
148
+ // Setting it activates the item, so before the page hears of its clicks.
149
+ if (page.checked) gtk_check_menu_item_set_active(GTK_CHECK_MENU_ITEM(made), *page.checked);
150
+ gtk_widget_set_sensitive(made, page.isEnabled);
151
+ connect(made, Action{window, page.id, "", clicked});
152
+ }
153
+ gtk_menu_shell_append(shell, made);
154
+ if (page.accelerator) {
155
+ const Accelerator &accelerator = *page.accelerator;
156
+ const auto modifiers = static_cast<GdkModifierType>((accelerator.control ? GDK_CONTROL_MASK : 0) | (accelerator.alt ? GDK_MOD1_MASK : 0) |
157
+ (accelerator.shift ? GDK_SHIFT_MASK : 0));
158
+ gtk_widget_add_accelerator(made, "activate", accelerators, keyOf(accelerator), modifiers, GTK_ACCEL_VISIBLE);
159
+ }
160
+ }
161
+
162
+ // The page keeps the height its window gave it, so the window takes the bar's height on, unless it fills the screen.
163
+ void keepPageHeight(GtkWindow *window, GtkWidget *webView, int barHeight) {
164
+ GdkWindow *shown = gtk_widget_get_window(GTK_WIDGET(window));
165
+ if (!shown || (gdk_window_get_state(shown) & (GDK_WINDOW_STATE_MAXIMIZED | GDK_WINDOW_STATE_FULLSCREEN))) return;
166
+ int width = 0;
167
+ gtk_window_get_size(window, &width, nullptr);
168
+ gtk_window_resize(window, width, gtk_widget_get_allocated_height(webView) + barHeight);
169
+ }
170
+
171
+ gboolean onPress(GtkWidget *, GdkEvent *event, gpointer) {
172
+ if (lastPress) gdk_event_free(lastPress);
173
+ lastPress = gdk_event_copy(event);
174
+ lastPressTime = g_get_monotonic_time();
175
+ return FALSE;
176
+ }
177
+
178
+ // A popup as it shows: who hears its answer, the id of the item clicked, and what goes with it.
179
+ struct Popup {
180
+ std::function<void(Failure, std::optional<std::string>)> done;
181
+ std::shared_ptr<std::optional<std::string>> picked;
182
+ GtkWidget *menu;
183
+ GtkAccelGroup *accelerators;
184
+ };
185
+
186
+ // GTK takes the menu down before an item's activate, so the answer waits for the loop's next turn.
187
+ void onPopupClosed(GtkMenuShell *menu, gpointer data) {
188
+ g_signal_handlers_disconnect_by_func(menu, reinterpret_cast<gpointer>(onPopupClosed), data);
189
+ g_idle_add(
190
+ [](gpointer data) -> gboolean {
191
+ std::unique_ptr<Popup> popup(static_cast<Popup *>(data));
192
+ isPopupOpen = false;
193
+ popup->done(std::nullopt, *popup->picked);
194
+ gtk_widget_destroy(popup->menu);
195
+ g_object_unref(popup->accelerators);
196
+ return G_SOURCE_REMOVE;
197
+ },
198
+ data);
199
+ }
200
+
201
+ } // namespace
202
+
203
+ void setDockMenu(std::vector<MenuItem>, MenuClicked, std::function<void(Failure)>) {
204
+ throw std::runtime_error("crossbind: a macOS app alone has a Dock menu");
205
+ }
206
+
207
+ void keepPresses(void *webView) { g_signal_connect(GTK_WIDGET(webView), "button-press-event", G_CALLBACK(onPress), nullptr); }
208
+
209
+ // The menu opens at the page's point in its window's webview, which WebKitGTK zooms.
210
+ void popupMenu(uint64_t window, std::vector<MenuItem> items, double x, double y, std::function<void(Failure, std::optional<std::string>)> done) {
211
+ onMainThread([window, items = std::move(items), x, y, done = std::move(done)] {
212
+ const std::optional<Window> open = windowOf(window);
213
+ GtkWidget *view = open ? GTK_WIDGET(open->webView()) : nullptr;
214
+ if (!view || !gtk_widget_get_realized(view)) {
215
+ done(std::string("crossbind: the app has no window to show a menu in"), std::nullopt);
216
+ return;
217
+ }
218
+ if (isPopupOpen) {
219
+ done(std::string("crossbind: a menu of the page's is open already"), std::nullopt);
220
+ return;
221
+ }
222
+ auto picked = std::make_shared<std::optional<std::string>>();
223
+ GtkWidget *menu = gtk_menu_new();
224
+ GtkAccelGroup *accelerators = gtk_accel_group_new();
225
+ try {
226
+ for (const auto &item : items) {
227
+ addItem(GTK_MENU_SHELL(menu), item, window, accelerators, [picked](uint64_t, std::string id, std::optional<bool>) { *picked = std::move(id); });
228
+ }
229
+ } catch (const std::exception &error) {
230
+ gtk_widget_destroy(menu);
231
+ g_object_unref(accelerators);
232
+ done(std::string(error.what()), std::nullopt);
233
+ return;
234
+ }
235
+ gtk_menu_attach_to_widget(GTK_MENU(menu), view, nullptr);
236
+ auto *popup = new Popup{done, picked, menu, accelerators};
237
+ g_signal_connect(menu, "deactivate", G_CALLBACK(onPopupClosed), popup);
238
+ const double zoom = webkit_web_view_get_zoom_level(WEBKIT_WEB_VIEW(view));
239
+ gtk_widget_show_all(menu);
240
+ GdkEvent *trigger = lastPress && g_get_monotonic_time() - lastPressTime < FRESH_PRESS_US ? lastPress : nullptr;
241
+ // The page's point in the webview's own GdkWindow: gtk_menu_popup_at_widget() takes a widget's allocation to be in
242
+ // that window, and so added the webview's place in its toplevel once more, the menu bar's height on X11 and with
243
+ // the client-side decorations' on Wayland.
244
+ GdkRectangle point{static_cast<int>(x * zoom), static_cast<int>(y * zoom), 1, 1};
245
+ gtk_menu_popup_at_rect(GTK_MENU(menu), gtk_widget_get_window(view), &point, GDK_GRAVITY_NORTH_WEST, GDK_GRAVITY_NORTH_WEST, trigger);
246
+ // GTK shows no menu it could not grab the pointer for, and tells of none: it closes without a click.
247
+ if (!gtk_widget_get_visible(gtk_widget_get_toplevel(menu))) {
248
+ g_signal_handlers_disconnect_by_func(menu, reinterpret_cast<gpointer>(onPopupClosed), popup);
249
+ delete popup;
250
+ gtk_widget_destroy(menu);
251
+ g_object_unref(accelerators);
252
+ done(std::nullopt, std::nullopt);
253
+ return;
254
+ }
255
+ isPopupOpen = true;
256
+ });
257
+ }
258
+
259
+ // A Linux app is a directory: the executable, and the app's files in resources/ beside it.
260
+ std::string resourceFolder() {
261
+ char path[PATH_MAX];
262
+ const ssize_t length = readlink("/proc/self/exe", path, sizeof path);
263
+ const std::string executable = length > 0 ? std::string(path, static_cast<size_t>(length)) : std::string();
264
+ return executable.substr(0, executable.find_last_of('/')) + "/resources";
265
+ }
266
+
267
+ // The bar goes above the window's webview, in the box the host holds it in, and its accelerators come before the
268
+ // webview's keys; no menus hide it.
269
+ void setMenu(uint64_t window, std::vector<MenuItem> menus, MenuClicked clicked, std::function<void(Failure)> done) {
270
+ onMainThread([window, menus = std::move(menus), clicked = std::move(clicked), done = std::move(done)] {
271
+ const std::optional<Window> open = windowOf(window);
272
+ GtkWidget *view = open ? GTK_WIDGET(open->webView()) : nullptr;
273
+ GtkWidget *box = view ? gtk_widget_get_parent(view) : nullptr;
274
+ if (!GTK_IS_BOX(box)) {
275
+ done("crossbind: the page's window has no place to show its menus in");
276
+ return;
277
+ }
278
+ GtkWindow *shown = GTK_WINDOW(open->native());
279
+ GtkAccelGroup *accelerators = gtk_accel_group_new();
280
+ GtkWidget *next = gtk_menu_bar_new();
281
+ g_object_ref_sink(next);
282
+ try {
283
+ for (const auto &menu : menus) addItem(GTK_MENU_SHELL(next), menu, window, accelerators, clicked);
284
+ } catch (const std::exception &error) {
285
+ gtk_widget_destroy(next);
286
+ g_object_unref(next);
287
+ g_object_unref(accelerators);
288
+ done(std::string(error.what()));
289
+ return;
290
+ }
291
+ Bar &before = bars[window];
292
+ if (before.bar) gtk_widget_destroy(before.bar);
293
+ if (before.accelerators) {
294
+ gtk_window_remove_accel_group(shown, before.accelerators);
295
+ g_object_unref(before.accelerators);
296
+ }
297
+ // Above the webview, which the box holds at its end.
298
+ gtk_container_add(GTK_CONTAINER(box), next);
299
+ int height = 0;
300
+ if (!menus.empty()) {
301
+ gtk_widget_show_all(next);
302
+ gtk_widget_get_preferred_height(next, nullptr, &height);
303
+ }
304
+ g_object_unref(next);
305
+ gtk_window_add_accel_group(shown, accelerators);
306
+ before = Bar{next, accelerators};
307
+ keepPageHeight(shown, view, height);
308
+ done(std::nullopt);
309
+ });
310
+ }
311
+
312
+ // The bar went with its window, which held the accelerators as well.
313
+ void dropMenus(uint64_t window) {
314
+ const auto found = bars.find(window);
315
+ if (found == bars.end()) return;
316
+ if (found->second.accelerators) g_object_unref(found->second.accelerators);
317
+ bars.erase(found);
318
+ }
319
+
320
+ } // namespace crossbind::menu
@@ -0,0 +1,196 @@
1
+ // The page's menus, a desktop app's tray icon and the system's notifications, which crossbind/app's menu, tray and
2
+ // notification call: the items the page gives, held to what crossbind/app holds a page to, and what each platform shows
3
+ // of them. The host's own extension, it adds the functions of tray and notification beside menu's.
4
+ #include "menu.h"
5
+
6
+ #include "crossbind/app.h"
7
+
8
+ #include <algorithm>
9
+ #include <stdexcept>
10
+ #include <utility>
11
+
12
+ namespace crossbind::menu {
13
+ namespace {
14
+
15
+ std::string trayId;
16
+
17
+ std::function<void(Failure)> answered(Call call) {
18
+ return [call = std::move(call)](Failure failure) {
19
+ if (failure) call.reject(*failure);
20
+ else call.resolve();
21
+ };
22
+ }
23
+
24
+ // The roles crossbind/app takes, which a page that went around it is held to as well.
25
+ const std::vector<std::string> MENU_ROLES{"editMenu", "windowMenu", "undo", "redo", "cut", "copy", "paste", "selectAll", "minimize", "close", "quit"};
26
+ constexpr int FUNCTION_KEYS = 12;
27
+
28
+ Accelerator acceleratorOf(const std::string &text) {
29
+ Accelerator accelerator;
30
+ size_t start = 0;
31
+ for (size_t end = text.find('+'); end != std::string::npos; end = text.find('+', start)) {
32
+ const std::string modifier = text.substr(start, end - start);
33
+ #ifdef __APPLE__
34
+ const bool isCommand = modifier == "CmdOrCtrl";
35
+ #else
36
+ const bool isCommand = false;
37
+ #endif
38
+ if (isCommand) {
39
+ accelerator.command = true;
40
+ } else if (modifier == "CmdOrCtrl" || modifier == "Ctrl") {
41
+ accelerator.control = true;
42
+ } else if (modifier == "Alt") {
43
+ accelerator.alt = true;
44
+ } else if (modifier == "Shift") {
45
+ accelerator.shift = true;
46
+ } else {
47
+ throw std::invalid_argument("crossbind: no accelerator has the modifier " + modifier);
48
+ }
49
+ start = end + 1;
50
+ }
51
+ accelerator.key = text.substr(start);
52
+ for (int number = 1; number <= FUNCTION_KEYS; number += 1) {
53
+ if (accelerator.key == "F" + std::to_string(number)) accelerator.function = number;
54
+ }
55
+ return accelerator;
56
+ }
57
+
58
+ #ifndef __APPLE__
59
+ // A standard menu off macOS is its standard items, which each platform makes as its apps have them.
60
+ MenuItem standardMenu(const MenuItem &menu) {
61
+ const bool isEdit = menu.role == "editMenu";
62
+ MenuItem out;
63
+ out.isSubmenu = true;
64
+ out.label = !menu.label.empty() ? menu.label : isEdit ? "Edit" : "Window";
65
+ const std::vector<std::string> roles = isEdit ? std::vector<std::string>{"undo", "redo", "", "cut", "copy", "paste", "", "selectAll"}
66
+ : std::vector<std::string>{"minimize", "close"};
67
+ for (const auto &role : roles) {
68
+ MenuItem item;
69
+ item.role = role;
70
+ item.isSeparator = role.empty();
71
+ out.items.push_back(std::move(item));
72
+ }
73
+ return out;
74
+ }
75
+ #endif
76
+
77
+ std::vector<MenuItem> menuItems(const Value &list) {
78
+ std::vector<MenuItem> out;
79
+ for (const Value &value : list.asArray()) {
80
+ MenuItem item;
81
+ item.label = value["label"].asString("");
82
+ item.id = value["id"].asString("");
83
+ item.role = value["role"].asString("");
84
+ if (!item.role.empty() && std::find(MENU_ROLES.begin(), MENU_ROLES.end(), item.role) == MENU_ROLES.end()) {
85
+ throw std::invalid_argument("crossbind: no menu item has the role " + item.role);
86
+ }
87
+ const std::string accelerator = value["accelerator"].asString("");
88
+ if (!accelerator.empty()) item.accelerator = acceleratorOf(accelerator);
89
+ if (!value["checked"].isNull()) item.checked = value["checked"].asBool();
90
+ item.isEnabled = value["enabled"].asBool(true);
91
+ item.isSeparator = value["type"].asString("") == "separator";
92
+ const Value &children = value["items"];
93
+ item.isSubmenu = !children.isNull();
94
+ if (item.isSubmenu) item.items = menuItems(children);
95
+ #ifndef __APPLE__
96
+ if (item.role == "editMenu" || item.role == "windowMenu") item = standardMenu(item);
97
+ #endif
98
+ out.push_back(std::move(item));
99
+ }
100
+ return out;
101
+ }
102
+
103
+ // A tray's menu and the Dock's have neither standard items nor keys, and each item but a separator has a label, as
104
+ // crossbind/app holds a page to.
105
+ void checkTrayItems(const std::vector<MenuItem> &items, const std::string &menu) {
106
+ for (const auto &item : items) {
107
+ if (!item.role.empty() || item.accelerator) throw std::invalid_argument("crossbind: " + menu + " takes no role or accelerator");
108
+ if (!item.isSeparator && item.label.empty()) throw std::invalid_argument("crossbind: " + menu + " has an item without a label");
109
+ checkTrayItems(item.items, menu);
110
+ }
111
+ }
112
+
113
+ // A click the pages hear of as the event menu: one in a window's menus reaches the page that window shows, and one in the
114
+ // tray's menu or the Dock's the page in front that listens.
115
+ void menuClicked(uint64_t window, std::string id, std::optional<bool> checked) {
116
+ Value::Object click{{"id", std::move(id)}};
117
+ if (checked) click.emplace_back("checked", *checked);
118
+ const std::string json = Value(std::move(click)).json();
119
+ #if !defined(__ANDROID__)
120
+ if (window != 0) {
121
+ if (const std::optional<Window> open = windowOf(window)) app::emitEvent("menu", json, open->page().id());
122
+ return;
123
+ }
124
+ #endif
125
+ app::emitEventToFront("menu", json);
126
+ }
127
+
128
+ // The window of the page whose call sets menus or opens one: the app's own code has none. A phone's platform refuses
129
+ // menus itself.
130
+ uint64_t windowOfCall(const Call &call) {
131
+ const uint64_t window = call.page().windowId();
132
+ #if !defined(__ANDROID__) && !TARGET_OS_IPHONE
133
+ if (window == 0) throw std::invalid_argument("crossbind: " + call.name() + " sets the menus of a page's window, which the app's own code has none of");
134
+ #endif
135
+ return window;
136
+ }
137
+
138
+ } // namespace
139
+
140
+ const std::string &appId() { return trayId; }
141
+
142
+ } // namespace crossbind::menu
143
+
144
+ void crossbind_extension_menu(crossbind::Registry &registry) {
145
+ using namespace crossbind::menu;
146
+ using crossbind::Call;
147
+ trayId = registry.options()["id"].asString("");
148
+ #if defined(__linux__) && !defined(__ANDROID__)
149
+ registry.onWindow([](const crossbind::Window &window) { keepPresses(window.webView()); });
150
+ #elif defined(__APPLE__) && !TARGET_OS_IPHONE
151
+ registry.onWindow([](const crossbind::Window &) { answerDockMenu(); });
152
+ #endif
153
+ registry.onWindowClosed([](const crossbind::Window &window) { dropMenus(window.id()); });
154
+ #ifdef _WIN32
155
+ registry.onQuit(quitTray);
156
+ #endif
157
+ registry.function("menu.set", [](Call call) {
158
+ std::vector<MenuItem> menus = menuItems(call.options()["menus"]);
159
+ setMenu(windowOfCall(call), std::move(menus), menuClicked, answered(call));
160
+ });
161
+ registry.function("menu.popup", [](Call call) {
162
+ const crossbind::Value &options = call.options();
163
+ std::vector<MenuItem> items = menuItems(options["items"]);
164
+ popupMenu(windowOfCall(call), std::move(items), options["x"].asNumber(), options["y"].asNumber(), [call](Failure failure, std::optional<std::string> id) {
165
+ if (failure) {
166
+ call.reject(*failure);
167
+ } else {
168
+ call.resolve(id ? crossbind::Value(*id) : crossbind::Value());
169
+ }
170
+ });
171
+ });
172
+ registry.function("tray.set", [](Call call) {
173
+ const crossbind::Value &options = call.options();
174
+ TrayOptions tray{options["tooltip"].asString(""), options["menu"].isNull() ? std::vector<MenuItem>() : menuItems(options["menu"])};
175
+ checkTrayItems(tray.menu, "tray.set's menu");
176
+ setTray(std::move(tray), menuClicked, answered(call));
177
+ });
178
+ registry.function("tray.remove", [](Call call) { removeTray(answered(call)); });
179
+ registry.function("menu.setDock", [](Call call) {
180
+ std::vector<MenuItem> items = menuItems(call.options()["items"]);
181
+ checkTrayItems(items, "menu.setDock's menu");
182
+ setDockMenu(std::move(items), menuClicked, answered(call));
183
+ });
184
+ // A tap of one of showNotification()'s, which the host's notification center hands over on macOS: a push's sender
185
+ // names its own.
186
+ registry.onNotification([](const crossbind::Notification &notification) {
187
+ if (notification.isTap() && !notification.isRemote() && notification.id().rfind(NOTIFICATION_ID_PREFIX, 0) == 0) {
188
+ crossbind::app::emitEventToFront("notification");
189
+ }
190
+ });
191
+ registry.function("notification.show", [](Call call) {
192
+ const std::string title = call.options()["title"].asString("");
193
+ if (title.empty()) throw std::invalid_argument("crossbind: notification.show takes a title");
194
+ showNotification({title, call.options()["body"].asString("")}, answered(call));
195
+ });
196
+ }
package/native/menu.h ADDED
@@ -0,0 +1,99 @@
1
+ #pragma once
2
+
3
+ // What each platform does for the page's menus, a desktop app's tray icon and the system's notifications. Each is called
4
+ // on the module's thread, works on the main thread and calls done there with why it failed, or nothing. A platform that
5
+ // has no such function throws.
6
+ #include "crossbind/extension.h"
7
+
8
+ #include <functional>
9
+ #include <optional>
10
+ #include <string>
11
+ #include <vector>
12
+
13
+ namespace crossbind::menu {
14
+
15
+ using Failure = std::optional<std::string>;
16
+
17
+ // A key and its modifiers, as crossbind/app checks them: the key is A–Z, 0–9, F1–F12 or one of crossbind/app's names,
18
+ // and CmdOrCtrl is command on macOS and control elsewhere.
19
+ struct Accelerator {
20
+ bool command = false;
21
+ bool control = false;
22
+ bool alt = false;
23
+ bool shift = false;
24
+ std::string key;
25
+ // 1 to 12 for F1 to F12, else 0.
26
+ int function = 0;
27
+ };
28
+
29
+ // An item of the page's menus: a separator, a standard menu or item by its role, a submenu, or one whose clicks the page
30
+ // hears of by its id.
31
+ struct MenuItem {
32
+ std::string label;
33
+ std::string id;
34
+ std::string role;
35
+ std::optional<Accelerator> accelerator;
36
+ std::optional<bool> checked;
37
+ bool isSeparator = false;
38
+ bool isEnabled = true;
39
+ bool isSubmenu = false;
40
+ std::vector<MenuItem> items;
41
+ };
42
+
43
+ // A click of an item with an id, on the main thread: the window whose menus it is in, 0 for the tray's and the Dock's,
44
+ // and whether a checked one now is.
45
+ using MenuClicked = std::function<void(uint64_t window, std::string id, std::optional<bool> checked)>;
46
+ // Sets the menus of the window of this id, on the main thread, which calls done: the bar of that window on Windows and
47
+ // Linux, and on macOS the menu bar while it is the key window. A click of an item with an id calls clicked there.
48
+ void setMenu(uint64_t window, std::vector<MenuItem> menus, MenuClicked clicked, std::function<void(Failure)> done);
49
+ // The menus of a window that closed go with it, on the main thread.
50
+ void dropMenus(uint64_t window);
51
+ // Opens a menu of these items at the point x, y of the page of the window of this id, in CSS pixels from its top left,
52
+ // as a right click's menu opens, on the main thread: done gets there the id of the item clicked, or none when the menu
53
+ // closed without one. A standard item does what its role says.
54
+ void popupMenu(uint64_t window, std::vector<MenuItem> items, double x, double y, std::function<void(Failure, std::optional<std::string> id)> done);
55
+ // The menu a macOS app's Dock icon shows above the system's items, on the main thread, which calls done; a click of an
56
+ // item with an id calls clicked there. None takes it away.
57
+ void setDockMenu(std::vector<MenuItem> items, MenuClicked clicked, std::function<void(Failure)> done);
58
+ // The app's icon in the menu bar on macOS, the notification area on Windows and a desktop's status tray on Linux, with
59
+ // its tooltip and the menu a click opens. A click of an item with an id calls clicked, and a click of the icon that opens
60
+ // no menu tells the page in front that listens as the event tray. Set again, it changes the icon. On the main thread,
61
+ // which calls done.
62
+ struct TrayOptions {
63
+ std::string tooltip;
64
+ std::vector<MenuItem> menu;
65
+ };
66
+ void setTray(TrayOptions options, MenuClicked clicked, std::function<void(Failure)> done);
67
+ void removeTray(std::function<void(Failure)> done);
68
+ // A notification of the system's, a click on which tells the page in front that listens as the event notification. On
69
+ // the main thread, which calls done once the system has taken it.
70
+ struct Notification {
71
+ std::string title;
72
+ std::string body;
73
+ };
74
+ void showNotification(Notification notification, std::function<void(Failure)> done);
75
+ // What the identifier of each notification showNotification() shows starts with, whose taps the page hears of on macOS.
76
+ inline constexpr char NOTIFICATION_ID_PREFIX[] = "crossbind.menu.";
77
+
78
+ // The app's id, which a Linux tray names itself by on the session bus, as the build gives it in the extension's options.
79
+ const std::string &appId();
80
+
81
+ #if defined(__APPLE__) && !TARGET_OS_IPHONE
82
+ // Gives the app's delegate the Dock's menu, none until setDockMenu() sets one, as the window opens: AppKit asks a delegate
83
+ // that answered as it was set alone.
84
+ void answerDockMenu();
85
+ #endif
86
+
87
+ #ifdef _WIN32
88
+ // Takes the app's icon from the notification area as the app quits, where it would stay until a pointer passed over it.
89
+ void quitTray();
90
+ #endif
91
+
92
+ #if defined(__linux__) && !defined(__ANDROID__)
93
+ // The folder of a Linux app's files, resources/ beside its executable, which holds the app's icon.png.
94
+ std::string resourceFolder();
95
+ // Keeps the last button press of a window's webview, which popupMenu() opens from, as the window opens.
96
+ void keepPresses(void *webView);
97
+ #endif
98
+
99
+ } // namespace crossbind::menu