react-x11 2.15.3 → 2.16.1

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.
Files changed (67) hide show
  1. package/README.md +37 -0
  2. package/package.json +3 -3
  3. package/src/Reconciler.js +85 -22
  4. package/src/acceleratorhooks.js +40 -6
  5. package/src/anchor.js +79 -19
  6. package/src/capabilities.js +29 -4
  7. package/src/cocoa/app.js +211 -11
  8. package/src/cocoa/context2d.js +23 -0
  9. package/src/cocoa/fonts.js +78 -0
  10. package/src/cocoa/presenter.js +17 -0
  11. package/src/cocoa/promotion.js +20 -0
  12. package/src/cocoa/relaunch.js +8 -3
  13. package/src/cocoa/symbols.js +64 -0
  14. package/src/cocoa/threaded.js +24 -4
  15. package/src/cocoa/window.js +362 -139
  16. package/src/components/ProgressBar.js +1 -1
  17. package/src/components/Slider.js +72 -39
  18. package/src/components/anchor.js +7 -2
  19. package/src/components/index.js +1 -0
  20. package/src/components/theme.js +32 -28
  21. package/src/desktopcapabilityhooks.js +29 -6
  22. package/src/filedialoghooks.js +3 -5
  23. package/src/frame/childmain.js +8 -20
  24. package/src/frame/env.js +2 -10
  25. package/src/icontheme.js +240 -0
  26. package/src/imagesource.js +83 -1
  27. package/src/index.d.ts +10 -1
  28. package/src/index.js +3 -0
  29. package/src/keysymchars.js +47 -0
  30. package/src/keysyms.d.ts +19 -1
  31. package/src/keysyms.js +107 -8
  32. package/src/node.d.ts +7 -0
  33. package/src/nodes/animation.js +17 -47
  34. package/src/nodes/cascade.js +17 -2
  35. package/src/nodes/image.js +63 -1
  36. package/src/nodes/kinds.js +12 -0
  37. package/src/nodes/layout.js +5 -1
  38. package/src/nodes/node.js +17 -3
  39. package/src/nodes/paint.js +117 -0
  40. package/src/nodes/scope.js +259 -0
  41. package/src/nodes/scrollable.js +53 -6
  42. package/src/nodes/text.js +2 -0
  43. package/src/nodes/textarea.js +1 -1
  44. package/src/nodes/textinput.js +1 -1
  45. package/src/nodes/window/anchoring.js +45 -18
  46. package/src/nodes/window/flush.js +6 -5
  47. package/src/nodes/window/popup.js +10 -0
  48. package/src/nodes/window/size.js +40 -2
  49. package/src/nodes/window/window.js +41 -14
  50. package/src/registry.js +2 -1
  51. package/src/screens.js +159 -24
  52. package/src/settings.js +332 -0
  53. package/src/statusnotifier.js +164 -17
  54. package/src/styles.js +212 -8
  55. package/src/symbols.js +200 -0
  56. package/src/testing/mock-app.js +10 -0
  57. package/src/trayhooks.js +21 -5
  58. package/src/types/capabilities.d.ts +13 -1
  59. package/src/types/components.d.ts +33 -0
  60. package/src/types/elements.d.ts +57 -6
  61. package/src/types/events.d.ts +5 -0
  62. package/src/types/filedialog.d.ts +3 -1
  63. package/src/types/style.d.ts +57 -0
  64. package/src/types/system.d.ts +104 -0
  65. package/src/types/tray.d.ts +14 -2
  66. package/src/wayland/xkb.js +170 -59
  67. package/src/windowid.js +62 -20
