react-x11 2.38.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/cocoa/sprites.js +95 -18
- 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 +44 -3
- package/src/nodes/editmenupopup.js +21 -2
- package/src/nodes/sprites.js +6 -3
- 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
package/src/cocoa/sprites.js
CHANGED
|
@@ -27,7 +27,10 @@
|
|
|
27
27
|
// `@windowkit/appkit` 0.19 (`transformForms()`) along with the negative
|
|
28
28
|
// delay a part joined half way through needs, so that is the bridge sprites
|
|
29
29
|
// ask for: on an older one nothing is asked and every part is drawn by its
|
|
30
|
-
// element.
|
|
30
|
+
// element. Or it is CSS's `matrix3d()`, a part turned out of the plane and
|
|
31
|
+
// seen in a perspective, which Core Animation draws through the whole
|
|
32
|
+
// matrix on the GPU: a part none of whose corners is behind the viewer, on a
|
|
33
|
+
// bridge that takes one (`matrix3d`).
|
|
31
34
|
//
|
|
32
35
|
// A part none of which shows — scrolled out of its pane, or outside its own
|
|
33
36
|
// clip — keeps its layer, cut to show nothing, up to a budget of raster for
|
|
@@ -68,6 +71,14 @@ const AFTER_CHILDREN = Number.MAX_SAFE_INTEGER;
|
|
|
68
71
|
|
|
69
72
|
const IDENTITY = Object.freeze([1, 0, 0, 1, 0, 0]);
|
|
70
73
|
|
|
74
|
+
// what a part with no bounds may reach (`_underDrawn`)
|
|
75
|
+
const EVERYWHERE = Object.freeze({
|
|
76
|
+
x: -1e9,
|
|
77
|
+
y: -1e9,
|
|
78
|
+
width: 2e9,
|
|
79
|
+
height: 2e9,
|
|
80
|
+
});
|
|
81
|
+
|
|
71
82
|
const ORIGIN = Object.freeze({ x: 0, y: 0 });
|
|
72
83
|
|
|
73
84
|
// what a part with no part lifted inside it is handed
|
|
@@ -98,7 +109,20 @@ const isRect = (r) =>
|
|
|
98
109
|
r.width > 0 &&
|
|
99
110
|
r.height > 0;
|
|
100
111
|
|
|
101
|
-
|
|
112
|
+
// CSS's `matrix(a, b, c, d, e, f)`, or its `matrix3d()`'s sixteen numbers
|
|
113
|
+
const isMatrix = (m) =>
|
|
114
|
+
Array.isArray(m) && (m.length === 6 || m.length === 16) && m.every(finite);
|
|
115
|
+
|
|
116
|
+
const isSolid = (m) => m.length === 16;
|
|
117
|
+
|
|
118
|
+
/** Does `sprite` turn out of the plane, at rest or in an animation? */
|
|
119
|
+
function solidSprite(sprite) {
|
|
120
|
+
if (sprite.transform && isSolid(sprite.transform)) return true;
|
|
121
|
+
for (const a of sprite.animations ?? []) {
|
|
122
|
+
if (a.property === 'transform' && a.values.some(isSolid)) return true;
|
|
123
|
+
}
|
|
124
|
+
return false;
|
|
125
|
+
}
|
|
102
126
|
|
|
103
127
|
const isCurve = (t) =>
|
|
104
128
|
typeof t === 'string' ||
|
|
@@ -203,12 +227,15 @@ const originOf = (sprite) =>
|
|
|
203
227
|
y: sprite.rect.y + sprite.rect.height / 2,
|
|
204
228
|
};
|
|
205
229
|
|
|
206
|
-
/** `rect` through `m` about `origin`: the bounds of its four corners
|
|
230
|
+
/** `rect` through `m` about `origin`: the bounds of its four corners, or
|
|
231
|
+
* null where `m` is a matrix3d that puts a corner behind the viewer, past
|
|
232
|
+
* which the plane's picture has no bounds. */
|
|
207
233
|
function mapRect(rect, m, origin) {
|
|
208
234
|
let x0 = Infinity;
|
|
209
235
|
let y0 = Infinity;
|
|
210
236
|
let x1 = -Infinity;
|
|
211
237
|
let y1 = -Infinity;
|
|
238
|
+
const solid = isSolid(m);
|
|
212
239
|
for (const [px, py] of [
|
|
213
240
|
[rect.x, rect.y],
|
|
214
241
|
[rect.x + rect.width, rect.y],
|
|
@@ -217,8 +244,18 @@ function mapRect(rect, m, origin) {
|
|
|
217
244
|
]) {
|
|
218
245
|
const dx = px - origin.x;
|
|
219
246
|
const dy = py - origin.y;
|
|
220
|
-
|
|
221
|
-
|
|
247
|
+
let x;
|
|
248
|
+
let y;
|
|
249
|
+
if (solid) {
|
|
250
|
+
// column-major, as `matrix3d()` writes it: the point (dx, dy, 0, 1)
|
|
251
|
+
const w = m[3] * dx + m[7] * dy + m[15];
|
|
252
|
+
if (!(w > W_NEAR)) return null;
|
|
253
|
+
x = (m[0] * dx + m[4] * dy + m[12]) / w + origin.x;
|
|
254
|
+
y = (m[1] * dx + m[5] * dy + m[13]) / w + origin.y;
|
|
255
|
+
} else {
|
|
256
|
+
x = m[0] * dx + m[2] * dy + m[4] + origin.x;
|
|
257
|
+
y = m[1] * dx + m[3] * dy + m[5] + origin.y;
|
|
258
|
+
}
|
|
222
259
|
if (x < x0) x0 = x;
|
|
223
260
|
if (y < y0) y0 = y;
|
|
224
261
|
if (x > x1) x1 = x;
|
|
@@ -227,6 +264,10 @@ function mapRect(rect, m, origin) {
|
|
|
227
264
|
return { x: x0, y: y0, width: x1 - x0, height: y1 - y0 };
|
|
228
265
|
}
|
|
229
266
|
|
|
267
|
+
// A corner whose w is this close to the viewer's plane is as good as behind
|
|
268
|
+
// it: projected that near, it lands off any window.
|
|
269
|
+
const W_NEAR = 1e-6;
|
|
270
|
+
|
|
230
271
|
function unionRect(a, b) {
|
|
231
272
|
if (!a) return b;
|
|
232
273
|
const x = Math.min(a.x, b.x);
|
|
@@ -242,7 +283,8 @@ function unionRect(a, b) {
|
|
|
242
283
|
/**
|
|
243
284
|
* Everywhere `sprite` can draw while its animations run: its reach through
|
|
244
285
|
* its own transform and every transform it is animated through, out to
|
|
245
|
-
* whole pixels and the antialiasing a raster is padded for
|
|
286
|
+
* whole pixels and the antialiasing a raster is padded for — or null, where
|
|
287
|
+
* one of those puts a corner of it behind the viewer. Between two
|
|
246
288
|
* keyframes Core Animation interpolates the matrices by their parts, which
|
|
247
289
|
* stays within a pixel of the two for the densely sampled keyframes a CSS
|
|
248
290
|
* transform arrives as; an element whose keyframes are far apart, or whose
|
|
@@ -271,12 +313,18 @@ function extentOf(sprite) {
|
|
|
271
313
|
if (about?.key !== key) {
|
|
272
314
|
let box = mapRect(rel, m0, ORIGIN);
|
|
273
315
|
for (const a of sprite.animations ?? []) {
|
|
274
|
-
if (a.property !== 'transform') continue;
|
|
275
|
-
for (const m of a.values)
|
|
316
|
+
if (a.property !== 'transform' || !box) continue;
|
|
317
|
+
for (const m of a.values) {
|
|
318
|
+
const at = mapRect(rel, m, ORIGIN);
|
|
319
|
+
box = at && unionRect(box, at);
|
|
320
|
+
if (!box) break;
|
|
321
|
+
}
|
|
276
322
|
}
|
|
277
323
|
about = { key, box };
|
|
278
324
|
if (turns) EXTENTS.set(turns, about);
|
|
279
325
|
}
|
|
326
|
+
// a corner behind the viewer, at rest or on the way: nowhere bounded
|
|
327
|
+
if (!about.box) return null;
|
|
280
328
|
const box = {
|
|
281
329
|
x: about.box.x + origin.x,
|
|
282
330
|
y: about.box.y + origin.y,
|
|
@@ -311,7 +359,31 @@ function rasterRectOf(sprite) {
|
|
|
311
359
|
}
|
|
312
360
|
|
|
313
361
|
const sameMatrix = (a, b) =>
|
|
314
|
-
a === b ||
|
|
362
|
+
a === b ||
|
|
363
|
+
(a != null &&
|
|
364
|
+
b != null &&
|
|
365
|
+
a.length === b.length &&
|
|
366
|
+
a.every((v, i) => v === b[i]));
|
|
367
|
+
|
|
368
|
+
/**
|
|
369
|
+
* A part's matrix in device pixels as the bridge takes it, in points: its
|
|
370
|
+
* translation over the scale and, for a matrix3d, its perspective row times
|
|
371
|
+
* it — S⁻¹·M·S, with S scaling x, y and z, so that a turn and a perspective
|
|
372
|
+
* of so many CSS pixels look the same at any scale.
|
|
373
|
+
*/
|
|
374
|
+
function inPoints(m, s) {
|
|
375
|
+
if (!isSolid(m)) {
|
|
376
|
+
return { matrix: [m[0], m[1], m[2], m[3], m[4] / s, m[5] / s] };
|
|
377
|
+
}
|
|
378
|
+
const p = m.slice();
|
|
379
|
+
p[12] /= s;
|
|
380
|
+
p[13] /= s;
|
|
381
|
+
p[14] /= s;
|
|
382
|
+
p[3] *= s;
|
|
383
|
+
p[7] *= s;
|
|
384
|
+
p[11] *= s;
|
|
385
|
+
return { matrix3d: p };
|
|
386
|
+
}
|
|
315
387
|
|
|
316
388
|
/**
|
|
317
389
|
* The lifted parts of every element in one window: a layer each, its raster,
|
|
@@ -326,9 +398,10 @@ export class SpriteLayers {
|
|
|
326
398
|
this.app = promotion.app;
|
|
327
399
|
// a matrix and a negative delay are what a CSS animation needs from the
|
|
328
400
|
// bridge, and @windowkit/appkit 0.19 is where both are
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
401
|
+
const forms = this.native.transformForms?.();
|
|
402
|
+
this.supported = Boolean(forms?.includes?.('matrix'));
|
|
403
|
+
// and a part turned out of the plane, its matrix3d, the same release
|
|
404
|
+
this.solid = Boolean(forms?.includes?.('matrix3d'));
|
|
332
405
|
// and a video's frames on a layer need its video surfaces
|
|
333
406
|
this.videos = canLiftVideo(this.native);
|
|
334
407
|
this.hosts = new Map(); // element -> Map(key -> state)
|
|
@@ -429,7 +502,12 @@ export class SpriteLayers {
|
|
|
429
502
|
radius: sprite.clip ? (sprite.clipRadius ?? 0) : 0,
|
|
430
503
|
bounds: null,
|
|
431
504
|
};
|
|
505
|
+
// turned out of the plane, on a bridge that takes no matrix3d
|
|
506
|
+
if (!this.solid && solidSprite(sprite)) continue;
|
|
432
507
|
const extent = extentOf(sprite);
|
|
508
|
+
// a corner behind the viewer: the element draws it, which it can cut
|
|
509
|
+
// where the plane meets the viewer's
|
|
510
|
+
if (!extent) continue;
|
|
433
511
|
const bounds = cut.clip ? intersectRects(extent, cut.clip) : extent;
|
|
434
512
|
if (parent) {
|
|
435
513
|
// the parent was asked about everywhere it can be, and its layer
|
|
@@ -544,8 +622,9 @@ export class SpriteLayers {
|
|
|
544
622
|
drawn.push(state.extent);
|
|
545
623
|
continue;
|
|
546
624
|
}
|
|
547
|
-
// the element draws it, where its own clip lets it show
|
|
548
|
-
|
|
625
|
+
// the element draws it, where its own clip lets it show — anywhere,
|
|
626
|
+
// for one with a corner behind the viewer and no clip
|
|
627
|
+
const extent = extentOf(sprite) ?? EVERYWHERE;
|
|
549
628
|
const shows = sprite.clip ? intersectRects(extent, sprite.clip) : extent;
|
|
550
629
|
if (shows) drawn.push(shows);
|
|
551
630
|
}
|
|
@@ -776,7 +855,7 @@ export class SpriteLayers {
|
|
|
776
855
|
}
|
|
777
856
|
}
|
|
778
857
|
if (!sameMatrix(prev.matrix, m)) {
|
|
779
|
-
out.transform =
|
|
858
|
+
out.transform = inPoints(m, s);
|
|
780
859
|
any = true;
|
|
781
860
|
}
|
|
782
861
|
if (any) this.native.setLayerProps(state.layer, out);
|
|
@@ -843,9 +922,7 @@ export class SpriteLayers {
|
|
|
843
922
|
const opts = {
|
|
844
923
|
values:
|
|
845
924
|
a.property === 'transform'
|
|
846
|
-
? a.values.map((m) => (
|
|
847
|
-
matrix: [m[0], m[1], m[2], m[3], m[4] / s, m[5] / s],
|
|
848
|
-
}))
|
|
925
|
+
? a.values.map((m) => inPoints(m, s))
|
|
849
926
|
: a.values,
|
|
850
927
|
duration: a.duration / 1000,
|
|
851
928
|
id: caId,
|
package/src/components/Menu.js
CHANGED
|
@@ -24,9 +24,11 @@ import {
|
|
|
24
24
|
useDismissOnWindowBlur,
|
|
25
25
|
} from './anchor.js';
|
|
26
26
|
import { typeAheadChar, useTypeAhead } from './typeahead.js';
|
|
27
|
-
import {
|
|
27
|
+
import { useGlobalMenuExport } from '../globalmenu.js';
|
|
28
28
|
import { acceleratedItem } from '../accelerators.js';
|
|
29
29
|
import { useAcceleratorEntry } from '../acceleratorhooks.js';
|
|
30
|
+
import { useAppOrNull } from '../appcontext.js';
|
|
31
|
+
import { primaryModifier } from '../keysyms.js';
|
|
30
32
|
import { useTopLevelWindow } from '../windowid.js';
|
|
31
33
|
import { MENU_TEXT_WEIGHT } from '../faces.js';
|
|
32
34
|
import {
|
|
@@ -121,12 +123,17 @@ function menuMetrics(theme, native, fontSize) {
|
|
|
121
123
|
};
|
|
122
124
|
}
|
|
123
125
|
|
|
126
|
+
// `primary` rides along with the sizes because it is one too: the shortcut
|
|
127
|
+
// column is measured from the same string the row then draws — `⌘S` on the
|
|
128
|
+
// Cocoa backend, `Ctrl+S` elsewhere — and the binding matches the key that
|
|
129
|
+
// string names (`useMenuAccelerators`).
|
|
124
130
|
function useMenuMetrics(fontSize) {
|
|
125
131
|
const theme = useTheme();
|
|
126
132
|
const native = useNativeControls();
|
|
133
|
+
const primary = primaryModifier(useAppOrNull());
|
|
127
134
|
return useMemo(
|
|
128
|
-
() => menuMetrics(theme, native, fontSize),
|
|
129
|
-
[theme, native, fontSize],
|
|
135
|
+
() => ({ ...menuMetrics(theme, native, fontSize), primary }),
|
|
136
|
+
[theme, native, fontSize, primary],
|
|
130
137
|
);
|
|
131
138
|
}
|
|
132
139
|
|
|
@@ -272,7 +279,9 @@ function menuListWidth(node, items, metrics) {
|
|
|
272
279
|
size: metrics.fontSize,
|
|
273
280
|
weight: metrics.weight,
|
|
274
281
|
}).width;
|
|
275
|
-
const accelerator = formatShortcut(item.shortcut
|
|
282
|
+
const accelerator = formatShortcut(item.shortcut, {
|
|
283
|
+
primary: metrics.primary,
|
|
284
|
+
});
|
|
276
285
|
const shortcut = accelerator
|
|
277
286
|
? measureLabel(node, accelerator, {
|
|
278
287
|
size: metrics.fontSize,
|
|
@@ -471,7 +480,9 @@ function MenuRow({
|
|
|
471
480
|
}
|
|
472
481
|
const dim = !isEnabled(item);
|
|
473
482
|
const submenu = hasSubmenu(item);
|
|
474
|
-
const accelerator = formatShortcut(item.shortcut
|
|
483
|
+
const accelerator = formatShortcut(item.shortcut, {
|
|
484
|
+
primary: metrics.primary,
|
|
485
|
+
});
|
|
475
486
|
const rowInk = dim ? theme.textMuted : active ? theme.hoverText : theme.text;
|
|
476
487
|
if (process.env.NODE_ENV !== 'production') checkShortcut(item);
|
|
477
488
|
return h(
|
|
@@ -489,7 +500,9 @@ function MenuRow({
|
|
|
489
500
|
// the label alone: without it, name-from-contents would read the
|
|
490
501
|
// shortcut and the submenu arrow into the name ("New Ctrl+N")
|
|
491
502
|
'aria-label': typeof item.label === 'string' ? item.label : undefined,
|
|
492
|
-
'aria-keyshortcuts': ariaKeyShortcuts(item.shortcut
|
|
503
|
+
'aria-keyshortcuts': ariaKeyShortcuts(item.shortcut, {
|
|
504
|
+
primary: metrics.primary,
|
|
505
|
+
}),
|
|
493
506
|
'aria-checked': item.toggleType
|
|
494
507
|
? ariaChecked(item.toggleState)
|
|
495
508
|
: undefined,
|
|
@@ -922,17 +935,40 @@ function handleMenuKey(
|
|
|
922
935
|
* `anchorRef` is what the binding hangs off — the bar, or the window when
|
|
923
936
|
* there is no bar to hang off — and decides what a modal `<popup>` takes it
|
|
924
937
|
* away from (`EventManager._runAccelerators`).
|
|
938
|
+
*
|
|
939
|
+
* `primary` is the app's primary modifier, so a chord's `Control` is ⌘ on
|
|
940
|
+
* the Cocoa backend — the key the row prints, and the key the macOS menu
|
|
941
|
+
* bar installs. Which is where the one exception comes from:
|
|
942
|
+
* `answeredElsewhere(shortcut)` names the chords a delegate fires the item
|
|
943
|
+
* for by itself. The bridge hands JS every key *before* AppKit dispatches
|
|
944
|
+
* it, so ⌘S reaches the window's bindings and then the menu bar's key
|
|
945
|
+
* equivalent; selecting here too would save twice. So such a chord is
|
|
946
|
+
* *reserved*: spent before any binding in the window is offered it, this
|
|
947
|
+
* one included, and left to the menu bar — a `useAccelerator` on ⌘S
|
|
948
|
+
* mounted after the bar would otherwise be the same double run by another
|
|
949
|
+
* route. A chord the menu bar cannot spell (F5, a second alternative,
|
|
950
|
+
* `plus`) is not one, and stays this binding's.
|
|
925
951
|
*/
|
|
926
|
-
function useMenuAccelerators(
|
|
952
|
+
function useMenuAccelerators(
|
|
953
|
+
items,
|
|
954
|
+
select,
|
|
955
|
+
anchorRef,
|
|
956
|
+
{ enabled, primary, answeredElsewhere },
|
|
957
|
+
) {
|
|
927
958
|
useAcceleratorEntry(
|
|
928
959
|
anchorRef,
|
|
929
960
|
(ev) => {
|
|
930
|
-
const item = acceleratedItem(items, ev);
|
|
961
|
+
const item = acceleratedItem(items, ev, { primary });
|
|
931
962
|
if (!item) return false;
|
|
932
963
|
select(item);
|
|
933
964
|
return true;
|
|
934
965
|
},
|
|
935
966
|
enabled,
|
|
967
|
+
answeredElsewhere &&
|
|
968
|
+
((ev) => {
|
|
969
|
+
const item = acceleratedItem(items, ev, { primary });
|
|
970
|
+
return Boolean(item && answeredElsewhere(item.shortcut));
|
|
971
|
+
}),
|
|
936
972
|
);
|
|
937
973
|
}
|
|
938
974
|
|
|
@@ -970,7 +1006,10 @@ export function ContextMenu({
|
|
|
970
1006
|
onSelect?.(item);
|
|
971
1007
|
};
|
|
972
1008
|
|
|
973
|
-
useMenuAccelerators(items, select, ref,
|
|
1009
|
+
useMenuAccelerators(items, select, ref, {
|
|
1010
|
+
enabled: accelerators,
|
|
1011
|
+
primary: metrics.primary,
|
|
1012
|
+
});
|
|
974
1013
|
|
|
975
1014
|
// the wrapper keeps the focus the right-click gave it, so `onBlur` below
|
|
976
1015
|
// never fires when the *window* loses focus — and the menu is holding a
|
|
@@ -1101,7 +1140,10 @@ export function MenuBar({
|
|
|
1101
1140
|
// `visible` as a property, so hiding one there is a patch the panel applies
|
|
1102
1141
|
// rather than a structural change that renumbers everything after it.
|
|
1103
1142
|
const bar = useMemo(() => visibleItems(menus), [menus]);
|
|
1104
|
-
const delegated =
|
|
1143
|
+
const { delegated, answersShortcut } = useGlobalMenuExport(menus, {
|
|
1144
|
+
onSelect,
|
|
1145
|
+
enabled: globalMenu,
|
|
1146
|
+
});
|
|
1105
1147
|
|
|
1106
1148
|
// What the bar turned out to be, which is the one thing here that cannot
|
|
1107
1149
|
// be worked out in a render: layout runs on the frame clock, after the
|
|
@@ -1263,7 +1305,11 @@ export function MenuBar({
|
|
|
1263
1305
|
}),
|
|
1264
1306
|
[owner],
|
|
1265
1307
|
);
|
|
1266
|
-
useMenuAccelerators(menus, select, acceleratorAnchor,
|
|
1308
|
+
useMenuAccelerators(menus, select, acceleratorAnchor, {
|
|
1309
|
+
enabled: accelerators,
|
|
1310
|
+
primary: metrics.primary,
|
|
1311
|
+
answeredElsewhere: delegated ? answersShortcut : null,
|
|
1312
|
+
});
|
|
1267
1313
|
|
|
1268
1314
|
const moveMenu = (dir) => {
|
|
1269
1315
|
if (!entries.length) return;
|
|
@@ -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
|