@gjsify/adwaita-web 0.64.13 → 0.64.15-next.287
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/lib/types/application.d.ts +3 -3
- package/lib/types/elements/adw-application-window.d.ts +4 -4
- package/lib/types/elements/adw-split-button.d.ts +19 -28
- package/lib/types/elements/adw-tab-overview.d.ts +4 -4
- package/lib/types/elements/gtk-menu-button.d.ts +30 -42
- package/lib/types/elements/gtk-popover-bin.d.ts +18 -11
- package/lib/types/elements/gtk-popover-menu-bar.d.ts +15 -8
- package/lib/types/elements/gtk-popover-menu.d.ts +13 -15
- package/lib/types/elements/menu-slot.d.ts +49 -0
- package/lib/types/elements/popover-menu.d.ts +11 -7
- package/lib/types/index.d.ts +2 -1
- package/lib/types/namespace/gio.d.ts +1 -1
- package/package.json +13 -12
- package/src/adw-application-window.spec.ts +5 -2
- package/src/application.ts +4 -4
- package/src/elements/adw-application-window.ts +17 -21
- package/src/elements/adw-split-button.ts +35 -59
- package/src/elements/adw-tab-overview.ts +18 -28
- package/src/elements/gtk-menu-button.ts +60 -82
- package/src/elements/gtk-popover-bin.ts +32 -38
- package/src/elements/gtk-popover-menu-bar.ts +41 -46
- package/src/elements/gtk-popover-menu.ts +35 -54
- package/src/elements/menu-slot.ts +139 -0
- package/src/elements/popover-menu.ts +26 -16
- package/src/gio-menu.spec.ts +173 -34
- package/src/gtk-image.spec.ts +3 -2
- package/src/gtk-menu-button.spec.ts +19 -16
- package/src/gtk-popover-bin.spec.ts +6 -5
- package/src/gtk-popover-menu-bar.spec.ts +11 -6
- package/src/gtk-popover-menu.spec.ts +22 -22
- package/src/index.ts +10 -2
- package/src/keyboard-operable.spec.ts +6 -5
- package/src/menu-actions.spec.ts +6 -1
- package/src/namespace/gio.ts +19 -2
- package/src/shared-tree-builder.ts +6 -1
- package/src/split-button.spec.ts +25 -8
|
@@ -32,7 +32,8 @@
|
|
|
32
32
|
// handle-input — boolean, default FALSE — GTK's default. When set, a right click and a
|
|
33
33
|
// long press open the popover at the pointer.
|
|
34
34
|
// Properties:
|
|
35
|
-
// menuModel — the
|
|
35
|
+
// menuModel — the real `Gio.MenuModel`, or `null` (get/set; ADR 0128). Setting it
|
|
36
|
+
// REPLACES any popover.
|
|
36
37
|
// popoverElement — the `<gtk-popover>` this bin presents (get/set; setting it drops
|
|
37
38
|
// the model, as GTK does). `GtkPopoverBin`'s own name for it is `popover`,
|
|
38
39
|
// which `HTMLElement` already spells as a string attribute for the DOM
|
|
@@ -60,14 +61,9 @@
|
|
|
60
61
|
// Copyright (c) GNOME contributors (GTK, libadwaita). LGPLv2.1+
|
|
61
62
|
// Modifications: Implemented as a Web Component for @gjsify/adwaita-web.
|
|
62
63
|
|
|
63
|
-
import {
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
menuRefusals,
|
|
67
|
-
normalizeMenuModel,
|
|
68
|
-
parseMenuModel,
|
|
69
|
-
} from '@gjsify/adwaita-core';
|
|
70
|
-
import type { AdwMenuActions, AdwMenuInput, AdwMenuModel } from '@gjsify/adwaita-core';
|
|
64
|
+
import type { AdwMenuActions, AdwMenuModel } from '@gjsify/adwaita-core';
|
|
65
|
+
|
|
66
|
+
import { MenuSlot } from './menu-slot.js';
|
|
71
67
|
|
|
72
68
|
// Both definitions have to be there before this module's own can build them in
|
|
73
69
|
// `connectedCallback`; a combined `import { GtkPopoverMenu }` would be elided, because the
|
|
@@ -82,7 +78,14 @@ import { WidgetElement } from '../widget-element.js';
|
|
|
82
78
|
const LONG_PRESS_MS = 500;
|
|
83
79
|
|
|
84
80
|
export class GtkPopoverBin extends WidgetElement {
|
|
85
|
-
|
|
81
|
+
/**
|
|
82
|
+
* The model's door and `items-changed` subscription (ADR 0128), with NOTHING to do
|
|
83
|
+
* when the menu it holds mutates: this bin draws no rows. The `<gtk-popover-menu>` it
|
|
84
|
+
* built holds the SAME model and watches it through a slot of its own, so the rows
|
|
85
|
+
* follow one level down — and GTK sends no `notify::menu-model` for a mutation either
|
|
86
|
+
* (measured, `MENU_MODEL_NOTIFY_VECTORS`).
|
|
87
|
+
*/
|
|
88
|
+
private readonly _slot = new MenuSlot(() => undefined);
|
|
86
89
|
private _popover: GtkPopoverMenu | null = null;
|
|
87
90
|
private _actions: AdwMenuActions | null = null;
|
|
88
91
|
private _initialized = false;
|
|
@@ -92,9 +95,9 @@ export class GtkPopoverBin extends WidgetElement {
|
|
|
92
95
|
return ['menu-model', 'handle-input'];
|
|
93
96
|
}
|
|
94
97
|
|
|
95
|
-
/** The menu
|
|
96
|
-
get menuModel(): AdwMenuModel {
|
|
97
|
-
return this.
|
|
98
|
+
/** The menu this bin pops up, by identity, or `null` (ADR 0128). */
|
|
99
|
+
get menuModel(): AdwMenuModel | null {
|
|
100
|
+
return this._slot.model;
|
|
98
101
|
}
|
|
99
102
|
|
|
100
103
|
/**
|
|
@@ -102,9 +105,8 @@ export class GtkPopoverBin extends WidgetElement {
|
|
|
102
105
|
* setters clear each other (gtkpopoverbin.c:474-500, :543-551). A model REPLACES the
|
|
103
106
|
* popover; a popover DROPS the model, and both notify for both properties.
|
|
104
107
|
*/
|
|
105
|
-
set menuModel(value:
|
|
106
|
-
this.
|
|
107
|
-
assertMenuRenderable(this._model, ADW_MENU_SURFACE_WEB);
|
|
108
|
+
set menuModel(value: AdwMenuModel | string | null) {
|
|
109
|
+
this._slot.write(value);
|
|
108
110
|
this._adoptPopover(this._buildPopover());
|
|
109
111
|
this._notifyModel();
|
|
110
112
|
}
|
|
@@ -123,7 +125,7 @@ export class GtkPopoverBin extends WidgetElement {
|
|
|
123
125
|
}
|
|
124
126
|
|
|
125
127
|
set popoverElement(value: GtkPopoverMenu | null) {
|
|
126
|
-
this.
|
|
128
|
+
this._slot.write(null);
|
|
127
129
|
this._adoptPopover(value);
|
|
128
130
|
this._notifyModel();
|
|
129
131
|
}
|
|
@@ -157,8 +159,8 @@ export class GtkPopoverBin extends WidgetElement {
|
|
|
157
159
|
this.addEventListener('contextmenu', (event) => this._onContextMenu(event));
|
|
158
160
|
this.addEventListener('keydown', (event) => this._onKeyDown(event));
|
|
159
161
|
this._wireGestures();
|
|
160
|
-
if (this.
|
|
161
|
-
|
|
162
|
+
if (this._slot.model === null) this._slot.writeMarkup(this.getAttribute('menu-model'));
|
|
163
|
+
this._adoptPopover(this._buildPopover());
|
|
162
164
|
}
|
|
163
165
|
|
|
164
166
|
disconnectedCallback() {
|
|
@@ -176,8 +178,8 @@ export class GtkPopoverBin extends WidgetElement {
|
|
|
176
178
|
return;
|
|
177
179
|
}
|
|
178
180
|
if (name === 'menu-model') {
|
|
179
|
-
this.
|
|
180
|
-
this._adoptPopover(this.
|
|
181
|
+
this._slot.writeMarkup(this.getAttribute('menu-model'));
|
|
182
|
+
this._adoptPopover(this._buildPopover());
|
|
181
183
|
this._notifyModel();
|
|
182
184
|
}
|
|
183
185
|
}
|
|
@@ -265,14 +267,20 @@ export class GtkPopoverBin extends WidgetElement {
|
|
|
265
267
|
this.popup();
|
|
266
268
|
}
|
|
267
269
|
|
|
270
|
+
/**
|
|
271
|
+
* The popover follows the PRESENCE of a model, not its length: an empty non-null
|
|
272
|
+
* `Gio.Menu` at `menu-model` still builds a `GtkPopoverMenu` in GTK and still
|
|
273
|
+
* dissociates an application-set popover (measured, `MENU_MODEL_DOOR_VECTORS`), and
|
|
274
|
+
* `null` is the one spelling of "no menu".
|
|
275
|
+
*/
|
|
268
276
|
private _buildPopover(): GtkPopoverMenu | null {
|
|
269
|
-
if (this.
|
|
277
|
+
if (this._slot.model === null) return null;
|
|
270
278
|
const popover = document.createElement('gtk-popover-menu') as GtkPopoverMenu;
|
|
271
279
|
// `gtk_popover_set_position (popover, GTK_POS_BOTTOM)` in the same function
|
|
272
280
|
// (gtkpopoverbin.c:493), on a popover `gtk_popover_menu_new_from_model` made.
|
|
273
281
|
popover.setAttribute('position', 'bottom');
|
|
274
282
|
popover.setAttribute('align', 'start');
|
|
275
|
-
popover.menuModel = this.
|
|
283
|
+
popover.menuModel = this._slot.model;
|
|
276
284
|
popover.actions = this._actions;
|
|
277
285
|
popover.anchor = this;
|
|
278
286
|
return popover;
|
|
@@ -313,24 +321,10 @@ export class GtkPopoverBin extends WidgetElement {
|
|
|
313
321
|
this.dispatchEvent(
|
|
314
322
|
new CustomEvent('notify::menu-model', {
|
|
315
323
|
bubbles: true,
|
|
316
|
-
detail: { menuModel: this.
|
|
324
|
+
detail: { menuModel: this._slot.model, popover: this._popover },
|
|
317
325
|
}),
|
|
318
326
|
);
|
|
319
327
|
}
|
|
320
|
-
|
|
321
|
-
/**
|
|
322
|
-
* The `menu-model` ATTRIBUTE through the core parser, refusing a `custom` item the way
|
|
323
|
-
* every menu-bearing surface here does: markup is parsed by the browser, so a throw from
|
|
324
|
-
* `connectedCallback` would reach nobody and the menu would be silently empty.
|
|
325
|
-
*/
|
|
326
|
-
private _parseMenuAttr(): AdwMenuModel {
|
|
327
|
-
const model = normalizeMenuModel(parseMenuModel(this.getAttribute('menu-model')));
|
|
328
|
-
const refusals = menuRefusals(model, ADW_MENU_SURFACE_WEB);
|
|
329
|
-
if (refusals.length === 0) return model;
|
|
330
|
-
console.error(`${ADW_MENU_SURFACE_WEB.name} cannot render this menu, so it has none:`);
|
|
331
|
-
for (const refusal of refusals) console.error(` [${refusal.path.join('.')}] ${refusal.message}`);
|
|
332
|
-
return [];
|
|
333
|
-
}
|
|
334
328
|
}
|
|
335
329
|
|
|
336
330
|
customElements.define('gtk-popover-bin', GtkPopoverBin);
|
|
@@ -37,7 +37,8 @@
|
|
|
37
37
|
// menu-model — the portable menu model as JSON (ADR 0042), whose TOPLEVEL entries must
|
|
38
38
|
// all be submenus.
|
|
39
39
|
// Properties:
|
|
40
|
-
// menuModel — the
|
|
40
|
+
// menuModel — the real `Gio.MenuModel`, or `null` (get/set; ADR 0128 — a `custom` item
|
|
41
|
+
// throws here).
|
|
41
42
|
// actions — what the action group publishes about the actions the menus name.
|
|
42
43
|
// activeItem — the focused/selected item's button, or `null`. GTK's `active_item`.
|
|
43
44
|
// Methods:
|
|
@@ -57,16 +58,10 @@
|
|
|
57
58
|
// Copyright (c) GNOME contributors (GTK, libadwaita). LGPLv2.1+
|
|
58
59
|
// Modifications: Implemented as a Web Component for @gjsify/adwaita-web.
|
|
59
60
|
|
|
60
|
-
import {
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
menuLabelText,
|
|
65
|
-
menuRefusals,
|
|
66
|
-
normalizeMenuModel,
|
|
67
|
-
parseMenuModel,
|
|
68
|
-
} from '@gjsify/adwaita-core';
|
|
69
|
-
import type { AdwMenuActions, AdwMenuInput, AdwMenuModel, AdwMenuPath, AdwMenuSubmenu } from '@gjsify/adwaita-core';
|
|
61
|
+
import { buildMenu, menuItemAt, menuLabelText } from '@gjsify/adwaita-core';
|
|
62
|
+
import type { AdwMenuActions, AdwMenuModel, AdwMenuPath, AdwMenuSubmenu } from '@gjsify/adwaita-core';
|
|
63
|
+
|
|
64
|
+
import { MenuSlot } from './menu-slot.js';
|
|
70
65
|
|
|
71
66
|
// The definition has to be there before this module's own can build one in
|
|
72
67
|
// `connectedCallback`; a combined `import { GtkPopoverMenu }` would be elided, because the
|
|
@@ -88,7 +83,8 @@ interface MenuBarItem {
|
|
|
88
83
|
}
|
|
89
84
|
|
|
90
85
|
export class GtkPopoverMenuBar extends WidgetElement {
|
|
91
|
-
|
|
86
|
+
/** The model, its tree snapshot and its `items-changed` subscription (ADR 0128). */
|
|
87
|
+
private readonly _slot = new MenuSlot(() => this._menuChanged());
|
|
92
88
|
private _actions: AdwMenuActions | null = null;
|
|
93
89
|
private _items: MenuBarItem[] = [];
|
|
94
90
|
private _active: MenuBarItem | null = null;
|
|
@@ -98,14 +94,29 @@ export class GtkPopoverMenuBar extends WidgetElement {
|
|
|
98
94
|
return ['menu-model'];
|
|
99
95
|
}
|
|
100
96
|
|
|
101
|
-
/** The menu
|
|
102
|
-
get menuModel(): AdwMenuModel {
|
|
103
|
-
return this.
|
|
97
|
+
/** The menu this bar draws, by identity, or `null` (ADR 0128). */
|
|
98
|
+
get menuModel(): AdwMenuModel | null {
|
|
99
|
+
return this._slot.model;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
set menuModel(value: AdwMenuModel | string | null) {
|
|
103
|
+
this._slot.write(value);
|
|
104
|
+
this._menuChanged();
|
|
104
105
|
}
|
|
105
106
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
107
|
+
/**
|
|
108
|
+
* The one place a changed menu is answered — from either door AND from a mutation of
|
|
109
|
+
* the menu the bar is holding, which is why the `console.warn` below is here rather
|
|
110
|
+
* than beside the attribute parse it used to live in: a toplevel entry that is not a
|
|
111
|
+
* submenu can arrive by `append_item` just as easily as by markup.
|
|
112
|
+
*/
|
|
113
|
+
private _menuChanged(): void {
|
|
114
|
+
for (const [index, node] of this._slot.tree.entries()) {
|
|
115
|
+
if (node.kind === 'submenu') continue;
|
|
116
|
+
// A WARNING and not an error, and the distinction is GTK's: `g_warning` is
|
|
117
|
+
// recoverable, and the submenus beside the offending entry still work.
|
|
118
|
+
console.warn(`Don't know how to handle this item: top-level entry ${index} is a ${node.kind}.`);
|
|
119
|
+
}
|
|
109
120
|
if (this._initialized) this._rebuild();
|
|
110
121
|
}
|
|
111
122
|
|
|
@@ -136,14 +147,14 @@ export class GtkPopoverMenuBar extends WidgetElement {
|
|
|
136
147
|
this.addEventListener('pointerleave', () => {
|
|
137
148
|
if (this._active !== null && !this._active.popover.open) this._setActive(null, false);
|
|
138
149
|
});
|
|
139
|
-
if (this.
|
|
140
|
-
this.
|
|
150
|
+
if (this._slot.model === null) this._slot.writeMarkup(this.getAttribute('menu-model'));
|
|
151
|
+
this._menuChanged();
|
|
141
152
|
}
|
|
142
153
|
|
|
143
154
|
attributeChangedCallback(name?: string) {
|
|
144
155
|
if (!this._initialized || name !== 'menu-model') return;
|
|
145
|
-
this.
|
|
146
|
-
this.
|
|
156
|
+
this._slot.writeMarkup(this.getAttribute('menu-model'));
|
|
157
|
+
this._menuChanged();
|
|
147
158
|
}
|
|
148
159
|
|
|
149
160
|
/** `gtk_popover_menu_bar_select_first` — activate and OPEN the first item. */
|
|
@@ -213,7 +224,7 @@ export class GtkPopoverMenuBar extends WidgetElement {
|
|
|
213
224
|
this._items = [];
|
|
214
225
|
this._active = null;
|
|
215
226
|
const built: MenuBarItem[] = [];
|
|
216
|
-
this.
|
|
227
|
+
this._slot.tree.forEach((node, index) => {
|
|
217
228
|
if (node.kind !== 'submenu') return;
|
|
218
229
|
built.push(this._buildItem(node, [index]));
|
|
219
230
|
});
|
|
@@ -237,8 +248,8 @@ export class GtkPopoverMenuBar extends WidgetElement {
|
|
|
237
248
|
/**
|
|
238
249
|
* One `menubar > item > popover` (the node tree in gtkpopovermenubar.c:38-46).
|
|
239
250
|
*
|
|
240
|
-
* A toplevel node that is not a submenu is
|
|
241
|
-
* wording rather than drawn: `tracker_insert` builds nothing for it
|
|
251
|
+
* A toplevel node that is not a submenu is WARNED about by {@link _menuChanged} with
|
|
252
|
+
* GTK's own wording rather than drawn: `tracker_insert` builds nothing for it
|
|
242
253
|
* (gtkpopovermenubar.c:466), and a row that opens nothing is the same lie in another
|
|
243
254
|
* shape.
|
|
244
255
|
*/
|
|
@@ -269,9 +280,10 @@ export class GtkPopoverMenuBar extends WidgetElement {
|
|
|
269
280
|
// (gtkpopovermenubar.c:435-436).
|
|
270
281
|
popover.setAttribute('position', 'bottom');
|
|
271
282
|
popover.setAttribute('align', 'start');
|
|
272
|
-
// The submenu's OWN items,
|
|
273
|
-
//
|
|
274
|
-
popover
|
|
283
|
+
// The submenu's OWN items, as a menu OF THEIR OWN: since ADR 0128 the popover takes
|
|
284
|
+
// a real `Gio.MenuModel`, and `buildMenu` is how a tree becomes one. Handing over
|
|
285
|
+
// the bar's own model instead would give the popover the whole bar.
|
|
286
|
+
popover.menuModel = buildMenu(node.items);
|
|
275
287
|
popover.actions = this._actions;
|
|
276
288
|
popover.anchor = button;
|
|
277
289
|
// `gtk_accessible_update_relation (…, LABELLED_BY, item->label, CONTROLS,
|
|
@@ -324,7 +336,7 @@ export class GtkPopoverMenuBar extends WidgetElement {
|
|
|
324
336
|
* row is not the row that opened a submenu (gtkpopovermenu.c:257-259).
|
|
325
337
|
*/
|
|
326
338
|
private _activate(item: MenuBarItem, path: AdwMenuPath): void {
|
|
327
|
-
const target = menuItemAt(this.
|
|
339
|
+
const target = menuItemAt(this._slot.tree, path);
|
|
328
340
|
item.popover.popdown();
|
|
329
341
|
item.button.focus();
|
|
330
342
|
if (target === null) return;
|
|
@@ -335,23 +347,6 @@ export class GtkPopoverMenuBar extends WidgetElement {
|
|
|
335
347
|
}),
|
|
336
348
|
);
|
|
337
349
|
}
|
|
338
|
-
|
|
339
|
-
private _parseMenuAttr(): AdwMenuModel {
|
|
340
|
-
const model = normalizeMenuModel(parseMenuModel(this.getAttribute('menu-model')));
|
|
341
|
-
const refusals = menuRefusals(model, ADW_MENU_SURFACE_WEB);
|
|
342
|
-
if (refusals.length > 0) {
|
|
343
|
-
console.error(`${ADW_MENU_SURFACE_WEB.name} cannot render this menu, so it has none:`);
|
|
344
|
-
for (const refusal of refusals) console.error(` [${refusal.path.join('.')}] ${refusal.message}`);
|
|
345
|
-
return [];
|
|
346
|
-
}
|
|
347
|
-
for (const [index, node] of model.entries()) {
|
|
348
|
-
if (node.kind === 'submenu') continue;
|
|
349
|
-
// A WARNING and not an error, and the distinction is GTK's: `g_warning` is
|
|
350
|
-
// recoverable, and the submenus beside the offending entry still work.
|
|
351
|
-
console.warn(`Don't know how to handle this item: top-level entry ${index} is a ${node.kind}.`);
|
|
352
|
-
}
|
|
353
|
-
return model;
|
|
354
|
-
}
|
|
355
350
|
}
|
|
356
351
|
|
|
357
352
|
customElements.define('gtk-popover-menu-bar', GtkPopoverMenuBar);
|
|
@@ -40,9 +40,9 @@
|
|
|
40
40
|
// is its name. `gtk_popover_menu_open_submenu(popover, "main")` is
|
|
41
41
|
// the way back, so this is writeable and readable.
|
|
42
42
|
// Properties:
|
|
43
|
-
// menuModel — the
|
|
44
|
-
//
|
|
45
|
-
//
|
|
43
|
+
// menuModel — the real `Gio.MenuModel` (ADR 0128), or `null` (get/set; a `custom`
|
|
44
|
+
// item throws here). A menu bar hands one of these over for a submenu
|
|
45
|
+
// of its own; a JSON string is the authored spelling.
|
|
46
46
|
// actions — what the action group publishes about the actions the model names
|
|
47
47
|
// (ADR 0042), the source of every row's enabled/checked state.
|
|
48
48
|
// visibleSubmenu — the open page's name (get/set).
|
|
@@ -72,26 +72,22 @@
|
|
|
72
72
|
// Copyright (c) GNOME contributors (GTK, libadwaita). LGPLv2.1+
|
|
73
73
|
// Modifications: Implemented as a Web Component for @gjsify/adwaita-web.
|
|
74
74
|
|
|
75
|
-
import {
|
|
76
|
-
ADW_MENU_SURFACE_WEB,
|
|
77
|
-
assertMenuRenderable,
|
|
78
|
-
menuItemAt,
|
|
79
|
-
menuNodeAt,
|
|
80
|
-
menuRefusals,
|
|
81
|
-
normalizeMenuModel,
|
|
82
|
-
parseMenuModel,
|
|
83
|
-
} from '@gjsify/adwaita-core';
|
|
75
|
+
import { menuItemAt, menuNodeAt } from '@gjsify/adwaita-core';
|
|
84
76
|
import { activateElementMenuItem, menuActionsOfElement } from '../application.js';
|
|
85
|
-
import type { AdwMenuActions,
|
|
77
|
+
import type { AdwMenuActions, AdwMenuModel, AdwMenuPath, AdwMenuTree } from '@gjsify/adwaita-core';
|
|
86
78
|
|
|
87
79
|
import { GtkPopover } from './gtk-popover.js';
|
|
80
|
+
import { MenuSlot } from './menu-slot.js';
|
|
88
81
|
import { PopoverMenuView } from './popover-menu.js';
|
|
89
82
|
|
|
90
83
|
/** The name the ROOT page carries — `gtk_popover_menu_open_submenu`'s own "main". */
|
|
91
84
|
const MAIN_PAGE = 'main';
|
|
92
85
|
|
|
93
86
|
export class GtkPopoverMenu extends GtkPopover {
|
|
94
|
-
|
|
87
|
+
/** The model, its tree snapshot and its `items-changed` subscription (ADR 0128). */
|
|
88
|
+
private readonly _slot = new MenuSlot(() => {
|
|
89
|
+
if (this._menuInitialized) this._renderMenu();
|
|
90
|
+
});
|
|
95
91
|
private _actions: AdwMenuActions | null = null;
|
|
96
92
|
private _menuView!: PopoverMenuView;
|
|
97
93
|
/** Named apart from `GtkPopover`'s own: two `private` fields of one name are two fields. */
|
|
@@ -101,26 +97,23 @@ export class GtkPopoverMenu extends GtkPopover {
|
|
|
101
97
|
return [...GtkPopover.observedAttributes, 'menu-model', 'menu-title', 'visible-submenu'];
|
|
102
98
|
}
|
|
103
99
|
|
|
104
|
-
/** The menu,
|
|
105
|
-
get menuModel(): AdwMenuModel {
|
|
106
|
-
return this.
|
|
100
|
+
/** The menu this popover draws, by identity, or `null` (ADR 0128). */
|
|
101
|
+
get menuModel(): AdwMenuModel | null {
|
|
102
|
+
return this._slot.model;
|
|
107
103
|
}
|
|
108
104
|
|
|
109
105
|
/**
|
|
110
|
-
*
|
|
111
|
-
* HERE — at the assignment, where it is a call the caller can catch.
|
|
112
|
-
* markup the browser parses, so
|
|
113
|
-
* says why on `console.error
|
|
106
|
+
* Take a real model, a JSON string in the portable spelling, or `null`, refusing a
|
|
107
|
+
* `custom` item HERE — at the assignment, where it is a call the caller can catch.
|
|
108
|
+
* An ATTRIBUTE is markup the browser parses, so that path refuses the whole menu and
|
|
109
|
+
* says why on `console.error`; {@link MenuSlot} owns both doors.
|
|
114
110
|
*
|
|
115
|
-
*
|
|
116
|
-
*
|
|
117
|
-
*
|
|
118
|
-
* straight back. The one cast is where the core's parameter type (`AdwMenuInput`, whose
|
|
119
|
-
* entries are the AUTHORED shapes) is narrower than the behaviour it documents.
|
|
111
|
+
* A REAL MODEL IS WHAT A MENU BAR HANDS OVER for one of its submenus, and the model
|
|
112
|
+
* it hands over is `buildMenu(node.items)` — a menu of its own rather than this
|
|
113
|
+
* element's authored one, so the two cannot overwrite each other.
|
|
120
114
|
*/
|
|
121
|
-
set menuModel(value:
|
|
122
|
-
this.
|
|
123
|
-
assertMenuRenderable(this._model, ADW_MENU_SURFACE_WEB);
|
|
115
|
+
set menuModel(value: AdwMenuModel | string | null) {
|
|
116
|
+
this._slot.write(value);
|
|
124
117
|
if (this._menuInitialized) this._renderMenu();
|
|
125
118
|
}
|
|
126
119
|
|
|
@@ -183,10 +176,10 @@ export class GtkPopoverMenu extends GtkPopover {
|
|
|
183
176
|
this._menuView = new PopoverMenuView(this, 'adw-popover-menu', (path) => this._activate(path));
|
|
184
177
|
this._menuView.setActions(this._actions);
|
|
185
178
|
|
|
186
|
-
if (this.
|
|
187
|
-
// The VIEW is what draws, and it holds the
|
|
188
|
-
//
|
|
189
|
-
//
|
|
179
|
+
if (this._slot.model === null) this._slot.writeMarkup(this.getAttribute('menu-model'));
|
|
180
|
+
// The VIEW is what draws, and it holds the TREE it walks — seeding the slot
|
|
181
|
+
// without handing the view the same snapshot would leave it drawing an EMPTY
|
|
182
|
+
// page over a model the element believes it has.
|
|
190
183
|
this._renderMenu();
|
|
191
184
|
|
|
192
185
|
// The visible page RESETS when the popover closes, as `gtk_popover_menu_show` does
|
|
@@ -194,7 +187,7 @@ export class GtkPopoverMenu extends GtkPopover {
|
|
|
194
187
|
// a menu that reopens three levels deep is one the reader cannot get out of.
|
|
195
188
|
this.subscribe((open) => {
|
|
196
189
|
if (open) {
|
|
197
|
-
this._menuView.setActions(this._actions ?? menuActionsOfElement(this, this.
|
|
190
|
+
this._menuView.setActions(this._actions ?? menuActionsOfElement(this, this._slot.tree));
|
|
198
191
|
this._menuView.render();
|
|
199
192
|
this._menuView.focusableRows[0]?.focus();
|
|
200
193
|
} else this._menuView.reset();
|
|
@@ -205,7 +198,7 @@ export class GtkPopoverMenu extends GtkPopover {
|
|
|
205
198
|
super.attributeChangedCallback(name, previous, value);
|
|
206
199
|
if (!this._menuInitialized) return;
|
|
207
200
|
if (name === 'menu-model') {
|
|
208
|
-
this.
|
|
201
|
+
this._slot.writeMarkup(this.getAttribute('menu-model'));
|
|
209
202
|
this._renderMenu();
|
|
210
203
|
return;
|
|
211
204
|
}
|
|
@@ -258,7 +251,7 @@ export class GtkPopoverMenu extends GtkPopover {
|
|
|
258
251
|
} else {
|
|
259
252
|
const path = this._findSubmenu(name);
|
|
260
253
|
if (path === null) return false;
|
|
261
|
-
this._menuView.
|
|
254
|
+
this._menuView.setMenuTree(this._slot.tree);
|
|
262
255
|
this._menuView.showPage(path);
|
|
263
256
|
}
|
|
264
257
|
this._menuView.render();
|
|
@@ -276,12 +269,12 @@ export class GtkPopoverMenu extends GtkPopover {
|
|
|
276
269
|
private _openPageLabel(): string | null {
|
|
277
270
|
const path = this._menuView.page;
|
|
278
271
|
if (path.length === 0) return null;
|
|
279
|
-
const node = menuNodeAt(this.
|
|
272
|
+
const node = menuNodeAt(this._slot.tree, path);
|
|
280
273
|
return node !== null && node.kind !== 'item' ? node.label : null;
|
|
281
274
|
}
|
|
282
275
|
|
|
283
276
|
private _findSubmenu(label: string): AdwMenuPath | null {
|
|
284
|
-
const walk = (nodes:
|
|
277
|
+
const walk = (nodes: AdwMenuTree, path: AdwMenuPath): AdwMenuPath | null => {
|
|
285
278
|
for (const [index, node] of nodes.entries()) {
|
|
286
279
|
if (node.kind === 'item') continue;
|
|
287
280
|
const here = [...path, index];
|
|
@@ -291,7 +284,7 @@ export class GtkPopoverMenu extends GtkPopover {
|
|
|
291
284
|
}
|
|
292
285
|
return null;
|
|
293
286
|
};
|
|
294
|
-
return walk(this.
|
|
287
|
+
return walk(this._slot.tree, []);
|
|
295
288
|
}
|
|
296
289
|
|
|
297
290
|
/**
|
|
@@ -313,7 +306,7 @@ export class GtkPopoverMenu extends GtkPopover {
|
|
|
313
306
|
* open by choosing from it.
|
|
314
307
|
*/
|
|
315
308
|
private _activate(path: AdwMenuPath): void {
|
|
316
|
-
const target = menuItemAt(this.
|
|
309
|
+
const target = menuItemAt(this._slot.tree, path);
|
|
317
310
|
this.popdown();
|
|
318
311
|
this.anchor?.focus();
|
|
319
312
|
if (target === null) return;
|
|
@@ -327,22 +320,10 @@ export class GtkPopoverMenu extends GtkPopover {
|
|
|
327
320
|
}
|
|
328
321
|
|
|
329
322
|
private _renderMenu(): void {
|
|
330
|
-
this._menuView.
|
|
323
|
+
this._menuView.setMenuTree(this._slot.tree);
|
|
331
324
|
this._menuView.setTitle(this.menuTitle);
|
|
332
325
|
this._menuView.render();
|
|
333
|
-
if (this.
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
private _parseMenuAttr(): AdwMenuModel {
|
|
337
|
-
const model = normalizeMenuModel(parseMenuModel(this.getAttribute('menu-model')));
|
|
338
|
-
const refusals = menuRefusals(model, ADW_MENU_SURFACE_WEB);
|
|
339
|
-
if (refusals.length === 0) return model;
|
|
340
|
-
// A throw from `connectedCallback` is reported as an uncaught PAGE error and
|
|
341
|
-
// reaches nobody, so markup refuses the menu and says why. See
|
|
342
|
-
// `<gtk-menu-button>`'s `_acceptMenu` for the same two doors.
|
|
343
|
-
console.error(`${ADW_MENU_SURFACE_WEB.name} cannot render this menu, so it has none:`);
|
|
344
|
-
for (const refusal of refusals) console.error(` [${refusal.path.join('.')}] ${refusal.message}`);
|
|
345
|
-
return [];
|
|
326
|
+
if (this._slot.isEmpty) this.popdown();
|
|
346
327
|
}
|
|
347
328
|
}
|
|
348
329
|
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
// The `menu-model` SLOT the menu-bearing elements of this package share.
|
|
2
|
+
//
|
|
3
|
+
// ADR 0128 made `menu-model` a real `Gio.MenuModel`, and that turned one assignment into
|
|
4
|
+
// four steps that only work in one order:
|
|
5
|
+
//
|
|
6
|
+
// 1. read the write — `readMenuModelWrite` is the core's one door, and it is what tells a
|
|
7
|
+
// real model apart from the JSON string an attribute carries (ADR 0128 § 4);
|
|
8
|
+
// 2. REFUSE what this surface cannot draw, before the element renders it;
|
|
9
|
+
// 3. take a TREE SNAPSHOT, because every walker (`flattenMenu`, `menuNodeAt`,
|
|
10
|
+
// `menuItemAt`, `menuRefusals`) takes an `AdwMenuTree` and not a model — one snapshot
|
|
11
|
+
// per change rather than a chain of `get_item_link` calls per row;
|
|
12
|
+
// 4. connect `items-changed` — on the root AND on every section and submenu it links,
|
|
13
|
+
// which is what `watchMenuModel` does, because a link's emission does NOT bubble.
|
|
14
|
+
//
|
|
15
|
+
// THE STEP THAT IS EASIEST TO GET WRONG IS THE LAST ONE: the watcher has to be RE-RUN after
|
|
16
|
+
// every snapshot, since a mutation can add a section that was not there to connect to. Six
|
|
17
|
+
// elements here need all four steps, so the arithmetic lives once and there is no sixth
|
|
18
|
+
// place to forget it.
|
|
19
|
+
//
|
|
20
|
+
// THE TWO WRITES REFUSE DIFFERENTLY, and the difference is which side can hear it, exactly
|
|
21
|
+
// as it was before the model became real. {@link MenuSlot.write} is a property assignment,
|
|
22
|
+
// so it is a CALL and it THROWS; {@link MenuSlot.writeMarkup} is an attribute the browser
|
|
23
|
+
// parsed, where a throw out of `connectedCallback` reaches nobody and is reported as an
|
|
24
|
+
// uncaught page error (measured: it broke `adwaita-upgrade-order.spec.ts`, which counts
|
|
25
|
+
// exactly those) — so markup refuses the MENU and says why on `console.error`.
|
|
26
|
+
//
|
|
27
|
+
// THE WATCHER'S LIFETIME IS THE ELEMENT'S, with no `release()` to call. GTK disconnects in
|
|
28
|
+
// `dispose`, and a custom element has no dispose: it has `disconnectedCallback`, which also
|
|
29
|
+
// fires when a node is MOVED, and an element re-inserted after dropping its watcher would
|
|
30
|
+
// be deaf to the menu it still holds. A held model keeping its element alive is the same
|
|
31
|
+
// shape GTK has, where the widget owns a reference to the model it reads.
|
|
32
|
+
|
|
33
|
+
import {
|
|
34
|
+
ADW_MENU_SURFACE_WEB,
|
|
35
|
+
GjsifyMenu,
|
|
36
|
+
assertMenuRenderable,
|
|
37
|
+
menuRefusals,
|
|
38
|
+
menuTree,
|
|
39
|
+
readMenuModelWrite,
|
|
40
|
+
watchMenuModel,
|
|
41
|
+
} from '@gjsify/adwaita-core';
|
|
42
|
+
import type { AdwMenuTree, GioMenuModel } from '@gjsify/adwaita-core';
|
|
43
|
+
|
|
44
|
+
export class MenuSlot {
|
|
45
|
+
/**
|
|
46
|
+
* The menu this element OWNS, so an edit to the same `menu-model="[…]"` attribute is a
|
|
47
|
+
* content change rather than a new menu — the popover already open follows it instead of
|
|
48
|
+
* being rebuilt (ADR 0128 § 5, the same rule `GjsifyOptionList` has for a list).
|
|
49
|
+
*/
|
|
50
|
+
private readonly _authored = new GjsifyMenu();
|
|
51
|
+
private _model: GioMenuModel | null = null;
|
|
52
|
+
private _tree: AdwMenuTree = [];
|
|
53
|
+
private _unwatch: (() => void) | null = null;
|
|
54
|
+
|
|
55
|
+
/** @param onItemsChanged what to do when the HELD menu mutates under the element. */
|
|
56
|
+
constructor(private readonly onItemsChanged: () => void) {}
|
|
57
|
+
|
|
58
|
+
/** The model itself, by identity — what `menuModel` answers and what a child is handed. */
|
|
59
|
+
get model(): GioMenuModel | null {
|
|
60
|
+
return this._model;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** The snapshot every walker reads. Re-taken on each `items-changed`, never aliased. */
|
|
64
|
+
get tree(): AdwMenuTree {
|
|
65
|
+
return this._tree;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Whether there is anything to show — NOT whether there is a menu.
|
|
70
|
+
*
|
|
71
|
+
* `null` and an empty `Gio.Menu` are different menus and GTK treats them differently: an
|
|
72
|
+
* empty non-null model still builds a `GtkPopoverMenu` and still dissociates an
|
|
73
|
+
* application-set popover (measured, `MENU_MODEL_DOOR_VECTORS`). This answers the
|
|
74
|
+
* question the ROWS ask — "is there a row to draw" — and the elements ask
|
|
75
|
+
* `model === null` where GTK asks about the model.
|
|
76
|
+
*/
|
|
77
|
+
get isEmpty(): boolean {
|
|
78
|
+
return this._tree.length === 0;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* A PROPERTY write: a real model by identity, a JSON string into the authored menu,
|
|
83
|
+
* `null` for no menu — and a `TypeError` for an array, which this door no longer takes
|
|
84
|
+
* (`buildMenu` is how an array becomes a model now).
|
|
85
|
+
*
|
|
86
|
+
* A REFUSAL PUTS THE AUTHORED MENU BACK, because the door has already reconciled it by
|
|
87
|
+
* the time the tree can be refused: without the restore a refused string would leave the
|
|
88
|
+
* element holding a menu it threw over, and the next render would draw it.
|
|
89
|
+
*/
|
|
90
|
+
write(value: unknown): void {
|
|
91
|
+
const before = this._tree;
|
|
92
|
+
const next = readMenuModelWrite(value, this._authored);
|
|
93
|
+
try {
|
|
94
|
+
if (next !== null) assertMenuRenderable(menuTree(next), ADW_MENU_SURFACE_WEB);
|
|
95
|
+
} catch (error) {
|
|
96
|
+
if (next === this._authored) this._authored.reconcile(before);
|
|
97
|
+
throw error;
|
|
98
|
+
}
|
|
99
|
+
this._adopt(next);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* The `menu-model` ATTRIBUTE, through the same door. A `null` attribute is no menu.
|
|
104
|
+
*
|
|
105
|
+
* A REFUSAL AND A TYPO ARE STILL DIFFERENT THINGS: `parseMenuModel` is total because
|
|
106
|
+
* malformed JSON is an author slip that must not stop the element upgrading, while a
|
|
107
|
+
* `custom` item is well-formed, deliberate, and unhonourable on this surface.
|
|
108
|
+
*/
|
|
109
|
+
writeMarkup(value: string | null): void {
|
|
110
|
+
const next = readMenuModelWrite(value, this._authored);
|
|
111
|
+
const refusals = next === null ? [] : menuRefusals(menuTree(next), ADW_MENU_SURFACE_WEB);
|
|
112
|
+
if (refusals.length === 0) {
|
|
113
|
+
this._adopt(next);
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
// `console.error` is the browser's own channel for "this is wrong, the page
|
|
117
|
+
// continues" — and the alternative here is a row that lies.
|
|
118
|
+
console.error(`${ADW_MENU_SURFACE_WEB.name} cannot render this menu, so it has none:`);
|
|
119
|
+
for (const refusal of refusals) console.error(` [${refusal.path.join('.')}] ${refusal.message}`);
|
|
120
|
+
this._adopt(null);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** Hold `next`, snapshot it, and listen to it — the three that always happen together. */
|
|
124
|
+
private _adopt(next: GioMenuModel | null): void {
|
|
125
|
+
this._unwatch?.();
|
|
126
|
+
this._unwatch = null;
|
|
127
|
+
this._model = next;
|
|
128
|
+
this._tree = next === null ? [] : menuTree(next);
|
|
129
|
+
if (next === null) return;
|
|
130
|
+
// Re-adopting from inside the handler re-snapshots AND re-watches, which is what
|
|
131
|
+
// makes a section added by the mutation audible. The port's `emit` copies its
|
|
132
|
+
// handler list and skips ones disconnected mid-emission, so disconnecting here is
|
|
133
|
+
// safe (`gobject.ts`, `emit`).
|
|
134
|
+
this._unwatch = watchMenuModel(next, () => {
|
|
135
|
+
this._adopt(this._model);
|
|
136
|
+
this.onItemsChanged();
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
}
|