react-x11 2.16.1 → 2.17.1
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 +38 -23
- package/package.json +3 -1
- package/src/Reconciler.js +82 -23
- package/src/a11y.js +18 -1
- package/src/appcontext.js +8 -0
- package/src/appearance.js +36 -0
- package/src/{cocoa → backend}/context2d.js +27 -7
- package/src/capabilities.js +99 -1
- package/src/cocoa/app.js +17 -9
- package/src/cocoa/fonts.js +1 -1
- package/src/cocoa/glarea.js +48 -10
- package/src/cocoa/overlay.js +2 -2
- package/src/cocoa/panewindow.js +2 -2
- package/src/cocoa/presenter.js +2 -2
- package/src/cocoa/surface.js +3 -3
- package/src/cocoa/window.js +23 -2
- package/src/events.js +21 -0
- package/src/foreignnodes.js +8 -3
- package/src/frame/index.js +30 -4
- package/src/glnodes.js +21 -7
- package/src/idle.js +59 -1
- package/src/index.d.ts +41 -0
- package/src/index.js +30 -3
- package/src/launcher.js +17 -8
- package/src/launcherhooks.js +24 -10
- package/src/node.d.ts +1 -1
- package/src/nodes/cascade.js +9 -0
- package/src/nodes/node.js +6 -1
- package/src/nodes/window/hints.js +21 -2
- package/src/nodes/window/window.js +2 -2
- package/src/notifications.js +39 -14
- package/src/taskbarhooks.js +164 -0
- package/src/transfer.js +20 -1
- package/src/trayhooks.js +1 -1
- package/src/types/capabilities.d.ts +32 -3
- package/src/types/elements.d.ts +23 -1
- package/src/types/events.d.ts +16 -0
- package/src/types/launcher.d.ts +20 -6
- package/src/types/taskbar.d.ts +79 -0
- package/src/wayland/context2d.js +1 -1
- package/src/win32/a11y.js +604 -0
- package/src/win32/app.js +768 -0
- package/src/win32/bezels.js +158 -0
- package/src/win32/dnd.js +283 -0
- package/src/win32/fonts.js +497 -0
- package/src/win32/glarea.js +548 -0
- package/src/win32/ime.js +267 -0
- package/src/win32/keymap.js +116 -0
- package/src/win32/native.js +54 -0
- package/src/win32/panehost.js +106 -0
- package/src/win32/panewindow.js +343 -0
- package/src/win32/shell.js +426 -0
- package/src/win32/surface.js +192 -0
- package/src/win32/window.js +659 -0
- package/src/windowid.js +66 -0
package/src/index.js
CHANGED
|
@@ -4,7 +4,13 @@ import './bootstrap.js';
|
|
|
4
4
|
|
|
5
5
|
export { createRoot, Renderer } from './Reconciler.js';
|
|
6
6
|
export { createStyles, flattenStyle } from './styles.js';
|
|
7
|
-
export {
|
|
7
|
+
export {
|
|
8
|
+
windowIdOf,
|
|
9
|
+
useWindowId,
|
|
10
|
+
windowHandleOf,
|
|
11
|
+
useWindowHandle,
|
|
12
|
+
useTopLevelWindow,
|
|
13
|
+
} from './windowid.js';
|
|
8
14
|
export { launchTimestamp, notifyStartupComplete } from './startup.js';
|
|
9
15
|
export { activateWindow } from './activate.js';
|
|
10
16
|
export { lastInputTime, serverTime } from './inputtime.js';
|
|
@@ -14,9 +20,30 @@ export {
|
|
|
14
20
|
registerApplication,
|
|
15
21
|
} from './application.js';
|
|
16
22
|
export { useAppActivate, useAppOpen } from './apphooks.js';
|
|
17
|
-
export {
|
|
18
|
-
|
|
23
|
+
export {
|
|
24
|
+
setBadge,
|
|
25
|
+
setLauncherMenu,
|
|
26
|
+
setProgress,
|
|
27
|
+
setQuicklist,
|
|
28
|
+
setUrgent,
|
|
29
|
+
} from './launcher.js';
|
|
30
|
+
export {
|
|
31
|
+
useBadge,
|
|
32
|
+
useDockMenu,
|
|
33
|
+
useLauncherMenu,
|
|
34
|
+
useProgress,
|
|
35
|
+
} from './launcherhooks.js';
|
|
19
36
|
export { useTray } from './trayhooks.js';
|
|
37
|
+
// The Windows taskbar's own surfaces. Exported unconditionally and inert
|
|
38
|
+
// where the backend has none — `useDesktopCapability('launcher').features`
|
|
39
|
+
// carries `tasks`, `thumbnailToolbar` and `recentDocuments`, so an app
|
|
40
|
+
// branches on what this desktop has rather than on the platform.
|
|
41
|
+
export {
|
|
42
|
+
noteRecentDocument,
|
|
43
|
+
useJumpList,
|
|
44
|
+
useRecentDocument,
|
|
45
|
+
useThumbnailToolbar,
|
|
46
|
+
} from './taskbarhooks.js';
|
|
20
47
|
export {
|
|
21
48
|
CAPABILITIES,
|
|
22
49
|
NO_CAPABILITY,
|
package/src/launcher.js
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
// desktops, two mechanisms, one call each:
|
|
7
7
|
//
|
|
8
8
|
// 1. **the app's own tile** — the cocoa backend's `NSDockTile`, reached
|
|
9
|
-
// through the app object (`setDockBadge`, `
|
|
9
|
+
// through the app object (`setDockBadge`, `setLauncherMenu`, src/cocoa/
|
|
10
10
|
// app.js).
|
|
11
11
|
// 2. **`com.canonical.Unity.LauncherEntry`** over the session bus — the
|
|
12
12
|
// protocol Unity defined and the KDE, elementary, Cairo-Dock and
|
|
@@ -34,13 +34,13 @@
|
|
|
34
34
|
// is runtime, it follows state, and Dash-to-Dock/ubuntu-dock, Plank and the
|
|
35
35
|
// Unity heritage launchers all render it.
|
|
36
36
|
//
|
|
37
|
-
// So `
|
|
37
|
+
// So `useLauncherMenu()` has a Linux rung after all, and the three menus an app
|
|
38
38
|
// puts on the desktop — panel, tray, launcher — are now one authoring model
|
|
39
39
|
// on both backends. `.desktop` actions are still the right place for entries
|
|
40
40
|
// that must work *while the app is not running*; they are a different feature
|
|
41
41
|
// wearing a similar hat.
|
|
42
42
|
//
|
|
43
|
-
// Nothing here imports react: `useBadge`/`
|
|
43
|
+
// Nothing here imports react: `useBadge`/`useLauncherMenu` (launcherhooks.js) are
|
|
44
44
|
// the hooks, and these are the functions under them, callable from host-side
|
|
45
45
|
// code with no tree. The entry on the bus is held for as long as anything is
|
|
46
46
|
// shown and released when the last of it is cleared — a held bus ref is a
|
|
@@ -300,7 +300,8 @@ export async function setUrgent(urgent, { app } = {}) {
|
|
|
300
300
|
}
|
|
301
301
|
|
|
302
302
|
/**
|
|
303
|
-
* The menu behind a right-click on the app's launcher icon — the
|
|
303
|
+
* The menu behind a right-click on the app's launcher icon — the Dock menu on
|
|
304
|
+
* macOS, the quicklist on Linux.
|
|
304
305
|
*
|
|
305
306
|
* Takes `MenuBar`'s item vocabulary and exports it as a `com.canonical.dbusmenu`
|
|
306
307
|
* tree, exactly as the tray and the global menu do. `null` takes it down.
|
|
@@ -310,12 +311,12 @@ export async function setUrgent(urgent, { app } = {}) {
|
|
|
310
311
|
* launcher that builds its client the instant it sees `quicklist` finds an
|
|
311
312
|
* object there.
|
|
312
313
|
*/
|
|
313
|
-
export async function
|
|
314
|
+
export async function setLauncherMenu(items, { app } = {}) {
|
|
314
315
|
const target = app ?? soleApp();
|
|
315
316
|
|
|
316
|
-
// Rung 1: the app's own
|
|
317
|
-
if (typeof target?.
|
|
318
|
-
target.
|
|
317
|
+
// Rung 1: the app's own icon menu.
|
|
318
|
+
if (typeof target?.setLauncherMenu === 'function') {
|
|
319
|
+
target.setLauncherMenu(items ?? null);
|
|
319
320
|
return true;
|
|
320
321
|
}
|
|
321
322
|
|
|
@@ -367,6 +368,14 @@ export async function setQuicklist(items, { app } = {}) {
|
|
|
367
368
|
return true;
|
|
368
369
|
}
|
|
369
370
|
|
|
371
|
+
/**
|
|
372
|
+
* @deprecated Renamed to {@link setLauncherMenu}. "Quicklist" is the Unity
|
|
373
|
+
* launcher's word for the menu macOS calls the Dock menu; this function
|
|
374
|
+
* always drove both, and the name only ever named one of them. Kept working
|
|
375
|
+
* and kept quiet.
|
|
376
|
+
*/
|
|
377
|
+
export const setQuicklist = setLauncherMenu;
|
|
378
|
+
|
|
370
379
|
/** Test seam, not public: drop the exported entry without emitting. */
|
|
371
380
|
export async function _resetLauncher() {
|
|
372
381
|
await releaseEntry();
|
package/src/launcherhooks.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// `useBadge()`, `useProgress()` and `
|
|
1
|
+
// `useBadge()`, `useProgress()` and `useLauncherMenu()` — the launcher's view of
|
|
2
2
|
// the app, as things a component declares rather than manages.
|
|
3
3
|
//
|
|
4
4
|
// All three now have a rung on both backends except progress, which has one
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
// feature an app should branch on, and a development warning for every
|
|
8
8
|
// desktop that lacks one is a warning nobody can act on.
|
|
9
9
|
//
|
|
10
|
-
// The one that changed shape is `
|
|
10
|
+
// The one that changed shape is `useLauncherMenu`. It used to be cocoa-only on
|
|
11
11
|
// the grounds that the freedesktop counterpart was an install step — see
|
|
12
12
|
// `launcher.js`, where that reasoning is corrected: the launcher protocol
|
|
13
13
|
// carries a `quicklist` dbusmenu, so the Dock menu is runtime code on both.
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
import { useEffect, useRef } from 'react';
|
|
16
16
|
|
|
17
17
|
import { useAppOrNull } from './appcontext.js';
|
|
18
|
-
import { setBadge,
|
|
18
|
+
import { setBadge, setLauncherMenu, setProgress } from './launcher.js';
|
|
19
19
|
|
|
20
20
|
/**
|
|
21
21
|
* Show `value` on the app's icon while this component is mounted, and clear
|
|
@@ -63,12 +63,12 @@ export function useProgress(value) {
|
|
|
63
63
|
}
|
|
64
64
|
|
|
65
65
|
/**
|
|
66
|
-
* The menu behind a right-click on the app's icon
|
|
67
|
-
* from the same `items` vocabulary `MenuBar`
|
|
68
|
-
* item's `onSelect` fires when the user picks it.
|
|
66
|
+
* The menu behind a right-click on the app's **launcher icon** — the Dock on
|
|
67
|
+
* macOS, the launcher on Linux — from the same `items` vocabulary `MenuBar`
|
|
68
|
+
* and `ContextMenu` take. An item's `onSelect` fires when the user picks it.
|
|
69
69
|
*
|
|
70
70
|
* ```jsx
|
|
71
|
-
*
|
|
71
|
+
* useLauncherMenu([
|
|
72
72
|
* { label: 'New Window', onSelect: openWindow },
|
|
73
73
|
* { type: 'separator' },
|
|
74
74
|
* { label: 'Recent', items: recent.map(toItem) },
|
|
@@ -80,8 +80,14 @@ export function useProgress(value) {
|
|
|
80
80
|
* backend; the launcher protocol's `quicklist` on Linux, which needs the
|
|
81
81
|
* identity `registerApplication({ appId })` establishes and a `.desktop` file
|
|
82
82
|
* of that name for a launcher to attach it to.
|
|
83
|
+
*
|
|
84
|
+
* It reads `useDesktopCapability('launcher').features.menu`, which is where
|
|
85
|
+
* the name comes from: every desktop has one icon standing for this
|
|
86
|
+
* application and calls it something different — Dock, taskbar, panel, dash —
|
|
87
|
+
* and `launcher` is the one word that is none of their words and all of their
|
|
88
|
+
* meanings (AGENTS.md, "Vocabulary").
|
|
83
89
|
*/
|
|
84
|
-
export function
|
|
90
|
+
export function useLauncherMenu(items) {
|
|
85
91
|
const app = useAppOrNull();
|
|
86
92
|
// read at activation time, so a pick three minutes from now runs the
|
|
87
93
|
// handler from the current render rather than the mounting one
|
|
@@ -89,12 +95,20 @@ export function useDockMenu(items) {
|
|
|
89
95
|
live.current = items;
|
|
90
96
|
|
|
91
97
|
useEffect(() => {
|
|
92
|
-
|
|
98
|
+
setLauncherMenu(items ?? null, { app }).catch(() => {});
|
|
93
99
|
}, [items, app]);
|
|
94
100
|
|
|
95
101
|
useEffect(() => {
|
|
96
102
|
return () => {
|
|
97
|
-
|
|
103
|
+
setLauncherMenu(null, { app }).catch(() => {});
|
|
98
104
|
};
|
|
99
105
|
}, [app]);
|
|
100
106
|
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* @deprecated Renamed to {@link useLauncherMenu}. "Dock" is one desktop's
|
|
110
|
+
* word for the icon every desktop has; this hook drove the Linux launcher's
|
|
111
|
+
* quicklist long before the name caught up. Kept working, and kept quiet —
|
|
112
|
+
* an alias that warned would punish an app for code that is still correct.
|
|
113
|
+
*/
|
|
114
|
+
export const useDockMenu = useLauncherMenu;
|
package/src/node.d.ts
CHANGED
|
@@ -40,7 +40,7 @@ export interface ImageDataLike {
|
|
|
40
40
|
/**
|
|
41
41
|
* The 2d context a node paints into — the canvas-shaped subset **both**
|
|
42
42
|
* backends implement: ntk's `RenderingContext2D` over XRender on X11, and
|
|
43
|
-
* `
|
|
43
|
+
* `BackendContext2D` over CoreGraphics on macOS. Declared as the contract an
|
|
44
44
|
* element may rely on rather than as either class: what is here is on
|
|
45
45
|
* both, and a member one backend has and the other does not is optional
|
|
46
46
|
* here or absent. Coordinates are device pixels in the owning window's
|
package/src/nodes/cascade.js
CHANGED
|
@@ -422,6 +422,15 @@ export class NodeCascade {
|
|
|
422
422
|
get placed() {
|
|
423
423
|
const owner = this.isWindow ? this : this.root;
|
|
424
424
|
if (!owner) return false;
|
|
425
|
+
// A window written under a root `<ThemeProvider>` is handed that palette
|
|
426
|
+
// when the scope inserts it, which is after it and its subtree were built
|
|
427
|
+
// and first resolved (nodes/scope.js `insertBefore`). Until then its
|
|
428
|
+
// ancestry is as incomplete as a popup's without its parent, and every
|
|
429
|
+
// token the provider defines would be reported as unknown — a warning for
|
|
430
|
+
// a style that then resolves correctly, and under
|
|
431
|
+
// `REACT_X11_STRICT_TOKENS=1` a throw that killed an app whose palette was
|
|
432
|
+
// fine.
|
|
433
|
+
if (owner._awaitsRootScope && owner._scope == null) return false;
|
|
425
434
|
return owner.isPopup ? owner.parent != null : true;
|
|
426
435
|
}
|
|
427
436
|
|
package/src/nodes/node.js
CHANGED
|
@@ -70,7 +70,12 @@ export class Node {
|
|
|
70
70
|
return DEVTOOLS_FAKE_DOCUMENT;
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
-
constructor(kind, props, app, { yoga = true } = {}) {
|
|
73
|
+
constructor(kind, props, app, { yoga = true, awaitsRootScope = false } = {}) {
|
|
74
|
+
// Set before anything resolves a style: a `<window>`'s own style is
|
|
75
|
+
// resolved by this constructor, and whether its ancestry is complete
|
|
76
|
+
// decides whether an unresolved `$token` is reported (cascade.js
|
|
77
|
+
// `placed`). A window under a root `<ThemeProvider>` has no palette yet.
|
|
78
|
+
this._awaitsRootScope = awaitsRootScope;
|
|
74
79
|
this.kind = kind;
|
|
75
80
|
this.props = props;
|
|
76
81
|
this.app = app;
|
|
@@ -232,6 +232,23 @@ export function windowAttributes(props, scale = 1) {
|
|
|
232
232
|
: props[key];
|
|
233
233
|
}
|
|
234
234
|
if (Object.keys(hints).length > 0) attributes.sizeHints = hints;
|
|
235
|
+
// The desktop identity of this window, under the one name every desktop's
|
|
236
|
+
// own word maps onto: `WM_CLASS` on X11, `app_id` on Wayland,
|
|
237
|
+
// AppUserModelID on Windows. `wmClass` was X11's word for it and is still
|
|
238
|
+
// accepted, so both are normalised here and a backend reads one key.
|
|
239
|
+
//
|
|
240
|
+
// X11 carries a *pair* — an instance naming this window and a class naming
|
|
241
|
+
// the application — and everything since carries one string. The class is
|
|
242
|
+
// the application's, so the class is what a single-id backend is given; the
|
|
243
|
+
// pair itself still reaches `setClass` from the props (`applyWindowHints`).
|
|
244
|
+
const identity = attributes.appId ?? attributes.wmClass;
|
|
245
|
+
if (identity !== undefined) {
|
|
246
|
+
attributes.appId = Array.isArray(identity)
|
|
247
|
+
? (identity[1] ?? identity[0])
|
|
248
|
+
: identity && typeof identity === 'object'
|
|
249
|
+
? (identity.class ?? identity.instance)
|
|
250
|
+
: identity;
|
|
251
|
+
}
|
|
235
252
|
if (props.style !== undefined) {
|
|
236
253
|
const style = flattenStyle(props.style);
|
|
237
254
|
if (style.backgroundColor !== undefined) {
|
|
@@ -325,8 +342,10 @@ export class WindowHints {
|
|
|
325
342
|
this._sendSizeHints(next);
|
|
326
343
|
}
|
|
327
344
|
}
|
|
328
|
-
|
|
329
|
-
|
|
345
|
+
const identity = next.appId ?? next.wmClass;
|
|
346
|
+
const wasIdentity = prev.appId ?? prev.wmClass;
|
|
347
|
+
if (!shallowEqual(identity, wasIdentity) && identity) {
|
|
348
|
+
const c = identity;
|
|
330
349
|
if (Array.isArray(c)) wnd.setClass?.(c[0], c[1]);
|
|
331
350
|
else if (typeof c === 'object') wnd.setClass?.(c.instance, c.class);
|
|
332
351
|
else wnd.setClass?.(c);
|
|
@@ -138,8 +138,8 @@ export function flushWindowRestacks() {
|
|
|
138
138
|
* issue #4).
|
|
139
139
|
*/
|
|
140
140
|
export class WindowNode extends Scrollable(Node) {
|
|
141
|
-
constructor(app, attributes, props) {
|
|
142
|
-
super('window', props, app, { yoga: true });
|
|
141
|
+
constructor(app, attributes, props, { awaitsRootScope = false } = {}) {
|
|
142
|
+
super('window', props, app, { yoga: true, awaitsRootScope });
|
|
143
143
|
assertWindowSize(props, this.kind);
|
|
144
144
|
this.root = this;
|
|
145
145
|
this.attributes = attributes;
|
package/src/notifications.js
CHANGED
|
@@ -429,6 +429,19 @@ function centreFor(options) {
|
|
|
429
429
|
return app?.notifications ?? null;
|
|
430
430
|
}
|
|
431
431
|
|
|
432
|
+
/**
|
|
433
|
+
* Which rung a backend's own notification centre is.
|
|
434
|
+
*
|
|
435
|
+
* The seam is the backend's, not macOS's: a centre answers `available()` and
|
|
436
|
+
* `post()`, and the cocoa one was simply the first. `kind` is how a centre
|
|
437
|
+
* says which rung it is, so `notificationBackend()` reports the truth rather
|
|
438
|
+
* than the name of whoever got here first. Cocoa's predates the field and is
|
|
439
|
+
* the default.
|
|
440
|
+
*/
|
|
441
|
+
function kindOf(centre) {
|
|
442
|
+
return centre?.kind ?? 'cocoa';
|
|
443
|
+
}
|
|
444
|
+
|
|
432
445
|
/**
|
|
433
446
|
* Which rung this machine lands on, without posting anything.
|
|
434
447
|
*
|
|
@@ -442,10 +455,11 @@ function centreFor(options) {
|
|
|
442
455
|
export async function notificationBackend(options = {}) {
|
|
443
456
|
const backend = options.backend;
|
|
444
457
|
const want = (rung) => !backend || backend === rung;
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
if (
|
|
458
|
+
const centre = centreFor(options);
|
|
459
|
+
const kind = kindOf(centre);
|
|
460
|
+
if (want(kind)) {
|
|
461
|
+
if (centre && (await centre.available())) return kind;
|
|
462
|
+
if (backend === kind) return null;
|
|
449
463
|
}
|
|
450
464
|
if (want('dbus')) {
|
|
451
465
|
const ref = await sessionBus();
|
|
@@ -461,7 +475,14 @@ export async function notificationBackend(options = {}) {
|
|
|
461
475
|
if (want('osascript') && (process.platform === 'darwin' || backend)) {
|
|
462
476
|
return 'osascript';
|
|
463
477
|
}
|
|
464
|
-
|
|
478
|
+
// `notify-send` is a freedesktop tool. Windows has neither it nor a shell
|
|
479
|
+
// command that shows a notification, so the rung above is the floor there
|
|
480
|
+
// and `null` is the honest answer when the tree is not on the win32
|
|
481
|
+
// backend — a spawn that would fail with ENOENT is not a rung.
|
|
482
|
+
if (
|
|
483
|
+
want('notify-send') &&
|
|
484
|
+
(backend || (process.platform !== 'darwin' && process.platform !== 'win32'))
|
|
485
|
+
) {
|
|
465
486
|
return 'notify-send';
|
|
466
487
|
}
|
|
467
488
|
return null;
|
|
@@ -497,26 +518,28 @@ export async function notify(options = {}) {
|
|
|
497
518
|
const backend = options.backend;
|
|
498
519
|
const want = (rung) => !backend || backend === rung;
|
|
499
520
|
|
|
500
|
-
|
|
501
|
-
|
|
521
|
+
const centre = centreFor(options);
|
|
522
|
+
const kind = kindOf(centre);
|
|
523
|
+
if (want(kind)) {
|
|
502
524
|
if (centre && (await centre.available())) {
|
|
503
525
|
const handle = await centre.post(options);
|
|
504
526
|
if (handle) return handle;
|
|
505
527
|
// The bundle is right and the centre is there, but the system never
|
|
506
528
|
// put the prompt in front of anybody, so nobody declined: not the
|
|
507
529
|
// refusal above, and no reason to stop. The ladder moves on.
|
|
508
|
-
if (backend ===
|
|
530
|
+
if (backend === kind) {
|
|
509
531
|
throw new NoNotificationServiceError(
|
|
510
|
-
|
|
532
|
+
`backend: '${kind}' — the centre could not ask for authorization ` +
|
|
511
533
|
'(the status is still notDetermined). A bundle macOS has not ' +
|
|
512
534
|
'registered never gets the prompt (docs/notifications.md).',
|
|
513
535
|
);
|
|
514
536
|
}
|
|
515
|
-
} else if (backend ===
|
|
537
|
+
} else if (backend === kind) {
|
|
516
538
|
throw new NoNotificationServiceError(
|
|
517
|
-
|
|
518
|
-
'on
|
|
519
|
-
'process is not an app bundle
|
|
539
|
+
`backend: '${kind}' — no notification centre here: the tree is not ` +
|
|
540
|
+
'on a backend that has one, the bridge is older than the version ' +
|
|
541
|
+
'that added it, or on macOS this process is not an app bundle ' +
|
|
542
|
+
'(docs/notifications.md).',
|
|
520
543
|
);
|
|
521
544
|
}
|
|
522
545
|
}
|
|
@@ -534,7 +557,9 @@ export async function notify(options = {}) {
|
|
|
534
557
|
const shell =
|
|
535
558
|
want('osascript') && (process.platform === 'darwin' || backend)
|
|
536
559
|
? 'osascript'
|
|
537
|
-
: want('notify-send') &&
|
|
560
|
+
: want('notify-send') &&
|
|
561
|
+
(backend ||
|
|
562
|
+
(process.platform !== 'darwin' && process.platform !== 'win32'))
|
|
538
563
|
? 'notify-send'
|
|
539
564
|
: null;
|
|
540
565
|
if (shell) {
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
// The Windows taskbar's own surfaces, as hooks.
|
|
2
|
+
//
|
|
3
|
+
// Three things the taskbar has that no other desktop does, so none of them is
|
|
4
|
+
// a rung on an existing ladder and none of them pretends to be portable. What
|
|
5
|
+
// makes them safe to use anyway is that the answer to "is this here" is a
|
|
6
|
+
// value a component branches on rather than a platform check, and the hooks
|
|
7
|
+
// below do nothing at all where the backend has not installed them. An app
|
|
8
|
+
// writes the same tree everywhere and gets the feature where it exists.
|
|
9
|
+
//
|
|
10
|
+
// ```jsx
|
|
11
|
+
// const launcher = useDesktopCapability('launcher');
|
|
12
|
+
// useThumbnailToolbar(
|
|
13
|
+
// launcher.features.thumbnailToolbar ? [
|
|
14
|
+
// { id: 'prev', tooltip: 'Previous', icon: prevIcon },
|
|
15
|
+
// { id: 'play', tooltip: playing ? 'Pause' : 'Play', icon: playIcon },
|
|
16
|
+
// ] : null,
|
|
17
|
+
// (id) => transport(id),
|
|
18
|
+
// );
|
|
19
|
+
// ```
|
|
20
|
+
//
|
|
21
|
+
// They are **features of the launcher** — the same rung as the badge and the
|
|
22
|
+
// progress bar, since all of them hang off the one icon the desktop shows for
|
|
23
|
+
// this app — and not entries in `useSupports()`, which answers for the
|
|
24
|
+
// display. The seam is one rule: the backend installs a method, and its
|
|
25
|
+
// presence is the capability; the probe in `src/capabilities.js` reads those
|
|
26
|
+
// same methods, so the prediction and the hook cannot disagree. Nothing here
|
|
27
|
+
// knows what Windows is, and neither does anything in an app that uses it.
|
|
28
|
+
import { useEffect, useRef } from 'react';
|
|
29
|
+
|
|
30
|
+
import { useAppOrNull } from './appcontext.js';
|
|
31
|
+
import { liveApps } from './trace-registry.js';
|
|
32
|
+
import { useTopLevelWindow } from './windowid.js';
|
|
33
|
+
|
|
34
|
+
/** The app to act on when the caller did not say — the same rule the
|
|
35
|
+
* launcher's own imperative calls use. */
|
|
36
|
+
function soleApp() {
|
|
37
|
+
const apps = liveApps();
|
|
38
|
+
if (apps.length <= 1) return apps[0] ?? null;
|
|
39
|
+
const showing = apps.filter((app) => (app._rootChildren ?? []).length > 0);
|
|
40
|
+
return showing.length === 1 ? showing[0] : null;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** The value a caller passed, read by an effect that must not re-run for it. */
|
|
44
|
+
function useLatest(value) {
|
|
45
|
+
const ref = useRef(value);
|
|
46
|
+
ref.current = value;
|
|
47
|
+
return ref;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** A stable key for a button list, so an effect re-runs when one actually
|
|
51
|
+
* changed rather than on every render. Icons compare by identity — a caller
|
|
52
|
+
* holding its icons still is the fast path, and one that rebuilds them every
|
|
53
|
+
* render was going to re-upload them anyway. */
|
|
54
|
+
function signatureOf(buttons) {
|
|
55
|
+
if (!buttons) return '';
|
|
56
|
+
return buttons
|
|
57
|
+
.map(
|
|
58
|
+
(b) =>
|
|
59
|
+
`${b.id ?? ''}\u0000${b.tooltip ?? b.label ?? ''}\u0000` +
|
|
60
|
+
`${b.enabled === false ? 0 : 1}${b.dismissOnClick ? 1 : 0}`,
|
|
61
|
+
)
|
|
62
|
+
.join('\u0001');
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Up to seven buttons under this window's taskbar hover preview — where a
|
|
67
|
+
* media player puts play and skip.
|
|
68
|
+
*
|
|
69
|
+
* `buttons` is `[{ id, tooltip, icon, enabled, dismissOnClick }]`, and `icon`
|
|
70
|
+
* is whatever `useTray` takes: an ntk `Image`, raw RGBA with a size, or a
|
|
71
|
+
* path. `null` takes the toolbar down as far as the shell allows, which is to
|
|
72
|
+
* hide the buttons — a toolbar cannot be removed once the window has one, and
|
|
73
|
+
* saying so is better than a call that looks like it worked.
|
|
74
|
+
*
|
|
75
|
+
* `onClick` is called with the button's own `id`.
|
|
76
|
+
*
|
|
77
|
+
* Does nothing where the backend has no toolbar. The eighth button and beyond
|
|
78
|
+
* are dropped rather than refused, because the shell refuses the whole call
|
|
79
|
+
* for an eighth and one silently missing button is a better outcome than a
|
|
80
|
+
* toolbar that never appears.
|
|
81
|
+
*/
|
|
82
|
+
export function useThumbnailToolbar(buttons, onClick) {
|
|
83
|
+
const app = useAppOrNull();
|
|
84
|
+
const owner = useTopLevelWindow();
|
|
85
|
+
const handler = useLatest(onClick);
|
|
86
|
+
const latest = useLatest(buttons);
|
|
87
|
+
const signature = signatureOf(buttons);
|
|
88
|
+
|
|
89
|
+
useEffect(() => {
|
|
90
|
+
if (typeof app?.thumbnailToolbar !== 'function') return;
|
|
91
|
+
// Read inside the effect: `useTopLevelWindow` answers through a getter,
|
|
92
|
+
// so the window a tree is in is whatever it is when the effect runs
|
|
93
|
+
// rather than what it was at render.
|
|
94
|
+
const wnd = owner?.current?.window;
|
|
95
|
+
if (!wnd?.id) return;
|
|
96
|
+
app.thumbnailToolbar(wnd.id, latest.current ?? []);
|
|
97
|
+
|
|
98
|
+
const fire = (event) => handler.current?.(event.id, event);
|
|
99
|
+
wnd.on?.('thumbbutton', fire);
|
|
100
|
+
return () => {
|
|
101
|
+
wnd.off?.('thumbbutton', fire);
|
|
102
|
+
// Hidden rather than removed, which is all the shell offers.
|
|
103
|
+
app.thumbnailToolbar(wnd.id, []);
|
|
104
|
+
};
|
|
105
|
+
// `signature` is the dependency; the buttons themselves are read through
|
|
106
|
+
// a ref, so a new array holding the same buttons does not re-send them.
|
|
107
|
+
}, [app, owner, signature, latest, handler]);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* The Tasks category of this application's jump list — the menu on a right
|
|
112
|
+
* click of its taskbar button.
|
|
113
|
+
*
|
|
114
|
+
* `tasks` is `[{ title, arguments, description }]`. Each one **relaunches
|
|
115
|
+
* this executable** with the arguments given, which is what a jump-list task
|
|
116
|
+
* is: the shell starts the program, it does not call back into the running
|
|
117
|
+
* one. An app that wants the running instance to answer needs the single
|
|
118
|
+
* instance path, which is not built (docs/windows-integrations.md) — so until
|
|
119
|
+
* then a task is for something the app can do from a cold start.
|
|
120
|
+
*
|
|
121
|
+
* `null` deletes the category.
|
|
122
|
+
*/
|
|
123
|
+
export function useJumpList(tasks) {
|
|
124
|
+
const app = useAppOrNull();
|
|
125
|
+
const latest = useLatest(tasks);
|
|
126
|
+
// The tasks are plain data — a title, arguments, a description — so their
|
|
127
|
+
// own JSON is the dependency, and a caller rebuilding an equal array every
|
|
128
|
+
// render does not re-send it. Read through a ref for the same reason
|
|
129
|
+
// `useThumbnailToolbar` does: the value is what it is when the effect runs.
|
|
130
|
+
const signature = JSON.stringify(tasks ?? null);
|
|
131
|
+
|
|
132
|
+
useEffect(() => {
|
|
133
|
+
if (typeof app?.jumpList !== 'function') return;
|
|
134
|
+
app.jumpList(latest.current ?? []);
|
|
135
|
+
return () => app.jumpList([]);
|
|
136
|
+
}, [app, signature, latest]);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Note a document this app just opened, for the shell's Recent lists — the
|
|
141
|
+
* jump list's own Recent section, and Explorer's quick access.
|
|
142
|
+
*
|
|
143
|
+
* The same act as macOS's `noteNewRecentDocumentURL:`; it lives here rather
|
|
144
|
+
* than in a cross-platform hook because this is the only backend with one to
|
|
145
|
+
* call today. Where the file type is not associated with this application the
|
|
146
|
+
* shell may keep it and show it nowhere, which is its decision to make.
|
|
147
|
+
*
|
|
148
|
+
* `null` clears the list.
|
|
149
|
+
*/
|
|
150
|
+
export function noteRecentDocument(path, { app } = {}) {
|
|
151
|
+
const target = app ?? soleApp();
|
|
152
|
+
if (typeof target?.noteRecentDocument !== 'function') return false;
|
|
153
|
+
target.noteRecentDocument(path);
|
|
154
|
+
return true;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/** The hook form: notes `path` whenever it changes. */
|
|
158
|
+
export function useRecentDocument(path) {
|
|
159
|
+
const app = useAppOrNull();
|
|
160
|
+
useEffect(() => {
|
|
161
|
+
if (typeof app?.noteRecentDocument !== 'function' || path == null) return;
|
|
162
|
+
app.noteRecentDocument(path);
|
|
163
|
+
}, [app, path]);
|
|
164
|
+
}
|
package/src/transfer.js
CHANGED
|
@@ -70,6 +70,25 @@ export function decodeData(data, target) {
|
|
|
70
70
|
* URIs that are actually local — a remote `file://host/...` has no local
|
|
71
71
|
* path and must not pretend to.
|
|
72
72
|
*/
|
|
73
|
+
/**
|
|
74
|
+
* A `file:` URL's pathname as a path this machine can open.
|
|
75
|
+
*
|
|
76
|
+
* On a POSIX path the two are the same string. A Windows one is not: the URL
|
|
77
|
+
* form of `C:\Users\a` is `file:///C:/Users/a`, whose pathname is
|
|
78
|
+
* `/C:/Users/a` — a leading slash that no Windows API accepts, and forward
|
|
79
|
+
* slashes that most of them tolerate and no user recognises. A drive letter
|
|
80
|
+
* after the slash is the tell, and it is unambiguous: no POSIX path begins
|
|
81
|
+
* `/C:`.
|
|
82
|
+
*
|
|
83
|
+
* Decided by the shape of the path rather than by `process.platform`, so a
|
|
84
|
+
* uri-list that arrived from another machine reads the same way on both.
|
|
85
|
+
*/
|
|
86
|
+
function pathOfFileUrl(pathname) {
|
|
87
|
+
const drive = /^\/([A-Za-z]:)(\/.*)?$/.exec(pathname);
|
|
88
|
+
if (!drive) return pathname;
|
|
89
|
+
return (drive[1] + (drive[2] ?? '\\')).replace(/\//g, '\\');
|
|
90
|
+
}
|
|
91
|
+
|
|
73
92
|
export function parseUriList(text) {
|
|
74
93
|
const files = [];
|
|
75
94
|
for (const line of String(text).split(/\r?\n/)) {
|
|
@@ -82,7 +101,7 @@ export function parseUriList(text) {
|
|
|
82
101
|
url.protocol === 'file:' &&
|
|
83
102
|
(url.hostname === '' || url.hostname === 'localhost')
|
|
84
103
|
) {
|
|
85
|
-
entry.path = decodeURIComponent(url.pathname);
|
|
104
|
+
entry.path = pathOfFileUrl(decodeURIComponent(url.pathname));
|
|
86
105
|
}
|
|
87
106
|
} catch {
|
|
88
107
|
// not a parseable URI — keep the raw line, claim no path
|
package/src/trayhooks.js
CHANGED
|
@@ -73,7 +73,7 @@ import { StatusNotifierItem, allocateItemSlot } from './statusnotifier.js';
|
|
|
73
73
|
* ```
|
|
74
74
|
*
|
|
75
75
|
* With `menu`, a click opens it — the same `items` vocabulary `MenuBar` and
|
|
76
|
-
* `
|
|
76
|
+
* `useLauncherMenu` take, an item's `onSelect` firing when picked. Without one,
|
|
77
77
|
* `onClick` is called with the button and where the click was, in logical
|
|
78
78
|
* screen pixels — with the item's rect, where the backend knows it. `null`
|
|
79
79
|
* means no item. Every field follows its value while mounted; the item is
|
|
@@ -10,7 +10,13 @@ export type DesktopBackend =
|
|
|
10
10
|
| 'osascript'
|
|
11
11
|
| 'notify-send'
|
|
12
12
|
| 'statusnotifier'
|
|
13
|
-
| 'launcherentry'
|
|
13
|
+
| 'launcherentry'
|
|
14
|
+
/** A Shell_NotifyIcon balloon. */
|
|
15
|
+
| 'win32'
|
|
16
|
+
/** The notify icon itself, as a tray. */
|
|
17
|
+
| 'shellnotifyicon'
|
|
18
|
+
/** `ITaskbarList3` and the jump list, as a launcher. */
|
|
19
|
+
| 'taskbar';
|
|
14
20
|
|
|
15
21
|
/** The capabilities {@link desktopCapability} can be asked about. */
|
|
16
22
|
export type DesktopCapabilityName = 'notifications' | 'tray' | 'launcher';
|
|
@@ -60,14 +66,37 @@ export interface TrayFeatures {
|
|
|
60
66
|
|
|
61
67
|
export interface LauncherFeatures {
|
|
62
68
|
badge: boolean;
|
|
63
|
-
/**
|
|
69
|
+
/**
|
|
70
|
+
* A string badge rather than a count. False on the launcher protocol,
|
|
71
|
+
* which carries a number; the taskbar draws the text into its overlay
|
|
72
|
+
* icon, where about three glyphs fit before it becomes `99+`.
|
|
73
|
+
*/
|
|
64
74
|
badgeText: boolean;
|
|
65
75
|
progress: boolean;
|
|
66
76
|
urgent: boolean;
|
|
67
|
-
/**
|
|
77
|
+
/**
|
|
78
|
+
* The Dock menu / quicklist: entries that **call back into this process**.
|
|
79
|
+
* False on the taskbar, whose menu is the jump list — see `tasks`.
|
|
80
|
+
*/
|
|
68
81
|
menu: boolean;
|
|
69
82
|
/** The launcher needs an installed `.desktop` file to attach this to. */
|
|
70
83
|
needsDesktopFile: boolean;
|
|
84
|
+
/**
|
|
85
|
+
* Static entries on the icon's menu that start a **new process** with
|
|
86
|
+
* arguments, rather than calling back into this one: the jump list's Tasks
|
|
87
|
+
* category. A different feature from `menu`, and `useJumpList` drives it.
|
|
88
|
+
*/
|
|
89
|
+
tasks: boolean;
|
|
90
|
+
/**
|
|
91
|
+
* Buttons under the icon's hover preview, which `useThumbnailToolbar`
|
|
92
|
+
* drives. The taskbar's alone.
|
|
93
|
+
*/
|
|
94
|
+
thumbnailToolbar: boolean;
|
|
95
|
+
/**
|
|
96
|
+
* The launcher keeps a Recent list this app can add to, through
|
|
97
|
+
* `useRecentDocument`.
|
|
98
|
+
*/
|
|
99
|
+
recentDocuments: boolean;
|
|
71
100
|
}
|
|
72
101
|
|
|
73
102
|
export interface DesktopCapabilityResult<F = Record<string, boolean>> {
|