@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
|
@@ -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
|
+
}
|
|
@@ -47,7 +47,7 @@
|
|
|
47
47
|
// Copyright (c) GNOME contributors (GTK, libadwaita). LGPLv2.1+.
|
|
48
48
|
|
|
49
49
|
import { flattenMenu, menuLabelText, menuNodeAt, resolveMenuItemState } from '@gjsify/adwaita-core';
|
|
50
|
-
import type { AdwMenuActions, AdwMenuItem,
|
|
50
|
+
import type { AdwMenuActions, AdwMenuItem, AdwMenuPath, AdwMenuTree } from '@gjsify/adwaita-core';
|
|
51
51
|
|
|
52
52
|
import { createGtkImage } from './gtk-image.js';
|
|
53
53
|
import type { GtkPopover } from './gtk-popover.js';
|
|
@@ -71,7 +71,7 @@ const BACK_LABEL = 'Back';
|
|
|
71
71
|
* reopens three levels deep is a menu the reader cannot get out of.
|
|
72
72
|
*/
|
|
73
73
|
export class PopoverMenuView {
|
|
74
|
-
private
|
|
74
|
+
private _tree: AdwMenuTree = [];
|
|
75
75
|
private _actions: AdwMenuActions | null = null;
|
|
76
76
|
private _title = '';
|
|
77
77
|
/** The submenu whose items are showing; `[]` is the root page. */
|
|
@@ -80,7 +80,7 @@ export class PopoverMenuView {
|
|
|
80
80
|
private _openers = new Set<HTMLElement>();
|
|
81
81
|
/** The row that LEAVES a page — what ArrowLeft acts on. */
|
|
82
82
|
private _backRow: HTMLElement | null = null;
|
|
83
|
-
/** The canonical text of the
|
|
83
|
+
/** The canonical text of the tree showing, so an unchanged re-render keeps the page. */
|
|
84
84
|
private _key = '[]';
|
|
85
85
|
private _rows: HTMLButtonElement[] = [];
|
|
86
86
|
|
|
@@ -181,24 +181,34 @@ export class PopoverMenuView {
|
|
|
181
181
|
}
|
|
182
182
|
|
|
183
183
|
/**
|
|
184
|
-
* Named for the value it takes,
|
|
184
|
+
* Named for the value it takes, which since ADR 0128 is a TREE and not a model: this
|
|
185
|
+
* view never holds a `Gio.MenuModel`, it walks the snapshot its element took, and a
|
|
186
|
+
* `setMenuModel` here would name a type it has no way to read. The element is the one
|
|
187
|
+
* place that owns the model.
|
|
188
|
+
*
|
|
189
|
+
* It was `setMenuModel` for the reason the rename keeps: the portable LIST model gave
|
|
185
190
|
* `ComboState` a `setModel` (ADR 0046), and two methods called `setModel` over two
|
|
186
191
|
* unrelated values on one surface is the collision the vocabulary work exists to
|
|
187
|
-
* remove.
|
|
188
|
-
* collection call by the setter's name.
|
|
192
|
+
* remove.
|
|
189
193
|
*/
|
|
190
|
-
|
|
191
|
-
// A CHANGED
|
|
194
|
+
setMenuTree(tree: AdwMenuTree): void {
|
|
195
|
+
// A CHANGED tree invalidates the open page: the submenu it named may not exist
|
|
192
196
|
// any more, and rendering a page that is gone leaves an empty popover with a
|
|
193
197
|
// back row and no way to see what happened. An UNCHANGED one must not — every
|
|
194
198
|
// element re-render calls this, and resetting unconditionally shut the submenu
|
|
195
199
|
// the reader had just opened.
|
|
196
|
-
|
|
200
|
+
//
|
|
201
|
+
// COMPARED BY VALUE, not by identity, which is the opposite of the guard
|
|
202
|
+
// `SplitButtonState.setMenuModel` has: the element's AUTHORED menu keeps its
|
|
203
|
+
// identity while its content is replaced (ADR 0128 § 5), so an identity guard
|
|
204
|
+
// here would never see an edit to a `menu-model="[…]"` attribute and would keep
|
|
205
|
+
// showing a page that is gone.
|
|
206
|
+
const key = JSON.stringify(tree);
|
|
197
207
|
if (key !== this._key) {
|
|
198
208
|
this._key = key;
|
|
199
209
|
this._page = [];
|
|
200
210
|
}
|
|
201
|
-
this.
|
|
211
|
+
this._tree = tree;
|
|
202
212
|
}
|
|
203
213
|
|
|
204
214
|
setActions(actions: AdwMenuActions | null): void {
|
|
@@ -215,10 +225,10 @@ export class PopoverMenuView {
|
|
|
215
225
|
}
|
|
216
226
|
|
|
217
227
|
/** The items of the page currently showing. */
|
|
218
|
-
private currentItems():
|
|
219
|
-
if (this._page.length === 0) return this.
|
|
220
|
-
const node = menuNodeAt(this.
|
|
221
|
-
return node !== null && node.kind !== 'item' ? node.items : this.
|
|
228
|
+
private currentItems(): AdwMenuTree {
|
|
229
|
+
if (this._page.length === 0) return this._tree;
|
|
230
|
+
const node = menuNodeAt(this._tree, this._page);
|
|
231
|
+
return node !== null && node.kind !== 'item' ? node.items : this._tree;
|
|
222
232
|
}
|
|
223
233
|
|
|
224
234
|
/** Rebuild the surface's children from the model. */
|
|
@@ -228,7 +238,7 @@ export class PopoverMenuView {
|
|
|
228
238
|
this._openers = new Set();
|
|
229
239
|
this._backRow = null;
|
|
230
240
|
|
|
231
|
-
const open = menuNodeAt(this.
|
|
241
|
+
const open = menuNodeAt(this._tree, this._page);
|
|
232
242
|
if (this._page.length > 0 && open !== null && open.kind === 'submenu') {
|
|
233
243
|
this.surface.appendChild(this.backRow(open.label));
|
|
234
244
|
} else if (this._title.length > 0) {
|
|
@@ -263,7 +273,7 @@ export class PopoverMenuView {
|
|
|
263
273
|
/** The label of the section at `path`, or `null` when it is not a labelled section. */
|
|
264
274
|
private sectionHeading(path: AdwMenuPath): string | null {
|
|
265
275
|
if (path.length <= this._page.length) return null;
|
|
266
|
-
const parent = menuNodeAt(this.
|
|
276
|
+
const parent = menuNodeAt(this._tree, path);
|
|
267
277
|
return parent !== null && parent.kind === 'section' && parent.label !== undefined ? parent.label : null;
|
|
268
278
|
}
|
|
269
279
|
|