react-x11 2.15.2 → 2.16.0
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/README.md +37 -0
- package/package.json +4 -3
- package/src/Reconciler.js +85 -22
- package/src/anchor.js +60 -18
- package/src/application.js +25 -1
- package/src/capabilities.js +349 -0
- package/src/cocoa/app.js +28 -9
- package/src/cocoa/context2d.js +139 -6
- package/src/cocoa/fonts.js +78 -0
- package/src/cocoa/presenter.js +17 -0
- package/src/cocoa/promotion.js +20 -0
- package/src/cocoa/relaunch.js +8 -3
- package/src/cocoa/symbols.js +64 -0
- package/src/cocoa/threaded.js +24 -4
- package/src/cocoa/window.js +362 -139
- package/src/components/ProgressBar.js +1 -1
- package/src/components/Slider.js +72 -39
- package/src/components/anchor.js +7 -2
- package/src/components/index.js +1 -0
- package/src/components/theme.js +32 -28
- package/src/dbusmenuexport.js +243 -0
- package/src/desktopcapabilityhooks.js +160 -0
- package/src/filedialoghooks.js +3 -5
- package/src/frame/childmain.js +8 -20
- package/src/frame/env.js +2 -10
- package/src/globalmenu.js +3 -205
- package/src/icontheme.js +240 -0
- package/src/imagesource.js +98 -3
- package/src/index.d.ts +1 -0
- package/src/index.js +11 -2
- package/src/launcher.js +235 -32
- package/src/launcherhooks.js +47 -28
- package/src/node.d.ts +7 -0
- package/src/nodes/animation.js +17 -47
- package/src/nodes/cascade.js +17 -2
- package/src/nodes/image.js +65 -2
- package/src/nodes/kinds.js +12 -0
- package/src/nodes/layout.js +5 -1
- package/src/nodes/node.js +17 -3
- package/src/nodes/paint.js +117 -0
- package/src/nodes/scope.js +259 -0
- package/src/nodes/scrollable.js +53 -6
- package/src/nodes/text.js +2 -0
- package/src/nodes/textarea.js +1 -1
- package/src/nodes/textinput.js +1 -1
- package/src/nodes/window/anchoring.js +45 -18
- package/src/nodes/window/flush.js +6 -5
- package/src/nodes/window/popup.js +10 -0
- package/src/nodes/window/size.js +40 -2
- package/src/nodes/window/window.js +41 -14
- package/src/registry.js +2 -1
- package/src/settings.js +332 -0
- package/src/statusnotifier.js +752 -0
- package/src/styles.js +212 -8
- package/src/symbols.js +200 -0
- package/src/testing/mock-app.js +10 -0
- package/src/trayhooks.js +193 -29
- package/src/types/capabilities.d.ts +139 -0
- package/src/types/components.d.ts +33 -0
- package/src/types/elements.d.ts +57 -6
- package/src/types/launcher.d.ts +50 -4
- package/src/types/style.d.ts +57 -0
- package/src/types/system.d.ts +104 -0
- package/src/types/tray.d.ts +64 -6
|
@@ -0,0 +1,160 @@
|
|
|
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 { useAppOrNull } from './appcontext.js';
|
|
26
|
+
import { sessionBus } from './bus.js';
|
|
27
|
+
import {
|
|
28
|
+
NO_CAPABILITY,
|
|
29
|
+
capabilityNow,
|
|
30
|
+
desktopCapability,
|
|
31
|
+
} from './capabilities.js';
|
|
32
|
+
|
|
33
|
+
/** No answer yet: nothing available, and not settled. */
|
|
34
|
+
const PENDING = Object.freeze({ ...NO_CAPABILITY, settled: false });
|
|
35
|
+
|
|
36
|
+
/** A probe's answer, as the hook hands it out. */
|
|
37
|
+
const settledState = (result) => Object.freeze({ ...result, settled: true });
|
|
38
|
+
|
|
39
|
+
/** Value equality over the two levels a capability result has. */
|
|
40
|
+
function same(a, b) {
|
|
41
|
+
if (a === b) return true;
|
|
42
|
+
if (!a || !b) return false;
|
|
43
|
+
if (a.available !== b.available || a.backend !== b.backend) return false;
|
|
44
|
+
if (a.reason !== b.reason || a.settled !== b.settled) return false;
|
|
45
|
+
const ka = Object.keys(a.features);
|
|
46
|
+
const kb = Object.keys(b.features);
|
|
47
|
+
if (ka.length !== kb.length) return false;
|
|
48
|
+
return ka.every((k) => a.features[k] === b.features[k]);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* What this desktop can do for one feature, as a value a component branches
|
|
53
|
+
* on.
|
|
54
|
+
*
|
|
55
|
+
* ```jsx
|
|
56
|
+
* const notifications = useDesktopCapability('notifications');
|
|
57
|
+
*
|
|
58
|
+
* // The portable question is about the feature, never about the platform.
|
|
59
|
+
* if (notifications.features.actions) {
|
|
60
|
+
* return <ReplyFromBanner />;
|
|
61
|
+
* }
|
|
62
|
+
* return <OpenAppToReply available={notifications.available} />;
|
|
63
|
+
* ```
|
|
64
|
+
*
|
|
65
|
+
* `{ available, backend, features }`, starting at
|
|
66
|
+
* {@link NO_CAPABILITY} and settling a tick later — see the header for why
|
|
67
|
+
* that order is deliberate. It re-probes whenever a name appears or vanishes
|
|
68
|
+
* on the session bus, so a panel that starts after the app does is picked up.
|
|
69
|
+
*
|
|
70
|
+
* Capability names: `'notifications'`, `'tray'`, `'launcher'`.
|
|
71
|
+
*/
|
|
72
|
+
export function useDesktopCapability(name) {
|
|
73
|
+
// The app this component is rendered into, when there is one: a process
|
|
74
|
+
// with several connections has several answers, and the one that matters
|
|
75
|
+
// here is this tree's.
|
|
76
|
+
const app = useAppOrNull() ?? undefined;
|
|
77
|
+
// Settled from the first frame where the answer needed no asking — the
|
|
78
|
+
// Cocoa app's tray and Dock tile — and pending everywhere else until the
|
|
79
|
+
// first probe answers, so an app can hold back its fallback instead of
|
|
80
|
+
// flashing it.
|
|
81
|
+
const [state, setState] = useState(() => {
|
|
82
|
+
const now = capabilityNow(name, { app });
|
|
83
|
+
return now ? settledState(now) : PENDING;
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
useEffect(() => {
|
|
87
|
+
let cancelled = false;
|
|
88
|
+
let subscription = null;
|
|
89
|
+
let ref = null;
|
|
90
|
+
let onChanged = null;
|
|
91
|
+
|
|
92
|
+
const probe = () => {
|
|
93
|
+
desktopCapability(name, { app })
|
|
94
|
+
.then((next) => {
|
|
95
|
+
if (cancelled) return;
|
|
96
|
+
// Replaced only when it differs by value: see the header.
|
|
97
|
+
const settled = settledState(next);
|
|
98
|
+
setState((prev) => (same(prev, settled) ? prev : settled));
|
|
99
|
+
})
|
|
100
|
+
.catch(() => {});
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
probe();
|
|
104
|
+
|
|
105
|
+
// Follow the session for anything appearing or going away. Deliberately
|
|
106
|
+
// *not* narrowed by `arg0`: one capability can depend on several names
|
|
107
|
+
// (the tray watcher, the notification daemon, a launcher), and the set is
|
|
108
|
+
// a detail of `capabilities.js` rather than of this hook. The handler is
|
|
109
|
+
// a re-probe, so the cost of a wide match is a bus round trip on an event
|
|
110
|
+
// that is rare in a settled session.
|
|
111
|
+
void (async () => {
|
|
112
|
+
ref = await sessionBus();
|
|
113
|
+
if (!ref || cancelled) {
|
|
114
|
+
await ref?.release();
|
|
115
|
+
ref = null;
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
try {
|
|
119
|
+
subscription = await ref.bus.watch(
|
|
120
|
+
"type='signal',sender='org.freedesktop.DBus'," +
|
|
121
|
+
"interface='org.freedesktop.DBus',member='NameOwnerChanged'",
|
|
122
|
+
);
|
|
123
|
+
} catch {
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
// The mount/unmount race `AddMatch` always has — see
|
|
127
|
+
// `GlobalMenuExport.watchRegistrar` for the long version.
|
|
128
|
+
if (cancelled) {
|
|
129
|
+
await subscription.remove().catch(() => {});
|
|
130
|
+
subscription = null;
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
const key = ref.bus.mangle(
|
|
134
|
+
'/org/freedesktop/DBus',
|
|
135
|
+
'org.freedesktop.DBus',
|
|
136
|
+
'NameOwnerChanged',
|
|
137
|
+
);
|
|
138
|
+
onChanged = () => probe();
|
|
139
|
+
ref.bus.signals.on(key, onChanged);
|
|
140
|
+
})();
|
|
141
|
+
|
|
142
|
+
return () => {
|
|
143
|
+
cancelled = true;
|
|
144
|
+
void (async () => {
|
|
145
|
+
if (ref && onChanged) {
|
|
146
|
+
const key = ref.bus.mangle(
|
|
147
|
+
'/org/freedesktop/DBus',
|
|
148
|
+
'org.freedesktop.DBus',
|
|
149
|
+
'NameOwnerChanged',
|
|
150
|
+
);
|
|
151
|
+
ref.bus.signals.removeListener(key, onChanged);
|
|
152
|
+
}
|
|
153
|
+
await subscription?.remove().catch(() => {});
|
|
154
|
+
await ref?.release();
|
|
155
|
+
})();
|
|
156
|
+
};
|
|
157
|
+
}, [name, app]);
|
|
158
|
+
|
|
159
|
+
return state;
|
|
160
|
+
}
|
package/src/filedialoghooks.js
CHANGED
|
@@ -40,11 +40,9 @@ async function showBuiltin(app, theme, props) {
|
|
|
40
40
|
const root = await createRoot({ app });
|
|
41
41
|
try {
|
|
42
42
|
return await new Promise((resolve) => {
|
|
43
|
-
//
|
|
44
|
-
//
|
|
45
|
-
// and the dialog
|
|
46
|
-
// it here in `<ThemeProvider>` would put a `<box>` between the root and
|
|
47
|
-
// the window, which throws.
|
|
43
|
+
// The dialog is on a root of its own, which no provider in the app's
|
|
44
|
+
// tree reaches — so the palette the caller resolved travels as a prop,
|
|
45
|
+
// and the dialog puts it on its window and in a provider inside it.
|
|
48
46
|
root.render(
|
|
49
47
|
React.createElement(FileDialog, { ...props, theme, onDone: resolve }),
|
|
50
48
|
);
|
package/src/frame/childmain.js
CHANGED
|
@@ -53,10 +53,7 @@ const h = React.createElement;
|
|
|
53
53
|
/** Recreate the bridged providers around the pane's window, outermost
|
|
54
54
|
* first — the parent's own nesting order, which the env Map records and
|
|
55
55
|
* structured clone preserves. A key the pane never registered wraps
|
|
56
|
-
* nothing: no module in this process reads it.
|
|
57
|
-
* `innermost` (the theme) wrap directly around the window, inside the
|
|
58
|
-
* rest — see registerFrameProvider (src/frame/env.js) for why the
|
|
59
|
-
* adjacency matters. */
|
|
56
|
+
* nothing: no module in this process reads it. */
|
|
60
57
|
function wrapEnv(env, inner) {
|
|
61
58
|
const wrap = (tree, [key, value]) => {
|
|
62
59
|
const registered = registeredFrameContext(key);
|
|
@@ -69,16 +66,8 @@ function wrapEnv(env, inner) {
|
|
|
69
66
|
tree,
|
|
70
67
|
);
|
|
71
68
|
};
|
|
72
|
-
const entries = [...env];
|
|
73
|
-
const isInnermost = ([key]) =>
|
|
74
|
-
registeredFrameContext(key)?.innermost === true;
|
|
75
69
|
let tree = inner;
|
|
76
|
-
for (const entry of
|
|
77
|
-
tree = wrap(tree, entry);
|
|
78
|
-
}
|
|
79
|
-
for (const entry of entries.filter((e) => !isInnermost(e)).reverse()) {
|
|
80
|
-
tree = wrap(tree, entry);
|
|
81
|
-
}
|
|
70
|
+
for (const entry of [...env].reverse()) tree = wrap(tree, entry);
|
|
82
71
|
return tree;
|
|
83
72
|
}
|
|
84
73
|
|
|
@@ -118,13 +107,12 @@ function Bridge({ Component, store, rect, invoke, onReady }) {
|
|
|
118
107
|
}, [onReady]);
|
|
119
108
|
|
|
120
109
|
// The bridged providers wrap the *window*, not the pane component — the
|
|
121
|
-
// same position they held in the host.
|
|
122
|
-
//
|
|
123
|
-
//
|
|
124
|
-
//
|
|
125
|
-
//
|
|
126
|
-
//
|
|
127
|
-
// a dark-desktop pane in a light-themed app, wrong in both directions.
|
|
110
|
+
// same position they held in the host. The theme has to reach the window
|
|
111
|
+
// itself: the window's own background follows the palette, and it is the
|
|
112
|
+
// top of the node tree every `$token` beneath resolves through. Mounted
|
|
113
|
+
// inside the window, the palette reached a box and the window kept
|
|
114
|
+
// resolving against the pane process's own desktop — a dark-desktop pane
|
|
115
|
+
// in a light-themed app, wrong in both directions.
|
|
128
116
|
return wrapEnv(
|
|
129
117
|
snapshot.env,
|
|
130
118
|
h(
|
package/src/frame/env.js
CHANGED
|
@@ -69,17 +69,9 @@ export function registeredFrameContext(key) {
|
|
|
69
69
|
* than `Context.Provider`, like `ThemeProvider`, which also plants the
|
|
70
70
|
* palette on a node so `$token` styles resolve. `render(value, children)`
|
|
71
71
|
* returns the wrapped element.
|
|
72
|
-
*
|
|
73
|
-
* `innermost: true` puts the provider directly around the pane's window,
|
|
74
|
-
* inside every other bridged provider. ThemeProvider needs the adjacency:
|
|
75
|
-
* it plants the palette on a window it finds among its *direct* children,
|
|
76
|
-
* and the window is where the palette must land — the window's own
|
|
77
|
-
* background follows it, and the node tree under it is what every `$token`
|
|
78
|
-
* resolves through. Plain context providers have no such constraint and
|
|
79
|
-
* keep the host's nesting order outside.
|
|
80
72
|
*/
|
|
81
|
-
export function registerFrameProvider(key, render
|
|
82
|
-
registry.set(key, { render
|
|
73
|
+
export function registerFrameProvider(key, render) {
|
|
74
|
+
registry.set(key, { render });
|
|
83
75
|
}
|
|
84
76
|
|
|
85
77
|
/**
|
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
|
/**
|