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
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
// `filter` on the native 2d context: canvas's own property, a CSS
|
|
2
|
+
// `<filter-value-list>` (Filter Effects 1), and what this context makes of
|
|
3
|
+
// it — the colour functions, each a matrix over straight RGB with an
|
|
4
|
+
// offset that leaves alpha as it is, and `opacity()`, which scales alpha.
|
|
5
|
+
//
|
|
6
|
+
// They are the functions the context can apply to everything it draws.
|
|
7
|
+
// Each leaves alpha alone, so on premultiplied colour each is a linear map
|
|
8
|
+
// that commutes with source-over: a fill, a stroke, a gradient's stops, a
|
|
9
|
+
// glyph run's ink and a symbol's colour are filtered by filtering the colour
|
|
10
|
+
// they are drawn in, exactly. An image and a surface are filtered pixel by
|
|
11
|
+
// pixel, and a text layout that carries colours of its own on a surface of
|
|
12
|
+
// its own (src/backend/context2d.js). `blur()`, `drop-shadow()` and a
|
|
13
|
+
// `url()` reach past what is drawn, which a colour cannot say; this context
|
|
14
|
+
// does not apply them, so a list with one in it does not stick, and a
|
|
15
|
+
// caller knows from reading the property back.
|
|
16
|
+
|
|
17
|
+
/** A function's amount where none is written: all of the effect for the
|
|
18
|
+
* ones whose argument is how much of it to take, and none for the others
|
|
19
|
+
* (Filter Effects 1, 13). */
|
|
20
|
+
const AMOUNTS = {
|
|
21
|
+
grayscale: 1,
|
|
22
|
+
sepia: 1,
|
|
23
|
+
invert: 1,
|
|
24
|
+
opacity: 1,
|
|
25
|
+
saturate: 1,
|
|
26
|
+
brightness: 1,
|
|
27
|
+
contrast: 1,
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
/** The functions this context applies; any other leaves a list unapplied. */
|
|
31
|
+
const COLOUR = new Set([...Object.keys(AMOUNTS), 'hue-rotate']);
|
|
32
|
+
|
|
33
|
+
const ANGLE_UNITS = {
|
|
34
|
+
deg: 1,
|
|
35
|
+
grad: 0.9,
|
|
36
|
+
rad: 180 / Math.PI,
|
|
37
|
+
turn: 360,
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* A filter list as this context applies it: `{ matrices, alpha }`, the
|
|
42
|
+
* colour matrices in the order written, each over straight RGB in 0–1 with
|
|
43
|
+
* an offset — none for a function that changes no colour — and what alpha
|
|
44
|
+
* is multiplied by. Null for `none`; undefined for what is no filter list,
|
|
45
|
+
* or one with a function this context does not apply.
|
|
46
|
+
*/
|
|
47
|
+
export function parseCanvasFilter(value) {
|
|
48
|
+
const text = String(value).trim();
|
|
49
|
+
if (text.toLowerCase() === 'none') return null;
|
|
50
|
+
const matrices = [];
|
|
51
|
+
let alpha = 1;
|
|
52
|
+
let rest = text;
|
|
53
|
+
let seen = 0;
|
|
54
|
+
while (rest.length) {
|
|
55
|
+
const m = /^([a-z-]+)\(([^()]*)\)\s*/i.exec(rest);
|
|
56
|
+
if (!m) return undefined;
|
|
57
|
+
rest = rest.slice(m[0].length);
|
|
58
|
+
seen += 1;
|
|
59
|
+
const name = m[1].toLowerCase();
|
|
60
|
+
if (!COLOUR.has(name)) return undefined;
|
|
61
|
+
const arg = m[2].trim();
|
|
62
|
+
if (name === 'hue-rotate') {
|
|
63
|
+
const angle = arg ? angleOf(arg) : 0;
|
|
64
|
+
if (angle === null) return undefined;
|
|
65
|
+
const matrix = hueRotate(angle);
|
|
66
|
+
if (matrix) matrices.push(matrix);
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
const amount = arg ? amountOf(arg) : AMOUNTS[name];
|
|
70
|
+
if (amount === null || amount < 0) return undefined;
|
|
71
|
+
if (name === 'opacity') {
|
|
72
|
+
alpha *= Math.min(1, amount);
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
const matrix = matrixOf(name, amount);
|
|
76
|
+
if (matrix) matrices.push(matrix);
|
|
77
|
+
}
|
|
78
|
+
if (!seen) return undefined;
|
|
79
|
+
return { matrices, alpha };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Whether a parsed filter changes anything drawn. */
|
|
83
|
+
export function filters(filter) {
|
|
84
|
+
return !!filter && (filter.matrices.length > 0 || filter.alpha < 1);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** A number or a percentage of one. */
|
|
88
|
+
function amountOf(arg) {
|
|
89
|
+
const m = /^([+-]?(?:\d+\.?\d*|\.\d+)(?:e[+-]?\d+)?)(%?)$/i.exec(arg);
|
|
90
|
+
if (!m) return null;
|
|
91
|
+
const n = Number(m[1]);
|
|
92
|
+
if (!Number.isFinite(n)) return null;
|
|
93
|
+
return m[2] ? n / 100 : n;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** An angle in degrees; a bare 0 is one. */
|
|
97
|
+
function angleOf(arg) {
|
|
98
|
+
const m = /^([+-]?(?:\d+\.?\d*|\.\d+)(?:e[+-]?\d+)?)([a-z]*)$/i.exec(arg);
|
|
99
|
+
if (!m) return null;
|
|
100
|
+
const n = Number(m[1]);
|
|
101
|
+
const unit = m[2].toLowerCase();
|
|
102
|
+
if (!unit) return n === 0 ? 0 : null;
|
|
103
|
+
const scale = ANGLE_UNITS[unit];
|
|
104
|
+
return scale === undefined ? null : n * scale;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const unit = (a) => (a < 0 ? 0 : a > 1 ? 1 : a);
|
|
108
|
+
|
|
109
|
+
/** Three rows of red, green and blue weights and an offset, 0–1. */
|
|
110
|
+
function rows(r, g, b) {
|
|
111
|
+
return Float64Array.of(...r, ...g, ...b);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** `feComponentTransfer`'s `linear`: each channel times a slope, plus an
|
|
115
|
+
* intercept. */
|
|
116
|
+
function linear(slope, intercept) {
|
|
117
|
+
return Float64Array.of(
|
|
118
|
+
slope,
|
|
119
|
+
0,
|
|
120
|
+
0,
|
|
121
|
+
intercept,
|
|
122
|
+
0,
|
|
123
|
+
slope,
|
|
124
|
+
0,
|
|
125
|
+
intercept,
|
|
126
|
+
0,
|
|
127
|
+
0,
|
|
128
|
+
slope,
|
|
129
|
+
intercept,
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** One function's matrix (Filter Effects 1, 13); null where it changes no
|
|
134
|
+
* colour. */
|
|
135
|
+
function matrixOf(name, amount) {
|
|
136
|
+
switch (name) {
|
|
137
|
+
case 'grayscale': {
|
|
138
|
+
const a = unit(amount);
|
|
139
|
+
if (a === 0) return null;
|
|
140
|
+
const k = 1 - a;
|
|
141
|
+
return rows(
|
|
142
|
+
[0.2126 + 0.7874 * k, 0.7152 - 0.7152 * k, 0.0722 - 0.0722 * k, 0],
|
|
143
|
+
[0.2126 - 0.2126 * k, 0.7152 + 0.2848 * k, 0.0722 - 0.0722 * k, 0],
|
|
144
|
+
[0.2126 - 0.2126 * k, 0.7152 - 0.7152 * k, 0.0722 + 0.9278 * k, 0],
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
case 'sepia': {
|
|
148
|
+
const a = unit(amount);
|
|
149
|
+
if (a === 0) return null;
|
|
150
|
+
const k = 1 - a;
|
|
151
|
+
return rows(
|
|
152
|
+
[0.393 + 0.607 * k, 0.769 - 0.769 * k, 0.189 - 0.189 * k, 0],
|
|
153
|
+
[0.349 - 0.349 * k, 0.686 + 0.314 * k, 0.168 - 0.168 * k, 0],
|
|
154
|
+
[0.272 - 0.272 * k, 0.534 - 0.534 * k, 0.131 + 0.869 * k, 0],
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
case 'saturate': {
|
|
158
|
+
const s = amount;
|
|
159
|
+
if (s === 1) return null;
|
|
160
|
+
return rows(
|
|
161
|
+
[0.213 + 0.787 * s, 0.715 - 0.715 * s, 0.072 - 0.072 * s, 0],
|
|
162
|
+
[0.213 - 0.213 * s, 0.715 + 0.285 * s, 0.072 - 0.072 * s, 0],
|
|
163
|
+
[0.213 - 0.213 * s, 0.715 - 0.715 * s, 0.072 + 0.928 * s, 0],
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
case 'brightness':
|
|
167
|
+
return amount === 1 ? null : linear(amount, 0);
|
|
168
|
+
case 'contrast':
|
|
169
|
+
return amount === 1 ? null : linear(amount, 0.5 - 0.5 * amount);
|
|
170
|
+
case 'invert': {
|
|
171
|
+
const a = unit(amount);
|
|
172
|
+
return a === 0 ? null : linear(1 - 2 * a, a);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
return null;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function hueRotate(degrees) {
|
|
179
|
+
if (degrees % 360 === 0) return null;
|
|
180
|
+
const t = (degrees * Math.PI) / 180;
|
|
181
|
+
const c = Math.cos(t);
|
|
182
|
+
const s = Math.sin(t);
|
|
183
|
+
return rows(
|
|
184
|
+
[
|
|
185
|
+
0.213 + c * 0.787 - s * 0.213,
|
|
186
|
+
0.715 - c * 0.715 - s * 0.715,
|
|
187
|
+
0.072 - c * 0.072 + s * 0.928,
|
|
188
|
+
0,
|
|
189
|
+
],
|
|
190
|
+
[
|
|
191
|
+
0.213 - c * 0.213 + s * 0.143,
|
|
192
|
+
0.715 + c * 0.285 + s * 0.14,
|
|
193
|
+
0.072 - c * 0.072 - s * 0.283,
|
|
194
|
+
0,
|
|
195
|
+
],
|
|
196
|
+
[
|
|
197
|
+
0.213 - c * 0.213 - s * 0.787,
|
|
198
|
+
0.715 - c * 0.715 + s * 0.715,
|
|
199
|
+
0.072 + c * 0.928 + s * 0.072,
|
|
200
|
+
0,
|
|
201
|
+
],
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/** A straight `[r, g, b, a]`, 0–1, through the filter: each function's
|
|
206
|
+
* result clamped before the next, as each one's is. */
|
|
207
|
+
export function filterColour([r, g, b, a], filter) {
|
|
208
|
+
for (const m of filter.matrices) {
|
|
209
|
+
const r1 = m[0] * r + m[1] * g + m[2] * b + m[3];
|
|
210
|
+
const g1 = m[4] * r + m[5] * g + m[6] * b + m[7];
|
|
211
|
+
const b1 = m[8] * r + m[9] * g + m[10] * b + m[11];
|
|
212
|
+
r = unit(r1);
|
|
213
|
+
g = unit(g1);
|
|
214
|
+
b = unit(b1);
|
|
215
|
+
}
|
|
216
|
+
return [r, g, b, a * filter.alpha];
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/** A gradient's stops, flat as `[offset, r, g, b, a, …]`, through the
|
|
220
|
+
* filter: interpolating straight colours commutes with a matrix over them,
|
|
221
|
+
* but for what a clamp cuts off between two stops. */
|
|
222
|
+
export function filterStops(flat, filter) {
|
|
223
|
+
const out = flat.slice();
|
|
224
|
+
for (let i = 0; i + 4 < out.length; i += 5) {
|
|
225
|
+
const [r, g, b, a] = filterColour(
|
|
226
|
+
[out[i + 1], out[i + 2], out[i + 3], out[i + 4]],
|
|
227
|
+
filter,
|
|
228
|
+
);
|
|
229
|
+
out[i + 1] = r;
|
|
230
|
+
out[i + 2] = g;
|
|
231
|
+
out[i + 3] = b;
|
|
232
|
+
out[i + 4] = a;
|
|
233
|
+
}
|
|
234
|
+
return out;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Straight RGBA pixels, 0–255, through the filter, `src` into `dst` — the
|
|
239
|
+
* two may be one. A pixel with no alpha keeps none: its colour is no
|
|
240
|
+
* colour.
|
|
241
|
+
*/
|
|
242
|
+
export function filterPixels(src, dst, filter) {
|
|
243
|
+
const { matrices } = filter;
|
|
244
|
+
const fade = filter.alpha;
|
|
245
|
+
const one = matrices.length === 1 ? matrices[0] : null;
|
|
246
|
+
for (let i = 0; i < src.length; i += 4) {
|
|
247
|
+
const a = src[i + 3];
|
|
248
|
+
if (a === 0) {
|
|
249
|
+
dst[i] = 0;
|
|
250
|
+
dst[i + 1] = 0;
|
|
251
|
+
dst[i + 2] = 0;
|
|
252
|
+
dst[i + 3] = 0;
|
|
253
|
+
continue;
|
|
254
|
+
}
|
|
255
|
+
let r = src[i];
|
|
256
|
+
let g = src[i + 1];
|
|
257
|
+
let b = src[i + 2];
|
|
258
|
+
if (one) {
|
|
259
|
+
const m = one;
|
|
260
|
+
const r1 = m[0] * r + m[1] * g + m[2] * b + m[3] * 255;
|
|
261
|
+
const g1 = m[4] * r + m[5] * g + m[6] * b + m[7] * 255;
|
|
262
|
+
const b1 = m[8] * r + m[9] * g + m[10] * b + m[11] * 255;
|
|
263
|
+
r = r1;
|
|
264
|
+
g = g1;
|
|
265
|
+
b = b1;
|
|
266
|
+
} else {
|
|
267
|
+
for (const m of matrices) {
|
|
268
|
+
const r1 = m[0] * r + m[1] * g + m[2] * b + m[3] * 255;
|
|
269
|
+
const g1 = m[4] * r + m[5] * g + m[6] * b + m[7] * 255;
|
|
270
|
+
const b1 = m[8] * r + m[9] * g + m[10] * b + m[11] * 255;
|
|
271
|
+
r = r1 < 0 ? 0 : r1 > 255 ? 255 : r1;
|
|
272
|
+
g = g1 < 0 ? 0 : g1 > 255 ? 255 : g1;
|
|
273
|
+
b = b1 < 0 ? 0 : b1 > 255 ? 255 : b1;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
dst[i] = clampByte(r);
|
|
277
|
+
dst[i + 1] = clampByte(g);
|
|
278
|
+
dst[i + 2] = clampByte(b);
|
|
279
|
+
dst[i + 3] = fade === 1 ? a : clampByte(a * fade);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function clampByte(v) {
|
|
284
|
+
return v <= 0 ? 0 : v >= 255 ? 255 : Math.round(v);
|
|
285
|
+
}
|
package/src/cocoa/app.js
CHANGED
|
@@ -286,6 +286,15 @@ export class CocoaApp {
|
|
|
286
286
|
// X11's, where every toolkit hovers in a window without the keyboard.
|
|
287
287
|
this.hoverNeedsActiveWindow = true;
|
|
288
288
|
|
|
289
|
+
// Shortcuts are pressed with ⌘ here (src/keysyms.js `primaryModifier`):
|
|
290
|
+
// the text controls' Undo/Cut/Copy/Paste/Select All answer ⌘ rather
|
|
291
|
+
// than ⌃, and the edit menu prints them the way a Mac menu does. The
|
|
292
|
+
// menu bar already read a chord's `Control` as ⌘ (cocoa/globalmenu.js);
|
|
293
|
+
// this is the same rule for everything else that reads one. Text is
|
|
294
|
+
// edited the Mac way with it too: ⌥ moves by word, ⌘ to the ends of a
|
|
295
|
+
// line and the document, ⌃ is Emacs's (src/nodes/textkeys.js).
|
|
296
|
+
this.primaryModifier = 'Super';
|
|
297
|
+
|
|
289
298
|
// AppKit-rendered control bezels. Its *presence* is the capability:
|
|
290
299
|
// `useSupports('nativeControls')` and the widget set's `controls:
|
|
291
300
|
// 'auto'` policy both test for this property, so a backend without it
|
|
@@ -927,8 +936,9 @@ export class CocoaApp {
|
|
|
927
936
|
|
|
928
937
|
/**
|
|
929
938
|
* The pane's end of the frame channel (childmain hands it over,
|
|
930
|
-
* feature-detected so the X11 pane path never notices): geometry
|
|
931
|
-
*
|
|
939
|
+
* feature-detected so the X11 pane path never notices): geometry, input
|
|
940
|
+
* and the host's word that it has looked a present up come in,
|
|
941
|
+
* pane-present and pane-cursor go out. `players` is the
|
|
932
942
|
* host's word that it plays the pane's `<video src>`s, which makes
|
|
933
943
|
* `createPlayer` here — before the pane's first render, which asks.
|
|
934
944
|
*/
|
|
@@ -956,7 +966,12 @@ export class CocoaApp {
|
|
|
956
966
|
}
|
|
957
967
|
const pane = this._paneWindow ?? [...this._windows.values()][0];
|
|
958
968
|
if (!pane) return;
|
|
959
|
-
if (msg?.type === 'pane-
|
|
969
|
+
if (msg?.type === 'pane-shown') {
|
|
970
|
+
// the host has looked up this window's presents to `seq`, so the
|
|
971
|
+
// buffers they named may go (`CocoaPaneWindow._releaseRing`)
|
|
972
|
+
const wnd = msg.window != null ? this._windows.get(msg.window) : pane;
|
|
973
|
+
wnd?._shown?.(msg.seq);
|
|
974
|
+
} else if (msg?.type === 'pane-rect') {
|
|
960
975
|
pane.setPaneSize(msg.width, msg.height, msg.scale, msg.screen);
|
|
961
976
|
this._flushPaneOutbox();
|
|
962
977
|
this._afterInput();
|
|
@@ -1203,11 +1218,12 @@ export class CocoaApp {
|
|
|
1203
1218
|
const surfaces = [];
|
|
1204
1219
|
for (const entry of queue) {
|
|
1205
1220
|
// A window whose last flip has not reached its layer yet (threaded
|
|
1206
|
-
// mode's fence, `CocoaWindow.
|
|
1207
|
-
//
|
|
1208
|
-
//
|
|
1221
|
+
// mode's fence, `CocoaWindow.frameHeld`), or a pane with no buffer the
|
|
1222
|
+
// host is done with (`CocoaPaneWindow.frameHeld`), waits before its
|
|
1223
|
+
// clock is asked, or the clock would count a frame that did not run.
|
|
1224
|
+
// The release is an event — a batch, a `pane-shown` — and it ticks
|
|
1209
1225
|
// again.
|
|
1210
|
-
if (entry.wnd?.
|
|
1226
|
+
if (entry.wnd?.frameHeld?.()) {
|
|
1211
1227
|
this._rafQueue.push(entry);
|
|
1212
1228
|
continue;
|
|
1213
1229
|
}
|
|
@@ -1553,6 +1569,8 @@ export class CocoaApp {
|
|
|
1553
1569
|
keysym: decoded.keysym,
|
|
1554
1570
|
baseKeysym: decoded.baseKeysym,
|
|
1555
1571
|
codepoint: decoded.codepoint,
|
|
1572
|
+
// more than one character, which no code point carries (keymap.js)
|
|
1573
|
+
text: decoded.text,
|
|
1556
1574
|
buttons: modifierMask(ev),
|
|
1557
1575
|
group: 0,
|
|
1558
1576
|
time: ev.time,
|
package/src/cocoa/globalmenu.js
CHANGED
|
@@ -35,6 +35,9 @@ const FLAG_COMMAND = 1 << 20;
|
|
|
35
35
|
* user presses. Multi-chord sequences and non-character keys have no
|
|
36
36
|
* NSMenu spelling and are dropped (the shortcut still works — the app's
|
|
37
37
|
* own key handling is untouched; only the menu's hint goes unshown).
|
|
38
|
+
*
|
|
39
|
+
* `named` is the modifiers the chord itself asked for, before the ⌘ a bare
|
|
40
|
+
* key is given so that NSMenu has something to show.
|
|
38
41
|
*/
|
|
39
42
|
function keyEquivalent(shortcut) {
|
|
40
43
|
const chord = Array.isArray(shortcut) ? shortcut[0] : null;
|
|
@@ -48,7 +51,26 @@ function keyEquivalent(shortcut) {
|
|
|
48
51
|
else key = part;
|
|
49
52
|
}
|
|
50
53
|
if (typeof key !== 'string' || [...key].length !== 1) return null;
|
|
51
|
-
return {
|
|
54
|
+
return {
|
|
55
|
+
key: key.toLowerCase(),
|
|
56
|
+
modifiers: modifiers || FLAG_COMMAND,
|
|
57
|
+
named: modifiers,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Does the menu bar answer this `shortcut` itself, on the chord the app
|
|
63
|
+
* wrote? The ones it holds as a key equivalent that carries ⌘ because the
|
|
64
|
+
* chord said `Control` or `Super`: AppKit offers a ⌘ key to the main menu's
|
|
65
|
+
* `performKeyEquivalent:` inside `[NSApp sendEvent:]`, so the item fires
|
|
66
|
+
* with no help from us. A bare key padded out to ⌘ is not one — the chord
|
|
67
|
+
* names the bare key, and the window still answers that.
|
|
68
|
+
*
|
|
69
|
+
* The bridge shows JS every key before AppKit sees it, so a `MenuBar` that
|
|
70
|
+
* matched these too would run the item twice (`useMenuAccelerators`).
|
|
71
|
+
*/
|
|
72
|
+
export function answersShortcut(shortcut) {
|
|
73
|
+
return Boolean(keyEquivalent(shortcut)?.named & FLAG_COMMAND);
|
|
52
74
|
}
|
|
53
75
|
|
|
54
76
|
/**
|
|
@@ -117,6 +139,15 @@ export class CocoaGlobalMenuExport {
|
|
|
117
139
|
if (this.app._activeGlobalMenu === this) this._install();
|
|
118
140
|
}
|
|
119
141
|
|
|
142
|
+
/**
|
|
143
|
+
* The delegate's half of the key: the shortcuts the menu bar answers on
|
|
144
|
+
* its own, which the window must then leave alone (`answersShortcut`).
|
|
145
|
+
* The D-Bus export has no such method, because a panel delivers no keys.
|
|
146
|
+
*/
|
|
147
|
+
answersShortcut(shortcut) {
|
|
148
|
+
return answersShortcut(shortcut);
|
|
149
|
+
}
|
|
150
|
+
|
|
120
151
|
_install() {
|
|
121
152
|
this.app._native.setMainMenu(this._spec());
|
|
122
153
|
}
|
package/src/cocoa/keymap.js
CHANGED
|
@@ -5,8 +5,39 @@
|
|
|
5
5
|
// have fixed keysyms looked up here by kVK_* virtual key code. NSEvent's
|
|
6
6
|
// three character views map onto the three keysym roles events.js reads:
|
|
7
7
|
//
|
|
8
|
-
// charsBase (modifiers stripped)
|
|
9
|
-
//
|
|
8
|
+
// charsBase (modifiers stripped) -> baseKeysym — what chords match
|
|
9
|
+
// chars (every modifier applied) -> keysym / codepoint — what was
|
|
10
|
+
// typed, unless ⌘ or ⌃ is held
|
|
11
|
+
// charsShifted (only Shift applied) -> keysym / codepoint of a ⌘ or
|
|
12
|
+
// ⌃ chord, which types nothing
|
|
13
|
+
//
|
|
14
|
+
// **⌥ types on a Mac.** It is the layout's third and fourth level: ⌥S is ß on
|
|
15
|
+
// a US keyboard, and on a German one ⌥L is @, ⌥5 [ and ⌥8 { — the only way
|
|
16
|
+
// to type them. `charsShifted` is `charactersIgnoringModifiers`, which drops
|
|
17
|
+
// ⌥ along with everything but Shift, so reading the typed character there
|
|
18
|
+
// made ⌥S type `s` and left a German keyboard no @ at all. `chars` is
|
|
19
|
+
// `characters`, the layout's own answer with ⌥, Shift and Caps Lock applied —
|
|
20
|
+
// Caps Lock is the other thing `charsShifted` leaves out, so a field typed
|
|
21
|
+
// lower case with it on. It is also exactly the DOM's `key` on macOS: ⌥S is
|
|
22
|
+
// `ß`, ⌘S is `s`.
|
|
23
|
+
//
|
|
24
|
+
// There is no input method behind this. The bridge's view swallows
|
|
25
|
+
// `keyDown:` without `interpretKeyEvents:` and is no `NSTextInputClient`
|
|
26
|
+
// (docs/macos.md, "IME"), so these fields are all the text there is. Two
|
|
27
|
+
// consequences, both handled below:
|
|
28
|
+
//
|
|
29
|
+
// - **A dead key types nothing.** ⌥E on a US layout leaves `chars` empty, and
|
|
30
|
+
// the key it is waiting for decides what the accent becomes. Typing the
|
|
31
|
+
// key's own letter, which is what reading `charsShifted` did, put an `e` in
|
|
32
|
+
// the field. Whether the next key's `chars` carries the composed `é` is the
|
|
33
|
+
// window server's business — an NSEvent built in-process does not carry the
|
|
34
|
+
// state, and a real keyboard was not measured — so nothing here depends on
|
|
35
|
+
// it: a composed character is taken as typed, and a plain one is the
|
|
36
|
+
// accent lost rather than a letter too many.
|
|
37
|
+
// - **A key can type more than one character** — the accent and a letter it
|
|
38
|
+
// does not combine with, `´s`, where the layout composes. No code point
|
|
39
|
+
// carries two, so the whole string rides `text` and events.js commits it
|
|
40
|
+
// the way it commits an input method's text.
|
|
10
41
|
//
|
|
11
42
|
// Function and editing keys type Unicode private-use characters (U+F700…),
|
|
12
43
|
// which must never leak out as code points; the kVK table wins over the
|
|
@@ -48,30 +79,56 @@ const VK_KEYSYMS = new Map([
|
|
|
48
79
|
[111, 0xffc9], // F12
|
|
49
80
|
]);
|
|
50
81
|
|
|
51
|
-
|
|
82
|
+
// The block NSEvent's function-key constants live in (NSUpArrowFunctionKey is
|
|
83
|
+
// U+F700). Not the whole U+F700–U+F8FF range Apple reserves: U+F8FF is the
|
|
84
|
+
// Apple logo, which ⌥⇧K types and the system font draws.
|
|
85
|
+
const FUNCTION_KEY = (cp) => cp >= 0xf700 && cp <= 0xf7ff;
|
|
86
|
+
|
|
87
|
+
const printable = (cp) => cp != null && cp >= 0x20 && cp !== 0x7f;
|
|
52
88
|
|
|
53
89
|
function keysymFromChars(chars) {
|
|
54
90
|
if (!chars) return 0;
|
|
55
91
|
const cp = chars.codePointAt(0);
|
|
56
|
-
if (cp
|
|
92
|
+
if (!printable(cp) || FUNCTION_KEY(cp)) return 0;
|
|
57
93
|
return keysymOf(String.fromCodePoint(cp));
|
|
58
94
|
}
|
|
59
95
|
|
|
60
96
|
/**
|
|
61
|
-
* The
|
|
62
|
-
* @windowkit/appkit payload
|
|
97
|
+
* The key facts events.js reads off a native key event, from the raw
|
|
98
|
+
* @windowkit/appkit payload: `keysym`, `baseKeysym` and `codepoint`, and
|
|
99
|
+
* `text` when the key typed more than one character.
|
|
63
100
|
*/
|
|
64
101
|
export function decodeKey(ev) {
|
|
65
102
|
const fixed = VK_KEYSYMS.get(ev.keyCode);
|
|
66
103
|
if (fixed) {
|
|
67
104
|
return { keysym: fixed, baseKeysym: fixed, codepoint: undefined };
|
|
68
105
|
}
|
|
69
|
-
const
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
106
|
+
const baseKeysym =
|
|
107
|
+
keysymFromChars(ev.charsBase) || keysymFromChars(ev.charsShifted);
|
|
108
|
+
// A ⌘ or ⌃ chord types nothing, and is named by its key: ⌘⌥S is a chord
|
|
109
|
+
// on S, not on ß. (`chars` for ⌃S is a control character besides.)
|
|
110
|
+
// `chars` is absent from a bridge older than the field, and from a test's
|
|
111
|
+
// event that only says which key it was.
|
|
112
|
+
const chord = ev.command || ev.control;
|
|
113
|
+
const typed = chord ? ev.charsShifted : (ev.chars ?? ev.charsShifted);
|
|
114
|
+
const points = typed ? Array.from(typed) : [];
|
|
115
|
+
if (points.length > 1) {
|
|
116
|
+
return {
|
|
117
|
+
keysym: keysymFromChars(ev.charsShifted) || baseKeysym,
|
|
118
|
+
baseKeysym,
|
|
119
|
+
codepoint: undefined,
|
|
120
|
+
text: typed,
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
const keysym = keysymFromChars(typed);
|
|
124
|
+
const cp = typed?.codePointAt(0);
|
|
125
|
+
const codepoint = printable(cp) && !FUNCTION_KEY(cp) ? cp : undefined;
|
|
126
|
+
return {
|
|
127
|
+
// a dead key typed nothing; the key it sits on is what a chord names
|
|
128
|
+
keysym: keysym || keysymFromChars(ev.charsShifted) || baseKeysym,
|
|
129
|
+
baseKeysym,
|
|
130
|
+
codepoint,
|
|
131
|
+
};
|
|
75
132
|
}
|
|
76
133
|
|
|
77
134
|
/** AppKit modifier booleans -> the X-style state mask events carry. */
|