react-x11 2.39.0 → 2.41.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 +25 -7
- package/src/cocoa/globalmenu.js +32 -1
- package/src/cocoa/keymap.js +69 -12
- package/src/cocoa/panehost.js +161 -13
- package/src/cocoa/paneplayer.js +101 -24
- package/src/cocoa/panewindow.js +325 -44
- package/src/cocoa/window.js +9 -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 +36 -0
- package/src/frame/index.js +64 -18
- 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 +88 -16
- package/src/nodes/textinput.js +27 -6
- package/src/nodes/textkeys.js +322 -0
- 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 +27 -5
- package/src/types/nodes.d.ts +6 -0
- package/src/win32/fonts.js +521 -44
- package/src/win32/panewindow.js +25 -2
|
@@ -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
|
@@ -1672,6 +1672,18 @@ export class EventManager {
|
|
|
1672
1672
|
// its ordinary turn.
|
|
1673
1673
|
if (step.consumed) return;
|
|
1674
1674
|
}
|
|
1675
|
+
// A key that typed more than one character has no code point to type
|
|
1676
|
+
// them with: a backend whose keyboard layout composes on its own says
|
|
1677
|
+
// so with `text` — the accent and a letter it does not combine with,
|
|
1678
|
+
// `´s` (src/cocoa/keymap.js). Committed the way an input method's text
|
|
1679
|
+
// is, a composition that opens and closes on this key, which is where
|
|
1680
|
+
// an element that types inserts it — and like a composed key, it is
|
|
1681
|
+
// the key's whole answer.
|
|
1682
|
+
if (native.text && target.composes !== false) {
|
|
1683
|
+
this._composition('Start', target, '', native);
|
|
1684
|
+
this._composition('End', target, String(native.text), native);
|
|
1685
|
+
return;
|
|
1686
|
+
}
|
|
1675
1687
|
// The element's own behaviour first, focus traversal after it — Tab is
|
|
1676
1688
|
// an ordinary defaultable key rather than one the focus manager eats on
|
|
1677
1689
|
// the way past. Cycling first meant an editor could only keep Tab as an
|
|
@@ -1705,6 +1717,11 @@ export class EventManager {
|
|
|
1705
1717
|
*
|
|
1706
1718
|
* `entry.handle(ev)` answers whether it took the key.
|
|
1707
1719
|
*
|
|
1720
|
+
* `entry.reserves(ev)`, optional, answers whether something *outside* the
|
|
1721
|
+
* window is going to answer this key whatever we do — the macOS menu
|
|
1722
|
+
* bar's key equivalents (`MenuBar`, delegated). Such a key is spent
|
|
1723
|
+
* before any binding is offered it (`_runAccelerators`).
|
|
1724
|
+
*
|
|
1708
1725
|
* Registrations live on the **top-level** manager, which is the one thing
|
|
1709
1726
|
* a `<popup>`, a nested `<window>` and the window itself all share — the
|
|
1710
1727
|
* same grouping `_keyManager` resolves keys through. So a chord reaches
|
|
@@ -1740,11 +1757,30 @@ export class EventManager {
|
|
|
1740
1757
|
* A menu that is *open* suppresses them by another route entirely: it
|
|
1741
1758
|
* `preventDefault()`s the keys it is being driven with, one layer above
|
|
1742
1759
|
* this (`components/Menu.js`).
|
|
1760
|
+
*
|
|
1761
|
+
* And a key one of them *reserves* reaches none of them. The Cocoa bridge
|
|
1762
|
+
* shows us every key before `[NSApp sendEvent:]`, where the menu bar's
|
|
1763
|
+
* key equivalent then runs its item regardless — so a binding here that
|
|
1764
|
+
* answered ⌘S too would be a second command on one key, whichever of the
|
|
1765
|
+
* two mounted last. Neither gate applies to that: the menu bar is not in
|
|
1766
|
+
* the window, and a modal `<popup>` does not take keys from it.
|
|
1743
1767
|
*/
|
|
1744
1768
|
_runAccelerators(ev) {
|
|
1745
1769
|
const owner = this.topLevelManager;
|
|
1746
1770
|
const entries = owner._accelerators;
|
|
1747
1771
|
if (!entries?.size) return;
|
|
1772
|
+
for (const entry of entries) {
|
|
1773
|
+
let reserved = false;
|
|
1774
|
+
try {
|
|
1775
|
+
reserved = Boolean(entry.reserves?.(ev));
|
|
1776
|
+
} catch (error) {
|
|
1777
|
+
reportHandlerError(entry.anchor(), 'an accelerator', error);
|
|
1778
|
+
}
|
|
1779
|
+
if (reserved) {
|
|
1780
|
+
ev.preventDefault();
|
|
1781
|
+
return;
|
|
1782
|
+
}
|
|
1783
|
+
}
|
|
1748
1784
|
const scopeRoot = owner._scopeRoot();
|
|
1749
1785
|
const trapped = scopeRoot !== owner.node;
|
|
1750
1786
|
for (const entry of [...entries].reverse()) {
|
package/src/frame/index.js
CHANGED
|
@@ -192,6 +192,11 @@ function shallowEqual(a, b) {
|
|
|
192
192
|
|
|
193
193
|
const EMPTY_PROPS = {};
|
|
194
194
|
|
|
195
|
+
/** How long a rect sent to a pane that acknowledges sizes waits for its
|
|
196
|
+
* `pane-sized` before the newest is sent anyway: a frame of a slow page,
|
|
197
|
+
* several times over, so a lost answer costs a pause and not the size. */
|
|
198
|
+
export const PANE_SIZED_WAIT_MS = 500;
|
|
199
|
+
|
|
195
200
|
/**
|
|
196
201
|
* A module of this application, mounted in its own process, its window
|
|
197
202
|
* embedded here.
|
|
@@ -552,7 +557,54 @@ function PaneHostView({
|
|
|
552
557
|
// moves when the box does and when the window does, so it is sent on
|
|
553
558
|
// layout, on a scroll that shifts the box, and on every anchor change
|
|
554
559
|
// of the window, which a move of the window is.
|
|
560
|
+
//
|
|
561
|
+
// **The newest rect wins.** A drag sends the host a size a frame, and a
|
|
562
|
+
// pane whose page takes longer than that to lay out was sent them
|
|
563
|
+
// faster than it could take them: a pipe hands a pane process one
|
|
564
|
+
// message a turn of its loop, with a frame in between, so the pane laid
|
|
565
|
+
// out and painted every size the drag passed through, one a frame, and
|
|
566
|
+
// went on resizing seconds after the edge stopped. A pane that says it
|
|
567
|
+
// has painted a size (`pane-sized`) is sent one rect at a time: what
|
|
568
|
+
// the host comes to while one is out replaces what was waiting, and goes
|
|
569
|
+
// when the pane says it has painted the last. A pane that never says so
|
|
570
|
+
// is sent every rect, as before, and one that goes quiet is sent the
|
|
571
|
+
// newest anyway after `PANE_SIZED_WAIT_MS`.
|
|
555
572
|
let sent = null;
|
|
573
|
+
let pending = null;
|
|
574
|
+
let acks = false;
|
|
575
|
+
let inFlight = false;
|
|
576
|
+
let waiting = null;
|
|
577
|
+
const transmit = (rect) => {
|
|
578
|
+
sent = rect;
|
|
579
|
+
s.trySend?.({
|
|
580
|
+
type: 'pane-rect',
|
|
581
|
+
width: rect.width,
|
|
582
|
+
height: rect.height,
|
|
583
|
+
scale: rect.scale,
|
|
584
|
+
...(rect.screen && { screen: rect.screen }),
|
|
585
|
+
});
|
|
586
|
+
if (!acks) return;
|
|
587
|
+
inFlight = true;
|
|
588
|
+
if (waiting) clearTimeout(waiting);
|
|
589
|
+
waiting = setTimeout(sized, PANE_SIZED_WAIT_MS);
|
|
590
|
+
};
|
|
591
|
+
const sameRect = (a, b) =>
|
|
592
|
+
!!a &&
|
|
593
|
+
!!b &&
|
|
594
|
+
a.width === b.width &&
|
|
595
|
+
a.height === b.height &&
|
|
596
|
+
a.scale === b.scale &&
|
|
597
|
+
a.screen?.x === b.screen?.x &&
|
|
598
|
+
a.screen?.y === b.screen?.y;
|
|
599
|
+
const sized = () => {
|
|
600
|
+
if (waiting) clearTimeout(waiting);
|
|
601
|
+
waiting = null;
|
|
602
|
+
inFlight = false;
|
|
603
|
+
const next = pending;
|
|
604
|
+
pending = null;
|
|
605
|
+
// a drag that came back to where it was sent from waits for nothing
|
|
606
|
+
if (next && !sameRect(next, sent)) transmit(next);
|
|
607
|
+
};
|
|
556
608
|
const syncSize = () => {
|
|
557
609
|
host.setRect(node.abs);
|
|
558
610
|
const sc = node.scale ?? 1;
|
|
@@ -565,23 +617,10 @@ function PaneHostView({
|
|
|
565
617
|
y: Math.round(origin.y + node.abs.y),
|
|
566
618
|
}
|
|
567
619
|
: null;
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
sent.screen?.x === screen?.x &&
|
|
573
|
-
sent.screen?.y === screen?.y
|
|
574
|
-
) {
|
|
575
|
-
return;
|
|
576
|
-
}
|
|
577
|
-
sent = { width, height, screen };
|
|
578
|
-
s.trySend?.({
|
|
579
|
-
type: 'pane-rect',
|
|
580
|
-
width,
|
|
581
|
-
height,
|
|
582
|
-
scale: sc,
|
|
583
|
-
...(screen && { screen }),
|
|
584
|
-
});
|
|
620
|
+
const rect = { width, height, scale: sc, screen };
|
|
621
|
+
if (sameRect(rect, pending ?? sent)) return;
|
|
622
|
+
if (inFlight) pending = rect;
|
|
623
|
+
else transmit(rect);
|
|
585
624
|
};
|
|
586
625
|
const origAbsolutize = node.absolutize.bind(node);
|
|
587
626
|
node.absolutize = (ox, oy) => {
|
|
@@ -624,7 +663,12 @@ function PaneHostView({
|
|
|
624
663
|
host.windowCursor?.(msg);
|
|
625
664
|
} else if (msg?.type === 'pane-present') {
|
|
626
665
|
host.setRect(node.abs);
|
|
627
|
-
host.present(msg.id);
|
|
666
|
+
host.present(msg.id, msg);
|
|
667
|
+
} else if (msg?.type === 'pane-sized') {
|
|
668
|
+
// the pane has painted the rect it was last sent: the newest one
|
|
669
|
+
// waiting goes now, and from here on one is out at a time
|
|
670
|
+
acks = true;
|
|
671
|
+
sized();
|
|
628
672
|
} else if (msg?.type === 'pane-cursor') {
|
|
629
673
|
// The pane names its cursor as an element that draws names one for
|
|
630
674
|
// a point: this box is the node the host's pointer is over, so the
|
|
@@ -637,6 +681,8 @@ function PaneHostView({
|
|
|
637
681
|
});
|
|
638
682
|
return () => {
|
|
639
683
|
off();
|
|
684
|
+
if (waiting) clearTimeout(waiting);
|
|
685
|
+
waiting = null;
|
|
640
686
|
offAnchor?.();
|
|
641
687
|
offFocus?.();
|
|
642
688
|
node.defaultCursor = undefined;
|
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
|
}
|