ntk 7.3.3 → 7.5.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/lib/app.js +49 -0
- package/lib/drawable.js +5 -0
- package/lib/events_map.js +11 -1
- package/lib/index.js +19 -0
- package/lib/window.js +313 -41
- package/lib/xembed.js +922 -0
- package/lib/xi2.js +290 -0
- package/package.json +3 -2
package/lib/app.js
CHANGED
|
@@ -93,6 +93,8 @@ export default class App {
|
|
|
93
93
|
this._solidPictures = new Map();
|
|
94
94
|
this._rasterizer = undefined;
|
|
95
95
|
this._shm = undefined;
|
|
96
|
+
this._xinputPromise = null;
|
|
97
|
+
this._devicesPromise = null;
|
|
96
98
|
// node-x11 emits X errors it cannot route to a request callback as
|
|
97
99
|
// 'error' on the client — from inside its packet parser. With no
|
|
98
100
|
// listener that emit throws and the parser never re-arms, silently
|
|
@@ -299,6 +301,53 @@ export default class App {
|
|
|
299
301
|
});
|
|
300
302
|
}
|
|
301
303
|
|
|
304
|
+
/**
|
|
305
|
+
* The XInput extension, or `null` where the server has none — asked once
|
|
306
|
+
* per connection.
|
|
307
|
+
*
|
|
308
|
+
* `ext.xi2` is `null` on a server that answers the extension query but
|
|
309
|
+
* speaks XI1 only, and every XI2 request needs it, so callers check both.
|
|
310
|
+
*
|
|
311
|
+
* @returns {Promise<object|null>}
|
|
312
|
+
*/
|
|
313
|
+
xinput() {
|
|
314
|
+
if (!this._xinputPromise) {
|
|
315
|
+
this._xinputPromise = new Promise((resolve) => {
|
|
316
|
+
this.X.require('xinput', (err, ext) => resolve(err ? null : ext));
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
return this._xinputPromise;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* Every input device the server knows about, from XI2's `XIQueryDevice`:
|
|
324
|
+
* `{deviceId, use, attachment, enabled, name, classes}`, the classes
|
|
325
|
+
* describing each device's keys, buttons, valuators and scroll axes (see
|
|
326
|
+
* node-x11's docs/ext/xinput.md). `[]` where there is no XI2.
|
|
327
|
+
*
|
|
328
|
+
* Cached, because it is what `Window.selectXI2` consults to find the
|
|
329
|
+
* scroll axes behind a wheel delta and it must not cost a round trip per
|
|
330
|
+
* event. The cache is dropped whenever a window with XI2 selected sees an
|
|
331
|
+
* `XIDeviceChanged` — a master pointer's axes are those of whichever slave
|
|
332
|
+
* moved last, so they change under it when the user picks up the mouse
|
|
333
|
+
* after using the touchpad.
|
|
334
|
+
*
|
|
335
|
+
* @param {{refresh?: boolean}} [options]
|
|
336
|
+
* @returns {Promise<object[]>}
|
|
337
|
+
*/
|
|
338
|
+
inputDevices({ refresh = false } = {}) {
|
|
339
|
+
if (refresh) this._devicesPromise = null;
|
|
340
|
+
if (!this._devicesPromise) {
|
|
341
|
+
this._devicesPromise = this.xinput().then((XI) => {
|
|
342
|
+
if (!XI || !XI.xi2) return [];
|
|
343
|
+
return new Promise((resolve) => {
|
|
344
|
+
XI.XIQueryDevice(XI.AllDevices, (err, devices) => resolve(err ? [] : devices));
|
|
345
|
+
});
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
return this._devicesPromise;
|
|
349
|
+
}
|
|
350
|
+
|
|
302
351
|
/** selection/clipboard transfer: write()/read() text (docs/clipboard.md) */
|
|
303
352
|
get clipboard() {
|
|
304
353
|
if (!this._clipboard) this._clipboard = new Clipboard(this);
|
package/lib/drawable.js
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
import { EventEmitter } from 'node:events';
|
|
2
2
|
|
|
3
|
+
// lib/ may not statically import node builtins (see docs/packaging.md); this
|
|
4
|
+
// file is the one sanctioned exception for node:events, so it is also where
|
|
5
|
+
// the rest of lib/ takes EventEmitter from.
|
|
6
|
+
export { EventEmitter };
|
|
7
|
+
|
|
3
8
|
export default class Drawable extends EventEmitter {
|
|
4
9
|
getContext(name, ...args) {
|
|
5
10
|
const factory = Drawable.renderingContextFactory[name];
|
package/lib/events_map.js
CHANGED
|
@@ -67,6 +67,10 @@ export const mask = {
|
|
|
67
67
|
// not an X event of its own: Window derives it from the PropertyNotify for
|
|
68
68
|
// _NET_WM_STATE, so it needs the same mask a 'property' listener does
|
|
69
69
|
statechange: x11.eventMask.PropertyChange,
|
|
70
|
+
// also derived: a wheel is a click of button 4-7 in the core protocol, and
|
|
71
|
+
// a scroll valuator under XI2 (see lib/xi2.js). ButtonPress covers the core
|
|
72
|
+
// half; the XI2 half is opt-in per window and selects itself
|
|
73
|
+
wheel: x11.eventMask.ButtonPress,
|
|
70
74
|
selection: 0,
|
|
71
75
|
selection_request: 0,
|
|
72
76
|
message: 0
|
|
@@ -81,7 +85,12 @@ export const mask = {
|
|
|
81
85
|
export const coalesce = {
|
|
82
86
|
mousemove: 'last',
|
|
83
87
|
resize: 'last',
|
|
84
|
-
expose: 'union'
|
|
88
|
+
expose: 'union',
|
|
89
|
+
// 'accumulate' — scroll distance adds up. Keeping the last delta instead
|
|
90
|
+
// would throw away everything but the final step of a fast scroll, and a
|
|
91
|
+
// frame's worth of a touchpad's sub-notch deltas is exactly the case the
|
|
92
|
+
// smooth-scroll path exists for.
|
|
93
|
+
wheel: 'accumulate'
|
|
85
94
|
};
|
|
86
95
|
|
|
87
96
|
export const toSnake = {
|
|
@@ -90,6 +99,7 @@ export const toSnake = {
|
|
|
90
99
|
onMouseOut: 'mouseout',
|
|
91
100
|
onMouseDown: 'mousedown',
|
|
92
101
|
onMouseUp: 'mouseup',
|
|
102
|
+
onWheel: 'wheel',
|
|
93
103
|
onMap: 'map',
|
|
94
104
|
onUnmap: 'unmap',
|
|
95
105
|
onResize: 'resize',
|
package/lib/index.js
CHANGED
|
@@ -6,6 +6,16 @@ import { BLANK_CURSOR, CursorCache, cursorNames, cursorShapes, resolveCursorShap
|
|
|
6
6
|
import { DEFAULT_GL_POLICY, GLError, GL_MODES, resolveGLPolicy, wantsDirect } from './gl.js';
|
|
7
7
|
import { GLXError } from './glx.js';
|
|
8
8
|
import Window from './window.js';
|
|
9
|
+
import {
|
|
10
|
+
FocusProxy,
|
|
11
|
+
XEMBED,
|
|
12
|
+
XEMBED_OPCODE_NAMES,
|
|
13
|
+
XEmbedPlug,
|
|
14
|
+
XEmbedSocket,
|
|
15
|
+
decodeXEmbedInfo,
|
|
16
|
+
encodeXEmbedInfo,
|
|
17
|
+
readXEmbedInfo
|
|
18
|
+
} from './xembed.js';
|
|
9
19
|
import { loadLayout } from './yoga.js';
|
|
10
20
|
import { decodeKey, groupForState } from './keyboard.js';
|
|
11
21
|
import Pixmap from './pixmap.js';
|
|
@@ -210,6 +220,15 @@ export {
|
|
|
210
220
|
App,
|
|
211
221
|
Clipboard,
|
|
212
222
|
Window,
|
|
223
|
+
// XEmbed (docs/xembed.md): also available as `ntk/xembed`
|
|
224
|
+
XEmbedSocket,
|
|
225
|
+
XEmbedPlug,
|
|
226
|
+
FocusProxy,
|
|
227
|
+
XEMBED,
|
|
228
|
+
XEMBED_OPCODE_NAMES,
|
|
229
|
+
encodeXEmbedInfo,
|
|
230
|
+
decodeXEmbedInfo,
|
|
231
|
+
readXEmbedInfo,
|
|
213
232
|
BLANK_CURSOR,
|
|
214
233
|
CursorCache,
|
|
215
234
|
cursorNames,
|
package/lib/window.js
CHANGED
|
@@ -7,6 +7,17 @@ import { packIcons, unpackIcons } from './imagedata.js';
|
|
|
7
7
|
import Pixmap from './pixmap.js';
|
|
8
8
|
import * as xevents from './events_map.js';
|
|
9
9
|
import { decodeKey } from './keyboard.js';
|
|
10
|
+
import {
|
|
11
|
+
DEFAULT_XI2_EVENTS,
|
|
12
|
+
POINTER_EMULATED,
|
|
13
|
+
ScrollTracker,
|
|
14
|
+
WHEEL_BUTTONS,
|
|
15
|
+
normalizeXI2Types,
|
|
16
|
+
scrollAxes,
|
|
17
|
+
toNtkEvent,
|
|
18
|
+
wheelEvent,
|
|
19
|
+
xi2EventName
|
|
20
|
+
} from './xi2.js';
|
|
10
21
|
|
|
11
22
|
/**
|
|
12
23
|
* How many rectangles a frame's dirty region is allowed to hold.
|
|
@@ -435,10 +446,16 @@ export default class Window extends Drawable {
|
|
|
435
446
|
this._updateRegion = 0;
|
|
436
447
|
this._presentSerial = 0;
|
|
437
448
|
this._presentEid = 0;
|
|
449
|
+
// Generic Events (type 35) carry an extension opcode where a core event
|
|
450
|
+
// carries its code, so they are routed by opcode: Present's completions,
|
|
451
|
+
// XI2's device events, and whatever else selects them later.
|
|
452
|
+
this._geHandlers = new Map();
|
|
438
453
|
// A direct GL context's swap chain, when one is presenting on this window
|
|
439
|
-
// (see _setGenericEventSink)
|
|
454
|
+
// — an override ahead of that table (see _setGenericEventSink)
|
|
440
455
|
this._geSink = null;
|
|
441
456
|
this._geSinkOpcode = 0;
|
|
457
|
+
// XI2 state, once a window has selected it (see selectXI2)
|
|
458
|
+
this._xi2 = null;
|
|
442
459
|
// The vblank clock (see _onPresentComplete): the period learnt from
|
|
443
460
|
// completion events, the samples it is drawn from, and the latch the
|
|
444
461
|
// watchdog sets when completions stop arriving.
|
|
@@ -499,13 +516,15 @@ export default class Window extends Drawable {
|
|
|
499
516
|
// window was asked to be, so the window manager's first ConfigureNotify
|
|
500
517
|
// reports only what it actually overrode
|
|
501
518
|
this._deliveredGeom = { x: this.x, y: this.y, width: this.width, height: this.height };
|
|
519
|
+
this.windowClass = args.windowClass ?? 0;
|
|
502
520
|
const values = {
|
|
503
|
-
// NorthWest bit gravity: keep the old content anchored during
|
|
504
|
-
// resize instead of discarding it (less flicker between the resize
|
|
505
|
-
// and our redraw)
|
|
506
|
-
bitGravity: 1,
|
|
507
521
|
eventMask: this.eventMask
|
|
508
522
|
};
|
|
523
|
+
// NorthWest bit gravity: keep the old content anchored during resize
|
|
524
|
+
// instead of discarding it (less flicker between the resize and our
|
|
525
|
+
// redraw). An InputOnly window (class 2) has no content to anchor and
|
|
526
|
+
// the attribute is not one it accepts — setting it is a BadMatch.
|
|
527
|
+
if (this.windowClass !== 2) values.bitGravity = 1;
|
|
509
528
|
for (const name of forwardedXAttributes) {
|
|
510
529
|
if (args[name] !== undefined) values[name] = args[name];
|
|
511
530
|
}
|
|
@@ -514,7 +533,6 @@ export default class Window extends Drawable {
|
|
|
514
533
|
// parent of a different depth is a BadMatch (X11 CreateWindow)
|
|
515
534
|
this.visual = args.visual ?? 0;
|
|
516
535
|
this.depth = args.depth ?? 0;
|
|
517
|
-
this.windowClass = args.windowClass ?? 0;
|
|
518
536
|
if (this.visual) {
|
|
519
537
|
if (values.colormap === undefined) {
|
|
520
538
|
this._ownedColormap = app.createColormap(this.visual);
|
|
@@ -591,6 +609,14 @@ export default class Window extends Drawable {
|
|
|
591
609
|
if (args.alwaysOnTop) {
|
|
592
610
|
this.setAlwaysOnTop(true);
|
|
593
611
|
}
|
|
612
|
+
if (args.xi2) {
|
|
613
|
+
// fire and forget: until the extension answers, the window is on core
|
|
614
|
+
// events, which is where it would have been anyway. `await
|
|
615
|
+
// wnd.selectXI2()` is the form that says whether the server has XI2.
|
|
616
|
+
this.selectXI2(args.xi2 === true ? undefined : args.xi2).catch((err) =>
|
|
617
|
+
this.app.options.onXError?.(err)
|
|
618
|
+
);
|
|
619
|
+
}
|
|
594
620
|
if (args.syncRequest && !args.id) {
|
|
595
621
|
// fire and forget: the window manager only reads the counter when it
|
|
596
622
|
// starts managing the window, and map() is the caller's next line at
|
|
@@ -612,19 +638,20 @@ export default class Window extends Drawable {
|
|
|
612
638
|
|
|
613
639
|
X.event_consumers[this.id] = this;
|
|
614
640
|
this.on('event', (ev) => {
|
|
615
|
-
// Present
|
|
616
|
-
// and a sub-type where a core event carries its code — the name
|
|
617
|
-
// below cannot see them,
|
|
641
|
+
// Present and XI2 speak in X Generic Events, which carry an extension
|
|
642
|
+
// opcode and a sub-type where a core event carries its code — the name
|
|
643
|
+
// table below cannot see them, so they are routed by opcode instead
|
|
618
644
|
if (ev.type === 35) {
|
|
619
645
|
// A direct GL context presenting on this window selected these events
|
|
620
646
|
// itself and owns the pixmaps they name, so they are its to read — and
|
|
621
647
|
// it is the only user of Present there, a GL window having no backing
|
|
622
|
-
// store to blit.
|
|
648
|
+
// store to blit. Hence an override rather than another table entry:
|
|
649
|
+
// it claims the same extension the window's own blit path uses.
|
|
623
650
|
if (this._geSink && ev.extension === this._geSinkOpcode) {
|
|
624
651
|
this._geSink.handleEvent(ev);
|
|
625
|
-
|
|
626
|
-
this._handlePresentEvent(ev);
|
|
652
|
+
return;
|
|
627
653
|
}
|
|
654
|
+
this._geHandlers.get(ev.extension)?.(ev);
|
|
628
655
|
return;
|
|
629
656
|
}
|
|
630
657
|
const ntkev = ev; // todo: clone
|
|
@@ -658,9 +685,12 @@ export default class Window extends Drawable {
|
|
|
658
685
|
if (key.codepoint !== undefined) ev.codepoint = key.codepoint;
|
|
659
686
|
}
|
|
660
687
|
}
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
688
|
+
// a wheel is a button in the core protocol; say so in the units a
|
|
689
|
+
// consumer wants. Suppressed once XI2 is delivering the same scroll as
|
|
690
|
+
// valuators, which is the whole reason to select it (see selectXI2).
|
|
691
|
+
if (eventName === 'mousedown' && WHEEL_BUTTONS[ev.keycode] && !this._xi2?.smooth) {
|
|
692
|
+
const [deltaX, deltaY] = WHEEL_BUTTONS[ev.keycode];
|
|
693
|
+
this._deliverEvent('wheel', wheelEvent({ ...ntkev }, deltaX, deltaY, 'button'));
|
|
664
694
|
}
|
|
665
695
|
// the window manager reporting what it did with the window: a user who
|
|
666
696
|
// hit maximize or a fullscreen hotkey changes the state behind the
|
|
@@ -672,19 +702,7 @@ export default class Window extends Drawable {
|
|
|
672
702
|
() => {}
|
|
673
703
|
);
|
|
674
704
|
}
|
|
675
|
-
|
|
676
|
-
// them in the order they happened (a drag sees the move, then the up)
|
|
677
|
-
this._flushCoalesced();
|
|
678
|
-
if (eventName === 'resize') this._tagResize(ntkev);
|
|
679
|
-
this.emit(eventName, ntkev);
|
|
680
|
-
// a WM_DELETE_WINDOW ClientMessage is the window manager *asking*, and
|
|
681
|
-
// 'close' is that question in a form an application can answer
|
|
682
|
-
if (eventName === 'message') {
|
|
683
|
-
this._emitCloseRequest(ntkev);
|
|
684
|
-
this._handleSyncRequest(ntkev);
|
|
685
|
-
}
|
|
686
|
-
// anything drawn during the handlers becomes visible in one blit
|
|
687
|
-
if (this._dirty) this._present();
|
|
705
|
+
this._deliverEvent(eventName, ntkev);
|
|
688
706
|
});
|
|
689
707
|
|
|
690
708
|
// Events about a *child* of this window: the substructure requests a
|
|
@@ -1168,6 +1186,32 @@ export default class Window extends Drawable {
|
|
|
1168
1186
|
this._armTimer();
|
|
1169
1187
|
}
|
|
1170
1188
|
|
|
1189
|
+
/**
|
|
1190
|
+
* Deliver one named event: buffer it if its kind coalesces, and otherwise
|
|
1191
|
+
* flush what is buffered before emitting, so handlers see events in the
|
|
1192
|
+
* order they happened (a drag sees the move, then the up).
|
|
1193
|
+
*
|
|
1194
|
+
* Every source ends here — the core event stream, the XI2 one, and the
|
|
1195
|
+
* `wheel` both of them derive.
|
|
1196
|
+
*/
|
|
1197
|
+
_deliverEvent(name, ev) {
|
|
1198
|
+
if (this._coalesce && xevents.coalesce[name]) {
|
|
1199
|
+
this._enqueueCoalesced(name, ev);
|
|
1200
|
+
return;
|
|
1201
|
+
}
|
|
1202
|
+
this._flushCoalesced();
|
|
1203
|
+
if (name === 'resize') this._tagResize(ev);
|
|
1204
|
+
this.emit(name, ev);
|
|
1205
|
+
// a WM_DELETE_WINDOW ClientMessage is the window manager *asking*, and
|
|
1206
|
+
// 'close' is that question in a form an application can answer
|
|
1207
|
+
if (name === 'message') {
|
|
1208
|
+
this._emitCloseRequest(ev);
|
|
1209
|
+
this._handleSyncRequest(ev);
|
|
1210
|
+
}
|
|
1211
|
+
// anything drawn during the handlers becomes visible in one blit
|
|
1212
|
+
if (this._dirty) this._present();
|
|
1213
|
+
}
|
|
1214
|
+
|
|
1171
1215
|
_enqueueCoalesced(name, ev) {
|
|
1172
1216
|
const pending = this._frame.pending;
|
|
1173
1217
|
const prev = pending.get(name);
|
|
@@ -1177,6 +1221,21 @@ export default class Window extends Drawable {
|
|
|
1177
1221
|
ev.rects = [{ x: ev.x, y: ev.y, width: ev.width, height: ev.height }];
|
|
1178
1222
|
}
|
|
1179
1223
|
pending.set(name, ev);
|
|
1224
|
+
} else if (xevents.coalesce[name] === 'accumulate') {
|
|
1225
|
+
// scroll distance adds up, and the frame's scroll happened wherever the
|
|
1226
|
+
// pointer ended up. The first event of the frame carries the sum, the
|
|
1227
|
+
// way the union path grows its first event's rectangle.
|
|
1228
|
+
prev.coalesced.push(ev);
|
|
1229
|
+
prev.deltaX += ev.deltaX;
|
|
1230
|
+
prev.deltaY += ev.deltaY;
|
|
1231
|
+
prev.x = ev.x;
|
|
1232
|
+
prev.y = ev.y;
|
|
1233
|
+
prev.rootx = ev.rootx;
|
|
1234
|
+
prev.rooty = ev.rooty;
|
|
1235
|
+
prev.time = ev.time;
|
|
1236
|
+
prev.buttons = ev.buttons;
|
|
1237
|
+
prev.smooth = ev.smooth;
|
|
1238
|
+
prev.source = ev.source;
|
|
1180
1239
|
} else if (xevents.coalesce[name] === 'union') {
|
|
1181
1240
|
prev.coalesced.push(ev);
|
|
1182
1241
|
prev.rects.push({ x: ev.x, y: ev.y, width: ev.width, height: ev.height });
|
|
@@ -1284,6 +1343,163 @@ export default class Window extends Drawable {
|
|
|
1284
1343
|
this._geSinkOpcode = sink ? majorOpcode : 0;
|
|
1285
1344
|
}
|
|
1286
1345
|
|
|
1346
|
+
/**
|
|
1347
|
+
* Ask for XI2 device events on this window, and deliver them as the events
|
|
1348
|
+
* ntk already has names for.
|
|
1349
|
+
*
|
|
1350
|
+
* What this buys is the wheel: the core protocol reports a scroll as a
|
|
1351
|
+
* click of button 4/5/6/7, so a touchpad's two-finger gesture arrives as
|
|
1352
|
+
* whole notches however finely the hardware measured it. XI2 carries the
|
|
1353
|
+
* same gesture as a scroll *valuator*, and `wheel` then reports fractions
|
|
1354
|
+
* of a notch (see docs/window.md "Wheel and smooth scrolling"). Touch and
|
|
1355
|
+
* per-device input come with it — `ev.deviceId`/`ev.sourceId` are on every
|
|
1356
|
+
* translated event, and the core protocol has no way to say either.
|
|
1357
|
+
*
|
|
1358
|
+
* Opt-in per window, via `createWindow({ xi2: true })` or this call,
|
|
1359
|
+
* because selecting XI2 changes what the server sends: a window that has
|
|
1360
|
+
* not asked keeps costing exactly what it did before.
|
|
1361
|
+
*
|
|
1362
|
+
* **Selecting XI2 replaces the core events of the same types.** The server
|
|
1363
|
+
* delivers an event to a client once, and an XI2 selection wins, so
|
|
1364
|
+
* `selectXI2(['Motion'])` means this window's `mousemove` is built from
|
|
1365
|
+
* `XIMotion` from then on. It carries the same fields (plus `ev.xi2`,
|
|
1366
|
+
* the device ids and sub-pixel `preciseX`/`preciseY`), so handlers do not
|
|
1367
|
+
* change — but a window that selects XI2 KeyPress and nothing else still
|
|
1368
|
+
* gets core motion, and mixing the two per-type is what the `types`
|
|
1369
|
+
* argument is for.
|
|
1370
|
+
*
|
|
1371
|
+
* The emulated wheel buttons are dropped once the valuators are flowing:
|
|
1372
|
+
* the server sends both halves of a smooth scroll (the axis and a button
|
|
1373
|
+
* 4-7 click flagged `PointerEmulated`) so that clients which cannot read
|
|
1374
|
+
* valuators still scroll, and delivering both would count every notch
|
|
1375
|
+
* twice. So an app that opts in reads `wheel`, not `mousedown` on button 4.
|
|
1376
|
+
*
|
|
1377
|
+
* @param {string[]|string} [types] XI2 event type names —
|
|
1378
|
+
* `Motion`, `ButtonPress`, `ButtonRelease`, `KeyPress`, `KeyRelease`,
|
|
1379
|
+
* `TouchBegin`, `TouchUpdate`, `TouchEnd`. An empty array deselects.
|
|
1380
|
+
* @param {{deviceId?: number}} [options] which device to select for,
|
|
1381
|
+
* defaulting to every master device (the virtual pointer and keyboard
|
|
1382
|
+
* the user is actually driving). A selection is per device, so a
|
|
1383
|
+
* selection on one device id does not replace one made on another.
|
|
1384
|
+
* @returns {Promise<boolean>} false where the server has no XI2 — in which
|
|
1385
|
+
* case nothing changed and the window keeps its core events, `wheel`
|
|
1386
|
+
* included, at notch granularity
|
|
1387
|
+
*/
|
|
1388
|
+
selectXI2(types = DEFAULT_XI2_EVENTS, { deviceId } = {}) {
|
|
1389
|
+
const names = normalizeXI2Types(types);
|
|
1390
|
+
return Promise.all([this.app.xinput(), this.app.inputDevices()]).then(([XI, devices]) => {
|
|
1391
|
+
if (!XI || !XI.xi2 || this._destroyed) return false;
|
|
1392
|
+
const target = deviceId ?? XI.AllMasterDevices;
|
|
1393
|
+
if (!names.length) {
|
|
1394
|
+
safeRelease(this.X, () => XI.XISelectEvents(this.id, { deviceId: target, mask: 0 }));
|
|
1395
|
+
this._geHandlers.delete(XI.majorOpcode);
|
|
1396
|
+
this._xi2 = null;
|
|
1397
|
+
return true;
|
|
1398
|
+
}
|
|
1399
|
+
// DeviceChanged rides along uninvited: it is the only announcement that
|
|
1400
|
+
// a master pointer's valuators are now some other slave's, which
|
|
1401
|
+
// invalidates both the axis map and the accumulators built on it. Its
|
|
1402
|
+
// body is undecoded, and the device id is all this needs from it.
|
|
1403
|
+
safeRelease(this.X, () =>
|
|
1404
|
+
XI.XISelectEvents(this.id, { deviceId: target, mask: [...names, 'DeviceChanged'] })
|
|
1405
|
+
);
|
|
1406
|
+
this._xi2 = {
|
|
1407
|
+
ext: XI,
|
|
1408
|
+
deviceId: target,
|
|
1409
|
+
types: names,
|
|
1410
|
+
// evtype -> the ntk event it becomes, resolved through the extension's
|
|
1411
|
+
// own table rather than hardcoded numbers
|
|
1412
|
+
byType: new Map(names.map((name) => [XI.EventType[name], xi2EventName[name]])),
|
|
1413
|
+
scroll: this._xi2?.scroll ?? new ScrollTracker(),
|
|
1414
|
+
axes: new Map(devices.map((device) => [device.deviceId, scrollAxes(device)])),
|
|
1415
|
+
// whether the core wheel buttons are now redundant: a scroll-capable
|
|
1416
|
+
// device plus a selection that carries its valuators
|
|
1417
|
+
smooth: names.includes('Motion') && devices.some((device) => scrollAxes(device).size > 0)
|
|
1418
|
+
};
|
|
1419
|
+
this._geHandlers.set(XI.majorOpcode, (ev) => this._handleXI2Event(ev));
|
|
1420
|
+
return true;
|
|
1421
|
+
});
|
|
1422
|
+
}
|
|
1423
|
+
|
|
1424
|
+
/**
|
|
1425
|
+
* One XI2 Generic Event: scroll valuators become `wheel`, everything else
|
|
1426
|
+
* becomes the core-shaped event it stands in for.
|
|
1427
|
+
*/
|
|
1428
|
+
_handleXI2Event(ev) {
|
|
1429
|
+
const xi2 = this._xi2;
|
|
1430
|
+
if (!xi2) return;
|
|
1431
|
+
if (ev.evtype === xi2.ext.EventType.DeviceChanged) {
|
|
1432
|
+
this._refreshXI2Devices(ev.deviceId);
|
|
1433
|
+
return;
|
|
1434
|
+
}
|
|
1435
|
+
const name = xi2.byType.get(ev.evtype);
|
|
1436
|
+
if (!name) return;
|
|
1437
|
+
|
|
1438
|
+
const base = toNtkEvent(name, ev);
|
|
1439
|
+
base.window = this;
|
|
1440
|
+
base.target = this;
|
|
1441
|
+
|
|
1442
|
+
if (name === 'mousemove') {
|
|
1443
|
+
// a scroll is a Motion carrying scroll axes; the pointer itself did not
|
|
1444
|
+
// move, so it is a wheel event and not also a mouse move — including
|
|
1445
|
+
// the first one, which has nothing to subtract from yet
|
|
1446
|
+
const scroll = xi2.scroll.delta(ev, this._xi2Axes(ev));
|
|
1447
|
+
if (scroll) {
|
|
1448
|
+
if (scroll.moved) {
|
|
1449
|
+
this._deliverEvent('wheel', wheelEvent(base, scroll.deltaX, scroll.deltaY, 'valuator'));
|
|
1450
|
+
}
|
|
1451
|
+
return;
|
|
1452
|
+
}
|
|
1453
|
+
}
|
|
1454
|
+
if (name === 'mousedown' || name === 'mouseup') {
|
|
1455
|
+
const wheelButton = WHEEL_BUTTONS[ev.detail];
|
|
1456
|
+
// the click the server emulated for clients that cannot read valuators
|
|
1457
|
+
// — we can, and the valuator half of the same scroll is already on its
|
|
1458
|
+
// way, so this one is noise
|
|
1459
|
+
if (wheelButton && ev.flags & POINTER_EMULATED) return;
|
|
1460
|
+
if (wheelButton && name === 'mousedown') {
|
|
1461
|
+
const wheel = wheelEvent({ ...base }, wheelButton[0], wheelButton[1], 'button');
|
|
1462
|
+
this._deliverEvent('wheel', wheel);
|
|
1463
|
+
}
|
|
1464
|
+
}
|
|
1465
|
+
if (name === 'keydown' || name === 'keyup') {
|
|
1466
|
+
const key = decodeKey(this.X.keycode2keysyms[ev.detail], base.buttons);
|
|
1467
|
+
if (key) {
|
|
1468
|
+
base.keysym = key.keysym;
|
|
1469
|
+
base.baseKeysym = key.baseKeysym;
|
|
1470
|
+
base.group = key.group;
|
|
1471
|
+
if (key.codepoint !== undefined) base.codepoint = key.codepoint;
|
|
1472
|
+
}
|
|
1473
|
+
}
|
|
1474
|
+
this._deliverEvent(name, base);
|
|
1475
|
+
}
|
|
1476
|
+
|
|
1477
|
+
/** This window's scroll-axis map for the device an event came from. */
|
|
1478
|
+
_xi2Axes(ev) {
|
|
1479
|
+
return this._xi2?.axes.get(ev.deviceId);
|
|
1480
|
+
}
|
|
1481
|
+
|
|
1482
|
+
/**
|
|
1483
|
+
* A master device's slave changed: re-read what its axes are now, and drop
|
|
1484
|
+
* the accumulators, because the values arriving next belong to a different
|
|
1485
|
+
* device and subtracting across the switch is the jump seeding exists to
|
|
1486
|
+
* avoid.
|
|
1487
|
+
*/
|
|
1488
|
+
_refreshXI2Devices(deviceId) {
|
|
1489
|
+
const xi2 = this._xi2;
|
|
1490
|
+
if (!xi2) return;
|
|
1491
|
+
xi2.scroll.reset(deviceId);
|
|
1492
|
+
this.app.inputDevices({ refresh: true }).then(
|
|
1493
|
+
(devices) => {
|
|
1494
|
+
if (this._xi2 !== xi2) return; // deselected or reselected meanwhile
|
|
1495
|
+
xi2.axes = new Map(devices.map((device) => [device.deviceId, scrollAxes(device)]));
|
|
1496
|
+
xi2.smooth =
|
|
1497
|
+
xi2.types.includes('Motion') && devices.some((device) => scrollAxes(device).size > 0);
|
|
1498
|
+
},
|
|
1499
|
+
() => {}
|
|
1500
|
+
);
|
|
1501
|
+
}
|
|
1502
|
+
|
|
1287
1503
|
/**
|
|
1288
1504
|
* Present's events, which are the ones the core event table cannot name:
|
|
1289
1505
|
* they arrive as X Generic Events (type 35), carrying an extension opcode
|
|
@@ -1702,6 +1918,7 @@ export default class Window extends Drawable {
|
|
|
1702
1918
|
safeRelease(X, () => fixes.CreateRegion(this._updateRegion, []));
|
|
1703
1919
|
this._presentExt = present;
|
|
1704
1920
|
this._fixesExt = fixes;
|
|
1921
|
+
this._geHandlers.set(present.majorOpcode, (ev) => this._handlePresentEvent(ev));
|
|
1705
1922
|
this._selectPresentInput();
|
|
1706
1923
|
return this;
|
|
1707
1924
|
});
|
|
@@ -2612,16 +2829,21 @@ export default class Window extends Drawable {
|
|
|
2612
2829
|
|
|
2613
2830
|
if (mapped) {
|
|
2614
2831
|
const root = this.app.display.screen[0].root;
|
|
2615
|
-
|
|
2616
|
-
|
|
2617
|
-
|
|
2618
|
-
|
|
2832
|
+
// EWMH 7.7: delivered to the root with the substructure mask, so the
|
|
2833
|
+
// window manager watching there sees it, but about this window
|
|
2834
|
+
await this.sendClientMessage(
|
|
2835
|
+
'_NET_WM_STATE',
|
|
2836
|
+
[
|
|
2619
2837
|
mode,
|
|
2620
2838
|
ids[0],
|
|
2621
2839
|
ids[1] ?? 0,
|
|
2622
2840
|
1 // source indication: a normal application, not a pager
|
|
2623
|
-
]
|
|
2624
|
-
|
|
2841
|
+
],
|
|
2842
|
+
{
|
|
2843
|
+
target: root,
|
|
2844
|
+
mask: x11.eventMask.SubstructureRedirect | x11.eventMask.SubstructureNotify
|
|
2845
|
+
}
|
|
2846
|
+
);
|
|
2625
2847
|
} else {
|
|
2626
2848
|
await this._writeWmState(ids, mode);
|
|
2627
2849
|
}
|
|
@@ -3168,6 +3390,58 @@ export default class Window extends Drawable {
|
|
|
3168
3390
|
return this;
|
|
3169
3391
|
}
|
|
3170
3392
|
|
|
3393
|
+
/**
|
|
3394
|
+
* Send a ClientMessage about this window (X SendEvent with a
|
|
3395
|
+
* ClientMessage payload).
|
|
3396
|
+
*
|
|
3397
|
+
* ClientMessage is the carrier of every convention layered over the core
|
|
3398
|
+
* protocol — EWMH state changes, WM_PROTOCOLS, XEmbed, XDND, the system
|
|
3399
|
+
* tray — and they differ only in the atom naming the message and the five
|
|
3400
|
+
* 32-bit words in it. `type` is an atom name (or an already-interned id),
|
|
3401
|
+
* `data` the words; anything past the end of the list is sent as zero.
|
|
3402
|
+
*
|
|
3403
|
+
* await wnd.sendClientMessage('WM_PROTOCOLS', [deleteAtom, time]);
|
|
3404
|
+
*
|
|
3405
|
+
* Two options carry the meaning that the raw request buries in argument
|
|
3406
|
+
* order:
|
|
3407
|
+
*
|
|
3408
|
+
* - `target` — where the message is *delivered*, which is not always the
|
|
3409
|
+
* window it is *about*. EWMH messages are about a client window and
|
|
3410
|
+
* delivered to the root, so the window manager (which selected
|
|
3411
|
+
* SubstructureRedirect there) sees them.
|
|
3412
|
+
* - `mask` — who on the target gets it. `0`, the default, delivers to the
|
|
3413
|
+
* client that owns the target window whatever it selected, which is
|
|
3414
|
+
* what a message addressed to another client needs. Root-window EWMH
|
|
3415
|
+
* messages instead pass
|
|
3416
|
+
* `SubstructureRedirect | SubstructureNotify`.
|
|
3417
|
+
*
|
|
3418
|
+
* @param {string|number} type atom name of the message type, or its id
|
|
3419
|
+
* @param {number[]} [data] up to five 32-bit words (ten at format 16,
|
|
3420
|
+
* twenty at format 8)
|
|
3421
|
+
* @param {object} [options] `{ target = this.id, format = 32, mask = 0 }`
|
|
3422
|
+
* @returns {Promise<Window>}
|
|
3423
|
+
*/
|
|
3424
|
+
async sendClientMessage(type, data = [], options = {}) {
|
|
3425
|
+
const { target = this.id, format = 32, mask = 0 } = options;
|
|
3426
|
+
const slots = { 8: 20, 16: 10, 32: 5 }[format];
|
|
3427
|
+
if (!slots) {
|
|
3428
|
+
throw new TypeError(`sendClientMessage: format must be 8, 16 or 32, got ${format}`);
|
|
3429
|
+
}
|
|
3430
|
+
if (data.length > slots) {
|
|
3431
|
+
// the wire event is 32 bytes and no more; sending the first `slots`
|
|
3432
|
+
// words of a longer list would drop the rest without a sound
|
|
3433
|
+
throw new RangeError(
|
|
3434
|
+
`sendClientMessage: a format-${format} ClientMessage carries ${slots} values, got ${data.length}`
|
|
3435
|
+
);
|
|
3436
|
+
}
|
|
3437
|
+
const messageType = typeof type === 'number' ? type : await this.atom(type);
|
|
3438
|
+
if (this._destroyed) return this;
|
|
3439
|
+
safeRelease(this.X, () => {
|
|
3440
|
+
this.X.SendClientMessage(target, this.id, messageType, format, data, mask);
|
|
3441
|
+
});
|
|
3442
|
+
return this;
|
|
3443
|
+
}
|
|
3444
|
+
|
|
3171
3445
|
/**
|
|
3172
3446
|
* Ask the client to close, the polite way: a WM_DELETE_WINDOW client
|
|
3173
3447
|
* message if it advertised the protocol in WM_PROTOCOLS, so it can save
|
|
@@ -3187,13 +3461,11 @@ export default class Window extends Drawable {
|
|
|
3187
3461
|
safeRelease(this.X, () => this.X.KillClient(this.id));
|
|
3188
3462
|
return false;
|
|
3189
3463
|
}
|
|
3190
|
-
|
|
3191
|
-
//
|
|
3192
|
-
//
|
|
3193
|
-
//
|
|
3194
|
-
|
|
3195
|
-
this.X.SendClientMessage(this.id, this.id, wmProtocols, 32, [deleteAtom, 0 /* CurrentTime */], 0)
|
|
3196
|
-
);
|
|
3464
|
+
// the default mask of 0 delivers to the client that created the window,
|
|
3465
|
+
// not to whoever selected events on it — a WM_PROTOCOLS message is
|
|
3466
|
+
// addressed to the owner, so it must not go out with the EWMH
|
|
3467
|
+
// substructure mask
|
|
3468
|
+
await this.sendClientMessage('WM_PROTOCOLS', [deleteAtom, 0 /* CurrentTime */]);
|
|
3197
3469
|
return true;
|
|
3198
3470
|
}
|
|
3199
3471
|
|