ntk 3.7.2 → 3.9.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 +35 -2
- package/lib/events_map.js +15 -1
- package/lib/window.js +364 -14
- package/package.json +2 -2
package/lib/app.js
CHANGED
|
@@ -81,8 +81,41 @@ export default class App {
|
|
|
81
81
|
return chooseGLXConfig(this, spec);
|
|
82
82
|
}
|
|
83
83
|
|
|
84
|
-
rootWindow() {
|
|
85
|
-
return new Window(this, { id: this.display.screen[
|
|
84
|
+
rootWindow(screen = 0) {
|
|
85
|
+
return new Window(this, { id: this.display.screen[screen].root });
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Release a pointer or keyboard frozen by a synchronous grab (X
|
|
90
|
+
* AllowEvents). A window manager grabs buttons on client windows with
|
|
91
|
+
* `pointerMode: 0` so it sees the press first; this decides what happens
|
|
92
|
+
* to it:
|
|
93
|
+
*
|
|
94
|
+
* 'replay' — hand the event back to the client as if we never saw it
|
|
95
|
+
* (click-to-focus: raise the window, let the click through)
|
|
96
|
+
* 'async' — thaw, keeping the event for ourselves
|
|
97
|
+
* 'sync' — thaw for one more event, then freeze again
|
|
98
|
+
*
|
|
99
|
+
* The keyboard and both-devices variants carry their X names.
|
|
100
|
+
*
|
|
101
|
+
* @param {string|number} [mode] one of the names above, or a raw X mode
|
|
102
|
+
* @param {number} [time] CurrentTime by default
|
|
103
|
+
*/
|
|
104
|
+
allowEvents(mode = 'replay', time = 0) {
|
|
105
|
+
const MODES = {
|
|
106
|
+
async: 0, // AsyncPointer
|
|
107
|
+
sync: 1, // SyncPointer
|
|
108
|
+
replay: 2, // ReplayPointer
|
|
109
|
+
async_keyboard: 3,
|
|
110
|
+
sync_keyboard: 4,
|
|
111
|
+
replay_keyboard: 5,
|
|
112
|
+
async_both: 6,
|
|
113
|
+
sync_both: 7
|
|
114
|
+
};
|
|
115
|
+
const value = typeof mode === 'number' ? mode : MODES[mode];
|
|
116
|
+
if (value === undefined) throw new Error(`ntk: unknown AllowEvents mode '${mode}'`);
|
|
117
|
+
this.X.AllowEvents(value, time);
|
|
118
|
+
return this;
|
|
86
119
|
}
|
|
87
120
|
|
|
88
121
|
createPixmap(args) {
|
package/lib/events_map.js
CHANGED
|
@@ -14,6 +14,9 @@ eventName[8] = 'mouseout';
|
|
|
14
14
|
eventName[9] = 'focus';
|
|
15
15
|
eventName[10] = 'blur';
|
|
16
16
|
eventName[12] = 'expose';
|
|
17
|
+
// CreateNotify: a child window appeared under a parent we watch. Carries a
|
|
18
|
+
// `parent` field, so it arrives as a child-event like the *_request ones.
|
|
19
|
+
eventName[16] = 'create';
|
|
17
20
|
eventName[17] = 'destroy';
|
|
18
21
|
eventName[18] = 'unmap';
|
|
19
22
|
eventName[19] = 'map';
|
|
@@ -21,7 +24,7 @@ eventName[20] = 'map_request';
|
|
|
21
24
|
eventName[21] = 'reparent';
|
|
22
25
|
eventName[22] = 'resize';
|
|
23
26
|
eventName[23] = 'configure_request';
|
|
24
|
-
|
|
27
|
+
eventName[24] = 'gravity';
|
|
25
28
|
eventName[25] = 'resize_request';
|
|
26
29
|
eventName[26] = 'circulate';
|
|
27
30
|
eventName[27] = 'circulate_request';
|
|
@@ -49,6 +52,15 @@ export const mask = {
|
|
|
49
52
|
expose: x11.eventMask.Exposure,
|
|
50
53
|
map_request: x11.eventMask.SubstructureRedirect,
|
|
51
54
|
configure_request: x11.eventMask.SubstructureRedirect,
|
|
55
|
+
circulate_request: x11.eventMask.SubstructureRedirect,
|
|
56
|
+
// a child window appeared under a window we watch — the one
|
|
57
|
+
// SubstructureNotify event with its own name, because the rest
|
|
58
|
+
// (map/unmap/destroy/resize/reparent of a child) are the same event types
|
|
59
|
+
// a window gets about itself and are emitted on the child's own wrapper
|
|
60
|
+
create: x11.eventMask.SubstructureNotify,
|
|
61
|
+
gravity: x11.eventMask.StructureNotify,
|
|
62
|
+
circulate: x11.eventMask.StructureNotify,
|
|
63
|
+
resize_request: x11.eventMask.ResizeRedirect,
|
|
52
64
|
destroy: x11.eventMask.StructureNotify,
|
|
53
65
|
keyup: x11.eventMask.KeyRelease,
|
|
54
66
|
property: x11.eventMask.PropertyChange,
|
|
@@ -85,6 +97,8 @@ export const toSnake = {
|
|
|
85
97
|
onExpose: 'expose',
|
|
86
98
|
onMapRequest: 'map_request',
|
|
87
99
|
onConfigureRequest: 'configure_request',
|
|
100
|
+
onCirculateRequest: 'circulate_request',
|
|
101
|
+
onCreate: 'create',
|
|
88
102
|
onDestroy: 'destroy',
|
|
89
103
|
onKeyUp: 'keyup',
|
|
90
104
|
onPropertyChange: 'property',
|
package/lib/window.js
CHANGED
|
@@ -33,14 +33,49 @@ const forwardedXAttributes = [
|
|
|
33
33
|
'cursor'
|
|
34
34
|
];
|
|
35
35
|
|
|
36
|
+
/**
|
|
37
|
+
* Turn a GetProperty reply into the shape the caller asked for. X property
|
|
38
|
+
* types are atoms, so the only distinction we can make without a round trip
|
|
39
|
+
* is against the predefined ones: STRING is latin-1 by definition (ICCCM),
|
|
40
|
+
* and everything else carrying text — UTF8_STRING above all — is UTF-8.
|
|
41
|
+
*/
|
|
42
|
+
function decodeProperty(prop, as, X) {
|
|
43
|
+
if (as === 'string') {
|
|
44
|
+
const encoding = prop.type === X.atoms.STRING ? 'latin1' : 'utf8';
|
|
45
|
+
// these properties are conventionally NUL-terminated, sometimes twice
|
|
46
|
+
return prop.data.toString(encoding).replace(/\0+$/, '');
|
|
47
|
+
}
|
|
48
|
+
if (as === 'numbers') {
|
|
49
|
+
const out = [];
|
|
50
|
+
for (let i = 0; i + 4 <= prop.data.length; i += 4) out.push(prop.data.readUInt32LE(i));
|
|
51
|
+
return out;
|
|
52
|
+
}
|
|
53
|
+
return { type: prop.type, data: prop.data };
|
|
54
|
+
}
|
|
55
|
+
|
|
36
56
|
export default class Window extends Drawable {
|
|
37
57
|
// one wrapper per X window id and connection: constructing a Window for a
|
|
38
|
-
// known id returns the cached instance
|
|
39
|
-
|
|
58
|
+
// known id returns the cached instance. Keyed by connection as well as
|
|
59
|
+
// id, because ids are only unique within a server, not within a process —
|
|
60
|
+
// two connections to the same display see the same root window, and a
|
|
61
|
+
// window manager sees every other client's ids.
|
|
62
|
+
static cache = new WeakMap();
|
|
63
|
+
|
|
64
|
+
static _cacheFor(app) {
|
|
65
|
+
let byId = Window.cache.get(app);
|
|
66
|
+
if (!byId) Window.cache.set(app, (byId = new Map()));
|
|
67
|
+
return byId;
|
|
68
|
+
}
|
|
40
69
|
|
|
41
70
|
constructor(app, args = {}) {
|
|
71
|
+
// 0 is X None, not a window. Without this it fails the truthiness test
|
|
72
|
+
// below and falls through to the create path, silently making a real
|
|
73
|
+
// 800x800 window out of what was almost certainly a missing id.
|
|
74
|
+
if (args.id === 0) {
|
|
75
|
+
throw new Error('ntk: 0 (None) is not a window id');
|
|
76
|
+
}
|
|
42
77
|
if (args.id) {
|
|
43
|
-
const cached = Window.
|
|
78
|
+
const cached = Window._cacheFor(app).get(args.id);
|
|
44
79
|
// TODO: check event mask, if more events in args - update it
|
|
45
80
|
if (cached) return cached;
|
|
46
81
|
}
|
|
@@ -151,7 +186,7 @@ export default class Window extends Drawable {
|
|
|
151
186
|
});
|
|
152
187
|
}
|
|
153
188
|
|
|
154
|
-
Window.
|
|
189
|
+
Window._cacheFor(app).set(this.id, this);
|
|
155
190
|
|
|
156
191
|
if (args.title) {
|
|
157
192
|
this.setTitle(args.title);
|
|
@@ -218,14 +253,19 @@ export default class Window extends Drawable {
|
|
|
218
253
|
if (this._dirty) this._present();
|
|
219
254
|
});
|
|
220
255
|
|
|
256
|
+
// Events about a *child* of this window: the substructure requests a
|
|
257
|
+
// window manager lives on (map_request, configure_request) plus
|
|
258
|
+
// create. The whole X event is carried through — a ConfigureRequest
|
|
259
|
+
// without its geometry and value mask says only "someone wants
|
|
260
|
+
// something", which is not enough to answer it — with `parent` and
|
|
261
|
+
// `window` upgraded from raw ids to Window objects.
|
|
221
262
|
this.on('child-event', (ev) => {
|
|
222
263
|
const eventName = xevents.eventName[ev.type];
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
};
|
|
264
|
+
if (!eventName) return;
|
|
265
|
+
const child = new Window(app, { id: ev.wid });
|
|
266
|
+
const ntkev = { ...ev, parent: this, window: child, target: child };
|
|
227
267
|
// wait until we know that we track correct x,y,w,h values
|
|
228
|
-
|
|
268
|
+
child._readyPromise.then(() => {
|
|
229
269
|
this.emit(eventName, ntkev);
|
|
230
270
|
});
|
|
231
271
|
});
|
|
@@ -236,7 +276,14 @@ export default class Window extends Drawable {
|
|
|
236
276
|
if (!eventMask) return;
|
|
237
277
|
if ((eventMask & this.eventMask) === 0) {
|
|
238
278
|
this.eventMask |= eventMask;
|
|
239
|
-
|
|
279
|
+
// the selection can legitimately fail — SubstructureRedirect is
|
|
280
|
+
// one-client-only, so `on('map_request')` is how you find out
|
|
281
|
+
// another window manager owns this window. Route it to the app's
|
|
282
|
+
// error hook rather than dropping it; selectInput() is the
|
|
283
|
+
// explicit form that hands the error straight back.
|
|
284
|
+
X.ChangeWindowAttributes(this.id, { eventMask: this.eventMask }, (err) => {
|
|
285
|
+
if (err) this.app.options.onXError?.(err);
|
|
286
|
+
});
|
|
240
287
|
}
|
|
241
288
|
});
|
|
242
289
|
|
|
@@ -257,7 +304,7 @@ export default class Window extends Drawable {
|
|
|
257
304
|
}
|
|
258
305
|
|
|
259
306
|
_forget() {
|
|
260
|
-
Window.
|
|
307
|
+
Window._cacheFor(this.app).delete(this.id);
|
|
261
308
|
delete this.X.event_consumers[this.id];
|
|
262
309
|
}
|
|
263
310
|
|
|
@@ -997,6 +1044,308 @@ export default class Window extends Drawable {
|
|
|
997
1044
|
|
|
998
1045
|
reparentTo(newParent, x, y) {
|
|
999
1046
|
this.X.ReparentWindow(this.id, newParent.id, x, y);
|
|
1047
|
+
return this;
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
/** Move to the top of the stacking order among its siblings. */
|
|
1051
|
+
raise() {
|
|
1052
|
+
safeRelease(this.X, () => this.X.RaiseWindow(this.id));
|
|
1053
|
+
return this;
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
/** Move to the bottom of the stacking order among its siblings. */
|
|
1057
|
+
lower() {
|
|
1058
|
+
safeRelease(this.X, () => this.X.ConfigureWindow(this.id, { stackMode: 1 }));
|
|
1059
|
+
return this;
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
// ---------------------------------------------------------------------
|
|
1063
|
+
// Reading properties
|
|
1064
|
+
//
|
|
1065
|
+
// ntk could write window properties (setTitle, setSizeHints, setClass)
|
|
1066
|
+
// but never read them, which is fine for a client — it knows what it
|
|
1067
|
+
// wrote — and useless for a window manager, whose whole job is to act on
|
|
1068
|
+
// what *other* clients declare about themselves.
|
|
1069
|
+
// ---------------------------------------------------------------------
|
|
1070
|
+
|
|
1071
|
+
/**
|
|
1072
|
+
* Read a property. `name` is an atom name ('WM_NAME', '_NET_WM_STATE');
|
|
1073
|
+
* interned atoms are cached per connection, so repeat reads cost no
|
|
1074
|
+
* extra round trip.
|
|
1075
|
+
*
|
|
1076
|
+
* Resolves to `null` when the property is not set, otherwise to the
|
|
1077
|
+
* decoded value chosen by `as`:
|
|
1078
|
+
* 'buffer' (default) — `{ type, data }` with the raw bytes
|
|
1079
|
+
* 'string' — text, UTF-8 or latin-1 by the property's type
|
|
1080
|
+
* 'numbers' — an array of 32-bit values (atom lists, cardinals)
|
|
1081
|
+
*
|
|
1082
|
+
* @param {string} name
|
|
1083
|
+
* @param {object} [options] `{ as, type, length }` — `type` restricts the
|
|
1084
|
+
* read to one property type (0, the default, accepts any), `length` is
|
|
1085
|
+
* the cap in 32-bit units.
|
|
1086
|
+
*/
|
|
1087
|
+
async getProperty(name, options = {}) {
|
|
1088
|
+
const { as = 'buffer', type = 0, length = 0x1fffffff } = options;
|
|
1089
|
+
const atom = await this.atom(name);
|
|
1090
|
+
return new Promise((resolve, reject) => {
|
|
1091
|
+
safeRelease(this.X, () =>
|
|
1092
|
+
this.X.GetProperty(0, this.id, atom, type, 0, length, (err, prop) => {
|
|
1093
|
+
if (err) return reject(err);
|
|
1094
|
+
// an unset property answers with type None and no bytes
|
|
1095
|
+
if (!prop || !prop.type || !prop.data.length) return resolve(null);
|
|
1096
|
+
resolve(decodeProperty(prop, as, this.X));
|
|
1097
|
+
})
|
|
1098
|
+
);
|
|
1099
|
+
});
|
|
1100
|
+
}
|
|
1101
|
+
|
|
1102
|
+
/**
|
|
1103
|
+
* Write a property — the counterpart of getProperty, and the general form
|
|
1104
|
+
* of setTitle/setClass/setSizeHints. Strings are written as UTF8_STRING,
|
|
1105
|
+
* arrays of numbers as 32-bit lists; `type` names the property type atom
|
|
1106
|
+
* ('ATOM', 'CARDINAL', 'WINDOW', 'UTF8_STRING', 'STRING'), which is what
|
|
1107
|
+
* EWMH properties are picky about.
|
|
1108
|
+
*
|
|
1109
|
+
* wnd.setProperty('_NET_WM_NAME', 'a title');
|
|
1110
|
+
* wnd.setProperty('_NET_CLIENT_LIST', ids, { type: 'WINDOW' });
|
|
1111
|
+
*
|
|
1112
|
+
* @param {string} name atom name of the property
|
|
1113
|
+
* @param {string|number[]|Buffer} value
|
|
1114
|
+
* @param {object} [options] `{ type, format }`
|
|
1115
|
+
*/
|
|
1116
|
+
async setProperty(name, value, options = {}) {
|
|
1117
|
+
const isList = Array.isArray(value);
|
|
1118
|
+
const { type = isList ? 'CARDINAL' : 'UTF8_STRING', format = isList ? 32 : 8 } =
|
|
1119
|
+
options;
|
|
1120
|
+
const [property, typeAtom] = await Promise.all([this.atom(name), this.atom(type)]);
|
|
1121
|
+
let data = value;
|
|
1122
|
+
if (isList) {
|
|
1123
|
+
const words = new Uint32Array(value);
|
|
1124
|
+
data = Buffer.from(words.buffer, words.byteOffset, words.byteLength);
|
|
1125
|
+
} else if (typeof value === 'string') {
|
|
1126
|
+
data = Buffer.from(value, type === 'STRING' ? 'latin1' : 'utf8');
|
|
1127
|
+
}
|
|
1128
|
+
if (this._destroyed) return this;
|
|
1129
|
+
safeRelease(this.X, () => {
|
|
1130
|
+
this.X.ChangeProperty(0, this.id, property, typeAtom, format, data);
|
|
1131
|
+
});
|
|
1132
|
+
return this;
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1135
|
+
/**
|
|
1136
|
+
* Intern an atom by name. node-x11 caches them per connection, so asking
|
|
1137
|
+
* again for one already interned costs no round trip.
|
|
1138
|
+
* @returns {Promise<number>} the atom id
|
|
1139
|
+
*/
|
|
1140
|
+
atom(name) {
|
|
1141
|
+
return new Promise((resolve, reject) => {
|
|
1142
|
+
this.X.InternAtom(false, name, (err, id) => (err ? reject(err) : resolve(id)));
|
|
1143
|
+
});
|
|
1144
|
+
}
|
|
1145
|
+
|
|
1146
|
+
/**
|
|
1147
|
+
* The window's title as the ICCCM/EWMH pair defines it: _NET_WM_NAME
|
|
1148
|
+
* (UTF-8) if the client set one, else WM_NAME. Resolves to null when the
|
|
1149
|
+
* window has neither. The read counterpart of setTitle.
|
|
1150
|
+
*/
|
|
1151
|
+
async getTitle() {
|
|
1152
|
+
const netName = await this.getProperty('_NET_WM_NAME', { as: 'string' }).catch(() => null);
|
|
1153
|
+
if (netName) return netName;
|
|
1154
|
+
return this.getProperty('WM_NAME', { as: 'string' }).catch(() => null);
|
|
1155
|
+
}
|
|
1156
|
+
|
|
1157
|
+
/**
|
|
1158
|
+
* WM_NORMAL_HINTS as an object shaped like setSizeHints' argument —
|
|
1159
|
+
* `{ minWidth, minHeight, maxWidth, maxHeight, widthInc, heightInc,
|
|
1160
|
+
* baseWidth, baseHeight, minAspect, maxAspect, gravity }`, each present
|
|
1161
|
+
* only if the client set its flag. Resolves to `{}` when the window has
|
|
1162
|
+
* no hints, so callers can destructure without a null check.
|
|
1163
|
+
*/
|
|
1164
|
+
async getSizeHints() {
|
|
1165
|
+
const v = await this.getProperty('WM_NORMAL_HINTS', { as: 'numbers' }).catch(() => null);
|
|
1166
|
+
if (!v || v.length < 18) return {};
|
|
1167
|
+
const flags = v[0];
|
|
1168
|
+
const hints = {};
|
|
1169
|
+
if (flags & 16) {
|
|
1170
|
+
hints.minWidth = v[5];
|
|
1171
|
+
hints.minHeight = v[6];
|
|
1172
|
+
}
|
|
1173
|
+
if (flags & 32) {
|
|
1174
|
+
hints.maxWidth = v[7];
|
|
1175
|
+
hints.maxHeight = v[8];
|
|
1176
|
+
}
|
|
1177
|
+
if (flags & 64) {
|
|
1178
|
+
hints.widthInc = v[9];
|
|
1179
|
+
hints.heightInc = v[10];
|
|
1180
|
+
}
|
|
1181
|
+
if (flags & 128) {
|
|
1182
|
+
hints.minAspect = [v[11], v[12]];
|
|
1183
|
+
hints.maxAspect = [v[13], v[14]];
|
|
1184
|
+
}
|
|
1185
|
+
if (flags & 256) {
|
|
1186
|
+
hints.baseWidth = v[15];
|
|
1187
|
+
hints.baseHeight = v[16];
|
|
1188
|
+
}
|
|
1189
|
+
if (flags & 512) hints.gravity = v[17];
|
|
1190
|
+
return hints;
|
|
1191
|
+
}
|
|
1192
|
+
|
|
1193
|
+
/**
|
|
1194
|
+
* GetWindowAttributes — `{ mapState, overrideRedirect, ... }`. A window
|
|
1195
|
+
* manager adopting the windows that already existed when it started
|
|
1196
|
+
* needs both: override-redirect windows are none of its business, and
|
|
1197
|
+
* only mapped ones should get a frame.
|
|
1198
|
+
*/
|
|
1199
|
+
getAttributes() {
|
|
1200
|
+
return new Promise((resolve, reject) => {
|
|
1201
|
+
safeRelease(this.X, () =>
|
|
1202
|
+
this.X.GetWindowAttributes(this.id, (err, attrs) => (err ? reject(err) : resolve(attrs)))
|
|
1203
|
+
);
|
|
1204
|
+
});
|
|
1205
|
+
}
|
|
1206
|
+
|
|
1207
|
+
// ---------------------------------------------------------------------
|
|
1208
|
+
// Managing other clients' windows
|
|
1209
|
+
// ---------------------------------------------------------------------
|
|
1210
|
+
|
|
1211
|
+
/**
|
|
1212
|
+
* Select an event mask explicitly, reporting failure. Handler-driven
|
|
1213
|
+
* selection (`on('map_request')`, the onXxx constructor args) covers the
|
|
1214
|
+
* ordinary case; this exists for the mask that can be refused —
|
|
1215
|
+
* SubstructureRedirect belongs to one client at a time, so
|
|
1216
|
+
*
|
|
1217
|
+
* await app.rootWindow().selectInput(SubstructureRedirect | SubstructureNotify)
|
|
1218
|
+
*
|
|
1219
|
+
* either makes you the window manager or rejects with BadAccess because
|
|
1220
|
+
* something else already is. The mask is OR-ed into whatever handlers
|
|
1221
|
+
* have already asked for.
|
|
1222
|
+
*/
|
|
1223
|
+
selectInput(mask) {
|
|
1224
|
+
this.eventMask |= mask;
|
|
1225
|
+
return new Promise((resolve, reject) => {
|
|
1226
|
+
this.X.ChangeWindowAttributes(this.id, { eventMask: this.eventMask }, (err) =>
|
|
1227
|
+
err ? reject(err) : resolve(this)
|
|
1228
|
+
);
|
|
1229
|
+
});
|
|
1230
|
+
}
|
|
1231
|
+
|
|
1232
|
+
/**
|
|
1233
|
+
* Add this window to our save-set (X ChangeSaveSet). A window manager
|
|
1234
|
+
* reparents clients into frames it owns; without the save-set, the
|
|
1235
|
+
* clients would be destroyed along with those frames if the WM exits.
|
|
1236
|
+
* With it the server reparents them back to the root and remaps them.
|
|
1237
|
+
*/
|
|
1238
|
+
addToSaveSet() {
|
|
1239
|
+
safeRelease(this.X, () => this.X.ChangeSaveSet(1, this.id));
|
|
1240
|
+
return this;
|
|
1241
|
+
}
|
|
1242
|
+
|
|
1243
|
+
removeFromSaveSet() {
|
|
1244
|
+
safeRelease(this.X, () => this.X.ChangeSaveSet(0, this.id));
|
|
1245
|
+
return this;
|
|
1246
|
+
}
|
|
1247
|
+
|
|
1248
|
+
/**
|
|
1249
|
+
* Tell the client where it really ended up (ICCCM 4.1.5). A reparented
|
|
1250
|
+
* window's own ConfigureNotify carries coordinates relative to its
|
|
1251
|
+
* frame, which is not what the client asked about, and a
|
|
1252
|
+
* ConfigureRequest the window manager decided to refuse produces no
|
|
1253
|
+
* ConfigureNotify at all — clients that resize themselves hang waiting
|
|
1254
|
+
* for one. Both cases are answered by a synthetic event carrying
|
|
1255
|
+
* root-relative coordinates.
|
|
1256
|
+
*
|
|
1257
|
+
* @param {object} geometry `{ x, y, width, height, borderWidth }` in
|
|
1258
|
+
* root coordinates; anything omitted comes from the window.
|
|
1259
|
+
*/
|
|
1260
|
+
sendConfigureNotify(geometry = {}) {
|
|
1261
|
+
const {
|
|
1262
|
+
x = this.x,
|
|
1263
|
+
y = this.y,
|
|
1264
|
+
width = this.width,
|
|
1265
|
+
height = this.height,
|
|
1266
|
+
borderWidth = 0
|
|
1267
|
+
} = geometry;
|
|
1268
|
+
const ev = Buffer.alloc(32);
|
|
1269
|
+
ev[0] = 22; // ConfigureNotify
|
|
1270
|
+
ev.writeUInt32LE(this.id >>> 0, 4); // event window
|
|
1271
|
+
ev.writeUInt32LE(this.id >>> 0, 8); // the window itself
|
|
1272
|
+
ev.writeUInt32LE(0, 12); // above-sibling: None
|
|
1273
|
+
ev.writeInt16LE(x, 16);
|
|
1274
|
+
ev.writeInt16LE(y, 18);
|
|
1275
|
+
ev.writeUInt16LE(width, 20);
|
|
1276
|
+
ev.writeUInt16LE(height, 22);
|
|
1277
|
+
ev.writeUInt16LE(borderWidth, 24);
|
|
1278
|
+
ev[26] = 0; // override-redirect
|
|
1279
|
+
safeRelease(this.X, () =>
|
|
1280
|
+
this.X.SendEvent(this.id, 0, x11.eventMask.StructureNotify, ev)
|
|
1281
|
+
);
|
|
1282
|
+
return this;
|
|
1283
|
+
}
|
|
1284
|
+
|
|
1285
|
+
/**
|
|
1286
|
+
* Ask the client to close, the polite way: a WM_DELETE_WINDOW client
|
|
1287
|
+
* message if it advertised the protocol in WM_PROTOCOLS, so it can save
|
|
1288
|
+
* work or put up a confirmation. Clients that did not advertise it have
|
|
1289
|
+
* no such path and are killed outright (X KillClient), which is what
|
|
1290
|
+
* `xkill` does.
|
|
1291
|
+
*
|
|
1292
|
+
* This is the window manager's side of the protocol `setActions()` opts
|
|
1293
|
+
* a window into.
|
|
1294
|
+
*
|
|
1295
|
+
* @returns {Promise<boolean>} true if asked politely, false if killed
|
|
1296
|
+
*/
|
|
1297
|
+
async close() {
|
|
1298
|
+
const protocols = await this.getProperty('WM_PROTOCOLS', { as: 'numbers' }).catch(() => null);
|
|
1299
|
+
const deleteAtom = await this.atom('WM_DELETE_WINDOW').catch(() => 0);
|
|
1300
|
+
if (!deleteAtom || !protocols || !protocols.includes(deleteAtom)) {
|
|
1301
|
+
safeRelease(this.X, () => this.X.KillClient(this.id));
|
|
1302
|
+
return false;
|
|
1303
|
+
}
|
|
1304
|
+
const wmProtocols = await this.atom('WM_PROTOCOLS');
|
|
1305
|
+
const ev = Buffer.alloc(32);
|
|
1306
|
+
ev[0] = 33; // ClientMessage
|
|
1307
|
+
ev[1] = 32; // format
|
|
1308
|
+
ev.writeUInt32LE(this.id >>> 0, 4);
|
|
1309
|
+
ev.writeUInt32LE(wmProtocols >>> 0, 8);
|
|
1310
|
+
ev.writeUInt32LE(deleteAtom >>> 0, 12);
|
|
1311
|
+
ev.writeUInt32LE(0, 16); // CurrentTime
|
|
1312
|
+
safeRelease(this.X, () => this.X.SendEvent(this.id, 0, 0, ev));
|
|
1313
|
+
return true;
|
|
1314
|
+
}
|
|
1315
|
+
|
|
1316
|
+
/**
|
|
1317
|
+
* Grab a mouse button on this window (X GrabButton). A window manager
|
|
1318
|
+
* uses this to see clicks that belong to the client: with
|
|
1319
|
+
* `pointerMode: 0` (synchronous) the pointer freezes on press until
|
|
1320
|
+
* `app.allowEvents()` decides what happens — 'replay' hands the click
|
|
1321
|
+
* back to the client, so click-to-focus can raise the window without
|
|
1322
|
+
* swallowing the click.
|
|
1323
|
+
*
|
|
1324
|
+
* @param {object} [options] `{ button (0 = any), modifiers (0x8000 =
|
|
1325
|
+
* any), ownerEvents, events, pointerMode, keyboardMode, confineTo,
|
|
1326
|
+
* cursor }`
|
|
1327
|
+
*/
|
|
1328
|
+
grabButton(options = {}) {
|
|
1329
|
+
const {
|
|
1330
|
+
button = 0,
|
|
1331
|
+
modifiers = 0x8000,
|
|
1332
|
+
ownerEvents = false,
|
|
1333
|
+
events = x11.eventMask.ButtonPress | x11.eventMask.ButtonRelease,
|
|
1334
|
+
pointerMode = 1,
|
|
1335
|
+
keyboardMode = 1,
|
|
1336
|
+
confineTo = 0,
|
|
1337
|
+
cursor = 0
|
|
1338
|
+
} = options;
|
|
1339
|
+
this.X.GrabButton(
|
|
1340
|
+
this.id, ownerEvents, events, pointerMode, keyboardMode,
|
|
1341
|
+
confineTo, cursor, button, modifiers
|
|
1342
|
+
);
|
|
1343
|
+
return this;
|
|
1344
|
+
}
|
|
1345
|
+
|
|
1346
|
+
ungrabButton(button = 0, modifiers = 0x8000) {
|
|
1347
|
+
safeRelease(this.X, () => this.X.UngrabButton(this.id, button, modifiers));
|
|
1348
|
+
return this;
|
|
1000
1349
|
}
|
|
1001
1350
|
|
|
1002
1351
|
queryTree(callback) {
|
|
@@ -1004,12 +1353,13 @@ export default class Window extends Drawable {
|
|
|
1004
1353
|
this.X.QueryTree(this.id, (err, tree) => {
|
|
1005
1354
|
if (err) return callback(err);
|
|
1006
1355
|
const children = tree.children.map((id) => new Window(app, { id }));
|
|
1007
|
-
|
|
1356
|
+
// the root window has no parent — QueryTree answers None (0) for it
|
|
1357
|
+
const parent = tree.parent ? new Window(app, { id: tree.parent }) : null;
|
|
1008
1358
|
// note that this root may be different from app.rootWindow() because
|
|
1009
1359
|
// there can be multiple screens (and roots)
|
|
1010
1360
|
const root = new Window(app, { id: tree.root });
|
|
1011
|
-
const
|
|
1012
|
-
Promise.all(
|
|
1361
|
+
const wrappers = [...children, root, ...(parent ? [parent] : [])];
|
|
1362
|
+
Promise.all(wrappers.map((w) => w._readyPromise)).then(() => {
|
|
1013
1363
|
callback(null, { parent, root, children });
|
|
1014
1364
|
});
|
|
1015
1365
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ntk",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.9.0",
|
|
4
4
|
"description": "Desktop UI toolkit for X11 with canvas-like 2d and OpenGL rendering",
|
|
5
5
|
"author": "Andrey Sidorov <sidorares@yandex.ru>",
|
|
6
6
|
"license": "MIT",
|
|
@@ -51,7 +51,7 @@
|
|
|
51
51
|
"parse-color": "^1.0.0",
|
|
52
52
|
"pngjs": "^7.0.0",
|
|
53
53
|
"postcss": "^8.5.23",
|
|
54
|
-
"x11": "^3.
|
|
54
|
+
"x11": "^3.2.0",
|
|
55
55
|
"yoga-layout": "^3.2.1"
|
|
56
56
|
},
|
|
57
57
|
"scripts": {
|