react-x11 2.16.0 → 2.17.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +38 -23
- package/package.json +3 -1
- package/src/Reconciler.js +82 -23
- package/src/a11y.js +18 -1
- package/src/acceleratorhooks.js +40 -6
- package/src/anchor.js +20 -2
- package/src/appcontext.js +8 -0
- package/src/appearance.js +36 -0
- package/src/{cocoa → backend}/context2d.js +27 -7
- package/src/capabilities.js +99 -1
- package/src/cocoa/app.js +204 -6
- package/src/cocoa/fonts.js +1 -1
- package/src/cocoa/overlay.js +2 -2
- package/src/cocoa/panewindow.js +2 -2
- package/src/cocoa/presenter.js +2 -2
- package/src/cocoa/surface.js +3 -3
- package/src/cocoa/window.js +2 -2
- package/src/events.js +21 -0
- package/src/foreignnodes.js +8 -3
- package/src/frame/index.js +30 -4
- package/src/glnodes.js +12 -1
- package/src/idle.js +59 -1
- package/src/index.d.ts +51 -1
- package/src/index.js +30 -3
- package/src/keysymchars.js +47 -0
- package/src/keysyms.d.ts +19 -1
- package/src/keysyms.js +107 -8
- package/src/launcher.js +17 -8
- package/src/launcherhooks.js +24 -10
- package/src/node.d.ts +1 -1
- package/src/nodes/cascade.js +9 -0
- package/src/nodes/node.js +6 -1
- package/src/nodes/window/hints.js +21 -2
- package/src/nodes/window/window.js +2 -2
- package/src/notifications.js +39 -14
- package/src/screens.js +159 -24
- package/src/taskbarhooks.js +164 -0
- package/src/transfer.js +20 -1
- package/src/trayhooks.js +1 -1
- package/src/types/capabilities.d.ts +32 -3
- package/src/types/elements.d.ts +23 -1
- package/src/types/events.d.ts +21 -0
- package/src/types/filedialog.d.ts +3 -1
- package/src/types/launcher.d.ts +20 -6
- package/src/types/taskbar.d.ts +79 -0
- package/src/wayland/context2d.js +1 -1
- package/src/wayland/xkb.js +170 -59
- package/src/win32/a11y.js +604 -0
- package/src/win32/app.js +768 -0
- package/src/win32/bezels.js +158 -0
- package/src/win32/dnd.js +283 -0
- package/src/win32/fonts.js +497 -0
- package/src/win32/glarea.js +548 -0
- package/src/win32/ime.js +267 -0
- package/src/win32/keymap.js +116 -0
- package/src/win32/native.js +54 -0
- package/src/win32/panehost.js +106 -0
- package/src/win32/panewindow.js +343 -0
- package/src/win32/shell.js +426 -0
- package/src/win32/surface.js +192 -0
- package/src/win32/window.js +659 -0
- package/src/windowid.js +128 -20
package/src/types/elements.d.ts
CHANGED
|
@@ -371,7 +371,29 @@ export interface WindowProps
|
|
|
371
371
|
y?: number;
|
|
372
372
|
/** Palette that `$token` style values resolve against, for this subtree. */
|
|
373
373
|
theme?: Record<string, string | number>;
|
|
374
|
-
/**
|
|
374
|
+
/**
|
|
375
|
+
* The desktop identity of this window — which application it belongs to,
|
|
376
|
+
* and so which launcher icon it groups under and which `.desktop` entry a
|
|
377
|
+
* launcher matches it to.
|
|
378
|
+
*
|
|
379
|
+
* One name for what every desktop calls something else: ICCCM `WM_CLASS` on
|
|
380
|
+
* X11, `xdg_toplevel.set_app_id` on Wayland, the AppUserModelID on Windows.
|
|
381
|
+
* Normally the same string as `registerApplication({ appId })`, which is
|
|
382
|
+
* what a launcher matches against.
|
|
383
|
+
*
|
|
384
|
+
* A string is the modern single-id form. X11's instance/class pair is still
|
|
385
|
+
* accepted and passed through on that backend; every other backend takes
|
|
386
|
+
* the class, which is the part naming the application.
|
|
387
|
+
*
|
|
388
|
+
* Honoured on X11, on Windows — where it is the window's AppUserModelID,
|
|
389
|
+
* and where the jump list is attached to the same id — and, for windows
|
|
390
|
+
* opened after it is set, on Wayland, whose `app_id` is fixed when the
|
|
391
|
+
* surface is created. The cocoa backend accepts it and does nothing with
|
|
392
|
+
* it: macOS identity is the bundle's, decided at build time.
|
|
393
|
+
*/
|
|
394
|
+
appId?: string | [string, string] | { instance: string; class?: string };
|
|
395
|
+
/** @deprecated Renamed to {@link WindowProps.appId} — `wmClass` is X11's
|
|
396
|
+
* word for an identity every desktop has. Still accepted. */
|
|
375
397
|
wmClass?: string | [string, string] | { instance: string; class?: string };
|
|
376
398
|
/** EWMH `_NET_WM_WINDOW_TYPE`, or a list of fallbacks. */
|
|
377
399
|
windowType?: WindowType | WindowType[];
|
package/src/types/events.d.ts
CHANGED
|
@@ -39,6 +39,22 @@ export interface SyntheticEvent<T = DrawnNode> {
|
|
|
39
39
|
* what a node's `abs` is in (docs/scale.md). */
|
|
40
40
|
x: number;
|
|
41
41
|
y: number;
|
|
42
|
+
/**
|
|
43
|
+
* Where the pointer is on the **virtual screen**, in the same logical
|
|
44
|
+
* pixels `x`/`y` are in — what to reach for when placing something outside
|
|
45
|
+
* the window, such as a context menu at the pointer.
|
|
46
|
+
*
|
|
47
|
+
* Present exactly where the backend reported a position. That is most
|
|
48
|
+
* events, including keys on X11, whose KeyPress carries where the pointer
|
|
49
|
+
* was; it is undefined rather than `0` where a backend reported none, since
|
|
50
|
+
* zero is the screen's top-left corner and not an absence.
|
|
51
|
+
*
|
|
52
|
+
* `nativeEvent.rootx`/`rooty` is X11's name for the same point and is still
|
|
53
|
+
* there, in *device* pixels — the same split as `x` against
|
|
54
|
+
* `nativeEvent.x`.
|
|
55
|
+
*/
|
|
56
|
+
screenX?: number;
|
|
57
|
+
screenY?: number;
|
|
42
58
|
/** Coordinates relative to `target`'s box. */
|
|
43
59
|
localX: number;
|
|
44
60
|
localY: number;
|
|
@@ -541,6 +557,11 @@ export interface AcceleratorOptions {
|
|
|
541
557
|
* matched against the Latin keysym so a layout switch does not turn it off,
|
|
542
558
|
* and behind whatever a focused element consumed with `preventDefault()`.
|
|
543
559
|
* See docs/events.md.
|
|
560
|
+
*
|
|
561
|
+
* With no `scope` the binding belongs to the tree's top-level `<window>`, or,
|
|
562
|
+
* in an app that has none, to the root-level `<popup>` holding the keyboard —
|
|
563
|
+
* a tray popover. One that can reach neither binds nothing and says so once
|
|
564
|
+
* in development.
|
|
544
565
|
*/
|
|
545
566
|
export function useAccelerator(
|
|
546
567
|
shortcut: MenuShortcut,
|
|
@@ -162,7 +162,9 @@ export interface FileDialogs {
|
|
|
162
162
|
*
|
|
163
163
|
* Exact when the tree has one top-level window, which is nearly every app.
|
|
164
164
|
* With several it prefers the focused one and warns in development when it
|
|
165
|
-
* has to guess —
|
|
165
|
+
* has to guess; with none — a menu-bar app, which is a tray item and a
|
|
166
|
+
* popover — it answers the root-level `<popup>` that took the keyboard. See
|
|
167
|
+
* docs/filedialog.md.
|
|
166
168
|
*/
|
|
167
169
|
export declare function useTopLevelWindow(): {
|
|
168
170
|
readonly current: NtkWindow | DrawnNode | null;
|
package/src/types/launcher.d.ts
CHANGED
|
@@ -35,17 +35,23 @@ export declare function setBadge(
|
|
|
35
35
|
export declare function useBadge(value: BadgeValue): void;
|
|
36
36
|
|
|
37
37
|
/**
|
|
38
|
-
* The menu behind a right-click on the app's icon
|
|
39
|
-
* from the same item vocabulary `MenuBar`
|
|
40
|
-
* when picked. Installed while mounted,
|
|
41
|
-
* down on unmount.
|
|
38
|
+
* The menu behind a right-click on the app's **launcher icon** — the Dock on
|
|
39
|
+
* macOS, the launcher on Linux — from the same item vocabulary `MenuBar`
|
|
40
|
+
* takes; an item's `onSelect` fires when picked. Installed while mounted,
|
|
41
|
+
* replaced when `items` changes, taken down on unmount.
|
|
42
42
|
*
|
|
43
43
|
* `NSDockTile`'s menu on the cocoa backend; the launcher protocol's
|
|
44
44
|
* **quicklist** on Linux — a `com.canonical.dbusmenu` tree, the same menu
|
|
45
45
|
* protocol the tray and the global menu speak. Needs the identity
|
|
46
46
|
* `registerApplication({ appId })` establishes and a `.desktop` file of that
|
|
47
47
|
* name, like the badge.
|
|
48
|
+
*
|
|
49
|
+
* Reads `useDesktopCapability('launcher').features.menu`.
|
|
48
50
|
*/
|
|
51
|
+
export declare function useLauncherMenu(items: MenuItem[] | null): void;
|
|
52
|
+
|
|
53
|
+
/** @deprecated Renamed to {@link useLauncherMenu} — "Dock" is one desktop's
|
|
54
|
+
* word for the icon every desktop has. Still exported and still works. */
|
|
49
55
|
export declare function useDockMenu(items: MenuItem[] | null): void;
|
|
50
56
|
|
|
51
57
|
/**
|
|
@@ -80,9 +86,17 @@ export declare function setUrgent(
|
|
|
80
86
|
): Promise<boolean>;
|
|
81
87
|
|
|
82
88
|
/**
|
|
83
|
-
* {@link
|
|
84
|
-
* takes the menu down.
|
|
89
|
+
* {@link useLauncherMenu}'s imperative twin, for code with no component.
|
|
90
|
+
* `null` takes the menu down.
|
|
85
91
|
*/
|
|
92
|
+
export declare function setLauncherMenu(
|
|
93
|
+
items: MenuItem[] | null,
|
|
94
|
+
options?: SetBadgeOptions,
|
|
95
|
+
): Promise<boolean>;
|
|
96
|
+
|
|
97
|
+
/** @deprecated Renamed to {@link setLauncherMenu} — "quicklist" is the Unity
|
|
98
|
+
* launcher's word for the menu macOS calls the Dock menu, and this drove
|
|
99
|
+
* both all along. Still exported and still works. */
|
|
86
100
|
export declare function setQuicklist(
|
|
87
101
|
items: MenuItem[] | null,
|
|
88
102
|
options?: SetBadgeOptions,
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Windows taskbar's own surfaces. See docs/windows-integrations.md.
|
|
3
|
+
*
|
|
4
|
+
* Three things the taskbar has that no other desktop does, so none of them is
|
|
5
|
+
* a rung on an existing ladder. They are reported as features of the
|
|
6
|
+
* **launcher** — `useDesktopCapability('launcher').features.thumbnailToolbar`
|
|
7
|
+
* and its two siblings — because all of them hang off the one icon the
|
|
8
|
+
* desktop shows for this app, beside the badge and the progress bar. The
|
|
9
|
+
* hooks do nothing where the backend has none, so they are safe to call
|
|
10
|
+
* unconditionally and an app needs no platform check.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* An icon for a toolbar button: raw RGBA with its size, an ntk `Image`, or a
|
|
15
|
+
* path to a file. The shell tints it for the theme, so the shape is the whole
|
|
16
|
+
* of it and a white glyph on transparent is the usual choice.
|
|
17
|
+
*/
|
|
18
|
+
export type TaskbarIcon =
|
|
19
|
+
| string
|
|
20
|
+
| { data: Uint8Array; width: number; height: number }
|
|
21
|
+
| { width: number; height: number };
|
|
22
|
+
|
|
23
|
+
export interface ThumbnailToolbarButton {
|
|
24
|
+
/** What a click is reported as. Not the shell's index. */
|
|
25
|
+
id: string;
|
|
26
|
+
/** The hover tooltip; `label` is accepted as a synonym. */
|
|
27
|
+
tooltip?: string;
|
|
28
|
+
label?: string;
|
|
29
|
+
icon?: TaskbarIcon | null;
|
|
30
|
+
/** Default true. A disabled button is shown greyed rather than hidden. */
|
|
31
|
+
enabled?: boolean;
|
|
32
|
+
/** Close the hover preview when this one is clicked. */
|
|
33
|
+
dismissOnClick?: boolean;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Up to seven buttons under this window's taskbar hover preview — where a
|
|
38
|
+
* media player puts play and skip. `null` hides them, which is as far down as
|
|
39
|
+
* the shell allows: a window that has had a toolbar keeps one.
|
|
40
|
+
*
|
|
41
|
+
* The eighth button and beyond are dropped rather than refused, because the
|
|
42
|
+
* shell refuses the whole call for an eighth.
|
|
43
|
+
*/
|
|
44
|
+
export declare function useThumbnailToolbar(
|
|
45
|
+
buttons: readonly ThumbnailToolbarButton[] | null | undefined,
|
|
46
|
+
onClick?: (id: string, event: unknown) => void,
|
|
47
|
+
): void;
|
|
48
|
+
|
|
49
|
+
export interface JumpListTask {
|
|
50
|
+
title: string;
|
|
51
|
+
/** Passed to a **new** process; nothing calls back into this one. */
|
|
52
|
+
arguments?: string;
|
|
53
|
+
description?: string;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* The Tasks category of this application's jump list — the menu on a right
|
|
58
|
+
* click of its taskbar button. `null` deletes the category.
|
|
59
|
+
*
|
|
60
|
+
* Each entry **relaunches this executable** with the arguments given, which
|
|
61
|
+
* is what a jump-list task is. That is why this is not `useLauncherMenu`, whose
|
|
62
|
+
* items carry a callback: reported as `features.tasks` rather than
|
|
63
|
+
* `features.menu`, so an app cannot mistake one for the other.
|
|
64
|
+
*/
|
|
65
|
+
export declare function useJumpList(
|
|
66
|
+
tasks: readonly JumpListTask[] | null | undefined,
|
|
67
|
+
): void;
|
|
68
|
+
|
|
69
|
+
/** Notes `path` for the shell's Recent lists whenever it changes. */
|
|
70
|
+
export declare function useRecentDocument(path?: string | null): void;
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* The imperative form of {@link useRecentDocument}. Returns false where the
|
|
74
|
+
* backend has no Recent list, rather than throwing.
|
|
75
|
+
*/
|
|
76
|
+
export declare function noteRecentDocument(
|
|
77
|
+
path: string | null,
|
|
78
|
+
options?: { app?: unknown },
|
|
79
|
+
): boolean;
|
package/src/wayland/context2d.js
CHANGED
|
@@ -206,7 +206,7 @@ function scaleOf(m) {
|
|
|
206
206
|
|
|
207
207
|
/**
|
|
208
208
|
* The Render ops a `drawGlyphs` call can name, numbered as XRender numbers
|
|
209
|
-
* them — the same table the
|
|
209
|
+
* them — the same table the native backends answer (`src/backend/context2d.js`),
|
|
210
210
|
* so `ctx.Render.PictOp.Over` reads the same on every backend. The op is
|
|
211
211
|
* ignored here: text composites Over, and for the opaque inks text uses Src
|
|
212
212
|
* and Over agree.
|
package/src/wayland/xkb.js
CHANGED
|
@@ -19,8 +19,10 @@
|
|
|
19
19
|
// xkb_symbols `key <AD01> { [ q, Q ] };` keycode -> keysyms per level,
|
|
20
20
|
// with `symbols[Group2]`/`symbols[2]` and `type=` where given
|
|
21
21
|
// `modifier_map Mod5 { <LVL3> }` real modifier -> keycodes
|
|
22
|
-
// xkb_compat `interpret ISO_Level3_Shift { virtualModifier= LevelThree;
|
|
23
|
-
//
|
|
22
|
+
// xkb_compat `interpret ISO_Level3_Shift { virtualModifier= LevelThree;
|
|
23
|
+
// useModMapMods= level1; }` keysym -> virtual modifier,
|
|
24
|
+
// and which of a key's symbols
|
|
25
|
+
// may name it
|
|
24
26
|
//
|
|
25
27
|
// From the last two, a virtual modifier like `LevelThree` resolves to the
|
|
26
28
|
// real modifier bit the compositor will actually report in
|
|
@@ -29,8 +31,10 @@
|
|
|
29
31
|
// that does not say.
|
|
30
32
|
//
|
|
31
33
|
// What is not read: key actions (the compositor applies those; we only see
|
|
32
|
-
// their effect in the modifier state), indicators
|
|
33
|
-
//
|
|
34
|
+
// their effect in the modifier state), indicators and geometry. The per-type
|
|
35
|
+
// `preserve` rules *are* read, for the one thing they decide here: whether
|
|
36
|
+
// Caps Lock still has a capitalisation to do after the type has chosen a
|
|
37
|
+
// level (`_capitalises`).
|
|
34
38
|
//
|
|
35
39
|
// The output is two things. `keycode2keysyms` is the X core shape —
|
|
36
40
|
// `[g1l1, g1l2, g2l1, g2l2, …]` — because that is what `keyboard.js`'s
|
|
@@ -38,7 +42,7 @@
|
|
|
38
42
|
// `decode()` is the full answer, using the key's real type so that level 3
|
|
39
43
|
// and 4 (AltGr) resolve where the two-level core shape cannot express them.
|
|
40
44
|
|
|
41
|
-
import { charOf } from '../keysyms.js';
|
|
45
|
+
import { charOf, keysymToUpper } from '../keysyms.js';
|
|
42
46
|
import { keysymFromName } from './keysymnames.js';
|
|
43
47
|
|
|
44
48
|
/** Real modifier bits, as X and XKB both number them. */
|
|
@@ -135,7 +139,7 @@ export class XkbKeymap {
|
|
|
135
139
|
while ((m = re.exec(s))) {
|
|
136
140
|
const body = balanced(s, m.index + m[0].length - 1);
|
|
137
141
|
re.lastIndex = m.index + m[0].length + body.length;
|
|
138
|
-
const type = { name: m[1], mods: [], map: [], levels: 1 };
|
|
142
|
+
const type = { name: m[1], mods: [], map: [], preserve: [], levels: 1 };
|
|
139
143
|
const mods = body.match(/modifiers\s*=\s*([^;]+);/);
|
|
140
144
|
if (mods)
|
|
141
145
|
type.mods = mods[1]
|
|
@@ -153,6 +157,17 @@ export class XkbKeymap {
|
|
|
153
157
|
type.map.push({ set, level });
|
|
154
158
|
if (level > type.levels) type.levels = level;
|
|
155
159
|
}
|
|
160
|
+
// `preserve[Lock+LevelThree]= Lock;` — the modifiers this state does
|
|
161
|
+
// *not* consume, which is the whole of whether Caps Lock still applies
|
|
162
|
+
// on a key whose type otherwise swallows Lock.
|
|
163
|
+
for (const e of body.matchAll(/preserve\[([^\]]+)\]\s*=\s*([^;]+);/gi)) {
|
|
164
|
+
const split = (x) =>
|
|
165
|
+
x
|
|
166
|
+
.split('+')
|
|
167
|
+
.map((y) => y.trim())
|
|
168
|
+
.filter((y) => y && y !== 'none' && y !== 'None');
|
|
169
|
+
type.preserve.push({ set: split(e[1]), kept: split(e[2]) });
|
|
170
|
+
}
|
|
156
171
|
for (const e of body.matchAll(/level_name\[(?:Level)?(\d+)\]/gi)) {
|
|
157
172
|
if (+e[1] > type.levels) type.levels = +e[1];
|
|
158
173
|
}
|
|
@@ -161,6 +176,10 @@ export class XkbKeymap {
|
|
|
161
176
|
}
|
|
162
177
|
|
|
163
178
|
_parseCompat(s) {
|
|
179
|
+
// `interpret.useModMapMods= AnyLevel;` — the section default, which every
|
|
180
|
+
// keymap states, and which an interpretation overrides for itself.
|
|
181
|
+
const dflt = s.match(/interpret\.useModMapMods\s*=\s*([A-Za-z0-9_]+)/i);
|
|
182
|
+
const level1 = (word) => /^level(?:1|One)$/i.test(word ?? '');
|
|
164
183
|
// interpret <keysym>[+cond] { virtualModifier= X; ... }
|
|
165
184
|
const re = /interpret\s+([A-Za-z0-9_]+)(?:\+[^{]*)?\s*\{/g;
|
|
166
185
|
let m;
|
|
@@ -170,7 +189,12 @@ export class XkbKeymap {
|
|
|
170
189
|
const vm = body.match(/virtualModifier\s*=\s*([A-Za-z0-9_]+)/i);
|
|
171
190
|
if (!vm) continue;
|
|
172
191
|
const sym = keysymFromName(m[1]);
|
|
173
|
-
if (sym)
|
|
192
|
+
if (!sym) continue;
|
|
193
|
+
const umm = body.match(/useModMapMods\s*=\s*([A-Za-z0-9_]+)/i);
|
|
194
|
+
(this._interp ??= new Map()).set(sym, {
|
|
195
|
+
vmod: vm[1],
|
|
196
|
+
level1Only: level1(umm ? umm[1] : dflt?.[1]),
|
|
197
|
+
});
|
|
174
198
|
}
|
|
175
199
|
}
|
|
176
200
|
|
|
@@ -256,24 +280,51 @@ export class XkbKeymap {
|
|
|
256
280
|
}
|
|
257
281
|
|
|
258
282
|
/**
|
|
259
|
-
* Virtual modifier -> real modifier:
|
|
260
|
-
*
|
|
261
|
-
*
|
|
283
|
+
* Virtual modifier -> real modifier: a key's `interpret`ed keysyms name the
|
|
284
|
+
* virtual modifiers the key sets, and the key's `modifier_map` bits are
|
|
285
|
+
* what those virtual modifiers turn out to mean.
|
|
286
|
+
*
|
|
287
|
+
* Which of a key's keysyms may name one is `useModMapMods`, and it is the
|
|
288
|
+
* whole of AltGr working. `<RALT>` sits in `modifier_map Mod1` beside the
|
|
289
|
+
* other Alt keys and *also* carries `ISO_Level3_Shift`, on a secondary
|
|
290
|
+
* group or a second level:
|
|
291
|
+
*
|
|
292
|
+
* key <RALT> { type= "ONE_LEVEL", symbols[1]= [ Alt_R ],
|
|
293
|
+
* symbols[2]= [ ISO_Level3_Shift ] };
|
|
294
|
+
* modifier_map Mod1 { <LALT>, <RALT>, <ALT>, <META> };
|
|
295
|
+
* modifier_map Mod5 { <LVL3> };
|
|
296
|
+
*
|
|
297
|
+
* Attributing a key's bits to every keysym on it collected Mod1 from
|
|
298
|
+
* `<RALT>` on top of Mod5 from `<LVL3>`, so `LevelThree` resolved to
|
|
299
|
+
* `Mod1|Mod5` — and since `_levelFor` matches the masked state for
|
|
300
|
+
* equality, a real AltGr press (Mod5 alone) never reached level 3. AltGr
|
|
301
|
+
* did nothing and the third and fourth level of every layout were
|
|
302
|
+
* unreachable. `useModMapMods= level1`, which is what the keymap says for
|
|
303
|
+
* `ISO_Level3_Shift`, means a key contributes only where the keysym is its
|
|
304
|
+
* **primary** symbol — group 1, level 1 — and `<RALT>`'s primary symbol is
|
|
305
|
+
* `Alt_R`, so it never should have contributed.
|
|
262
306
|
*/
|
|
263
307
|
_resolveVmods() {
|
|
264
|
-
const
|
|
308
|
+
const bind = (vmod, bits) =>
|
|
309
|
+
this.vmods.set(vmod, (this.vmods.get(vmod) ?? 0) | bits);
|
|
265
310
|
for (const [code, key] of this.keys) {
|
|
266
311
|
const bits = this.modmap.get(code);
|
|
267
312
|
if (!bits) continue;
|
|
268
|
-
for (
|
|
269
|
-
|
|
270
|
-
|
|
313
|
+
for (let gi = 0; gi < key.groups.length; gi++) {
|
|
314
|
+
const syms = key.groups[gi]?.syms ?? [];
|
|
315
|
+
for (let li = 0; li < syms.length; li++) {
|
|
316
|
+
const interp = syms[li] && this._interp?.get(syms[li]);
|
|
317
|
+
if (!interp) continue;
|
|
318
|
+
if (interp.level1Only && (gi || li)) continue;
|
|
319
|
+
bind(interp.vmod, bits);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
271
322
|
}
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
for (const
|
|
275
|
-
const
|
|
276
|
-
if (
|
|
323
|
+
// `modifier_map Shift { Shift_L };` — the bare-keysym form names no key,
|
|
324
|
+
// so there is no level to test it against.
|
|
325
|
+
for (const { sym, bit } of this._modmapSyms ?? []) {
|
|
326
|
+
const interp = this._interp?.get(sym);
|
|
327
|
+
if (interp) bind(interp.vmod, bit);
|
|
277
328
|
}
|
|
278
329
|
for (const [name, bit] of Object.entries(VMOD_FALLBACK)) {
|
|
279
330
|
if (!this.vmods.has(name)) this.vmods.set(name, bit);
|
|
@@ -286,6 +337,13 @@ export class XkbKeymap {
|
|
|
286
337
|
return this.vmods.get(name) ?? 0;
|
|
287
338
|
}
|
|
288
339
|
|
|
340
|
+
/** A list of modifier names as one real mask. */
|
|
341
|
+
_maskOf(names) {
|
|
342
|
+
let mask = 0;
|
|
343
|
+
for (const name of names) mask |= this._modMask(name);
|
|
344
|
+
return mask;
|
|
345
|
+
}
|
|
346
|
+
|
|
289
347
|
/**
|
|
290
348
|
* The X core keyboard mapping — two keysyms per group, up to four groups —
|
|
291
349
|
* which is what `GetKeyboardMapping` would have answered.
|
|
@@ -319,17 +377,37 @@ export class XkbKeymap {
|
|
|
319
377
|
*/
|
|
320
378
|
_levelFor(type, mods) {
|
|
321
379
|
if (!type) return mods & REAL_MODS.Shift ? 1 : 0;
|
|
322
|
-
|
|
323
|
-
for (const m of type.mods) relevant |= this._modMask(m);
|
|
380
|
+
const relevant = this._maskOf(type.mods);
|
|
324
381
|
const masked = mods & relevant;
|
|
325
382
|
for (const { set, level } of type.map) {
|
|
326
|
-
|
|
327
|
-
for (const m of set) want |= this._modMask(m);
|
|
328
|
-
if (want === masked) return level - 1;
|
|
383
|
+
if (this._maskOf(set) === masked) return level - 1;
|
|
329
384
|
}
|
|
330
385
|
return 0;
|
|
331
386
|
}
|
|
332
387
|
|
|
388
|
+
/**
|
|
389
|
+
* Whether Caps Lock still has a capitalisation to do.
|
|
390
|
+
*
|
|
391
|
+
* XKB's rule: Lock is effective, and the key's type did not **consume** it.
|
|
392
|
+
* A type that names Lock among its modifiers consumed it — `ALPHABETIC`'s
|
|
393
|
+
* `map[Lock]= 2` has already picked the level Caps Lock wanted, and
|
|
394
|
+
* capitalising on top would be doing it twice. Unless the type says
|
|
395
|
+
* otherwise: `preserve[Lock+LevelThree]= Lock` hands Lock back, which is how
|
|
396
|
+
* German's AltGr levels capitalise (`ſ` -> `S`) while its Shift levels,
|
|
397
|
+
* reached through `map[Lock]`, do not.
|
|
398
|
+
*/
|
|
399
|
+
_capitalises(type, mods) {
|
|
400
|
+
if (!(mods & REAL_MODS.Lock)) return false;
|
|
401
|
+
if (!type) return true;
|
|
402
|
+
const relevant = this._maskOf(type.mods);
|
|
403
|
+
if (!(relevant & REAL_MODS.Lock)) return true;
|
|
404
|
+
const masked = mods & relevant;
|
|
405
|
+
for (const { set, kept } of type.preserve)
|
|
406
|
+
if (this._maskOf(set) === masked)
|
|
407
|
+
return (this._maskOf(kept) & REAL_MODS.Lock) !== 0;
|
|
408
|
+
return false;
|
|
409
|
+
}
|
|
410
|
+
|
|
333
411
|
/**
|
|
334
412
|
* Decode a key event.
|
|
335
413
|
*
|
|
@@ -349,31 +427,17 @@ export class XkbKeymap {
|
|
|
349
427
|
let level = this._levelFor(g.type, mods);
|
|
350
428
|
if (level >= g.syms.length || !g.syms[level]) {
|
|
351
429
|
// Lock on a one-level key, or Shift on a key with no upper level:
|
|
352
|
-
// fall back the way the core protocol does
|
|
353
|
-
//
|
|
430
|
+
// fall back the way the core protocol does, to the first symbol. Any
|
|
431
|
+
// capitalisation it has coming is the separate step below.
|
|
354
432
|
level = 0;
|
|
355
433
|
}
|
|
356
434
|
let keysym = g.syms[level] ?? 0;
|
|
357
|
-
// Caps Lock
|
|
358
|
-
//
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
!(mods & REAL_MODS.Shift)
|
|
364
|
-
) {
|
|
365
|
-
const lower = charOf(g.syms[0]);
|
|
366
|
-
const upper = charOf(g.syms[1]);
|
|
367
|
-
if (
|
|
368
|
-
lower &&
|
|
369
|
-
upper &&
|
|
370
|
-
lower !== upper &&
|
|
371
|
-
lower.toUpperCase() === upper &&
|
|
372
|
-
g.type?.mods?.includes('Lock') === false
|
|
373
|
-
) {
|
|
374
|
-
keysym = g.syms[1];
|
|
375
|
-
}
|
|
376
|
-
}
|
|
435
|
+
// Caps Lock capitalises the keysym the type already chose, rather than
|
|
436
|
+
// reaching for an uppercase sibling level. Pairing levels works for
|
|
437
|
+
// `[a, A]` and nothing else: AZERTY's `é` key is `[é, 2, ~, ˘]`, where
|
|
438
|
+
// level 2 is a digit, and German's AltGr `ſ` has no sibling at all — so
|
|
439
|
+
// `é`, `à`, `è`, `ç`, `ù` and every Cyrillic letter stayed lowercase.
|
|
440
|
+
if (this._capitalises(g.type, mods)) keysym = keysymToUpper(keysym);
|
|
377
441
|
if (!keysym) return undefined;
|
|
378
442
|
const ch = charOf(keysym);
|
|
379
443
|
const codepoint = ch ? ch.codePointAt(0) : undefined;
|
|
@@ -398,24 +462,71 @@ export class XkbKeymap {
|
|
|
398
462
|
}
|
|
399
463
|
|
|
400
464
|
/**
|
|
401
|
-
*
|
|
402
|
-
*
|
|
403
|
-
*
|
|
465
|
+
* `XkbKSIsKeypad`: the one run of keysyms the keypad produces, `KP_Space`
|
|
466
|
+
* through `KP_Equal`. Every keypad key is in it on both of its levels — the
|
|
467
|
+
* navigation keysym (`KP_Home`) as much as the digit (`KP_7`).
|
|
468
|
+
*/
|
|
469
|
+
function isKeypad(sym) {
|
|
470
|
+
return sym >= 0xff80 && sym <= 0xffbd;
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
/**
|
|
474
|
+
* `XkbKSIsLower` and `XkbKSIsUpper`: a keysym has a case, and is the lower or
|
|
475
|
+
* the upper one of the pair.
|
|
476
|
+
*
|
|
477
|
+
* Two independent questions rather than one pairing — which is the same
|
|
478
|
+
* mistake `decode()` used to make about Caps Lock, in the one other place it
|
|
479
|
+
* was made. German's `s` is `[s, S, ſ, ẞ]`, and `ſ` does not *pair* with `ẞ`
|
|
480
|
+
* (`'ſ'.toUpperCase()` is `'S'`), but `ſ` is a lowercase letter and `ẞ` is an
|
|
481
|
+
* uppercase one, which is all the type ladder asks. Pairing them made the key
|
|
482
|
+
* FOUR_LEVEL_SEMIALPHABETIC, whose `preserve[Lock+LevelThree]` hands Lock back
|
|
483
|
+
* — so Caps+AltGr capitalised a level that was already capital.
|
|
484
|
+
*/
|
|
485
|
+
function isLower(sym) {
|
|
486
|
+
const ch = charOf(sym);
|
|
487
|
+
return !!ch && ch.toLowerCase() === ch && ch.toUpperCase() !== ch;
|
|
488
|
+
}
|
|
489
|
+
function isUpper(sym) {
|
|
490
|
+
const ch = charOf(sym);
|
|
491
|
+
return !!ch && ch.toUpperCase() === ch && ch.toLowerCase() !== ch;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
/**
|
|
495
|
+
* The type XKB assigns a key that names none — xkbcomp's `FindAutomaticType`,
|
|
496
|
+
* which libxkbcommon inherits. Two symbols are ALPHABETIC when they are a
|
|
497
|
+
* case pair, **KEYPAD when either of them is a keypad keysym**, and TWO_LEVEL
|
|
498
|
+
* otherwise; three or four are the FOUR_LEVEL family along the same ladder.
|
|
499
|
+
*
|
|
500
|
+
* The keypad rung is not decoration: libxkbcommon writes the keypad bare —
|
|
501
|
+
* `key <KP7> { [ KP_Home, KP_7 ] };` — so *every* keypad key lands here, and
|
|
502
|
+
* `[KP_Home, KP_7]` is not a case pair. Without the rung it fell through to
|
|
503
|
+
* TWO_LEVEL, whose `map[Shift]= 2` puts the digit on Shift and leaves NumLock
|
|
504
|
+
* with nothing to do, which inverted the whole keypad: the digits typed
|
|
505
|
+
* `KP_Home`/`KP_Up`/`KP_End` and moved the cursor, and holding Shift is what
|
|
506
|
+
* produced a number. The keymap's own KEYPAD type — `modifiers= Shift+NumLock;
|
|
507
|
+
* map[NumLock]= 2;` — is the one that belongs.
|
|
404
508
|
*/
|
|
405
509
|
function implicitType(syms) {
|
|
406
|
-
|
|
510
|
+
// The width is the length of the list with only *trailing* NoSymbols
|
|
511
|
+
// trimmed, which is how libxkbcommon counts it. A NoSymbol in the middle
|
|
512
|
+
// is a level that types nothing, not an absent one: counting the non-zero
|
|
513
|
+
// entries instead made `key <ALT> { [ NoSymbol, Alt_L ] };` a one-level key
|
|
514
|
+
// whose only level was NoSymbol, so <ALT>, <META>, <SUPR> and <HYPR>
|
|
515
|
+
// decoded to nothing in every modifier state.
|
|
516
|
+
let n = syms.length;
|
|
517
|
+
while (n > 0 && !syms[n - 1]) n--;
|
|
407
518
|
if (n <= 1) return 'ONE_LEVEL';
|
|
408
|
-
const
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
if (
|
|
415
|
-
return
|
|
519
|
+
const cased = (a, b) => isLower(a) && isUpper(b);
|
|
520
|
+
const keypad = isKeypad(syms[0]) || isKeypad(syms[1]);
|
|
521
|
+
if (n === 2) {
|
|
522
|
+
if (cased(syms[0], syms[1])) return 'ALPHABETIC';
|
|
523
|
+
return keypad ? 'KEYPAD' : 'TWO_LEVEL';
|
|
524
|
+
}
|
|
525
|
+
if (cased(syms[0], syms[1]))
|
|
526
|
+
return cased(syms[2], syms[3])
|
|
416
527
|
? 'FOUR_LEVEL_ALPHABETIC'
|
|
417
528
|
: 'FOUR_LEVEL_SEMIALPHABETIC';
|
|
418
|
-
return 'FOUR_LEVEL';
|
|
529
|
+
return keypad ? 'FOUR_LEVEL_KEYPAD' : 'FOUR_LEVEL';
|
|
419
530
|
}
|
|
420
531
|
|
|
421
532
|
/** Everything from `//` or `#` to the end of the line, outside strings. */
|