react-x11 1.2.0 → 2.0.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 +330 -81
- package/package.json +132 -14
- package/src/ClickToComponent.js +9 -2
- package/src/DevToolsIntegration.js +440 -32
- package/src/Reconciler.js +463 -165
- package/src/a11y.js +1299 -0
- package/src/acceleratorhooks.js +86 -0
- package/src/accelerators.js +128 -0
- package/src/activate.js +168 -0
- package/src/anchor.js +360 -0
- package/src/appcontext.js +161 -0
- package/src/appearance.js +748 -0
- package/src/appearancehooks.js +96 -0
- package/src/apphooks.js +73 -0
- package/src/application.js +748 -0
- package/src/atspi.js +1953 -0
- package/src/bus.js +545 -0
- package/src/bushooks.js +136 -0
- package/src/clientmessage.js +140 -0
- package/src/clipboard.js +121 -0
- package/src/components/Button.js +148 -33
- package/src/components/Checkbox.js +66 -32
- package/src/components/Dialog.js +96 -29
- package/src/components/FileDialog.js +491 -0
- package/src/components/Icon.js +329 -0
- package/src/components/Menu.js +797 -133
- package/src/components/PasswordInput.js +434 -0
- package/src/components/ProgressBar.js +91 -12
- package/src/components/Radio.js +78 -35
- package/src/components/Select.js +255 -65
- package/src/components/Slider.js +102 -41
- package/src/components/SplitPane.js +206 -0
- package/src/components/Switch.js +100 -22
- package/src/components/Table.js +529 -0
- package/src/components/Tabs.js +211 -0
- package/src/components/Tooltip.js +528 -54
- package/src/components/anchor.js +154 -107
- package/src/components/change.js +34 -0
- package/src/components/dnd.js +103 -0
- package/src/components/index.js +16 -3
- package/src/components/keys.js +17 -20
- package/src/components/scribble.js +247 -0
- package/src/components/theme.js +350 -42
- package/src/components/typeahead.js +12 -2
- package/src/compose.js +868 -0
- package/src/compositing.js +224 -0
- package/src/dbusmenu.js +384 -0
- package/src/debug.d.ts +63 -0
- package/src/debug.js +642 -0
- package/src/decorations.js +486 -0
- package/src/desktopsettings.js +209 -0
- package/src/desktopsettingshooks.js +62 -0
- package/src/dnd.js +1725 -0
- package/src/editmenu.js +272 -0
- package/src/errors.js +98 -0
- package/src/events.js +1406 -162
- package/src/extensions.js +45 -0
- package/src/filedialog.js +375 -0
- package/src/filedialoghooks.js +132 -0
- package/src/fonthooks.js +64 -0
- package/src/fonts.js +301 -0
- package/src/foreignnodes.js +519 -0
- package/src/frame/child.js +33 -0
- package/src/frame/childmain.js +274 -0
- package/src/frame/env.js +140 -0
- package/src/frame/index.js +444 -0
- package/src/frame/lifecycle.js +67 -0
- package/src/frame/protocol.js +179 -0
- package/src/frames.js +73 -0
- package/src/glbackend.js +114 -0
- package/src/glnodes.js +125 -67
- package/src/globalmenu.js +683 -0
- package/src/host.d.ts +88 -0
- package/src/host.js +42 -0
- package/src/idle.js +490 -0
- package/src/idlehooks.js +100 -0
- package/src/imagesource.js +349 -0
- package/src/index.d.ts +475 -0
- package/src/index.js +83 -9
- package/src/inputtime.js +206 -0
- package/src/jsx-dev-runtime.d.ts +21 -0
- package/src/jsx-dev-runtime.js +2 -0
- package/src/jsx-runtime.d.ts +39 -0
- package/src/jsx-runtime.js +6 -0
- package/src/keyboard.js +256 -0
- package/src/keyboardstate.js +278 -0
- package/src/keyboardstatehooks.js +58 -0
- package/src/keysyms.d.ts +149 -0
- package/src/keysyms.js +270 -0
- package/src/locale.js +170 -0
- package/src/localehooks.js +47 -0
- package/src/menuitem.js +223 -0
- package/src/node.d.ts +597 -0
- package/src/node.js +44 -0
- package/src/nodes.js +9546 -690
- package/src/ntk.d.ts +44 -0
- package/src/ntk.js +25 -0
- package/src/paintcache.js +366 -0
- package/src/palette.js +380 -0
- package/src/pastestate.js +66 -0
- package/src/portal.js +461 -0
- package/src/priority.js +26 -0
- package/src/refresh/index.d.ts +40 -0
- package/src/refresh/index.js +122 -0
- package/src/refresh/loader.d.ts +37 -0
- package/src/refresh/loader.js +401 -0
- package/src/refresh/register.d.ts +5 -0
- package/src/refresh/register.js +13 -0
- package/src/registry.js +232 -0
- package/src/scale.js +626 -0
- package/src/scalehooks.js +27 -0
- package/src/screencolor.js +640 -0
- package/src/screencolorhooks.js +101 -0
- package/src/screens.js +754 -0
- package/src/screenshooks.js +137 -0
- package/src/startup.js +302 -0
- package/src/style.d.ts +126 -0
- package/src/style.js +33 -0
- package/src/styles.js +1482 -6
- package/src/svgnodes.js +306 -0
- package/src/testing/a11y.js +484 -0
- package/src/testing/components.js +414 -0
- package/src/testing/events.js +407 -0
- package/src/testing/harness.js +455 -0
- package/src/testing/index.d.ts +558 -0
- package/src/testing/index.js +85 -0
- package/src/testing/mock-app.js +463 -0
- package/src/testing/pixels.js +152 -0
- package/src/testing/queries.js +224 -0
- package/src/textrange.js +83 -0
- package/src/textselection.js +439 -0
- package/src/trace-registry.js +63 -0
- package/src/transfer.js +93 -0
- package/src/types/appearance.d.ts +84 -0
- package/src/types/application.d.ts +173 -0
- package/src/types/components.d.ts +820 -0
- package/src/types/dbus.d.ts +177 -0
- package/src/types/elements.d.ts +897 -0
- package/src/types/events.d.ts +549 -0
- package/src/types/filedialog.d.ts +260 -0
- package/src/types/fonts.d.ts +124 -0
- package/src/types/frame.d.ts +146 -0
- package/src/types/globalmenu.d.ts +42 -0
- package/src/types/nodes.d.ts +199 -0
- package/src/types/screencolor.d.ts +84 -0
- package/src/types/style.d.ts +434 -0
- package/src/types/system.d.ts +287 -0
- package/src/windowid.js +151 -0
- package/src/windowstate.js +393 -0
- package/src/xsettings.js +336 -0
- package/src/yoga.d.ts +55 -0
- package/src/yoga.js +135 -0
- package/src/components/Canvas3D.js +0 -28
- package/src/geometry3d.js +0 -223
- package/src/pointer3d.js +0 -158
- package/src/raycast3d.js +0 -146
- package/src/richnodes.js +0 -436
- package/src/scene3d.js +0 -683
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
// The React lid on `accelerators.js`: `useAccelerator`, and the internal
|
|
2
|
+
// registration the menus are built on.
|
|
3
|
+
//
|
|
4
|
+
// Split from the matcher for the reason `fonthooks.js` is split from
|
|
5
|
+
// `fonts.js` — the half that has to be right about X11 and the half that has
|
|
6
|
+
// to be right about React have different failure modes, and only one of them
|
|
7
|
+
// can be tested without a tree.
|
|
8
|
+
|
|
9
|
+
import { useEffect, useMemo, useRef } from 'react';
|
|
10
|
+
|
|
11
|
+
import { matchesShortcut } from './accelerators.js';
|
|
12
|
+
import { useTopLevelWindow } from './windowid.js';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Bind a chord for as long as this component is mounted, anchored at
|
|
16
|
+
* `anchorRef` — the node the binding belongs to, which is what decides
|
|
17
|
+
* whether it is on the screen and whether a modal has taken the keyboard
|
|
18
|
+
* from it (`EventManager._runAccelerators`).
|
|
19
|
+
*
|
|
20
|
+
* `handle(ev)` returns whether it took the key.
|
|
21
|
+
*
|
|
22
|
+
* The handler is read from a ref rather than captured, and is deliberately
|
|
23
|
+
* *not* a dependency: a chord pressed three minutes from now must run the
|
|
24
|
+
* handler from the current render, and re-registering on every render would
|
|
25
|
+
* also reorder the bindings under it.
|
|
26
|
+
*/
|
|
27
|
+
export function useAcceleratorEntry(anchorRef, handle, enabled = true) {
|
|
28
|
+
const live = useRef(handle);
|
|
29
|
+
live.current = handle;
|
|
30
|
+
useEffect(() => {
|
|
31
|
+
if (!enabled) return undefined;
|
|
32
|
+
const manager = anchorRef.current?.root?.events;
|
|
33
|
+
if (!manager) return undefined;
|
|
34
|
+
return manager.registerAccelerator({
|
|
35
|
+
anchor: () => anchorRef.current ?? null,
|
|
36
|
+
handle: (ev) => live.current?.(ev) ?? false,
|
|
37
|
+
});
|
|
38
|
+
}, [anchorRef, enabled]);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* `useAccelerator([['Control', 'K']], () => openPalette())` — a shortcut that
|
|
43
|
+
* is not in a menu.
|
|
44
|
+
*
|
|
45
|
+
* Same chord vocabulary as a menu item's `shortcut` and the same matching, so
|
|
46
|
+
* a shortcut can be moved into or out of a menu without being rewritten, and
|
|
47
|
+
* the same rules apply to it: exact on Control/Alt/Shift/Super, indifferent
|
|
48
|
+
* to Caps Lock and Num Lock, matched against the Latin keysym so it survives
|
|
49
|
+
* a layout switch, and behind whatever a focused element consumed with
|
|
50
|
+
* `preventDefault()`.
|
|
51
|
+
*
|
|
52
|
+
* The handler is called with the key event, and the key is consumed.
|
|
53
|
+
*
|
|
54
|
+
* By default the binding belongs to the window the component is in, which is
|
|
55
|
+
* what an application-wide shortcut wants. Two options for when it is not:
|
|
56
|
+
* `enabled: false` unbinds it without unmounting anything, and `scope` takes
|
|
57
|
+
* a ref to a node the binding hangs off instead — the way to give a modal
|
|
58
|
+
* `<Dialog>` a shortcut of its own, since a binding on the window behind it
|
|
59
|
+
* is one the modal has taken the keyboard from.
|
|
60
|
+
*/
|
|
61
|
+
export function useAccelerator(shortcut, handler, options = {}) {
|
|
62
|
+
const { enabled = true, scope } = options;
|
|
63
|
+
const owner = useTopLevelWindow();
|
|
64
|
+
// `useTopLevelWindow` answers when it is read, so the fallback stays a
|
|
65
|
+
// live getter rather than a node captured on the render that mounted.
|
|
66
|
+
const anchorRef = useMemo(
|
|
67
|
+
() =>
|
|
68
|
+
scope ?? {
|
|
69
|
+
get current() {
|
|
70
|
+
return owner.current;
|
|
71
|
+
},
|
|
72
|
+
},
|
|
73
|
+
[scope, owner],
|
|
74
|
+
);
|
|
75
|
+
const live = useRef({ shortcut, handler });
|
|
76
|
+
live.current = { shortcut, handler };
|
|
77
|
+
useAcceleratorEntry(
|
|
78
|
+
anchorRef,
|
|
79
|
+
(ev) => {
|
|
80
|
+
if (!matchesShortcut(ev, live.current.shortcut)) return false;
|
|
81
|
+
live.current.handler?.(ev);
|
|
82
|
+
return true;
|
|
83
|
+
},
|
|
84
|
+
enabled,
|
|
85
|
+
);
|
|
86
|
+
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
// Accelerators: the half of a menu `shortcut` that was only ever drawn (#351).
|
|
2
|
+
//
|
|
3
|
+
// A `MenuBar` measured its items' shortcuts, drew them, announced them as
|
|
4
|
+
// `aria-keyshortcuts` and handed them to the panel as dbusmenu's `aas` — and
|
|
5
|
+
// bound none of them. An app wrote the binding a second time in an
|
|
6
|
+
// `onKeyDown`, so the promise on the screen and the behaviour behind it were
|
|
7
|
+
// two pieces of state that drifted.
|
|
8
|
+
//
|
|
9
|
+
// This is the matching half, and it is pure: an event and a chord in, a
|
|
10
|
+
// boolean out. The registry and the dispatch order live in `events.js`, next
|
|
11
|
+
// to the key path they are part of; the React lid is `acceleratorhooks.js`.
|
|
12
|
+
//
|
|
13
|
+
// ## Which keysym a chord is compared against
|
|
14
|
+
//
|
|
15
|
+
// `ev.keysym` — the **Latin** keysym for the physical key, whatever layout is
|
|
16
|
+
// live (`src/keyboard.js`, docs/events.md). That is what makes Ctrl+S keep
|
|
17
|
+
// saving while the user types Russian, and it is the reason this module has
|
|
18
|
+
// nothing to say about layouts at all.
|
|
19
|
+
//
|
|
20
|
+
// ## Exact on the four modifiers, indifferent to the locks
|
|
21
|
+
//
|
|
22
|
+
// Ctrl+S must not fire on Ctrl+Shift+S, and must still fire with Caps Lock or
|
|
23
|
+
// Num Lock on. Both fall out of comparing `ctrlKey`/`altKey`/`shiftKey`/
|
|
24
|
+
// `metaKey`, which are the four named modifier bits and nothing else: Lock is
|
|
25
|
+
// bit 1 and Num Lock is normally Mod2, so neither is in the comparison to
|
|
26
|
+
// begin with. This is the line every hand-rolled binding gets wrong, which is
|
|
27
|
+
// most of why it is here rather than in an application.
|
|
28
|
+
|
|
29
|
+
import {
|
|
30
|
+
isEnabled,
|
|
31
|
+
isSeparator,
|
|
32
|
+
splitChord,
|
|
33
|
+
visibleItems,
|
|
34
|
+
} from './menuitem.js';
|
|
35
|
+
import { keysymFromName } from './keysyms.js';
|
|
36
|
+
|
|
37
|
+
/** ASCII upper case folded down, so `'S'` and `'s'` are the same key. */
|
|
38
|
+
const fold = (keysym) =>
|
|
39
|
+
keysym >= 0x41 && keysym <= 0x5a ? keysym + 0x20 : keysym;
|
|
40
|
+
|
|
41
|
+
const isLetter = (keysym) => fold(keysym) >= 0x61 && fold(keysym) <= 0x7a;
|
|
42
|
+
|
|
43
|
+
/** The keysym one chord ends on, or `undefined` for one nothing can match. */
|
|
44
|
+
export function chordKeysym(chord) {
|
|
45
|
+
const { key } = splitChord(chord);
|
|
46
|
+
return key == null ? undefined : keysymFromName(key);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Does this key event *press* `chord` — `['Control', 'S']`?
|
|
51
|
+
*
|
|
52
|
+
* Two ways a chord's key can be recognised, and the second exists for the
|
|
53
|
+
* symbols:
|
|
54
|
+
*
|
|
55
|
+
* - **as the key it names.** `ev.keysym` is the Latin keysym for the physical
|
|
56
|
+
* key, case-folded here because dbusmenu carries the key and not the
|
|
57
|
+
* character it types, so `['Control', 's']` and `['Control', 'S']` are one
|
|
58
|
+
* binding. All four modifiers are compared exactly.
|
|
59
|
+
* - **as the character it typed.** `['Control', 'plus']` is a real shortcut
|
|
60
|
+
* that no US keyboard can press without Shift — `+` is the shifted `=` — so
|
|
61
|
+
* comparing the base keysym alone would draw `Ctrl++` and never fire it.
|
|
62
|
+
* Where the chord names a symbol, the character the key actually produced
|
|
63
|
+
* counts too, and Shift is then whatever it took to produce it rather than
|
|
64
|
+
* a modifier of its own. GTK reaches the same answer through XKB's consumed
|
|
65
|
+
* modifiers; this is that rule with the part we cannot ask the server for
|
|
66
|
+
* left out.
|
|
67
|
+
*
|
|
68
|
+
* The second path is **letters excluded** deliberately: it is exactly what
|
|
69
|
+
* would make Ctrl+Shift+S fire a Ctrl+S binding, which is the thing the
|
|
70
|
+
* modifier comparison is here to prevent.
|
|
71
|
+
*/
|
|
72
|
+
export function matchesChord(ev, chord) {
|
|
73
|
+
const { modifiers, key } = splitChord(chord);
|
|
74
|
+
if (key == null) return false;
|
|
75
|
+
const wanted = keysymFromName(key);
|
|
76
|
+
if (wanted === undefined) return false;
|
|
77
|
+
|
|
78
|
+
const ctrl = modifiers.has('Control');
|
|
79
|
+
const alt = modifiers.has('Alt');
|
|
80
|
+
const shift = modifiers.has('Shift');
|
|
81
|
+
const meta = modifiers.has('Super');
|
|
82
|
+
if (
|
|
83
|
+
Boolean(ev?.ctrlKey) !== ctrl ||
|
|
84
|
+
Boolean(ev?.altKey) !== alt ||
|
|
85
|
+
Boolean(ev?.metaKey) !== meta
|
|
86
|
+
) {
|
|
87
|
+
return false;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const pressed = ev?.keysym;
|
|
91
|
+
if (typeof pressed === 'number' && fold(pressed) === fold(wanted)) {
|
|
92
|
+
return Boolean(ev?.shiftKey) === shift;
|
|
93
|
+
}
|
|
94
|
+
if (isLetter(wanted)) return false;
|
|
95
|
+
// The character this key typed, which is where a Shift the layout needed
|
|
96
|
+
// has already been spent — so a chord that did not ask for Shift does not
|
|
97
|
+
// mind one here, and one that did still gets it.
|
|
98
|
+
return (!shift || Boolean(ev?.shiftKey)) && ev?.codepoint === wanted;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** Does this key event press any alternative of an `aas` shortcut? */
|
|
102
|
+
export function matchesShortcut(ev, shortcut) {
|
|
103
|
+
if (!Array.isArray(shortcut)) return false;
|
|
104
|
+
return shortcut.some(
|
|
105
|
+
(chord) => Array.isArray(chord) && matchesChord(ev, chord),
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* The item in a menu descriptor whose shortcut this key event presses, or
|
|
111
|
+
* `null` — submenus included, since a shortcut is a way to reach a command
|
|
112
|
+
* *without* opening the menu it lives in.
|
|
113
|
+
*
|
|
114
|
+
* `enabled` and `visible` gate the search rather than the result, and they
|
|
115
|
+
* gate a parent's children with it: an item nobody can reach by opening the
|
|
116
|
+
* menu is not one a chord should reach either. A gated item is skipped and
|
|
117
|
+
* the walk goes on, so a disabled "Save" does not also swallow the key from
|
|
118
|
+
* whatever else claims it.
|
|
119
|
+
*/
|
|
120
|
+
export function acceleratedItem(items, ev) {
|
|
121
|
+
for (const item of visibleItems(items)) {
|
|
122
|
+
if (isSeparator(item) || !isEnabled(item)) continue;
|
|
123
|
+
if (matchesShortcut(ev, item.shortcut)) return item;
|
|
124
|
+
const inner = acceleratedItem(item.items, ev);
|
|
125
|
+
if (inner) return inner;
|
|
126
|
+
}
|
|
127
|
+
return null;
|
|
128
|
+
}
|
package/src/activate.js
ADDED
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
// The raise: asking the window manager to bring one of this app's windows to
|
|
2
|
+
// the front, with the timestamp that makes the request legitimate.
|
|
3
|
+
//
|
|
4
|
+
// This is the half of the deep-link story that decides whether a user calls
|
|
5
|
+
// the feature working. The failure mode is not an exception — it is that the
|
|
6
|
+
// user finishes logging in, the browser redirects, the app receives the code
|
|
7
|
+
// correctly, sends `_NET_ACTIVE_WINDOW` with `CurrentTime` because nobody
|
|
8
|
+
// parsed the launch timestamp, the WM's focus-stealing prevention declines,
|
|
9
|
+
// and the taskbar entry blinks. Every layer reports success.
|
|
10
|
+
//
|
|
11
|
+
// So the timestamp is a first-class parameter here rather than an
|
|
12
|
+
// implementation detail, and its default is the value EWMH itself names:
|
|
13
|
+
//
|
|
14
|
+
// > `data.l[1]` — "Client's last user activity timestamp … at the time of the
|
|
15
|
+
// > request"
|
|
16
|
+
//
|
|
17
|
+
// which is exactly what `inputtime.js` already stashes off the event stream.
|
|
18
|
+
// A launch context has something better — the timestamp of the click in the
|
|
19
|
+
// *browser* that redirected here — and that is what a caller passes in.
|
|
20
|
+
//
|
|
21
|
+
// EWMH is explicit that the window manager may refuse, and that its fallback
|
|
22
|
+
// is `_NET_WM_STATE_DEMANDS_ATTENTION`. That is its call, so this reports what
|
|
23
|
+
// was **sent**, never what happened.
|
|
24
|
+
//
|
|
25
|
+
// See docs/uri-schemes.md. Issue #173.
|
|
26
|
+
|
|
27
|
+
import { lastInputTime } from './inputtime.js';
|
|
28
|
+
import { liveApps } from './trace-registry.js';
|
|
29
|
+
import { topLevelWindows, windowIdOf } from './windowid.js';
|
|
30
|
+
|
|
31
|
+
const ACTIVE_WINDOW = '_NET_ACTIVE_WINDOW';
|
|
32
|
+
|
|
33
|
+
/** `1` is an application asking; `2` is a pager, which we are not. */
|
|
34
|
+
const SOURCE_APPLICATION = 1;
|
|
35
|
+
|
|
36
|
+
let warnedAboutAmbiguity = false;
|
|
37
|
+
|
|
38
|
+
/** A ref object → what it holds. Anything else passes through. */
|
|
39
|
+
function deref(target) {
|
|
40
|
+
if (target && typeof target === 'object' && 'current' in target) {
|
|
41
|
+
return target.isWindow ? target : target.current;
|
|
42
|
+
}
|
|
43
|
+
return target;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* The ntk window behind a node, a ref or an ntk window. Recognised by having
|
|
48
|
+
* both an id and a connection, which is the pair this file needs anyway.
|
|
49
|
+
*/
|
|
50
|
+
function ntkWindowOf(node) {
|
|
51
|
+
if (!node || typeof node !== 'object') return null;
|
|
52
|
+
if (typeof node.id === 'number' && node.X) return node;
|
|
53
|
+
if (node.window?.X) return node.window;
|
|
54
|
+
if (node.root?.window?.X) return node.root.window;
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* The connection to send through, when the caller named a window that does not
|
|
60
|
+
* carry one — a raw XID, or nothing at all.
|
|
61
|
+
*
|
|
62
|
+
* One process usually has exactly one. Several is legal (a root per display,
|
|
63
|
+
* and a borrowed connection stays registered after its root unmounts), so the
|
|
64
|
+
* tie-break is which of them currently has a window: raising is about a window
|
|
65
|
+
* on screen, and a connection with none cannot be the one meant. Still
|
|
66
|
+
* ambiguous after that is a real `null` — an XID means nothing without the
|
|
67
|
+
* server that issued it.
|
|
68
|
+
*/
|
|
69
|
+
function soleApp() {
|
|
70
|
+
const apps = liveApps();
|
|
71
|
+
if (apps.length <= 1) return apps[0] ?? null;
|
|
72
|
+
const showing = apps.filter((app) => topLevelWindows(app).length > 0);
|
|
73
|
+
return showing.length === 1 ? showing[0] : null;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Which of this app's windows to raise when the caller did not say.
|
|
78
|
+
*
|
|
79
|
+
* The same inference `useTopLevelWindow()` makes, for the same reason: one
|
|
80
|
+
* top-level window is exact, and the focused one is right when there are
|
|
81
|
+
* several, because it is the window the user was last in.
|
|
82
|
+
*/
|
|
83
|
+
function inferWindow(app) {
|
|
84
|
+
const windows = topLevelWindows(app);
|
|
85
|
+
if (windows.length <= 1) return windows[0] ?? null;
|
|
86
|
+
const focused = windows.filter((w) => w.events?.windowFocused);
|
|
87
|
+
if (focused.length === 1) return focused[0];
|
|
88
|
+
if (process.env.NODE_ENV !== 'production' && !warnedAboutAmbiguity) {
|
|
89
|
+
warnedAboutAmbiguity = true;
|
|
90
|
+
console.warn(
|
|
91
|
+
`react-x11: activateWindow() with no window to raise, and this tree ` +
|
|
92
|
+
`has ${windows.length} top-level windows with none uniquely ` +
|
|
93
|
+
'focused — raising the most recently opened. Name the window to be ' +
|
|
94
|
+
'exact:\n' +
|
|
95
|
+
' const win = useRef(null);\n' +
|
|
96
|
+
' activateWindow(win, { timestamp });\n' +
|
|
97
|
+
' return <window ref={win}>…</window>;',
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
return windows[windows.length - 1];
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** This app's currently active toplevel, for EWMH's `data.l[2]`. */
|
|
104
|
+
function currentActive(app) {
|
|
105
|
+
const focused = topLevelWindows(app).filter((w) => w.events?.windowFocused);
|
|
106
|
+
return focused.length === 1 ? (windowIdOf(focused[0]) ?? 0) : 0;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Ask the window manager to raise and focus a window.
|
|
111
|
+
*
|
|
112
|
+
* ```jsx
|
|
113
|
+
* useAppOpen((uris, ctx) => {
|
|
114
|
+
* activateWindow(win, { timestamp: ctx.timestamp });
|
|
115
|
+
* route(uris[0]);
|
|
116
|
+
* });
|
|
117
|
+
* ```
|
|
118
|
+
*
|
|
119
|
+
* `target` is anything {@link windowIdOf} accepts — a `<window>` ref, a ref to
|
|
120
|
+
* any node inside it, a live ntk window, a raw XID — or nothing, in which case
|
|
121
|
+
* this app's only top-level window is used (its focused one, when it has
|
|
122
|
+
* several).
|
|
123
|
+
*
|
|
124
|
+
* `timestamp` is the X server time of the user action being answered.
|
|
125
|
+
* **Getting this wrong is the whole failure mode of the feature**, so:
|
|
126
|
+
*
|
|
127
|
+
* - omitted — the last input this app saw, which is EWMH's own definition of
|
|
128
|
+
* the field;
|
|
129
|
+
* - a number — the launch context's timestamp, which is what a deep link
|
|
130
|
+
* should pass: the click in the browser is the user action, not whatever
|
|
131
|
+
* this app last saw;
|
|
132
|
+
* - `null` — `CurrentTime`. Legal, and most window managers will decline it.
|
|
133
|
+
*
|
|
134
|
+
* Returns whether the request was **issued**: `false` means there was no
|
|
135
|
+
* window or no connection to send it through. `true` is not a promise that the
|
|
136
|
+
* window came forward — EWMH lets the WM refuse and set
|
|
137
|
+
* `_NET_WM_STATE_DEMANDS_ATTENTION` instead, which is the blinking taskbar
|
|
138
|
+
* entry, and no client can tell the difference from here.
|
|
139
|
+
*/
|
|
140
|
+
export function activateWindow(target, { timestamp, source } = {}) {
|
|
141
|
+
const node = deref(target);
|
|
142
|
+
const wnd = ntkWindowOf(node);
|
|
143
|
+
const app = node?.app ?? node?.root?.app ?? wnd?.app ?? soleApp();
|
|
144
|
+
|
|
145
|
+
const chosen = wnd ?? (windowIdOf(node) === null ? inferWindow(app) : null);
|
|
146
|
+
const xid = windowIdOf(chosen) ?? windowIdOf(node);
|
|
147
|
+
const X = ntkWindowOf(chosen)?.X ?? wnd?.X ?? app?.X;
|
|
148
|
+
const root = X?.display?.screen?.[0]?.root;
|
|
149
|
+
|
|
150
|
+
if (!xid || !root || !X.SendClientMessage || !X.InternAtom) return false;
|
|
151
|
+
|
|
152
|
+
const when =
|
|
153
|
+
timestamp === undefined ? (lastInputTime(app) ?? 0) : (timestamp ?? 0);
|
|
154
|
+
|
|
155
|
+
X.InternAtom(false, ACTIVE_WINDOW, (err, atom) => {
|
|
156
|
+
if (err || !atom) return;
|
|
157
|
+
// The message goes to the **root** and is *about* our window, which is why
|
|
158
|
+
// both are arguments. The default event mask is
|
|
159
|
+
// SubstructureRedirect|SubstructureNotify — what EWMH requires for
|
|
160
|
+
// messages sent to the root — so it is deliberately not passed.
|
|
161
|
+
X.SendClientMessage(root, xid, atom, 32, [
|
|
162
|
+
source ?? SOURCE_APPLICATION,
|
|
163
|
+
when >>> 0,
|
|
164
|
+
currentActive(app),
|
|
165
|
+
]);
|
|
166
|
+
});
|
|
167
|
+
return true;
|
|
168
|
+
}
|