react-x11 2.39.0 → 2.40.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/package.json +3 -3
- package/src/acceleratorhooks.js +24 -8
- package/src/accelerators.js +26 -9
- package/src/backend/context2d.js +292 -35
- package/src/backend/filter.js +285 -0
- package/src/cocoa/app.js +7 -0
- package/src/cocoa/globalmenu.js +32 -1
- package/src/cocoa/panehost.js +117 -1
- package/src/components/Menu.js +57 -11
- package/src/components/PasswordInput.js +11 -6
- package/src/editmenu.js +9 -2
- package/src/events.js +24 -0
- package/src/frame/index.js +1 -1
- package/src/globalmenu.js +21 -3
- package/src/index.d.ts +4 -1
- package/src/keysyms.d.ts +30 -0
- package/src/keysyms.js +29 -0
- package/src/menuitem.js +63 -17
- package/src/node.d.ts +23 -0
- package/src/nodes/editmenupopup.js +21 -2
- package/src/nodes/textarea.js +3 -1
- package/src/nodes/textinput.js +13 -3
- package/src/textselection.js +7 -7
- package/src/types/components.d.ts +8 -2
- package/src/types/elements.d.ts +1 -1
- package/src/types/events.d.ts +21 -3
- package/src/types/nodes.d.ts +6 -0
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
import React, { useEffect, useRef, useState } from 'react';
|
|
6
6
|
import { createStyles } from '../styles.js';
|
|
7
|
-
import { useClipboard } from '../appcontext.js';
|
|
7
|
+
import { useAppOrNull, useClipboard } from '../appcontext.js';
|
|
8
8
|
import { windowIdOf } from '../windowid.js';
|
|
9
9
|
import { useTheme } from './theme.js';
|
|
10
10
|
import { Icon } from './Icon.js';
|
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
XK_KP_ENTER,
|
|
20
20
|
XK_RETURN,
|
|
21
21
|
ctrlChordLetter,
|
|
22
|
+
primaryModifierHeld,
|
|
22
23
|
} from '../keysyms.js';
|
|
23
24
|
|
|
24
25
|
const h = React.createElement;
|
|
@@ -169,6 +170,7 @@ export function PasswordInput({
|
|
|
169
170
|
const inputRef = useRef(null);
|
|
170
171
|
const hideTimer = useRef(null);
|
|
171
172
|
const clipboard = useClipboard();
|
|
173
|
+
const app = useAppOrNull();
|
|
172
174
|
const [ownValue, setOwnValue] = useState(defaultValue ?? '');
|
|
173
175
|
const [ownRevealed, setOwnRevealed] = useState(false);
|
|
174
176
|
const [focused, setFocused] = useState(false);
|
|
@@ -282,13 +284,16 @@ export function PasswordInput({
|
|
|
282
284
|
if (ev.shiftKey) paste('CLIPBOARD');
|
|
283
285
|
return;
|
|
284
286
|
}
|
|
285
|
-
if (ev.ctrlKey) {
|
|
287
|
+
if (ev.ctrlKey || ev.metaKey) {
|
|
286
288
|
const letter = ctrlChordLetter(ev);
|
|
287
289
|
// Ctrl+U clears the line, as readline and every pinentry on the desktop
|
|
288
|
-
// do; Ctrl+V pastes
|
|
289
|
-
//
|
|
290
|
-
|
|
291
|
-
|
|
290
|
+
// do; Ctrl+V pastes — ⌘V on the Cocoa backend, where ⌃U still clears,
|
|
291
|
+
// as it does in a Mac terminal. Nothing for C, X or A — there is no
|
|
292
|
+
// selection to copy, and the secret does not leave by the clipboard.
|
|
293
|
+
// Any other chord is a command and never a character of the secret.
|
|
294
|
+
if (letter === CTRL_U && ev.ctrlKey) commit('');
|
|
295
|
+
else if (letter === CTRL_V && primaryModifierHeld(ev, app))
|
|
296
|
+
paste('CLIPBOARD');
|
|
292
297
|
return;
|
|
293
298
|
}
|
|
294
299
|
// A printable character and only that: a control code is not text, and
|
package/src/editmenu.js
CHANGED
|
@@ -86,6 +86,9 @@ export function editMenuColors(theme) {
|
|
|
86
86
|
export function editMenuItems(actions = {}, { canPaste = true } = {}) {
|
|
87
87
|
const has = (verb) => typeof actions[verb] === 'function';
|
|
88
88
|
const selected = Boolean(actions.hasSelection);
|
|
89
|
+
// `Control` is the primary modifier here, as it is in a menu bar's chord:
|
|
90
|
+
// ⌘ on the Cocoa backend, which is what the editors answer there and what
|
|
91
|
+
// `editMenuGeometry` prints when it is told so
|
|
89
92
|
const groups = [
|
|
90
93
|
[
|
|
91
94
|
has('undo') && {
|
|
@@ -147,8 +150,12 @@ export function editMenuItems(actions = {}, { canPaste = true } = {}) {
|
|
|
147
150
|
* @param {Array<{id?, label?, shortcut?, type?, enabled?}>} items
|
|
148
151
|
* @param {(text: string) => number|null} [measure] text width, or null when
|
|
149
152
|
* nothing can be measured yet
|
|
153
|
+
* @param {number} [scale]
|
|
154
|
+
* @param {{primary?: 'Control'|'Super'}} [options] the app's primary
|
|
155
|
+
* modifier (keysyms.js): `'Super'` prints the shortcuts the Mac way —
|
|
156
|
+
* `⌘Z`, `⇧⌘Z` — since ⌘ is what the text controls answer there
|
|
150
157
|
*/
|
|
151
|
-
export function editMenuGeometry(items, measure, scale = 1) {
|
|
158
|
+
export function editMenuGeometry(items, measure, scale = 1, { primary } = {}) {
|
|
152
159
|
const widthOf = (text) => {
|
|
153
160
|
if (!text) return 0;
|
|
154
161
|
const w = measure?.(text);
|
|
@@ -161,7 +168,7 @@ export function editMenuGeometry(items, measure, scale = 1) {
|
|
|
161
168
|
let widest = 0;
|
|
162
169
|
for (const item of items) {
|
|
163
170
|
const separator = isSeparator(item);
|
|
164
|
-
const shortcut = formatShortcut(item.shortcut);
|
|
171
|
+
const shortcut = formatShortcut(item.shortcut, { primary });
|
|
165
172
|
const height = (separator ? SEPARATOR_HEIGHT : ROW_HEIGHT) * scale;
|
|
166
173
|
rows.push({ ...item, separator, shortcut, y, height });
|
|
167
174
|
y += height;
|
package/src/events.js
CHANGED
|
@@ -1705,6 +1705,11 @@ export class EventManager {
|
|
|
1705
1705
|
*
|
|
1706
1706
|
* `entry.handle(ev)` answers whether it took the key.
|
|
1707
1707
|
*
|
|
1708
|
+
* `entry.reserves(ev)`, optional, answers whether something *outside* the
|
|
1709
|
+
* window is going to answer this key whatever we do — the macOS menu
|
|
1710
|
+
* bar's key equivalents (`MenuBar`, delegated). Such a key is spent
|
|
1711
|
+
* before any binding is offered it (`_runAccelerators`).
|
|
1712
|
+
*
|
|
1708
1713
|
* Registrations live on the **top-level** manager, which is the one thing
|
|
1709
1714
|
* a `<popup>`, a nested `<window>` and the window itself all share — the
|
|
1710
1715
|
* same grouping `_keyManager` resolves keys through. So a chord reaches
|
|
@@ -1740,11 +1745,30 @@ export class EventManager {
|
|
|
1740
1745
|
* A menu that is *open* suppresses them by another route entirely: it
|
|
1741
1746
|
* `preventDefault()`s the keys it is being driven with, one layer above
|
|
1742
1747
|
* this (`components/Menu.js`).
|
|
1748
|
+
*
|
|
1749
|
+
* And a key one of them *reserves* reaches none of them. The Cocoa bridge
|
|
1750
|
+
* shows us every key before `[NSApp sendEvent:]`, where the menu bar's
|
|
1751
|
+
* key equivalent then runs its item regardless — so a binding here that
|
|
1752
|
+
* answered ⌘S too would be a second command on one key, whichever of the
|
|
1753
|
+
* two mounted last. Neither gate applies to that: the menu bar is not in
|
|
1754
|
+
* the window, and a modal `<popup>` does not take keys from it.
|
|
1743
1755
|
*/
|
|
1744
1756
|
_runAccelerators(ev) {
|
|
1745
1757
|
const owner = this.topLevelManager;
|
|
1746
1758
|
const entries = owner._accelerators;
|
|
1747
1759
|
if (!entries?.size) return;
|
|
1760
|
+
for (const entry of entries) {
|
|
1761
|
+
let reserved = false;
|
|
1762
|
+
try {
|
|
1763
|
+
reserved = Boolean(entry.reserves?.(ev));
|
|
1764
|
+
} catch (error) {
|
|
1765
|
+
reportHandlerError(entry.anchor(), 'an accelerator', error);
|
|
1766
|
+
}
|
|
1767
|
+
if (reserved) {
|
|
1768
|
+
ev.preventDefault();
|
|
1769
|
+
return;
|
|
1770
|
+
}
|
|
1771
|
+
}
|
|
1748
1772
|
const scopeRoot = owner._scopeRoot();
|
|
1749
1773
|
const trapped = scopeRoot !== owner.node;
|
|
1750
1774
|
for (const entry of [...entries].reverse()) {
|
package/src/frame/index.js
CHANGED
|
@@ -624,7 +624,7 @@ function PaneHostView({
|
|
|
624
624
|
host.windowCursor?.(msg);
|
|
625
625
|
} else if (msg?.type === 'pane-present') {
|
|
626
626
|
host.setRect(node.abs);
|
|
627
|
-
host.present(msg.id);
|
|
627
|
+
host.present(msg.id, msg);
|
|
628
628
|
} else if (msg?.type === 'pane-cursor') {
|
|
629
629
|
// The pane names its cursor as an element that draws names one for
|
|
630
630
|
// a point: this box is the node the host's pointer is over, so the
|
package/src/globalmenu.js
CHANGED
|
@@ -54,7 +54,7 @@
|
|
|
54
54
|
// third-party menus consume dbusmenu. A second exporter is roughly double the
|
|
55
55
|
// work for no consumer that the first one does not already reach.
|
|
56
56
|
|
|
57
|
-
import { useEffect, useRef, useState } from 'react';
|
|
57
|
+
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
58
58
|
|
|
59
59
|
import { loadTransport, sessionBus } from './bus.js';
|
|
60
60
|
import { desktopIntegrationEnabled } from './desktopintegration.js';
|
|
@@ -539,7 +539,20 @@ export function _resetGlobalMenuState() {
|
|
|
539
539
|
* it. It is exported for a component that draws its own bar and wants the
|
|
540
540
|
* same behaviour.
|
|
541
541
|
*/
|
|
542
|
-
export function useGlobalMenu(
|
|
542
|
+
export function useGlobalMenu(menus, options) {
|
|
543
|
+
return useGlobalMenuExport(menus, options).delegated;
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
/**
|
|
547
|
+
* `useGlobalMenu`, with the one more thing `MenuBar` needs to know about the
|
|
548
|
+
* delegate: which shortcuts it answers by itself. A panel answers none — it
|
|
549
|
+
* draws the rows, and the key is pressed in our window — but the macOS menu
|
|
550
|
+
* bar answers the ⌘ chords it holds as key equivalents, and a bar that
|
|
551
|
+
* matched them as well would run the item twice
|
|
552
|
+
* (`CocoaGlobalMenuExport.answersShortcut`). Read at key time, so it is
|
|
553
|
+
* whatever delegate is live then. Not public.
|
|
554
|
+
*/
|
|
555
|
+
export function useGlobalMenuExport(
|
|
543
556
|
menus,
|
|
544
557
|
{ onSelect, onAboutToShow, enabled = true } = {},
|
|
545
558
|
) {
|
|
@@ -586,5 +599,10 @@ export function useGlobalMenu(
|
|
|
586
599
|
exportRef.current?.update(menus ?? []);
|
|
587
600
|
}, [menus]);
|
|
588
601
|
|
|
589
|
-
|
|
602
|
+
const answersShortcut = useCallback(
|
|
603
|
+
(shortcut) => Boolean(exportRef.current?.answersShortcut?.(shortcut)),
|
|
604
|
+
[],
|
|
605
|
+
);
|
|
606
|
+
|
|
607
|
+
return { delegated: enabled && exported, answersShortcut };
|
|
590
608
|
}
|
package/src/index.d.ts
CHANGED
|
@@ -290,7 +290,10 @@ export interface RootOptions {
|
|
|
290
290
|
* `resizeWait` is how long, in ms, AppKit may hold a live-resize tick for
|
|
291
291
|
* the app's frame at the new size under `react-x11/cocoa-main`, where
|
|
292
292
|
* the frame is painted on another thread (50 by default; 0 lets the edge
|
|
293
|
-
* move without waiting).
|
|
293
|
+
* move without waiting). A `<Frame>` pane's frames of a new size are held
|
|
294
|
+
* to it as well: within it, the pane's last frame stays at its size while
|
|
295
|
+
* the next comes, and past it, it is stretched to the pane's new size, as
|
|
296
|
+
* every one is at 0.
|
|
294
297
|
* `screenPoll` is how often the screen layout is re-read while a
|
|
295
298
|
* {@link useScreens} subscriber is mounted, in ms — 500 by default, 0 for
|
|
296
299
|
* never. macOS has no event for a display plugged in, unplugged or
|
package/src/keysyms.d.ts
CHANGED
|
@@ -50,6 +50,36 @@ export function ctrlChordLetter(ev: {
|
|
|
50
50
|
codepoint?: number | null;
|
|
51
51
|
}): number | null;
|
|
52
52
|
|
|
53
|
+
/** The modifier token a shortcut's primary means, in a menu `shortcut`'s
|
|
54
|
+
* vocabulary. */
|
|
55
|
+
export type PrimaryModifier = 'Control' | 'Super';
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* The modifier this app's shortcuts are pressed with: `'Super'` — ⌘ — where
|
|
59
|
+
* the backend says so (the Cocoa app does, as `app.primaryModifier`), and
|
|
60
|
+
* `'Control'` everywhere else. A backend fact, not a platform one: an X11
|
|
61
|
+
* app under XQuartz keeps Ctrl, like every other X client on that display.
|
|
62
|
+
*/
|
|
63
|
+
export function primaryModifier(
|
|
64
|
+
app?: { primaryModifier?: PrimaryModifier } | null,
|
|
65
|
+
): PrimaryModifier;
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Is the primary modifier down for this event — ⌘ on the Cocoa backend, Ctrl
|
|
69
|
+
* everywhere else? What the built-in editors test before their
|
|
70
|
+
* Undo/Cut/Copy/Paste/Select All chords, so the keys the edit menu prints
|
|
71
|
+
* are the keys that do it.
|
|
72
|
+
*
|
|
73
|
+
* ```js
|
|
74
|
+
* if (primaryModifierHeld(ev, node.app) && ctrlChordLetter(ev) === keysymOf('d'))
|
|
75
|
+
* duplicateLine();
|
|
76
|
+
* ```
|
|
77
|
+
*/
|
|
78
|
+
export function primaryModifierHeld(
|
|
79
|
+
ev: { ctrlKey?: boolean; metaKey?: boolean } | null | undefined,
|
|
80
|
+
app?: { primaryModifier?: PrimaryModifier } | null,
|
|
81
|
+
): boolean;
|
|
82
|
+
|
|
53
83
|
/**
|
|
54
84
|
* The keysym an X11 key *name* stands for — `'Return'`, `'plus'`, `'F5'`,
|
|
55
85
|
* `'s'` — or `undefined` for a name nothing here knows.
|
package/src/keysyms.js
CHANGED
|
@@ -247,6 +247,35 @@ export function ctrlChordLetter(ev) {
|
|
|
247
247
|
return code >= 0x41 && code <= 0x5a ? code + 0x20 : code;
|
|
248
248
|
}
|
|
249
249
|
|
|
250
|
+
/**
|
|
251
|
+
* The modifier this app's shortcuts are pressed with: `'Super'` — ⌘ — where
|
|
252
|
+
* the backend says so (the Cocoa app does, as `app.primaryModifier`), and
|
|
253
|
+
* `'Control'` everywhere else.
|
|
254
|
+
*
|
|
255
|
+
* A backend fact, not a platform one: an X11 app under XQuartz keeps Ctrl,
|
|
256
|
+
* which is what every other X client on that display answers to.
|
|
257
|
+
*/
|
|
258
|
+
export function primaryModifier(app) {
|
|
259
|
+
return app?.primaryModifier === 'Super' ? 'Super' : 'Control';
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Is the primary modifier down for this event — ⌘ on the Cocoa backend,
|
|
264
|
+
* Ctrl everywhere else? The test the built-in editors put in front of their
|
|
265
|
+
* Undo/Cut/Copy/Paste/Select All chords, so the keys the edit menu prints
|
|
266
|
+
* beside each row are the keys that do it.
|
|
267
|
+
*
|
|
268
|
+
* ```js
|
|
269
|
+
* if (primaryModifierHeld(ev, node.app) && ctrlChordLetter(ev) === keysymOf('d'))
|
|
270
|
+
* duplicateLine();
|
|
271
|
+
* ```
|
|
272
|
+
*/
|
|
273
|
+
export function primaryModifierHeld(ev, app) {
|
|
274
|
+
return primaryModifier(app) === 'Super'
|
|
275
|
+
? Boolean(ev?.metaKey)
|
|
276
|
+
: Boolean(ev?.ctrlKey);
|
|
277
|
+
}
|
|
278
|
+
|
|
250
279
|
/**
|
|
251
280
|
* The X11 modifier mask bits, as they arrive on `ev.nativeEvent.buttons`
|
|
252
281
|
* and as `fireEvent` takes them. Bit 3 (Mod1) is Alt and bit 6 (Mod4) is
|
package/src/menuitem.js
CHANGED
|
@@ -112,21 +112,62 @@ const KEY_LABELS = {
|
|
|
112
112
|
Next: 'PgDn',
|
|
113
113
|
};
|
|
114
114
|
|
|
115
|
+
// The Mac prints a chord as glyphs, in one fixed order whatever order the
|
|
116
|
+
// chord was written in — ⌃⌥⇧⌘ — and with nothing between them: `⇧⌘Z`, never
|
|
117
|
+
// `Cmd+Shift+Z`. `Control` and `Super` are both ⌘ there, for the reason
|
|
118
|
+
// `primaryModifier` gives below: a cross-platform chord is written in the
|
|
119
|
+
// primary modifier, and the Mac's is Command.
|
|
120
|
+
const MAC_MODIFIER_ORDER = ['Alt', 'Shift', 'Command'];
|
|
121
|
+
const MAC_MODIFIER_GLYPHS = { Alt: '⌥', Shift: '⇧', Command: '⌘' };
|
|
122
|
+
|
|
123
|
+
// The keys a Mac menu draws as a glyph rather than a word. The rest are
|
|
124
|
+
// `KEY_LABELS`' answer, which is already the Mac's for punctuation.
|
|
125
|
+
const MAC_KEY_LABELS = {
|
|
126
|
+
Return: '↩',
|
|
127
|
+
KP_Enter: '⌤',
|
|
128
|
+
Tab: '⇥',
|
|
129
|
+
BackSpace: '⌫',
|
|
130
|
+
Delete: '⌦',
|
|
131
|
+
Escape: '⎋',
|
|
132
|
+
Left: '←',
|
|
133
|
+
Right: '→',
|
|
134
|
+
Up: '↑',
|
|
135
|
+
Down: '↓',
|
|
136
|
+
Prior: '⇞',
|
|
137
|
+
Next: '⇟',
|
|
138
|
+
Home: '↖',
|
|
139
|
+
End: '↘',
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
const keyLabel = (key, labels) =>
|
|
143
|
+
labels[key] ??
|
|
144
|
+
KEY_LABELS[key] ??
|
|
145
|
+
(key.length === 1 ? key.toUpperCase() : key);
|
|
146
|
+
|
|
115
147
|
/**
|
|
116
148
|
* One `aas` alternative — `['Control', 'S']` — as the string a menu row
|
|
117
|
-
* shows: `Ctrl+S
|
|
149
|
+
* shows: `Ctrl+S`, or `⌘S` where the primary modifier is ⌘.
|
|
118
150
|
*
|
|
119
151
|
* A single-character key is upper-cased because that is how every toolkit
|
|
120
152
|
* prints one, and because dbusmenu carries the *key*, not the character it
|
|
121
153
|
* types: `['Control', 's']` and `['Control', 'S']` are the same binding and
|
|
122
154
|
* must not render differently.
|
|
123
155
|
*/
|
|
124
|
-
function formatChord(chord) {
|
|
156
|
+
function formatChord(chord, primary) {
|
|
125
157
|
const { modifiers, key } = splitChord(chord);
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
158
|
+
if (primary === 'Super') {
|
|
159
|
+
const held = new Set(
|
|
160
|
+
[...modifiers].map((token) =>
|
|
161
|
+
token === 'Control' || token === 'Super' ? 'Command' : token,
|
|
162
|
+
),
|
|
163
|
+
);
|
|
164
|
+
const glyphs = MAC_MODIFIER_ORDER.filter((token) => held.has(token))
|
|
165
|
+
.map((token) => MAC_MODIFIER_GLYPHS[token])
|
|
166
|
+
.join('');
|
|
167
|
+
return glyphs + (key == null ? '' : keyLabel(key, MAC_KEY_LABELS));
|
|
129
168
|
}
|
|
169
|
+
const parts = [...modifiers].map((token) => MODIFIER_LABELS[token]);
|
|
170
|
+
if (key != null) parts.push(keyLabel(key, {}));
|
|
130
171
|
return parts.join('+');
|
|
131
172
|
}
|
|
132
173
|
|
|
@@ -137,11 +178,14 @@ function formatChord(chord) {
|
|
|
137
178
|
* modifier tokens ending in the key. Only the first alternative is drawn —
|
|
138
179
|
* a row has one shortcut column, and a menu that printed `Ctrl+S / F2` in it
|
|
139
180
|
* would be wider than the menu for the benefit of the rarer binding.
|
|
181
|
+
*
|
|
182
|
+
* `primary` is the app's `primaryModifier` (keysyms.js): `'Super'` draws the
|
|
183
|
+
* chord the way a Mac menu does, `⇧⌘Z` for `['Control', 'Shift', 'Z']`.
|
|
140
184
|
*/
|
|
141
|
-
export function formatShortcut(shortcut) {
|
|
185
|
+
export function formatShortcut(shortcut, { primary = 'Control' } = {}) {
|
|
142
186
|
const first = shortcut?.[0];
|
|
143
187
|
if (!Array.isArray(first) || first.length === 0) return '';
|
|
144
|
-
return formatChord(first);
|
|
188
|
+
return formatChord(first, primary);
|
|
145
189
|
}
|
|
146
190
|
|
|
147
191
|
// What UI Events calls the modifiers, which is what `aria-keyshortcuts` is
|
|
@@ -160,19 +204,21 @@ const ARIA_MODIFIERS = {
|
|
|
160
204
|
* dbusmenu's `aas` already has — so unlike the drawn menu, **every**
|
|
161
205
|
* alternative is announced rather than only the first: a screen reader is
|
|
162
206
|
* reading them out, not fitting them in a column.
|
|
207
|
+
*
|
|
208
|
+
* `primary` is `formatShortcut`'s: under `'Super'` a `Control` is ⌘, which
|
|
209
|
+
* UI Events calls `Meta`, so what is announced is what is drawn and bound.
|
|
163
210
|
*/
|
|
164
|
-
export function ariaKeyShortcuts(shortcut) {
|
|
211
|
+
export function ariaKeyShortcuts(shortcut, { primary = 'Control' } = {}) {
|
|
165
212
|
if (!isValidShortcut(shortcut)) return undefined;
|
|
213
|
+
const name = (token) => {
|
|
214
|
+
if (!MODIFIER_TOKENS.has(token)) return KEY_LABELS[token] ?? token;
|
|
215
|
+
return primary === 'Super' && token === 'Control'
|
|
216
|
+
? 'Meta'
|
|
217
|
+
: ARIA_MODIFIERS[token];
|
|
218
|
+
};
|
|
219
|
+
// a chord naming both `Control` and `Super` is one ⌘ there, said once
|
|
166
220
|
return shortcut
|
|
167
|
-
.map((chord) =>
|
|
168
|
-
chord
|
|
169
|
-
.map((token) =>
|
|
170
|
-
MODIFIER_TOKENS.has(token)
|
|
171
|
-
? ARIA_MODIFIERS[token]
|
|
172
|
-
: (KEY_LABELS[token] ?? token),
|
|
173
|
-
)
|
|
174
|
-
.join('+'),
|
|
175
|
-
)
|
|
221
|
+
.map((chord) => [...new Set(chord.map(name))].join('+'))
|
|
176
222
|
.join(' ');
|
|
177
223
|
}
|
|
178
224
|
|
package/src/node.d.ts
CHANGED
|
@@ -73,6 +73,29 @@ export interface Context2D {
|
|
|
73
73
|
* `'globalCompositeOperation' in ctx`.
|
|
74
74
|
*/
|
|
75
75
|
globalCompositeOperation?: string;
|
|
76
|
+
/**
|
|
77
|
+
* Canvas's: whether an image drawn scaled or turned is smoothed, or
|
|
78
|
+
* drawn as its nearest pixels. Native contexts only (macOS, Windows), and
|
|
79
|
+
* only where the bridge can set it: assign and read back to know.
|
|
80
|
+
*/
|
|
81
|
+
imageSmoothingEnabled?: boolean;
|
|
82
|
+
/**
|
|
83
|
+
* Canvas's: how it is smoothed, `'low'` being bilinear. A native context
|
|
84
|
+
* starts at `'medium'`, which on macOS resamples the whole source for a
|
|
85
|
+
* draw through a matrix whatever the clip — so a surface drawn a tile at
|
|
86
|
+
* a time is far cheaper at `'low'`.
|
|
87
|
+
*/
|
|
88
|
+
imageSmoothingQuality?: 'low' | 'medium' | 'high';
|
|
89
|
+
/**
|
|
90
|
+
* Canvas's: a CSS filter list applied to what is drawn after it. Native
|
|
91
|
+
* contexts only (macOS, Windows), and only the colour functions —
|
|
92
|
+
* `grayscale()`, `sepia()`, `saturate()`, `hue-rotate()`, `invert()`,
|
|
93
|
+
* `brightness()`, `contrast()` and `opacity()` — over a bridge that reads
|
|
94
|
+
* a surface's pixels back. Anything else does not stick: assign and read
|
|
95
|
+
* back to know. Absent on X11, whose server runs no colour matrix and
|
|
96
|
+
* answers a read a round trip later.
|
|
97
|
+
*/
|
|
98
|
+
filter?: string;
|
|
76
99
|
save(): void;
|
|
77
100
|
restore(): void;
|
|
78
101
|
translate(x: number, y: number): void;
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
import { deviceAnchorArea, windowOrigin } from '../anchor.js';
|
|
6
6
|
import { armPasteState, canPaste } from '../pastestate.js';
|
|
7
7
|
import {
|
|
8
|
+
primaryModifier,
|
|
8
9
|
XK_RETURN,
|
|
9
10
|
XK_KP_ENTER,
|
|
10
11
|
XK_UP,
|
|
@@ -125,6 +126,7 @@ export function openEditMenu(node, at, actions = {}) {
|
|
|
125
126
|
items,
|
|
126
127
|
(text) => app?.fonts?.layout(text, style)?.width,
|
|
127
128
|
s,
|
|
129
|
+
{ primary: primaryModifier(app) },
|
|
128
130
|
);
|
|
129
131
|
const deviceAt = at && {
|
|
130
132
|
...at,
|
|
@@ -221,6 +223,17 @@ export function openEditMenu(node, at, actions = {}) {
|
|
|
221
223
|
node._editMenu = popup;
|
|
222
224
|
// read *before* the menu takes the keyboard, and handed back on close
|
|
223
225
|
node._editMenuRestore = node._focusManager()?.focused ?? null;
|
|
226
|
+
// The window losing focus closes it — the user went to another
|
|
227
|
+
// application, ⌘-Tab or a click on its window. Neither reaches us as a
|
|
228
|
+
// press: the grab redirects presses only between windows of ours on Cocoa,
|
|
229
|
+
// and a popup panel never takes the keyboard there, so this is the one
|
|
230
|
+
// event that says so. The node keeps its focus across the blur, so nothing
|
|
231
|
+
// in the tree hears it either; the same answer `Menu` and `Select` give
|
|
232
|
+
// (`useDismissOnWindowBlur`, components/anchor.js).
|
|
233
|
+
node._editMenuOffBlur =
|
|
234
|
+
node.root?.onWindowFocusChange?.((focused) => {
|
|
235
|
+
if (!focused) closeEditMenu(node);
|
|
236
|
+
}) ?? null;
|
|
224
237
|
// the menu takes the keyboard so arrows and Escape reach it rather than
|
|
225
238
|
// the element behind it
|
|
226
239
|
popup.events?.focus?.(canvas);
|
|
@@ -241,6 +254,8 @@ export function closeEditMenu(node) {
|
|
|
241
254
|
const popup = node?._editMenu;
|
|
242
255
|
if (!popup) return;
|
|
243
256
|
node._editMenu = null;
|
|
257
|
+
node._editMenuOffBlur?.();
|
|
258
|
+
node._editMenuOffBlur = null;
|
|
244
259
|
const restore = node._editMenuRestore;
|
|
245
260
|
node._editMenuRestore = null;
|
|
246
261
|
// the popup is a child, so a node destroyed while its menu was up took the
|
|
@@ -255,6 +270,10 @@ export function closeEditMenu(node) {
|
|
|
255
270
|
// menu and a ring appearing on the way back would be news to nobody. A
|
|
256
271
|
// surface that was not focusable in the first place, or that stopped being
|
|
257
272
|
// on screen while the menu was up, gets nothing back rather than the
|
|
258
|
-
// destroyed menu canvas keeping the keyboard.
|
|
259
|
-
|
|
273
|
+
// destroyed menu canvas keeping the keyboard. And a restore, which takes
|
|
274
|
+
// the window no keyboard: a menu closed because the user went to another
|
|
275
|
+
// application must not drag them back to this one.
|
|
276
|
+
events.focus(events._canRestoreTo(restore) ? restore : null, 'pointer', {
|
|
277
|
+
restoring: true,
|
|
278
|
+
});
|
|
260
279
|
}
|
package/src/nodes/textarea.js
CHANGED
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
XK_PAGE_UP,
|
|
11
11
|
XK_PAGE_DOWN,
|
|
12
12
|
XK_END,
|
|
13
|
+
primaryModifierHeld,
|
|
13
14
|
} from '../keysyms.js';
|
|
14
15
|
import {
|
|
15
16
|
scrollbarGeometry,
|
|
@@ -235,7 +236,8 @@ export class TextAreaNode extends TextInputNode {
|
|
|
235
236
|
const layout = this._valueLayout();
|
|
236
237
|
|
|
237
238
|
if (k === XK_RETURN || k === XK_KP_ENTER) {
|
|
238
|
-
|
|
239
|
+
// Ctrl+Enter, ⌘↩ on the Cocoa backend
|
|
240
|
+
if (primaryModifierHeld(ev, this.app)) {
|
|
239
241
|
this._fireValueEvent('onSubmit', this.value, ev.nativeEvent);
|
|
240
242
|
return true;
|
|
241
243
|
}
|
package/src/nodes/textinput.js
CHANGED
|
@@ -14,6 +14,7 @@ import { lastInputTime } from '../inputtime.js';
|
|
|
14
14
|
import {
|
|
15
15
|
ctrlChordLetter,
|
|
16
16
|
MOD,
|
|
17
|
+
primaryModifierHeld,
|
|
17
18
|
XK_BACKSPACE,
|
|
18
19
|
XK_RETURN,
|
|
19
20
|
XK_KP_ENTER,
|
|
@@ -550,7 +551,9 @@ export class TextInputNode extends Node {
|
|
|
550
551
|
this._moveCaret(this._chars().length, ev.shiftKey);
|
|
551
552
|
return true;
|
|
552
553
|
}
|
|
553
|
-
|
|
554
|
+
// Ctrl, or ⌘ on the Cocoa backend (`primaryModifierHeld`): the keys the
|
|
555
|
+
// edit menu prints beside its rows are the keys that do what they say
|
|
556
|
+
if (primaryModifierHeld(ev, this.app)) {
|
|
554
557
|
const letter = ctrlChordLetter(ev);
|
|
555
558
|
if (letter === 0x61 /* a */) {
|
|
556
559
|
this._selectAll();
|
|
@@ -562,10 +565,12 @@ export class TextInputNode extends Node {
|
|
|
562
565
|
} else if (letter === 0x76 /* v */) {
|
|
563
566
|
this._pasteFrom();
|
|
564
567
|
} else if (letter === 0x7a /* z */) {
|
|
565
|
-
// Ctrl+Shift+Z redoes, the way it does in GTK and Qt
|
|
568
|
+
// Ctrl+Shift+Z redoes, the way it does in GTK and Qt — and ⇧⌘Z is
|
|
569
|
+
// the Mac's own redo
|
|
566
570
|
if (ev.shiftKey) this.redo();
|
|
567
571
|
else this.undo();
|
|
568
|
-
} else if (letter === 0x79 /* y */) {
|
|
572
|
+
} else if (letter === 0x79 /* y */ && !ev.metaKey) {
|
|
573
|
+
// Ctrl+Y is Windows' redo; ⌘Y is nobody's, so it is not taken
|
|
569
574
|
this.redo();
|
|
570
575
|
} else {
|
|
571
576
|
// A chord this field has no answer for is not the field's: Ctrl+S
|
|
@@ -576,6 +581,11 @@ export class TextInputNode extends Node {
|
|
|
576
581
|
}
|
|
577
582
|
return true;
|
|
578
583
|
}
|
|
584
|
+
// …and a chord on the other one is not text either. AppKit hands over
|
|
585
|
+
// the key's bare character with ⌘ or ⌃ held, so ⌘S in a focused field
|
|
586
|
+
// typed an `s` before this, and ⌃A typed an `a` once ⌘ became the
|
|
587
|
+
// primary — Ctrl and Super chords are commands on every backend.
|
|
588
|
+
if (ev.ctrlKey || ev.metaKey) return false;
|
|
579
589
|
if (ev.codepoint != null && ev.codepoint >= 0x20 && ev.codepoint !== 0x7f) {
|
|
580
590
|
const ch = String.fromCodePoint(ev.codepoint);
|
|
581
591
|
this._insert(ch, 'type');
|
package/src/textselection.js
CHANGED
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
|
|
31
31
|
import { callHandler } from './errors.js';
|
|
32
32
|
import { lastInputTime } from './inputtime.js';
|
|
33
|
-
import { ctrlChordLetter } from './keysyms.js';
|
|
33
|
+
import { ctrlChordLetter, primaryModifierHeld } from './keysyms.js';
|
|
34
34
|
import { codePoints, wordRangeAt } from './textrange.js';
|
|
35
35
|
// --- who is showing a selection ------------------------------------------
|
|
36
36
|
//
|
|
@@ -201,14 +201,14 @@ export class TextSelection {
|
|
|
201
201
|
}
|
|
202
202
|
|
|
203
203
|
/**
|
|
204
|
-
* Ctrl+A and Ctrl+C
|
|
205
|
-
*
|
|
206
|
-
*
|
|
207
|
-
*
|
|
208
|
-
* default.
|
|
204
|
+
* Ctrl+A and Ctrl+C — ⌘A and ⌘C on the Cocoa backend — run as this
|
|
205
|
+
* element's default action, which is why a surface is a focus target
|
|
206
|
+
* (a11y.js): the copy has to have somewhere to arrive. Nothing else is
|
|
207
|
+
* bound. A read-only document has no caret, so shift+arrows would be caret
|
|
208
|
+
* browsing, which is a mode rather than a default.
|
|
209
209
|
*/
|
|
210
210
|
keyDown(ev) {
|
|
211
|
-
if (!ev.
|
|
211
|
+
if (!primaryModifierHeld(ev, this.node.app)) return;
|
|
212
212
|
const letter = ctrlChordLetter(ev);
|
|
213
213
|
if (letter === 0x61 /* a */) {
|
|
214
214
|
this.selectAll();
|
|
@@ -579,6 +579,10 @@ export interface MenuItem {
|
|
|
579
579
|
* the menu is mounted, without the menu being opened (#351). `enabled`
|
|
580
580
|
* and `visible` gate the binding with the row. `accelerators={false}` on
|
|
581
581
|
* the menu turns that off for an app with a dispatcher of its own.
|
|
582
|
+
*
|
|
583
|
+
* `Control` is the primary modifier: on the Cocoa backend `[['Control',
|
|
584
|
+
* 'S']]` is drawn `⌘S`, bound to ⌘S, and installed in the macOS menu bar
|
|
585
|
+
* as ⌘S — which then answers it by itself (docs/events.md).
|
|
582
586
|
*/
|
|
583
587
|
shortcut?: MenuShortcut;
|
|
584
588
|
/** With `toggleState`, draws a check mark or a radio dot in the gutter. */
|
|
@@ -654,8 +658,10 @@ export interface MenuBarProps extends WidgetProps {
|
|
|
654
658
|
/**
|
|
655
659
|
* Honour the items' own `shortcut`s while this bar is mounted — including
|
|
656
660
|
* when the desktop's panel has taken the menu over, since the panel draws
|
|
657
|
-
* the rows but the key is pressed in this window.
|
|
658
|
-
*
|
|
661
|
+
* the rows but the key is pressed in this window. The macOS menu bar is the
|
|
662
|
+
* exception: it answers the ⌘ chords it holds as key equivalents itself,
|
|
663
|
+
* and the window leaves those alone. On by default; `false` leaves the
|
|
664
|
+
* chords to an app that dispatches its own.
|
|
659
665
|
*/
|
|
660
666
|
accelerators?: boolean;
|
|
661
667
|
fontSize?: number;
|
package/src/types/elements.d.ts
CHANGED
|
@@ -739,7 +739,7 @@ export interface TextInputProps extends DrawnProps<TextInputNode> {
|
|
|
739
739
|
* both read naturally.
|
|
740
740
|
*/
|
|
741
741
|
onChange?: (ev: ChangeEvent<TextInputNode>) => void;
|
|
742
|
-
/** Enter — or Ctrl+Enter in a `<textarea
|
|
742
|
+
/** Enter — or Ctrl+Enter in a `<textarea>` (⌘↩ on the Cocoa backend). */
|
|
743
743
|
onSubmit?: (ev: SubmitEvent<TextInputNode>) => void;
|
|
744
744
|
placeholder?: string;
|
|
745
745
|
placeholderColor?: Color;
|
package/src/types/events.d.ts
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
import type { RefObject } from 'react';
|
|
8
8
|
import type { DrawnNode, NtkWindow, TextInputNode } from './nodes.js';
|
|
9
9
|
import type { MenuShortcut } from './components.js';
|
|
10
|
+
import type { PrimaryModifier } from '../keysyms.js';
|
|
10
11
|
|
|
11
12
|
/** The raw ntk/X11 event a synthetic one was made from. */
|
|
12
13
|
export interface NativeEvent {
|
|
@@ -331,7 +332,7 @@ export interface ChangeEvent<T = TextInputNode> extends Omit<
|
|
|
331
332
|
}
|
|
332
333
|
|
|
333
334
|
/**
|
|
334
|
-
* `<textinput onSubmit>` — Enter, or Ctrl+Enter in a `<textarea
|
|
335
|
+
* `<textinput onSubmit>` — Enter, or Ctrl+Enter in a `<textarea>` (⌘↩ on Cocoa). Same
|
|
335
336
|
* shape as {@link ChangeEvent}; `nativeEvent` is the X key event.
|
|
336
337
|
*/
|
|
337
338
|
export interface SubmitEvent<T = TextInputNode> extends Omit<
|
|
@@ -556,7 +557,8 @@ export interface AcceleratorOptions {
|
|
|
556
557
|
* Exact on Control/Alt/Shift/Super, indifferent to Caps Lock and Num Lock,
|
|
557
558
|
* matched against the Latin keysym so a layout switch does not turn it off,
|
|
558
559
|
* and behind whatever a focused element consumed with `preventDefault()`.
|
|
559
|
-
*
|
|
560
|
+
* `Control` is the app's primary modifier: ⌘ on the Cocoa backend, the key
|
|
561
|
+
* its menus print. See docs/events.md.
|
|
560
562
|
*
|
|
561
563
|
* With no `scope` the binding belongs to the tree's top-level `<window>`, or,
|
|
562
564
|
* in an app that has none, to the root-level `<popup>` holding the keyboard —
|
|
@@ -569,10 +571,25 @@ export function useAccelerator(
|
|
|
569
571
|
options?: AcceleratorOptions,
|
|
570
572
|
): void;
|
|
571
573
|
|
|
574
|
+
export interface MatchesShortcutOptions {
|
|
575
|
+
/**
|
|
576
|
+
* The app's primary modifier — `primaryModifier(app)` from
|
|
577
|
+
* `react-x11/keysyms`. `'Super'` reads a chord's `Control` (and its
|
|
578
|
+
* `Super`) as ⌘ and wants ⌃ up, which is the Cocoa backend's rule;
|
|
579
|
+
* `'Control'`, the default, reads the chord literally.
|
|
580
|
+
*/
|
|
581
|
+
primary?: PrimaryModifier;
|
|
582
|
+
}
|
|
583
|
+
|
|
572
584
|
/**
|
|
573
585
|
* Whether a key event presses any alternative of a `shortcut` — the matcher
|
|
574
586
|
* the menus and {@link useAccelerator} both run on, for an application
|
|
575
|
-
* dispatching chords its own way.
|
|
587
|
+
* dispatching chords its own way. Pass the app's primary modifier to match
|
|
588
|
+
* the way they do:
|
|
589
|
+
*
|
|
590
|
+
* ```ts
|
|
591
|
+
* matchesShortcut(ev, [['Control', 'S']], { primary: primaryModifier(app) });
|
|
592
|
+
* ```
|
|
576
593
|
*/
|
|
577
594
|
export function matchesShortcut(
|
|
578
595
|
ev: {
|
|
@@ -584,4 +601,5 @@ export function matchesShortcut(
|
|
|
584
601
|
metaKey?: boolean;
|
|
585
602
|
},
|
|
586
603
|
shortcut: MenuShortcut | undefined,
|
|
604
|
+
options?: MatchesShortcutOptions,
|
|
587
605
|
): boolean;
|