react-x11 2.15.1 → 2.15.3
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/package.json +2 -1
- package/src/application.js +25 -1
- package/src/capabilities.js +324 -0
- package/src/cocoa/app.js +13 -0
- package/src/cocoa/context2d.js +116 -6
- package/src/dbusmenuexport.js +243 -0
- package/src/desktopcapabilityhooks.js +137 -0
- package/src/globalmenu.js +3 -205
- package/src/imagesource.js +15 -2
- package/src/index.d.ts +1 -0
- package/src/index.js +8 -2
- package/src/launcher.js +235 -32
- package/src/launcherhooks.js +47 -28
- package/src/nodes/image.js +2 -1
- package/src/statusnotifier.js +605 -0
- package/src/trayhooks.js +177 -29
- package/src/types/capabilities.d.ts +127 -0
- package/src/types/launcher.d.ts +50 -4
- package/src/types/tray.d.ts +52 -6
- package/src/wayland/context2d.js +28 -2
- package/src/wayland/device.js +23 -0
- package/src/wayland/target.js +9 -1
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
// Serving `com.canonical.dbusmenu`: the half of the protocol that is the same
|
|
2
|
+
// wherever the menu is hung.
|
|
3
|
+
//
|
|
4
|
+
// `dbusmenu.js` is the pure part — ids, snapshots, the diff — and says why
|
|
5
|
+
// each of those is hard. This is the object that answers for it on a bus:
|
|
6
|
+
// `GetLayout`, the property reads, `Event`, `AboutToShow`, and the two update
|
|
7
|
+
// signals. Nothing here knows *why* a menu is exported, and that is the point.
|
|
8
|
+
//
|
|
9
|
+
// It was extracted from `GlobalMenuExport` when the tray arrived
|
|
10
|
+
// (react-x11#353), because a panel menu and a tray menu differ only in how the
|
|
11
|
+
// desktop is told where the menu is:
|
|
12
|
+
//
|
|
13
|
+
// - a **global menu** registers a *window* with `AppMenu.Registrar`, and
|
|
14
|
+
// follows the registrar's ownership so the bar moves back into the window
|
|
15
|
+
// when the panel exits (`globalmenu.js`);
|
|
16
|
+
// - a **tray menu** is named by the `Menu` property of a StatusNotifierItem
|
|
17
|
+
// and has no window at all (`statusnotifier.js`).
|
|
18
|
+
//
|
|
19
|
+
// Both serve the identical tree. Subclasses own the bus lifecycle — when to
|
|
20
|
+
// export, what to register, when to withdraw — and inherit everything below.
|
|
21
|
+
|
|
22
|
+
import {
|
|
23
|
+
DBUSMENU_IFACE,
|
|
24
|
+
PROPERTY_TYPES,
|
|
25
|
+
diffSnapshots,
|
|
26
|
+
groupProperties,
|
|
27
|
+
layoutOf,
|
|
28
|
+
snapshot,
|
|
29
|
+
IdAllocator,
|
|
30
|
+
} from './dbusmenu.js';
|
|
31
|
+
|
|
32
|
+
/** dbusmenu's recursive layout struct: `(id, properties, children)`. */
|
|
33
|
+
export const LAYOUT_SIGNATURE = '(ia{sv}av)';
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* A menu tree, serialised and served.
|
|
37
|
+
*
|
|
38
|
+
* `getMenus()` is read at construction and on every `update()`; `onSelect` and
|
|
39
|
+
* `onAboutToShow` receive the *item object* the app authored, not an id, so a
|
|
40
|
+
* caller never deals in dbusmenu's numbering.
|
|
41
|
+
*/
|
|
42
|
+
export class DbusMenuExport {
|
|
43
|
+
constructor({ getMenus, onSelect, onAboutToShow }) {
|
|
44
|
+
this.getMenus = getMenus;
|
|
45
|
+
this.onSelect = onSelect;
|
|
46
|
+
this.onAboutToShow = onAboutToShow;
|
|
47
|
+
|
|
48
|
+
this.alloc = new IdAllocator();
|
|
49
|
+
this.nodes = snapshot(getMenus(), this.alloc);
|
|
50
|
+
this.revision = 1;
|
|
51
|
+
|
|
52
|
+
/** Set by the subclass once the object is actually on a bus: until then
|
|
53
|
+
* `update()` must not emit, because nobody is listening and the
|
|
54
|
+
* revision would run ahead of the first `GetLayout`. */
|
|
55
|
+
this.exported = false;
|
|
56
|
+
this.iface = null;
|
|
57
|
+
this.dbus = null;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// ----------------------------------------------------------------- update
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* A new `menus` array. Serialise, diff, and send the *narrower* of the two
|
|
64
|
+
* signals the protocol has — see `diffSnapshots`, where the choice is made
|
|
65
|
+
* and why it matters.
|
|
66
|
+
*/
|
|
67
|
+
update(menus) {
|
|
68
|
+
const next = snapshot(menus, this.alloc);
|
|
69
|
+
const change = diffSnapshots(this.nodes, next);
|
|
70
|
+
this.nodes = next;
|
|
71
|
+
if (!this.exported || change.kind === 'none') return;
|
|
72
|
+
|
|
73
|
+
if (change.kind === 'properties') {
|
|
74
|
+
this.iface?.emit.ItemsPropertiesUpdated(
|
|
75
|
+
change.updated.map(([id, props]) => [id, this.wrap(props)]),
|
|
76
|
+
change.removed,
|
|
77
|
+
);
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
this.revision += 1;
|
|
81
|
+
this.iface?.emit.LayoutUpdated(this.revision, change.parent);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** Plain property values → the `a{sv}` the wire wants. */
|
|
85
|
+
wrap(props) {
|
|
86
|
+
const out = {};
|
|
87
|
+
for (const [name, value] of Object.entries(props)) {
|
|
88
|
+
const type = PROPERTY_TYPES[name];
|
|
89
|
+
if (type) out[name] = new this.dbus.Variant(type, value);
|
|
90
|
+
}
|
|
91
|
+
return out;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** `layoutOf`'s plain tree → the recursive `(ia{sv}av)`. */
|
|
95
|
+
wrapLayout(node) {
|
|
96
|
+
const [id, props, children] = node;
|
|
97
|
+
return [
|
|
98
|
+
id,
|
|
99
|
+
this.wrap(props),
|
|
100
|
+
children.map(
|
|
101
|
+
(child) =>
|
|
102
|
+
new this.dbus.Variant(LAYOUT_SIGNATURE, this.wrapLayout(child)),
|
|
103
|
+
),
|
|
104
|
+
];
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// --------------------------------------------------------------- protocol
|
|
108
|
+
|
|
109
|
+
itemFor(id) {
|
|
110
|
+
return this.nodes.get(id)?.item ?? null;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
defineMenu(dbus) {
|
|
114
|
+
this.dbus = dbus;
|
|
115
|
+
const fire = (id, eventId) => {
|
|
116
|
+
const item = this.itemFor(id);
|
|
117
|
+
if (!item) return;
|
|
118
|
+
if (eventId === 'clicked') this.onSelect?.(item);
|
|
119
|
+
else if (eventId === 'opened') this.onAboutToShow?.(item);
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
return dbus.defineInterface({
|
|
123
|
+
name: DBUSMENU_IFACE,
|
|
124
|
+
methods: {
|
|
125
|
+
GetLayout: {
|
|
126
|
+
in: { parentId: 'i', recursionDepth: 'i', propertyNames: 'as' },
|
|
127
|
+
out: { revision: 'u', layout: LAYOUT_SIGNATURE },
|
|
128
|
+
handler: ({ parentId, recursionDepth, propertyNames }) => {
|
|
129
|
+
const tree = layoutOf(
|
|
130
|
+
this.nodes,
|
|
131
|
+
parentId,
|
|
132
|
+
recursionDepth ?? -1,
|
|
133
|
+
propertyNames,
|
|
134
|
+
);
|
|
135
|
+
// An id the shell remembers from before a structural change is the
|
|
136
|
+
// normal way to arrive here, not a protocol violation: answer with
|
|
137
|
+
// an empty item rather than an error, and let the LayoutUpdated it
|
|
138
|
+
// has already been sent bring it back for the real tree.
|
|
139
|
+
return {
|
|
140
|
+
revision: this.revision,
|
|
141
|
+
layout: this.wrapLayout(tree ?? [parentId, {}, []]),
|
|
142
|
+
};
|
|
143
|
+
},
|
|
144
|
+
},
|
|
145
|
+
GetGroupProperties: {
|
|
146
|
+
in: { ids: 'ai', propertyNames: 'as' },
|
|
147
|
+
out: { properties: 'a(ia{sv})' },
|
|
148
|
+
handler: ({ ids, propertyNames }) =>
|
|
149
|
+
groupProperties(this.nodes, ids, propertyNames).map(
|
|
150
|
+
([id, props]) => [id, this.wrap(props)],
|
|
151
|
+
),
|
|
152
|
+
},
|
|
153
|
+
GetProperty: {
|
|
154
|
+
in: { id: 'i', name: 's' },
|
|
155
|
+
out: { value: 'v' },
|
|
156
|
+
handler: ({ id, name }) => {
|
|
157
|
+
const props = this.nodes.get(id)?.props;
|
|
158
|
+
// `hasOwn` on both, so a name like `constructor` reads as absent
|
|
159
|
+
// rather than as a function that then fails to marshal.
|
|
160
|
+
const known = Object.hasOwn(PROPERTY_TYPES, name);
|
|
161
|
+
const value =
|
|
162
|
+
props && Object.hasOwn(props, name) ? props[name] : undefined;
|
|
163
|
+
// A property this item does not carry is at its default, and the
|
|
164
|
+
// spec's own advice is to answer with it rather than to error.
|
|
165
|
+
if (value === undefined || !known) return new dbus.Variant('s', '');
|
|
166
|
+
return new dbus.Variant(PROPERTY_TYPES[name], value);
|
|
167
|
+
},
|
|
168
|
+
},
|
|
169
|
+
Event: {
|
|
170
|
+
in: { id: 'i', eventId: 's', data: 'v', timestamp: 'u' },
|
|
171
|
+
out: {},
|
|
172
|
+
handler: ({ id, eventId }) => fire(id, eventId),
|
|
173
|
+
},
|
|
174
|
+
EventGroup: {
|
|
175
|
+
in: { events: 'a(isvu)' },
|
|
176
|
+
out: { idErrors: 'ai' },
|
|
177
|
+
handler: ({ events }) => {
|
|
178
|
+
const errors = [];
|
|
179
|
+
for (const [id, eventId] of events ?? []) {
|
|
180
|
+
if (this.itemFor(id)) fire(id, eventId);
|
|
181
|
+
else errors.push(id);
|
|
182
|
+
}
|
|
183
|
+
return errors;
|
|
184
|
+
},
|
|
185
|
+
},
|
|
186
|
+
AboutToShow: {
|
|
187
|
+
in: { id: 'i' },
|
|
188
|
+
out: { needUpdate: 'b' },
|
|
189
|
+
handler: ({ id }) => this.aboutToShow(id),
|
|
190
|
+
},
|
|
191
|
+
AboutToShowGroup: {
|
|
192
|
+
in: { ids: 'ai' },
|
|
193
|
+
out: { updatesNeeded: 'ai', idErrors: 'ai' },
|
|
194
|
+
handler: ({ ids }) => {
|
|
195
|
+
const errors = [];
|
|
196
|
+
for (const id of ids ?? []) {
|
|
197
|
+
if (this.itemFor(id)) this.aboutToShow(id);
|
|
198
|
+
else errors.push(id);
|
|
199
|
+
}
|
|
200
|
+
return { updatesNeeded: [], idErrors: errors };
|
|
201
|
+
},
|
|
202
|
+
},
|
|
203
|
+
},
|
|
204
|
+
properties: {
|
|
205
|
+
Version: { type: 'u', access: 'read', get: () => 3 },
|
|
206
|
+
Status: { type: 's', access: 'read', get: () => 'normal' },
|
|
207
|
+
TextDirection: { type: 's', access: 'read', get: () => 'ltr' },
|
|
208
|
+
// Empty, and not a placeholder: `icon-name` is looked up in the
|
|
209
|
+
// desktop's own theme, which is where an app's icons should come from.
|
|
210
|
+
// A path here would be for icons shipped beside the app.
|
|
211
|
+
IconThemePath: { type: 'as', access: 'read', get: () => [] },
|
|
212
|
+
},
|
|
213
|
+
signals: {
|
|
214
|
+
LayoutUpdated: { args: { revision: 'u', parent: 'i' } },
|
|
215
|
+
ItemsPropertiesUpdated: {
|
|
216
|
+
args: { updated: 'a(ia{sv})', removed: 'a(ias)' },
|
|
217
|
+
},
|
|
218
|
+
ItemActivationRequested: { args: { id: 'i', timestamp: 'u' } },
|
|
219
|
+
},
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* The reply has to go out **now**, and React has not rendered yet.
|
|
225
|
+
*
|
|
226
|
+
* `setState` from here does not produce a new tree before this function
|
|
227
|
+
* returns, so there is no honest way to answer `true` — and answering `true`
|
|
228
|
+
* dishonestly is worse than answering `false`: the shell then blocks on a
|
|
229
|
+
* `GetLayout` for a subtree that has not been built, gets the old one, and
|
|
230
|
+
* caches it.
|
|
231
|
+
*
|
|
232
|
+
* So: answer `false`, let the handler run, and let the ordinary update path
|
|
233
|
+
* emit `LayoutUpdated` when the new items actually serialise. Shells listen
|
|
234
|
+
* for that unconditionally — it is the same signal a menu changing while
|
|
235
|
+
* open produces — so a lazily-built submenu still fills in, one round trip
|
|
236
|
+
* later than a synchronous toolkit would manage.
|
|
237
|
+
*/
|
|
238
|
+
aboutToShow(id) {
|
|
239
|
+
const item = this.itemFor(id);
|
|
240
|
+
if (item) this.onAboutToShow?.(item);
|
|
241
|
+
return false;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
// `useDesktopCapability()` — what this desktop can do, as render state.
|
|
2
|
+
//
|
|
3
|
+
// The vocabulary and the probes are `capabilities.js`; this is the part that
|
|
4
|
+
// makes them safe to branch on inside a component. Three things it has to get
|
|
5
|
+
// right, and each is a bug an app would otherwise hit:
|
|
6
|
+
//
|
|
7
|
+
// 1. **The first frame has no answer.** Probing takes a bus round trip, so
|
|
8
|
+
// the hook returns `NO_CAPABILITY` — available false, empty features —
|
|
9
|
+
// until one arrives. An app therefore renders its fallback first and
|
|
10
|
+
// upgrades, which is the right way round: the opposite order flashes a
|
|
11
|
+
// feature that turns out not to exist.
|
|
12
|
+
//
|
|
13
|
+
// 2. **The answer changes.** A panel restarts, an AppIndicator extension is
|
|
14
|
+
// toggled, a notification daemon is installed. The hook follows
|
|
15
|
+
// `NameOwnerChanged` for the whole session rather than sampling once, for
|
|
16
|
+
// the same reason `globalmenu.js` does: a cached "no" outlives the fix.
|
|
17
|
+
//
|
|
18
|
+
// 3. **The object identity must be stable.** A probe that returned a fresh
|
|
19
|
+
// object every time would re-render every consumer on every bus event,
|
|
20
|
+
// and a `features` object in a dependency array would never compare
|
|
21
|
+
// equal. Results are frozen and replaced only when they differ by value.
|
|
22
|
+
|
|
23
|
+
import { useEffect, useState } from 'react';
|
|
24
|
+
|
|
25
|
+
import { sessionBus } from './bus.js';
|
|
26
|
+
import { NO_CAPABILITY, desktopCapability } from './capabilities.js';
|
|
27
|
+
|
|
28
|
+
/** Value equality over the two levels a capability result has. */
|
|
29
|
+
function same(a, b) {
|
|
30
|
+
if (a === b) return true;
|
|
31
|
+
if (!a || !b) return false;
|
|
32
|
+
if (a.available !== b.available || a.backend !== b.backend) return false;
|
|
33
|
+
if (a.reason !== b.reason) return false;
|
|
34
|
+
const ka = Object.keys(a.features);
|
|
35
|
+
const kb = Object.keys(b.features);
|
|
36
|
+
if (ka.length !== kb.length) return false;
|
|
37
|
+
return ka.every((k) => a.features[k] === b.features[k]);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* What this desktop can do for one feature, as a value a component branches
|
|
42
|
+
* on.
|
|
43
|
+
*
|
|
44
|
+
* ```jsx
|
|
45
|
+
* const notifications = useDesktopCapability('notifications');
|
|
46
|
+
*
|
|
47
|
+
* // The portable question is about the feature, never about the platform.
|
|
48
|
+
* if (notifications.features.actions) {
|
|
49
|
+
* return <ReplyFromBanner />;
|
|
50
|
+
* }
|
|
51
|
+
* return <OpenAppToReply available={notifications.available} />;
|
|
52
|
+
* ```
|
|
53
|
+
*
|
|
54
|
+
* `{ available, backend, features }`, starting at
|
|
55
|
+
* {@link NO_CAPABILITY} and settling a tick later — see the header for why
|
|
56
|
+
* that order is deliberate. It re-probes whenever a name appears or vanishes
|
|
57
|
+
* on the session bus, so a panel that starts after the app does is picked up.
|
|
58
|
+
*
|
|
59
|
+
* Capability names: `'notifications'`, `'tray'`, `'launcher'`.
|
|
60
|
+
*/
|
|
61
|
+
export function useDesktopCapability(name) {
|
|
62
|
+
const [state, setState] = useState(NO_CAPABILITY);
|
|
63
|
+
|
|
64
|
+
useEffect(() => {
|
|
65
|
+
let cancelled = false;
|
|
66
|
+
let subscription = null;
|
|
67
|
+
let ref = null;
|
|
68
|
+
let onChanged = null;
|
|
69
|
+
|
|
70
|
+
const probe = () => {
|
|
71
|
+
desktopCapability(name)
|
|
72
|
+
.then((next) => {
|
|
73
|
+
if (cancelled) return;
|
|
74
|
+
// Replaced only when it differs by value: see the header.
|
|
75
|
+
setState((prev) => (same(prev, next) ? prev : next));
|
|
76
|
+
})
|
|
77
|
+
.catch(() => {});
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
probe();
|
|
81
|
+
|
|
82
|
+
// Follow the session for anything appearing or going away. Deliberately
|
|
83
|
+
// *not* narrowed by `arg0`: one capability can depend on several names
|
|
84
|
+
// (the tray watcher, the notification daemon, a launcher), and the set is
|
|
85
|
+
// a detail of `capabilities.js` rather than of this hook. The handler is
|
|
86
|
+
// a re-probe, so the cost of a wide match is a bus round trip on an event
|
|
87
|
+
// that is rare in a settled session.
|
|
88
|
+
void (async () => {
|
|
89
|
+
ref = await sessionBus();
|
|
90
|
+
if (!ref || cancelled) {
|
|
91
|
+
await ref?.release();
|
|
92
|
+
ref = null;
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
try {
|
|
96
|
+
subscription = await ref.bus.watch(
|
|
97
|
+
"type='signal',sender='org.freedesktop.DBus'," +
|
|
98
|
+
"interface='org.freedesktop.DBus',member='NameOwnerChanged'",
|
|
99
|
+
);
|
|
100
|
+
} catch {
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
// The mount/unmount race `AddMatch` always has — see
|
|
104
|
+
// `GlobalMenuExport.watchRegistrar` for the long version.
|
|
105
|
+
if (cancelled) {
|
|
106
|
+
await subscription.remove().catch(() => {});
|
|
107
|
+
subscription = null;
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
const key = ref.bus.mangle(
|
|
111
|
+
'/org/freedesktop/DBus',
|
|
112
|
+
'org.freedesktop.DBus',
|
|
113
|
+
'NameOwnerChanged',
|
|
114
|
+
);
|
|
115
|
+
onChanged = () => probe();
|
|
116
|
+
ref.bus.signals.on(key, onChanged);
|
|
117
|
+
})();
|
|
118
|
+
|
|
119
|
+
return () => {
|
|
120
|
+
cancelled = true;
|
|
121
|
+
void (async () => {
|
|
122
|
+
if (ref && onChanged) {
|
|
123
|
+
const key = ref.bus.mangle(
|
|
124
|
+
'/org/freedesktop/DBus',
|
|
125
|
+
'org.freedesktop.DBus',
|
|
126
|
+
'NameOwnerChanged',
|
|
127
|
+
);
|
|
128
|
+
ref.bus.signals.removeListener(key, onChanged);
|
|
129
|
+
}
|
|
130
|
+
await subscription?.remove().catch(() => {});
|
|
131
|
+
await ref?.release();
|
|
132
|
+
})();
|
|
133
|
+
};
|
|
134
|
+
}, [name]);
|
|
135
|
+
|
|
136
|
+
return state;
|
|
137
|
+
}
|
package/src/globalmenu.js
CHANGED
|
@@ -58,15 +58,7 @@ import { useEffect, useRef, useState } from 'react';
|
|
|
58
58
|
|
|
59
59
|
import { loadTransport, sessionBus } from './bus.js';
|
|
60
60
|
import { desktopIntegrationEnabled } from './desktopintegration.js';
|
|
61
|
-
import {
|
|
62
|
-
DBUSMENU_IFACE,
|
|
63
|
-
PROPERTY_TYPES,
|
|
64
|
-
diffSnapshots,
|
|
65
|
-
groupProperties,
|
|
66
|
-
layoutOf,
|
|
67
|
-
snapshot,
|
|
68
|
-
IdAllocator,
|
|
69
|
-
} from './dbusmenu.js';
|
|
61
|
+
import { DbusMenuExport } from './dbusmenuexport.js';
|
|
70
62
|
import { useAppOrNull } from './appcontext.js';
|
|
71
63
|
import { useTopLevelWindow, windowIdOf } from './windowid.js';
|
|
72
64
|
|
|
@@ -76,9 +68,6 @@ export const REGISTRAR_PATH = '/com/canonical/AppMenu/Registrar';
|
|
|
76
68
|
const KDE_SERVICE_PROPERTY = '_KDE_NET_WM_APPMENU_SERVICE_NAME';
|
|
77
69
|
const KDE_PATH_PROPERTY = '_KDE_NET_WM_APPMENU_OBJECT_PATH';
|
|
78
70
|
|
|
79
|
-
/** dbusmenu's recursive layout struct: `(id, properties, children)`. */
|
|
80
|
-
const LAYOUT_SIGNATURE = '(ia{sv}av)';
|
|
81
|
-
|
|
82
71
|
/**
|
|
83
72
|
* How long the registrar gets to answer.
|
|
84
73
|
*
|
|
@@ -162,20 +151,13 @@ function ntkWindowOf(target) {
|
|
|
162
151
|
* it, and the parts that are awkward (a panel restarting, a window closing
|
|
163
152
|
* mid-call, no bus at all) are awkward in ways React has nothing to say about.
|
|
164
153
|
*/
|
|
165
|
-
export class GlobalMenuExport {
|
|
154
|
+
export class GlobalMenuExport extends DbusMenuExport {
|
|
166
155
|
constructor({ getMenus, onSelect, onAboutToShow, target, onChange }) {
|
|
167
|
-
|
|
168
|
-
this.onSelect = onSelect;
|
|
169
|
-
this.onAboutToShow = onAboutToShow;
|
|
156
|
+
super({ getMenus, onSelect, onAboutToShow });
|
|
170
157
|
this.target = target;
|
|
171
158
|
this.onChange = onChange ?? (() => {});
|
|
172
159
|
|
|
173
|
-
this.alloc = new IdAllocator();
|
|
174
|
-
this.nodes = snapshot(getMenus(), this.alloc);
|
|
175
|
-
this.revision = 1;
|
|
176
|
-
|
|
177
160
|
this.stopped = false;
|
|
178
|
-
this.exported = false;
|
|
179
161
|
/** The in-flight `sync()`, which the next one queues behind. */
|
|
180
162
|
this.syncing = null;
|
|
181
163
|
this.ref = null;
|
|
@@ -443,190 +425,6 @@ export class GlobalMenuExport {
|
|
|
443
425
|
await this.ref?.release();
|
|
444
426
|
this.ref = null;
|
|
445
427
|
}
|
|
446
|
-
|
|
447
|
-
// ----------------------------------------------------------------- update
|
|
448
|
-
|
|
449
|
-
/**
|
|
450
|
-
* A new `menus` array. Serialise, diff, and send the *narrower* of the two
|
|
451
|
-
* signals the protocol has — see `diffSnapshots`, where the choice is made
|
|
452
|
-
* and why it matters.
|
|
453
|
-
*/
|
|
454
|
-
update(menus) {
|
|
455
|
-
const next = snapshot(menus, this.alloc);
|
|
456
|
-
const change = diffSnapshots(this.nodes, next);
|
|
457
|
-
this.nodes = next;
|
|
458
|
-
if (!this.exported || change.kind === 'none') return;
|
|
459
|
-
|
|
460
|
-
if (change.kind === 'properties') {
|
|
461
|
-
this.iface?.emit.ItemsPropertiesUpdated(
|
|
462
|
-
change.updated.map(([id, props]) => [id, this.wrap(props)]),
|
|
463
|
-
change.removed,
|
|
464
|
-
);
|
|
465
|
-
return;
|
|
466
|
-
}
|
|
467
|
-
this.revision += 1;
|
|
468
|
-
this.iface?.emit.LayoutUpdated(this.revision, change.parent);
|
|
469
|
-
}
|
|
470
|
-
|
|
471
|
-
/** Plain property values → the `a{sv}` the wire wants. */
|
|
472
|
-
wrap(props) {
|
|
473
|
-
const out = {};
|
|
474
|
-
for (const [name, value] of Object.entries(props)) {
|
|
475
|
-
const type = PROPERTY_TYPES[name];
|
|
476
|
-
if (type) out[name] = new this.dbus.Variant(type, value);
|
|
477
|
-
}
|
|
478
|
-
return out;
|
|
479
|
-
}
|
|
480
|
-
|
|
481
|
-
/** `layoutOf`'s plain tree → the recursive `(ia{sv}av)`. */
|
|
482
|
-
wrapLayout(node) {
|
|
483
|
-
const [id, props, children] = node;
|
|
484
|
-
return [
|
|
485
|
-
id,
|
|
486
|
-
this.wrap(props),
|
|
487
|
-
children.map(
|
|
488
|
-
(child) =>
|
|
489
|
-
new this.dbus.Variant(LAYOUT_SIGNATURE, this.wrapLayout(child)),
|
|
490
|
-
),
|
|
491
|
-
];
|
|
492
|
-
}
|
|
493
|
-
|
|
494
|
-
// --------------------------------------------------------------- protocol
|
|
495
|
-
|
|
496
|
-
itemFor(id) {
|
|
497
|
-
return this.nodes.get(id)?.item ?? null;
|
|
498
|
-
}
|
|
499
|
-
|
|
500
|
-
defineMenu(dbus) {
|
|
501
|
-
this.dbus = dbus;
|
|
502
|
-
const fire = (id, eventId) => {
|
|
503
|
-
const item = this.itemFor(id);
|
|
504
|
-
if (!item) return;
|
|
505
|
-
if (eventId === 'clicked') this.onSelect?.(item);
|
|
506
|
-
else if (eventId === 'opened') this.onAboutToShow?.(item);
|
|
507
|
-
};
|
|
508
|
-
|
|
509
|
-
return dbus.defineInterface({
|
|
510
|
-
name: DBUSMENU_IFACE,
|
|
511
|
-
methods: {
|
|
512
|
-
GetLayout: {
|
|
513
|
-
in: { parentId: 'i', recursionDepth: 'i', propertyNames: 'as' },
|
|
514
|
-
out: { revision: 'u', layout: LAYOUT_SIGNATURE },
|
|
515
|
-
handler: ({ parentId, recursionDepth, propertyNames }) => {
|
|
516
|
-
const tree = layoutOf(
|
|
517
|
-
this.nodes,
|
|
518
|
-
parentId,
|
|
519
|
-
recursionDepth ?? -1,
|
|
520
|
-
propertyNames,
|
|
521
|
-
);
|
|
522
|
-
// An id the shell remembers from before a structural change is the
|
|
523
|
-
// normal way to arrive here, not a protocol violation: answer with
|
|
524
|
-
// an empty item rather than an error, and let the LayoutUpdated it
|
|
525
|
-
// has already been sent bring it back for the real tree.
|
|
526
|
-
return {
|
|
527
|
-
revision: this.revision,
|
|
528
|
-
layout: this.wrapLayout(tree ?? [parentId, {}, []]),
|
|
529
|
-
};
|
|
530
|
-
},
|
|
531
|
-
},
|
|
532
|
-
GetGroupProperties: {
|
|
533
|
-
in: { ids: 'ai', propertyNames: 'as' },
|
|
534
|
-
out: { properties: 'a(ia{sv})' },
|
|
535
|
-
handler: ({ ids, propertyNames }) =>
|
|
536
|
-
groupProperties(this.nodes, ids, propertyNames).map(
|
|
537
|
-
([id, props]) => [id, this.wrap(props)],
|
|
538
|
-
),
|
|
539
|
-
},
|
|
540
|
-
GetProperty: {
|
|
541
|
-
in: { id: 'i', name: 's' },
|
|
542
|
-
out: { value: 'v' },
|
|
543
|
-
handler: ({ id, name }) => {
|
|
544
|
-
const props = this.nodes.get(id)?.props;
|
|
545
|
-
// `hasOwn` on both, so a name like `constructor` reads as absent
|
|
546
|
-
// rather than as a function that then fails to marshal.
|
|
547
|
-
const known = Object.hasOwn(PROPERTY_TYPES, name);
|
|
548
|
-
const value =
|
|
549
|
-
props && Object.hasOwn(props, name) ? props[name] : undefined;
|
|
550
|
-
// A property this item does not carry is at its default, and the
|
|
551
|
-
// spec's own advice is to answer with it rather than to error.
|
|
552
|
-
if (value === undefined || !known) return new dbus.Variant('s', '');
|
|
553
|
-
return new dbus.Variant(PROPERTY_TYPES[name], value);
|
|
554
|
-
},
|
|
555
|
-
},
|
|
556
|
-
Event: {
|
|
557
|
-
in: { id: 'i', eventId: 's', data: 'v', timestamp: 'u' },
|
|
558
|
-
out: {},
|
|
559
|
-
handler: ({ id, eventId }) => fire(id, eventId),
|
|
560
|
-
},
|
|
561
|
-
EventGroup: {
|
|
562
|
-
in: { events: 'a(isvu)' },
|
|
563
|
-
out: { idErrors: 'ai' },
|
|
564
|
-
handler: ({ events }) => {
|
|
565
|
-
const errors = [];
|
|
566
|
-
for (const [id, eventId] of events ?? []) {
|
|
567
|
-
if (this.itemFor(id)) fire(id, eventId);
|
|
568
|
-
else errors.push(id);
|
|
569
|
-
}
|
|
570
|
-
return errors;
|
|
571
|
-
},
|
|
572
|
-
},
|
|
573
|
-
AboutToShow: {
|
|
574
|
-
in: { id: 'i' },
|
|
575
|
-
out: { needUpdate: 'b' },
|
|
576
|
-
handler: ({ id }) => this.aboutToShow(id),
|
|
577
|
-
},
|
|
578
|
-
AboutToShowGroup: {
|
|
579
|
-
in: { ids: 'ai' },
|
|
580
|
-
out: { updatesNeeded: 'ai', idErrors: 'ai' },
|
|
581
|
-
handler: ({ ids }) => {
|
|
582
|
-
const errors = [];
|
|
583
|
-
for (const id of ids ?? []) {
|
|
584
|
-
if (this.itemFor(id)) this.aboutToShow(id);
|
|
585
|
-
else errors.push(id);
|
|
586
|
-
}
|
|
587
|
-
return { updatesNeeded: [], idErrors: errors };
|
|
588
|
-
},
|
|
589
|
-
},
|
|
590
|
-
},
|
|
591
|
-
properties: {
|
|
592
|
-
Version: { type: 'u', access: 'read', get: () => 3 },
|
|
593
|
-
Status: { type: 's', access: 'read', get: () => 'normal' },
|
|
594
|
-
TextDirection: { type: 's', access: 'read', get: () => 'ltr' },
|
|
595
|
-
// Empty, and not a placeholder: `icon-name` is looked up in the
|
|
596
|
-
// desktop's own theme, which is where an app's icons should come from.
|
|
597
|
-
// A path here would be for icons shipped beside the app.
|
|
598
|
-
IconThemePath: { type: 'as', access: 'read', get: () => [] },
|
|
599
|
-
},
|
|
600
|
-
signals: {
|
|
601
|
-
LayoutUpdated: { args: { revision: 'u', parent: 'i' } },
|
|
602
|
-
ItemsPropertiesUpdated: {
|
|
603
|
-
args: { updated: 'a(ia{sv})', removed: 'a(ias)' },
|
|
604
|
-
},
|
|
605
|
-
ItemActivationRequested: { args: { id: 'i', timestamp: 'u' } },
|
|
606
|
-
},
|
|
607
|
-
});
|
|
608
|
-
}
|
|
609
|
-
|
|
610
|
-
/**
|
|
611
|
-
* The reply has to go out **now**, and React has not rendered yet.
|
|
612
|
-
*
|
|
613
|
-
* `setState` from here does not produce a new tree before this function
|
|
614
|
-
* returns, so there is no honest way to answer `true` — and answering `true`
|
|
615
|
-
* dishonestly is worse than answering `false`: the shell then blocks on a
|
|
616
|
-
* `GetLayout` for a subtree that has not been built, gets the old one, and
|
|
617
|
-
* caches it.
|
|
618
|
-
*
|
|
619
|
-
* So: answer `false`, let the handler run, and let the ordinary update path
|
|
620
|
-
* emit `LayoutUpdated` when the new items actually serialise. Shells listen
|
|
621
|
-
* for that unconditionally — it is the same signal a menu changing while
|
|
622
|
-
* open produces — so a lazily-built submenu still fills in, one round trip
|
|
623
|
-
* later than a synchronous toolkit would manage.
|
|
624
|
-
*/
|
|
625
|
-
aboutToShow(id) {
|
|
626
|
-
const item = this.itemFor(id);
|
|
627
|
-
if (item) this.onAboutToShow?.(item);
|
|
628
|
-
return false;
|
|
629
|
-
}
|
|
630
428
|
}
|
|
631
429
|
|
|
632
430
|
/**
|
package/src/imagesource.js
CHANGED
|
@@ -250,7 +250,7 @@ export function acquireImageSource(app, key, load) {
|
|
|
250
250
|
entry.promise = null;
|
|
251
251
|
// every holder unmounted while it decoded — free, don't adopt
|
|
252
252
|
if (entry.released) {
|
|
253
|
-
image
|
|
253
|
+
freeImage(app, image);
|
|
254
254
|
return null;
|
|
255
255
|
}
|
|
256
256
|
entry.image = image;
|
|
@@ -269,10 +269,23 @@ export function releaseImageSource(app, entry) {
|
|
|
269
269
|
if (--entry.refs > 0) return;
|
|
270
270
|
sourceCaches.get(app)?.delete(entry.key);
|
|
271
271
|
entry.released = true;
|
|
272
|
-
entry.image
|
|
272
|
+
freeImage(app, entry.image);
|
|
273
273
|
entry.image = null;
|
|
274
274
|
}
|
|
275
275
|
|
|
276
|
+
/**
|
|
277
|
+
* Free an `Image` this module or a node owns, on every backend it may have
|
|
278
|
+
* been drawn on: ntk's `destroy()` frees the pixmaps it uploaded per X
|
|
279
|
+
* connection, and knows nothing of an upload a backend keeps for itself —
|
|
280
|
+
* the Cocoa app's CG bitmap — which that app's `releaseImage` seam frees.
|
|
281
|
+
* An X app has no such seam, so there it is `destroy()` alone.
|
|
282
|
+
*/
|
|
283
|
+
export function freeImage(app, image) {
|
|
284
|
+
if (!image) return;
|
|
285
|
+
image.destroy();
|
|
286
|
+
app?.releaseImage?.(image);
|
|
287
|
+
}
|
|
288
|
+
|
|
276
289
|
// --- server-side sources ----------------------------------------------------
|
|
277
290
|
|
|
278
291
|
/** RENDER's depth-implied standard formats — the ones a drawable can be
|
package/src/index.d.ts
CHANGED
|
@@ -25,6 +25,7 @@ export * from './types/screencolor.js';
|
|
|
25
25
|
export * from './types/appearance.js';
|
|
26
26
|
export * from './types/fonts.js';
|
|
27
27
|
export * from './types/system.js';
|
|
28
|
+
export * from './types/capabilities.js';
|
|
28
29
|
export * from './types/launcher.js';
|
|
29
30
|
export * from './types/tray.js';
|
|
30
31
|
export * from './types/permissions.js';
|
package/src/index.js
CHANGED
|
@@ -14,9 +14,15 @@ export {
|
|
|
14
14
|
registerApplication,
|
|
15
15
|
} from './application.js';
|
|
16
16
|
export { useAppActivate, useAppOpen } from './apphooks.js';
|
|
17
|
-
export { setBadge } from './launcher.js';
|
|
18
|
-
export { useBadge, useDockMenu } from './launcherhooks.js';
|
|
17
|
+
export { setBadge, setProgress, setQuicklist, setUrgent } from './launcher.js';
|
|
18
|
+
export { useBadge, useDockMenu, useProgress } from './launcherhooks.js';
|
|
19
19
|
export { useTray } from './trayhooks.js';
|
|
20
|
+
export {
|
|
21
|
+
CAPABILITIES,
|
|
22
|
+
NO_CAPABILITY,
|
|
23
|
+
desktopCapability,
|
|
24
|
+
} from './capabilities.js';
|
|
25
|
+
export { useDesktopCapability } from './desktopcapabilityhooks.js';
|
|
20
26
|
export {
|
|
21
27
|
NoPermissionServiceError,
|
|
22
28
|
openPrivacySettings,
|