@@ -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
- // keysym -> virtual modifier
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, geometry, and the
33
- // per-type `preserve` rules.
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) (this._interp ??= new Map()).set(sym, vm[1]);
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: the keys whose keysym `interpret`
260
- * binds to the virtual modifier are looked up in `modifier_map`, and the
261
- * real bits found there are the answer.
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 bySym = new Map(); // keysym -> real mask, from modmap via symbols
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 (const g of key.groups)
269
- for (const sym of g?.syms ?? [])
270
- if (sym) bySym.set(sym, (bySym.get(sym) ?? 0) | bits);
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
- for (const { sym, bit } of this._modmapSyms ?? [])
273
- bySym.set(sym, (bySym.get(sym) ?? 0) | bit);
274
- for (const [sym, vmod] of this._interp ?? []) {
275
- const real = bySym.get(sym);
276
- if (real) this.vmods.set(vmod, (this.vmods.get(vmod) ?? 0) | real);
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
- let relevant = 0;
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
- let want = 0;
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 the first symbol,
353
- // uppercased when it has a case.
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 on a plain two-level alphabetic key whose type does not
358
- // fold Lock (a keymap with only "TWO_LEVEL" for letters): X core rule.
359
- if (
360
- level === 0 &&
361
- mods & REAL_MODS.Lock &&
362
- g.syms.length >= 2 &&
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
- * The type XKB assigns a key that names none: one symbol is ONE_LEVEL, two
402
- * are ALPHABETIC when they are a case pair and TWO_LEVEL otherwise, four are
403
- * FOUR_LEVEL(_ALPHABETIC / _SEMIALPHABETIC).
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
- const n = syms.filter(Boolean).length;
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 casePair = (a, b) => {
409
- const la = charOf(a);
410
- const lb = charOf(b);
411
- return la && lb && la !== lb && la.toUpperCase() === lb;
412
- };
413
- if (n === 2) return casePair(syms[0], syms[1]) ? 'ALPHABETIC' : 'TWO_LEVEL';
414
- if (casePair(syms[0], syms[1]))
415
- return casePair(syms[2], syms[3])
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. */
package/src/windowid.js CHANGED
@@ -105,8 +105,54 @@ export function topLevelWindows(app) {
105
105
  );
106
106
  }
107
107
 
108
+ /**
109
+ * The root-level `<popup>`s this connection is currently rendering — the
110
+ * half `topLevelWindows()` leaves out, in the order they were added.
111
+ *
112
+ * Only interesting when there are no top-level windows at all: a menu-bar
113
+ * app whose whole UI is a popover a tray click opens has no `<window>` for
114
+ * anything to belong to, and the popup *is* the top of that tree
115
+ * (`useTopLevelWindow`). With a window in the tree these stay out of it, for
116
+ * the reason they are out of `topLevelWindows()` — override-redirect is
117
+ * never what a dialog should be transient for.
118
+ */
119
+ function rootLevelPopups(app) {
120
+ if (!app) return [];
121
+ return (app._rootChildren ?? []).filter(
122
+ (node) => node?.isWindow && node.isPopup && node.window?.id,
123
+ );
124
+ }
125
+
108
126
  let warnedAboutAmbiguity = false;
109
127
 
128
+ /**
129
+ * One of several candidates, on the inference `useTopLevelWindow` documents:
130
+ * one is exact, uniquely focused wins, and anything else is the most
131
+ * recently opened plus a development warning that says so.
132
+ */
133
+ function pickOwner(candidates, what) {
134
+ if (candidates.length <= 1) return candidates[0] ?? null;
135
+
136
+ const focused = candidates.filter((w) => w.events?.windowFocused);
137
+ if (focused.length === 1) return focused[0];
138
+
139
+ // Nothing separates them. `windowFocused` also defaults to true on an
140
+ // ntk too old to report focus changes, so "all of them" is the same
141
+ // answer as "none of them" and both land here.
142
+ if (process.env.NODE_ENV !== 'production' && !warnedAboutAmbiguity) {
143
+ warnedAboutAmbiguity = true;
144
+ console.warn(
145
+ `react-x11: this tree has ${candidates.length} ${what}, none of them ` +
146
+ 'uniquely focused, so the owner window is a guess (the most ' +
147
+ 'recently opened). Pass the window explicitly to be exact:\n' +
148
+ ' const win = useRef(null);\n' +
149
+ ' const { openFile } = useFileDialog({ parentWindow: win });\n' +
150
+ ' return <window ref={win}>…</window>;',
151
+ );
152
+ }
153
+ return candidates[candidates.length - 1];
154
+ }
155
+
110
156
  /**
111
157
  * The window a component belongs to, resolved when it is read.
112
158
  *
@@ -134,6 +180,13 @@ let warnedAboutAmbiguity = false;
134
180
  * - several with nothing to separate them: the most recently opened, and a
135
181
  * development warning naming `parentWindow` as the way to be exact. A
136
182
  * guess that says it is guessing beats a guess that does not.
183
+ * - **no top-level window at all: the root-level `<popup>` that has the
184
+ * keyboard.** A menu-bar app is a tray item and a popover, and nothing
185
+ * else — there is no `<window>` for a shortcut, a file dialog or the
186
+ * global menu to belong to, and answering `null` made every one of them
187
+ * quietly do nothing (issue #616). A `grabKeyboard` popup is where the
188
+ * keys are by construction, so it is preferred over one that is merely
189
+ * up; among equals the same focus/most-recent inference applies.
137
190
  *
138
191
  * Returns a **ref-like object** rather than a number: the window is not
139
192
  * realized on the first render, so a value read then would be `null` on the
@@ -146,27 +199,16 @@ export function useTopLevelWindow() {
146
199
  () => ({
147
200
  get current() {
148
201
  const windows = topLevelWindows(app);
149
- if (windows.length <= 1) return windows[0] ?? null;
150
-
151
- const focused = windows.filter((w) => w.events?.windowFocused);
152
- if (focused.length === 1) return focused[0];
202
+ if (windows.length) return pickOwner(windows, 'top-level windows');
153
203
 
154
- // Nothing separates them. `windowFocused` also defaults to true on an
155
- // ntk too old to report focus changes, so "all of them" is the same
156
- // answer as "none of them" and both land here.
157
- if (process.env.NODE_ENV !== 'production' && !warnedAboutAmbiguity) {
158
- warnedAboutAmbiguity = true;
159
- console.warn(
160
- `react-x11: this tree has ${windows.length} top-level windows and ` +
161
- 'none of them is uniquely focused, so the owner window is a ' +
162
- 'guess (the most recently opened). Pass the window explicitly ' +
163
- 'to be exact:\n' +
164
- ' const win = useRef(null);\n' +
165
- ' const { openFile } = useFileDialog({ parentWindow: win });\n' +
166
- ' return <window ref={win}>…</window>;',
167
- );
168
- }
169
- return windows[windows.length - 1];
204
+ // A popup-only tree the tray popover. Not reached while the app
205
+ // has a window, so nothing that already worked changes shape.
206
+ const popups = rootLevelPopups(app);
207
+ const keyboard = popups.filter((node) => node.props?.grabKeyboard);
208
+ return pickOwner(
209
+ keyboard.length ? keyboard : popups,
210
+ 'root-level popups and no window at all',
211
+ );
170
212
  },
171
213
  }),
172
214
  [app],