@gjsify/adwaita-web 0.64.13-next.283 → 0.64.14
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-bin.d.ts +13 -0
- 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/lib/types/widget-element.d.ts +8 -2
- package/package.json +12 -13
- package/src/adw-application-window.spec.ts +5 -2
- package/src/adw-bin.spec.ts +20 -0
- package/src/application.ts +4 -4
- package/src/elements/adw-application-window.ts +17 -21
- package/src/elements/adw-bin.ts +20 -1
- 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/pango-markup.spec.ts +11 -0
- package/src/pango-markup.ts +5 -2
- package/src/shared-tree-builder.ts +6 -1
- package/src/split-button.spec.ts +25 -8
- package/src/widget-element.ts +28 -7
- package/src/widget-members.spec.ts +42 -3
|
@@ -9,8 +9,12 @@
|
|
|
9
9
|
// `normalizeIconName` guard is what keeps a multi-token `icon-name="a b"` from
|
|
10
10
|
// shipping a stray CSS class — for the button icon and for every menu entry's.
|
|
11
11
|
//
|
|
12
|
-
// The `menu-model`
|
|
13
|
-
// items, sections and submenus, with the same attributes GTK reads.
|
|
12
|
+
// The `menu-model` ATTRIBUTE is a JSON array in the portable menu model (ADR 0042) —
|
|
13
|
+
// items, sections and submenus, with the same attributes GTK reads. The `menuModel`
|
|
14
|
+
// PROPERTY is a real `Gio.MenuModel` (ADR 0128), which is what GTK's own property is:
|
|
15
|
+
// {@link MenuSlot} is the door between the two, and it is what hears a menu mutate
|
|
16
|
+
// after it was handed over; a write of the property or the attribute fires
|
|
17
|
+
// `notify::menu-model` (CustomEvent, bubbles, detail `{ menuModel }`). Choosing an item
|
|
14
18
|
// closes the popover and fires `menu-item-activated` (CustomEvent, bubbles, detail
|
|
15
19
|
// `{ id, label, path }`) with `id` falling back to `label`, matching the NS twin. The
|
|
16
20
|
// rows themselves are `PopoverMenuView`'s, shared with `<adw-split-button>`: the split
|
|
@@ -24,19 +28,11 @@
|
|
|
24
28
|
// Copyright (c) GNOME contributors (libadwaita). LGPLv2.1+.
|
|
25
29
|
// Modifications: Implemented as a Web Component for @gjsify/adwaita-web.
|
|
26
30
|
|
|
27
|
-
import {
|
|
28
|
-
ADW_MENU_SURFACE_WEB,
|
|
29
|
-
assertMenuRenderable,
|
|
30
|
-
isSplitButtonDirection,
|
|
31
|
-
menuRefusals,
|
|
32
|
-
menuButtonPopupDirection,
|
|
33
|
-
menuItemAt,
|
|
34
|
-
normalizeMenuModel,
|
|
35
|
-
parseMenuModel,
|
|
36
|
-
} from '@gjsify/adwaita-core';
|
|
31
|
+
import { isSplitButtonDirection, menuButtonPopupDirection, menuItemAt } from '@gjsify/adwaita-core';
|
|
37
32
|
import { activateElementMenuItem, menuActionsOfElement } from '../application.js';
|
|
38
|
-
import type { AdwMenuActions,
|
|
33
|
+
import type { AdwMenuActions, AdwMenuModel, SplitButtonDirection } from '@gjsify/adwaita-core';
|
|
39
34
|
|
|
35
|
+
import { MenuSlot } from './menu-slot.js';
|
|
40
36
|
import { PopoverMenuView } from './popover-menu.js';
|
|
41
37
|
|
|
42
38
|
// SIDE-EFFECT import, deliberately separate from the type import below: it guarantees
|
|
@@ -69,7 +65,10 @@ const POPOVER_POSITIONS = {
|
|
|
69
65
|
export class GtkMenuButton extends WidgetElement {
|
|
70
66
|
private _buttonEl!: HTMLButtonElement;
|
|
71
67
|
private _popoverEl!: GtkPopover;
|
|
72
|
-
|
|
68
|
+
/** The model, its tree snapshot and its `items-changed` subscription (ADR 0128). */
|
|
69
|
+
private readonly _slot = new MenuSlot(() => {
|
|
70
|
+
if (this._initialized) this._renderMenu();
|
|
71
|
+
});
|
|
73
72
|
private _actions: AdwMenuActions | null = null;
|
|
74
73
|
private _menuTitle = '';
|
|
75
74
|
private _initialized = false;
|
|
@@ -84,74 +83,46 @@ export class GtkMenuButton extends WidgetElement {
|
|
|
84
83
|
return this._popoverEl?.open ?? false;
|
|
85
84
|
}
|
|
86
85
|
|
|
87
|
-
/**
|
|
88
|
-
|
|
89
|
-
|
|
86
|
+
/**
|
|
87
|
+
* The menu this button pops up, BY IDENTITY — the model itself, `null` when there is
|
|
88
|
+
* none, exactly as `gtk_menu_button_get_menu_model` answers (ADR 0128).
|
|
89
|
+
*
|
|
90
|
+
* It no longer answers an array, and `[]` is no longer how "no menu" is spelled: an
|
|
91
|
+
* EMPTY model is a menu that GTK still builds a popover for, and `null` is the one
|
|
92
|
+
* spelling of its absence (measured, `MENU_MODEL_DOOR_VECTORS`).
|
|
93
|
+
*/
|
|
94
|
+
get menuModel(): AdwMenuModel | null {
|
|
95
|
+
return this._slot.model;
|
|
90
96
|
}
|
|
91
97
|
|
|
92
98
|
/**
|
|
93
|
-
*
|
|
94
|
-
* assignment, for a `custom` item this surface cannot host —
|
|
95
|
-
* note on
|
|
99
|
+
* Take a real model, a JSON string in the portable spelling, or `null`. Refused HERE,
|
|
100
|
+
* at the assignment, for a `custom` item this surface cannot host — {@link MenuSlot}
|
|
101
|
+
* owns both doors and the note on why they refuse differently.
|
|
96
102
|
*/
|
|
97
|
-
set menuModel(value:
|
|
98
|
-
this.
|
|
103
|
+
set menuModel(value: AdwMenuModel | string | null) {
|
|
104
|
+
this._slot.write(value);
|
|
99
105
|
if (this._initialized) this._renderMenu();
|
|
106
|
+
this._notifyModel();
|
|
100
107
|
}
|
|
101
108
|
|
|
102
109
|
/** `Gtk.MenuButton:menu-model` — the GIR spelling of {@link menuModel}. */
|
|
103
|
-
get menu_model(): AdwMenuModel {
|
|
110
|
+
get menu_model(): AdwMenuModel | null {
|
|
104
111
|
return this.menuModel;
|
|
105
112
|
}
|
|
106
113
|
|
|
107
|
-
set menu_model(value:
|
|
114
|
+
set menu_model(value: AdwMenuModel | string | null) {
|
|
108
115
|
this.menuModel = value;
|
|
109
116
|
}
|
|
110
117
|
|
|
111
|
-
get_menu_model(): AdwMenuModel {
|
|
118
|
+
get_menu_model(): AdwMenuModel | null {
|
|
112
119
|
return this.menuModel;
|
|
113
120
|
}
|
|
114
121
|
|
|
115
|
-
set_menu_model(value:
|
|
122
|
+
set_menu_model(value: AdwMenuModel | string | null): void {
|
|
116
123
|
this.menuModel = value;
|
|
117
124
|
}
|
|
118
125
|
|
|
119
|
-
/**
|
|
120
|
-
* Normalise, REFUSE, store — the one door every menu comes through.
|
|
121
|
-
*
|
|
122
|
-
* The property setter is not the only way a menu arrives: an attribute is, and the
|
|
123
|
-
* first cut refused only the setter, so a `custom` item written in markup drew
|
|
124
|
-
* exactly the row `assertMenuRenderable` exists to prevent — one that reads like a
|
|
125
|
-
* command and does nothing.
|
|
126
|
-
*
|
|
127
|
-
* THE TWO DOORS REFUSE DIFFERENTLY, and the difference is which side can hear it.
|
|
128
|
-
* A property assignment is a CALL, so it throws and the caller can catch. An
|
|
129
|
-
* attribute is MARKUP, parsed by the browser: a throw from `connectedCallback` is
|
|
130
|
-
* not delivered to whoever appended the element, it is reported as an uncaught
|
|
131
|
-
* page error — measured, it broke `adwaita-upgrade-order.spec.ts`, which counts
|
|
132
|
-
* exactly those. Nobody can handle it and everybody else pays for it. So the
|
|
133
|
-
* attribute path REFUSES THE MENU (no menu, and a menu-less dropdown is
|
|
134
|
-
* insensitive, which is visible) and says why on `console.error`.
|
|
135
|
-
*
|
|
136
|
-
* A REFUSAL AND A TYPO ARE STILL DIFFERENT THINGS. `parseMenuModel` is total
|
|
137
|
-
* because malformed JSON is an author slip and must not stop the element
|
|
138
|
-
* upgrading; `custom` is well-formed, deliberate, and unhonourable here.
|
|
139
|
-
*/
|
|
140
|
-
private _acceptMenu(input: AdwMenuInput, strict = true): AdwMenuModel {
|
|
141
|
-
const model = normalizeMenuModel(input);
|
|
142
|
-
if (strict) {
|
|
143
|
-
assertMenuRenderable(model, ADW_MENU_SURFACE_WEB);
|
|
144
|
-
return model;
|
|
145
|
-
}
|
|
146
|
-
const refusals = menuRefusals(model, ADW_MENU_SURFACE_WEB);
|
|
147
|
-
if (refusals.length === 0) return model;
|
|
148
|
-
// `console.error` is the browser's own channel for "this is wrong, the page
|
|
149
|
-
// continues" — and the alternative here is a row that lies.
|
|
150
|
-
console.error(`${ADW_MENU_SURFACE_WEB.name} cannot render this menu, so it has none:`);
|
|
151
|
-
for (const refusal of refusals) console.error(` [${refusal.path.join('.')}] ${refusal.message}`);
|
|
152
|
-
return [];
|
|
153
|
-
}
|
|
154
|
-
|
|
155
126
|
/**
|
|
156
127
|
* What the action group publishes about the actions this menu names — the portable
|
|
157
128
|
* stand-in for a `GActionGroup`, and the only source of a menu's enabled/checked
|
|
@@ -212,7 +183,7 @@ export class GtkMenuButton extends WidgetElement {
|
|
|
212
183
|
this._popoverEl.subscribe((open) => this._onPopoverToggled(open));
|
|
213
184
|
|
|
214
185
|
this._menuView = new PopoverMenuView(this._popoverEl, 'adw-menu-button', (path) => {
|
|
215
|
-
const item = menuItemAt(this.
|
|
186
|
+
const item = menuItemAt(this._slot.tree, path);
|
|
216
187
|
if (item === null) return;
|
|
217
188
|
this._popoverEl.popdown();
|
|
218
189
|
this._buttonEl.focus();
|
|
@@ -229,12 +200,14 @@ export class GtkMenuButton extends WidgetElement {
|
|
|
229
200
|
this._buttonEl.addEventListener('click', () => {
|
|
230
201
|
if (this.hasAttribute('disabled')) return;
|
|
231
202
|
// Nothing to show is not a menu — never open an empty popover.
|
|
232
|
-
if (!this._popoverEl.open && this.
|
|
203
|
+
if (!this._popoverEl.open && this._slot.isEmpty) return;
|
|
233
204
|
this._popoverEl.open = !this._popoverEl.open;
|
|
234
205
|
});
|
|
235
206
|
|
|
236
|
-
// Seed the menu from the attribute if the property was not set
|
|
237
|
-
|
|
207
|
+
// Seed the menu from the attribute if the property was not set — and "not set"
|
|
208
|
+
// is now the ABSENCE of a model, not an empty one, since an empty model is a
|
|
209
|
+
// menu an author may have handed over deliberately.
|
|
210
|
+
if (this._slot.model === null) this._slot.writeMarkup(this.getAttribute('menu-model'));
|
|
238
211
|
// The flat default is set only now the DOM exists, so the observed-attribute
|
|
239
212
|
// callback (which renders) never runs before `_buttonEl` is created.
|
|
240
213
|
if (!this.hasAttribute('flat') && !this.hasAttribute('circular')) this.setAttribute('flat', '');
|
|
@@ -244,25 +217,14 @@ export class GtkMenuButton extends WidgetElement {
|
|
|
244
217
|
attributeChangedCallback(name: string) {
|
|
245
218
|
if (!this._initialized || !this._buttonEl) return;
|
|
246
219
|
if (name === 'menu-model') {
|
|
247
|
-
this.
|
|
220
|
+
this._slot.writeMarkup(this.getAttribute('menu-model'));
|
|
248
221
|
this._renderMenu();
|
|
222
|
+
this._notifyModel();
|
|
249
223
|
return;
|
|
250
224
|
}
|
|
251
225
|
this._render();
|
|
252
226
|
}
|
|
253
227
|
|
|
254
|
-
/**
|
|
255
|
-
* The `menu-model` attribute, through the core parser both elements share.
|
|
256
|
-
*
|
|
257
|
-
* The copy this replaces was weaker in a way markup can reach: it kept `id` and
|
|
258
|
-
* `icon` WHATEVER their runtime type, so `{"label":"Open","id":7}` produced an
|
|
259
|
-
* entry whose `id` is a number in a field typed `string` — and `id` is what the
|
|
260
|
-
* activation event reports. The core parser keeps only string-typed attributes.
|
|
261
|
-
*/
|
|
262
|
-
private _parseMenuAttr(): AdwMenuModel {
|
|
263
|
-
return this._acceptMenu(parseMenuModel(this.getAttribute('menu-model')), false);
|
|
264
|
-
}
|
|
265
|
-
|
|
266
228
|
private _onPopoverToggled(open: boolean): void {
|
|
267
229
|
this.classList.toggle('active', open);
|
|
268
230
|
this._buttonEl.setAttribute('aria-expanded', String(open));
|
|
@@ -295,15 +257,31 @@ export class GtkMenuButton extends WidgetElement {
|
|
|
295
257
|
|
|
296
258
|
/** The registry's answer, unless the application wrote `actions` itself (ADR 0097 § 2). */
|
|
297
259
|
private _refreshActions(): void {
|
|
298
|
-
this._menuView.setActions(this._actions ?? menuActionsOfElement(this, this.
|
|
260
|
+
this._menuView.setActions(this._actions ?? menuActionsOfElement(this, this._slot.tree));
|
|
299
261
|
this._menuView.render();
|
|
300
262
|
}
|
|
301
263
|
|
|
264
|
+
/**
|
|
265
|
+
* `gtk_menu_button_set_menu_model` notifies UNCONDITIONALLY — no identity guard, so the
|
|
266
|
+
* same model written twice notifies twice and `null` over `null` notifies as well
|
|
267
|
+
* (measured, `MENU_MODEL_NOTIFY_VECTORS`). Its `Adw.SplitButton` sibling guards on
|
|
268
|
+
* pointer equality, and the two disagree on purpose: a port with one rule for both is
|
|
269
|
+
* wrong about one of them.
|
|
270
|
+
*
|
|
271
|
+
* A MUTATION of the held model is not a write and notifies nothing — it arrives over
|
|
272
|
+
* `items-changed`, where it redraws the rows and stops.
|
|
273
|
+
*/
|
|
274
|
+
private _notifyModel(): void {
|
|
275
|
+
this.dispatchEvent(
|
|
276
|
+
new CustomEvent('notify::menu-model', { bubbles: true, detail: { menuModel: this._slot.model } }),
|
|
277
|
+
);
|
|
278
|
+
}
|
|
279
|
+
|
|
302
280
|
private _renderMenu(): void {
|
|
303
|
-
this._menuView.
|
|
281
|
+
this._menuView.setMenuTree(this._slot.tree);
|
|
304
282
|
this._menuView.setTitle(this.getAttribute('menu-title') ?? this._menuTitle);
|
|
305
283
|
this._menuView.render();
|
|
306
|
-
if (this.
|
|
284
|
+
if (this._slot.isEmpty) this._popoverEl.popdown();
|
|
307
285
|
}
|
|
308
286
|
}
|
|
309
287
|
|
|
@@ -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);
|