@gtkx/testing 1.0.0-rc.4 → 1.1.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.
Files changed (212) hide show
  1. package/README.md +5 -5
  2. package/dist/accessible-native.d.ts +16 -0
  3. package/dist/accessible-native.d.ts.map +1 -0
  4. package/dist/accessible-native.js +206 -0
  5. package/dist/accessible-native.js.map +1 -0
  6. package/dist/act.d.ts +1 -3
  7. package/dist/act.d.ts.map +1 -1
  8. package/dist/act.js +1 -1
  9. package/dist/act.js.map +1 -1
  10. package/dist/build-queries.d.ts +27 -10
  11. package/dist/build-queries.d.ts.map +1 -1
  12. package/dist/build-queries.js +22 -24
  13. package/dist/build-queries.js.map +1 -1
  14. package/dist/cleanup-registry.d.ts +1 -1
  15. package/dist/cleanup-registry.d.ts.map +1 -1
  16. package/dist/cleanup-registry.js.map +1 -1
  17. package/dist/config.d.ts +8 -1
  18. package/dist/config.d.ts.map +1 -1
  19. package/dist/config.js +1 -0
  20. package/dist/config.js.map +1 -1
  21. package/dist/errors.d.ts +10 -1
  22. package/dist/errors.d.ts.map +1 -1
  23. package/dist/errors.js +10 -1
  24. package/dist/errors.js.map +1 -1
  25. package/dist/fire-event.d.ts +35 -6
  26. package/dist/fire-event.d.ts.map +1 -1
  27. package/dist/fire-event.js +23 -7
  28. package/dist/fire-event.js.map +1 -1
  29. package/dist/frame-sync.d.ts +3 -2
  30. package/dist/frame-sync.d.ts.map +1 -1
  31. package/dist/frame-sync.js +19 -9
  32. package/dist/frame-sync.js.map +1 -1
  33. package/dist/harness-window.d.ts +5 -0
  34. package/dist/harness-window.d.ts.map +1 -0
  35. package/dist/harness-window.js +17 -0
  36. package/dist/harness-window.js.map +1 -0
  37. package/dist/index.d.ts +14 -7
  38. package/dist/index.d.ts.map +1 -1
  39. package/dist/index.js +9 -5
  40. package/dist/index.js.map +1 -1
  41. package/dist/internal.d.ts +5 -0
  42. package/dist/internal.d.ts.map +1 -0
  43. package/dist/internal.js +5 -0
  44. package/dist/internal.js.map +1 -0
  45. package/dist/matchers.d.ts +88 -46
  46. package/dist/matchers.d.ts.map +1 -1
  47. package/dist/matchers.js +178 -20
  48. package/dist/matchers.js.map +1 -1
  49. package/dist/pretty-format.d.ts +2 -0
  50. package/dist/pretty-format.d.ts.map +1 -0
  51. package/dist/pretty-format.js +2 -0
  52. package/dist/pretty-format.js.map +1 -0
  53. package/dist/pretty-widget.d.ts +5 -2
  54. package/dist/pretty-widget.d.ts.map +1 -1
  55. package/dist/pretty-widget.js +75 -57
  56. package/dist/pretty-widget.js.map +1 -1
  57. package/dist/production.d.ts +8 -0
  58. package/dist/production.d.ts.map +1 -0
  59. package/dist/production.js +34 -0
  60. package/dist/production.js.map +1 -0
  61. package/dist/queries.d.ts +98 -56
  62. package/dist/queries.d.ts.map +1 -1
  63. package/dist/queries.js +123 -83
  64. package/dist/queries.js.map +1 -1
  65. package/dist/query-helpers.d.ts +44 -0
  66. package/dist/query-helpers.d.ts.map +1 -0
  67. package/dist/query-helpers.js +57 -0
  68. package/dist/query-helpers.js.map +1 -0
  69. package/dist/render-children.d.ts +9 -0
  70. package/dist/render-children.d.ts.map +1 -0
  71. package/dist/render-children.js +11 -0
  72. package/dist/render-children.js.map +1 -0
  73. package/dist/render.d.ts +12 -1
  74. package/dist/render.d.ts.map +1 -1
  75. package/dist/render.js +52 -16
  76. package/dist/render.js.map +1 -1
  77. package/dist/role-helpers.d.ts +12 -3
  78. package/dist/role-helpers.d.ts.map +1 -1
  79. package/dist/role-helpers.js +15 -5
  80. package/dist/role-helpers.js.map +1 -1
  81. package/dist/role-naming.d.ts +5 -0
  82. package/dist/role-naming.d.ts.map +1 -0
  83. package/dist/role-naming.js +96 -0
  84. package/dist/role-naming.js.map +1 -0
  85. package/dist/screenshot.d.ts +12 -20
  86. package/dist/screenshot.d.ts.map +1 -1
  87. package/dist/screenshot.js +179 -93
  88. package/dist/screenshot.js.map +1 -1
  89. package/dist/timers.d.ts +9 -0
  90. package/dist/timers.d.ts.map +1 -0
  91. package/dist/timers.js +21 -0
  92. package/dist/timers.js.map +1 -0
  93. package/dist/traversal.d.ts +7 -1
  94. package/dist/traversal.d.ts.map +1 -1
  95. package/dist/traversal.js +40 -6
  96. package/dist/traversal.js.map +1 -1
  97. package/dist/types.d.ts +32 -15
  98. package/dist/types.d.ts.map +1 -1
  99. package/dist/types.js.map +1 -1
  100. package/dist/user-event/adjustment.d.ts +3 -0
  101. package/dist/user-event/adjustment.d.ts.map +1 -1
  102. package/dist/user-event/adjustment.js +25 -3
  103. package/dist/user-event/adjustment.js.map +1 -1
  104. package/dist/user-event/click.d.ts +44 -8
  105. package/dist/user-event/click.d.ts.map +1 -1
  106. package/dist/user-event/click.js +176 -44
  107. package/dist/user-event/click.js.map +1 -1
  108. package/dist/user-event/column-header-click.d.ts +4 -0
  109. package/dist/user-event/column-header-click.d.ts.map +1 -0
  110. package/dist/user-event/column-header-click.js +55 -0
  111. package/dist/user-event/column-header-click.js.map +1 -0
  112. package/dist/user-event/event-wrapper.d.ts.map +1 -1
  113. package/dist/user-event/event-wrapper.js +34 -20
  114. package/dist/user-event/event-wrapper.js.map +1 -1
  115. package/dist/user-event/index.d.ts +80 -13
  116. package/dist/user-event/index.d.ts.map +1 -1
  117. package/dist/user-event/index.js +44 -26
  118. package/dist/user-event/index.js.map +1 -1
  119. package/dist/user-event/indexed-children.d.ts +10 -0
  120. package/dist/user-event/indexed-children.d.ts.map +1 -0
  121. package/dist/user-event/indexed-children.js +50 -0
  122. package/dist/user-event/indexed-children.js.map +1 -0
  123. package/dist/user-event/keyboard.d.ts.map +1 -1
  124. package/dist/user-event/keyboard.js +115 -31
  125. package/dist/user-event/keyboard.js.map +1 -1
  126. package/dist/user-event/list-row-click.d.ts +4 -0
  127. package/dist/user-event/list-row-click.d.ts.map +1 -0
  128. package/dist/user-event/list-row-click.js +38 -0
  129. package/dist/user-event/list-row-click.js.map +1 -0
  130. package/dist/user-event/native-click.d.ts +6 -0
  131. package/dist/user-event/native-click.d.ts.map +1 -0
  132. package/dist/user-event/native-click.js +36 -0
  133. package/dist/user-event/native-click.js.map +1 -0
  134. package/dist/user-event/notebook-tab-click.d.ts +5 -0
  135. package/dist/user-event/notebook-tab-click.d.ts.map +1 -0
  136. package/dist/user-event/notebook-tab-click.js +39 -0
  137. package/dist/user-event/notebook-tab-click.js.map +1 -0
  138. package/dist/user-event/pointer.d.ts +1 -1
  139. package/dist/user-event/pointer.d.ts.map +1 -1
  140. package/dist/user-event/pointer.js +33 -9
  141. package/dist/user-event/pointer.js.map +1 -1
  142. package/dist/user-event/selection.d.ts +11 -8
  143. package/dist/user-event/selection.d.ts.map +1 -1
  144. package/dist/user-event/selection.js +49 -38
  145. package/dist/user-event/selection.js.map +1 -1
  146. package/dist/user-event/text.d.ts +12 -5
  147. package/dist/user-event/text.d.ts.map +1 -1
  148. package/dist/user-event/text.js +151 -46
  149. package/dist/user-event/text.js.map +1 -1
  150. package/dist/wait-for.d.ts.map +1 -1
  151. package/dist/wait-for.js +4 -3
  152. package/dist/wait-for.js.map +1 -1
  153. package/dist/widget-accessible-properties.d.ts +14 -26
  154. package/dist/widget-accessible-properties.d.ts.map +1 -1
  155. package/dist/widget-accessible-properties.js +135 -190
  156. package/dist/widget-accessible-properties.js.map +1 -1
  157. package/dist/widget-getters.d.ts +11 -0
  158. package/dist/widget-getters.d.ts.map +1 -0
  159. package/dist/widget-getters.js +46 -0
  160. package/dist/widget-getters.js.map +1 -0
  161. package/dist/window-state.d.ts +10 -0
  162. package/dist/window-state.d.ts.map +1 -0
  163. package/dist/window-state.js +41 -0
  164. package/dist/window-state.js.map +1 -0
  165. package/dist/within.d.ts +3 -2
  166. package/dist/within.d.ts.map +1 -1
  167. package/dist/within.js +3 -2
  168. package/dist/within.js.map +1 -1
  169. package/package.json +21 -8
  170. package/src/accessible-native.ts +333 -0
  171. package/src/act.ts +1 -1
  172. package/src/build-queries.ts +47 -41
  173. package/src/cleanup-registry.ts +1 -1
  174. package/src/config.ts +10 -1
  175. package/src/errors.ts +12 -1
  176. package/src/fire-event.ts +61 -8
  177. package/src/frame-sync.ts +30 -9
  178. package/src/harness-window.ts +23 -0
  179. package/src/index.ts +18 -11
  180. package/src/internal.ts +12 -0
  181. package/src/matchers.ts +457 -72
  182. package/src/pretty-format.ts +1 -0
  183. package/src/pretty-widget.ts +98 -70
  184. package/src/production.ts +51 -0
  185. package/src/queries.ts +127 -89
  186. package/src/query-helpers.ts +97 -0
  187. package/src/render-children.ts +25 -0
  188. package/src/render.tsx +77 -17
  189. package/src/role-helpers.ts +16 -5
  190. package/src/role-naming.ts +99 -0
  191. package/src/screenshot.ts +266 -104
  192. package/src/timers.ts +31 -0
  193. package/src/traversal.ts +66 -6
  194. package/src/types.ts +34 -17
  195. package/src/user-event/adjustment.ts +31 -3
  196. package/src/user-event/click.ts +248 -52
  197. package/src/user-event/column-header-click.ts +76 -0
  198. package/src/user-event/event-wrapper.ts +41 -23
  199. package/src/user-event/index.ts +130 -40
  200. package/src/user-event/indexed-children.ts +75 -0
  201. package/src/user-event/keyboard.ts +161 -43
  202. package/src/user-event/list-row-click.ts +54 -0
  203. package/src/user-event/native-click.ts +51 -0
  204. package/src/user-event/notebook-tab-click.ts +57 -0
  205. package/src/user-event/pointer.ts +48 -9
  206. package/src/user-event/selection.ts +67 -41
  207. package/src/user-event/text.ts +192 -46
  208. package/src/wait-for.ts +4 -4
  209. package/src/widget-accessible-properties.ts +183 -230
  210. package/src/widget-getters.ts +85 -0
  211. package/src/window-state.ts +70 -0
  212. package/src/within.ts +3 -2
@@ -1,9 +1,11 @@
1
1
  import * as Gdk from "@gtkx/gi/gdk";
2
+ import * as Gio from "@gtkx/gi/gio";
2
3
  import * as Gtk from "@gtkx/gi/gtk";
4
+ import { hasSignalListener } from "@gtkx/runtime/internal";
3
5
  import type { UserEventState } from "./state.js";
4
6
  import { getEditableDelegate } from "../editable.js";
5
7
  import { fireEvent } from "../fire-event.js";
6
- import { getOrCreateControllers } from "./controller.js";
8
+ import { ancestors, descendants } from "../traversal.js";
7
9
  import { wrapEvent } from "./event-wrapper.js";
8
10
 
9
11
  /** Options for `userEvent.tab`. */
@@ -14,6 +16,8 @@ type TabOptions = {
14
16
 
15
17
  type KeyAction = { keyval: number; isPress: boolean };
16
18
  type ParseStep = { actions: KeyAction[]; next: number };
19
+ type KeyStopController = Gtk.EventControllerKey | Gtk.ShortcutController;
20
+ type KeyStop = { widget: Gtk.Widget; controller: KeyStopController };
17
21
 
18
22
  const KEY_MAP: Record<string, number> = {
19
23
  Enter: Gdk.KEY_Return,
@@ -48,6 +52,8 @@ const KEY_MAP: Record<string, number> = {
48
52
  F12: Gdk.KEY_F12,
49
53
  };
50
54
 
55
+ const KEY_CONTROLLER_SIGNALS = ["key-pressed", "key-released"];
56
+
51
57
  const MODIFIER_KEYVAL_TO_MASK: Record<number, number> = {
52
58
  [Gdk.KEY_Shift_L]: Gdk.ModifierType.SHIFT_MASK,
53
59
  [Gdk.KEY_Shift_R]: Gdk.ModifierType.SHIFT_MASK,
@@ -179,6 +185,61 @@ const isTriggerMatch = (
179
185
  return false;
180
186
  };
181
187
 
188
+ const controllersOn = function* (widget: Gtk.Widget): Generator<Gtk.EventController> {
189
+ const controllers = widget.observeControllers();
190
+ const count = controllers.getNItems();
191
+
192
+ for (let i = 0; i < count; i++) {
193
+ const controller = controllers.getItem(i);
194
+
195
+ if (controller instanceof Gtk.EventController) {
196
+ yield controller;
197
+ }
198
+ }
199
+ };
200
+
201
+ const hasShortcut = (controller: Gtk.ShortcutController, shortcut: Gtk.Shortcut): boolean => {
202
+ const count = controller.getNItems();
203
+
204
+ for (let i = 0; i < count; i++) {
205
+ if (controller.getItem(i) === shortcut) {
206
+ return true;
207
+ }
208
+ }
209
+
210
+ return false;
211
+ };
212
+
213
+ const isManagingShortcut = (controller: Gtk.EventController, shortcut: Gtk.Shortcut): boolean =>
214
+ controller instanceof Gtk.ShortcutController &&
215
+ controller.getScope() !== Gtk.ShortcutScope.LOCAL &&
216
+ hasShortcut(controller, shortcut);
217
+
218
+ const managedShortcutHost = (manager: Gtk.Widget, shortcut: Gtk.Shortcut): Gtk.Widget | null => {
219
+ for (const current of [manager, ...descendants(manager)]) {
220
+ if (controllersOn(current).some((controller) => isManagingShortcut(controller, shortcut))) {
221
+ return current;
222
+ }
223
+ }
224
+
225
+ return null;
226
+ };
227
+
228
+ const isMovingFocus = (action: Gtk.ShortcutAction): boolean =>
229
+ action instanceof Gtk.SignalAction && action.getSignalName() === "move-focus";
230
+
231
+ const isShortcutLive = (host: Gtk.Widget): boolean => host.isSensitive() && host.getMapped();
232
+
233
+ const didRunShortcutAction = (shortcut: Gtk.Shortcut, host: Gtk.Widget): boolean => {
234
+ const action = shortcut.getAction();
235
+
236
+ if (action === null || isMovingFocus(action) || !isShortcutLive(host)) {
237
+ return false;
238
+ }
239
+
240
+ return action.activate(0 as Gtk.ShortcutActionFlags, host, shortcut.getArguments());
241
+ };
242
+
182
243
  const didActivateShortcut = (
183
244
  shortcut: Gtk.Shortcut,
184
245
  widget: Gtk.Widget,
@@ -189,13 +250,7 @@ const didActivateShortcut = (
189
250
  return false;
190
251
  }
191
252
 
192
- const action = shortcut.getAction();
193
-
194
- if (action instanceof Gtk.SignalAction && action.getSignalName() === "move-focus") {
195
- return false;
196
- }
197
-
198
- return action?.activate(0 as Gtk.ShortcutActionFlags, widget, shortcut.getArguments()) ?? false;
253
+ return didRunShortcutAction(shortcut, managedShortcutHost(widget, shortcut) ?? widget);
199
254
  };
200
255
 
201
256
  const didActivateMatchingShortcut = (
@@ -221,71 +276,134 @@ const didActivateMatchingShortcut = (
221
276
  return false;
222
277
  };
223
278
 
224
- const didDispatchShortcutsOnWidget = (widget: Gtk.Widget, keyval: number, modifiers: number): boolean => {
225
- const controllers = widget.observeControllers();
279
+ const isAccelMatch = (accel: string, keyval: number, modifiers: Gdk.ModifierType): boolean => {
280
+ const [isParsed, accelKeyval, accelModifiers] = Gtk.acceleratorParse(accel);
226
281
 
227
- for (let i = 0; i < controllers.getNItems(); i++) {
228
- const controller = controllers.getItem(i);
282
+ return isParsed && accelKeyval === keyval && accelModifiers === modifiers;
283
+ };
229
284
 
230
- if (
231
- controller instanceof Gtk.ShortcutController &&
232
- didActivateMatchingShortcut(controller, widget, keyval, modifiers)
233
- ) {
234
- return true;
285
+ const hasMatchingAccel = (
286
+ application: Gtk.Application,
287
+ detailedActionName: string,
288
+ keyval: number,
289
+ modifiers: number,
290
+ ): boolean =>
291
+ application.getAccelsForAction(detailedActionName).some((accel) => isAccelMatch(accel, keyval, modifiers));
292
+
293
+ const didActivateDetailedAction = (window: Gtk.Window, detailedActionName: string): boolean => {
294
+ const [isParsed, actionName, target] = Gio.Action.parseDetailedName(detailedActionName);
295
+
296
+ return isParsed && window.activateAction(actionName, target);
297
+ };
298
+
299
+ const didDispatchAccelsOnWindow = (window: Gtk.Window, keyval: number, modifiers: number): boolean => {
300
+ const application = window.getApplication();
301
+
302
+ if (!application) {
303
+ return false;
304
+ }
305
+
306
+ for (const detailedActionName of application.listActionDescriptions()) {
307
+ if (hasMatchingAccel(application, detailedActionName, keyval, modifiers)) {
308
+ return didActivateDetailedAction(window, detailedActionName);
235
309
  }
236
310
  }
237
311
 
238
312
  return false;
239
313
  };
240
314
 
241
- const didDispatchShortcuts = (widget: Gtk.Widget, keyval: number, modifiers: number): boolean => {
242
- const delegate = getEditableDelegate(widget);
315
+ const didDispatchApplicationAccels = (widget: Gtk.Widget, keyval: number, modifiers: number): boolean => {
316
+ const root = widget.getRoot();
317
+
318
+ return root instanceof Gtk.Window && didDispatchAccelsOnWindow(root, keyval, modifiers);
319
+ };
243
320
 
244
- if (delegate && didDispatchShortcutsOnWidget(delegate, keyval, modifiers)) {
245
- return true;
321
+ const isKeyStopController = (controller: Gtk.EventController): controller is KeyStopController => {
322
+ if (controller instanceof Gtk.ShortcutController) {
323
+ return controller.getScope() === Gtk.ShortcutScope.LOCAL;
246
324
  }
247
325
 
248
- for (let current: Gtk.Widget | null = widget; current; current = current.getParent()) {
249
- if (didDispatchShortcutsOnWidget(current, keyval, modifiers)) {
250
- return true;
326
+ return controller instanceof Gtk.EventControllerKey && hasSignalListener(controller, KEY_CONTROLLER_SIGNALS);
327
+ };
328
+
329
+ const keyStopsInPhase = (widget: Gtk.Widget, phase: Gtk.PropagationPhase): KeyStop[] => {
330
+ const stops: KeyStop[] = [];
331
+
332
+ for (const controller of controllersOn(widget)) {
333
+ if (controller.getPropagationPhase() === phase && isKeyStopController(controller)) {
334
+ stops.push({ widget, controller });
251
335
  }
252
336
  }
253
337
 
254
- return false;
338
+ return stops;
255
339
  };
256
340
 
257
- const handleKeyPress = async (widget: Gtk.Widget, keyval: number, modifiers: number): Promise<void> => {
258
- const isHandled = didDispatchShortcuts(widget, keyval, modifiers);
341
+ const propagationChain = (widget: Gtk.Widget): Gtk.Widget[] => {
342
+ const delegate = getEditableDelegate(widget);
343
+ const below = delegate === null ? [] : [delegate];
259
344
 
260
- if (!isHandled && keyval === Gdk.KEY_Return && widget instanceof Gtk.Editable) {
261
- await fireEvent(widget, "activate");
345
+ return [...below, widget, ...ancestors(widget)];
346
+ };
347
+
348
+ const keyPropagationChain = (widget: Gtk.Widget): KeyStop[] => {
349
+ const chain = propagationChain(widget);
350
+ const [eventWidget = widget] = chain;
351
+
352
+ return [
353
+ ...chain.toReversed().flatMap((current) => keyStopsInPhase(current, Gtk.PropagationPhase.CAPTURE)),
354
+ ...keyStopsInPhase(eventWidget, Gtk.PropagationPhase.TARGET),
355
+ ...chain.flatMap((current) => keyStopsInPhase(current, Gtk.PropagationPhase.BUBBLE)),
356
+ ];
357
+ };
358
+
359
+ const didStopHandlePress = (stop: KeyStop, keyval: number, modifiers: Gdk.ModifierType): boolean => {
360
+ const { controller, widget } = stop;
361
+
362
+ if (controller instanceof Gtk.ShortcutController) {
363
+ return didActivateMatchingShortcut(controller, widget, keyval, modifiers);
262
364
  }
365
+
366
+ return controller.emit("key-pressed", keyval, 0, modifiers);
263
367
  };
264
368
 
265
- const applyKeyAction = async (
266
- widget: Gtk.Widget,
267
- controllers: Gtk.EventControllerKey[],
268
- state: UserEventState,
269
- action: KeyAction,
270
- ): Promise<void> => {
271
- updateModifierState(state, action);
272
- const signalName = action.isPress ? "key-pressed" : "key-released";
369
+ const didHandlePress = (widget: Gtk.Widget, keyval: number, modifiers: Gdk.ModifierType): boolean =>
370
+ didDispatchApplicationAccels(widget, keyval, modifiers) ||
371
+ keyPropagationChain(widget).some((stop) => didStopHandlePress(stop, keyval, modifiers));
273
372
 
274
- for (const controller of controllers) {
275
- controller.emit(signalName, action.keyval, 0, state.modifierState);
373
+ const emitKeyReleased = (widget: Gtk.Widget, keyval: number, modifiers: Gdk.ModifierType): void => {
374
+ for (const stop of keyPropagationChain(widget)) {
375
+ if (stop.controller instanceof Gtk.EventControllerKey) {
376
+ stop.controller.emit("key-released", keyval, 0, modifiers);
377
+ }
378
+ }
379
+ };
380
+
381
+ const applyKeyPress = async (widget: Gtk.Widget, keyval: number, modifiers: Gdk.ModifierType): Promise<void> => {
382
+ if (didHandlePress(widget, keyval, modifiers)) {
383
+ return;
276
384
  }
277
385
 
386
+ if (keyval === Gdk.KEY_Return && widget instanceof Gtk.Editable) {
387
+ await fireEvent(widget, "activate");
388
+ }
389
+ };
390
+
391
+ const applyKeyAction = async (widget: Gtk.Widget, state: UserEventState, action: KeyAction): Promise<void> => {
392
+ updateModifierState(state, action);
393
+
278
394
  if (action.isPress) {
279
- await handleKeyPress(widget, action.keyval, state.modifierState);
395
+ await applyKeyPress(widget, action.keyval, state.modifierState);
396
+
397
+ return;
280
398
  }
399
+
400
+ emitKeyReleased(widget, action.keyval, state.modifierState);
281
401
  };
282
402
 
283
403
  const keyboard = (state: UserEventState, widget: Gtk.Widget, input: string): Promise<void> =>
284
404
  wrapEvent(widget, async () => {
285
- const controllers = getOrCreateControllers(widget, Gtk.EventControllerKey);
286
-
287
405
  for (const action of parseKeyboardInput(input)) {
288
- await applyKeyAction(widget, controllers, state, action);
406
+ await applyKeyAction(widget, state, action);
289
407
  }
290
408
  });
291
409
 
@@ -0,0 +1,54 @@
1
+ import type * as Gtk from "@gtkx/gi/gtk";
2
+ import * as GLib from "@gtkx/gi/glib";
3
+ import { ancestors } from "../traversal.js";
4
+ import { callBooleanGetter, hasWidgetMethod } from "../widget-getters.js";
5
+
6
+ const SELECT_ACTION = "listitem.select";
7
+ const SINGLE_CLICK_ACTIVATE_GETTER = "getSingleClickActivate";
8
+ const DOUBLE_CLICK_PRESS = 2;
9
+
10
+ const viewFor = (row: Gtk.Widget): Gtk.Widget | null => {
11
+ for (const ancestor of ancestors(row)) {
12
+ if (hasWidgetMethod(ancestor, SINGLE_CLICK_ACTIVATE_GETTER)) {
13
+ return ancestor;
14
+ }
15
+ }
16
+
17
+ return null;
18
+ };
19
+
20
+ const isViewSingleClickActivating = (row: Gtk.Widget): boolean => {
21
+ const view = viewFor(row);
22
+
23
+ return view === null ? false : callBooleanGetter(view, SINGLE_CLICK_ACTIVATE_GETTER) ?? false;
24
+ };
25
+
26
+ const selectRow = (row: Gtk.Widget): void => {
27
+ row.activateAction(
28
+ SELECT_ACTION,
29
+ GLib.Variant.newTuple([GLib.Variant.newBoolean(false), GLib.Variant.newBoolean(false)]),
30
+ );
31
+ };
32
+
33
+ const isRowActivatedByPress = (row: Gtk.Widget, press: number): boolean =>
34
+ press === DOUBLE_CLICK_PRESS || isViewSingleClickActivating(row);
35
+
36
+ const applyRowPress = (row: Gtk.Widget, press: number): void => {
37
+ if (row.getFocusOnClick()) {
38
+ row.grabFocus();
39
+ }
40
+
41
+ selectRow(row);
42
+
43
+ if (isRowActivatedByPress(row, press)) {
44
+ row.activate();
45
+ }
46
+ };
47
+
48
+ const applyRowClick = (row: Gtk.Widget, nPress: number): void => {
49
+ for (let press = 1; press <= nPress; press++) {
50
+ applyRowPress(row, press);
51
+ }
52
+ };
53
+
54
+ export { applyRowClick };
@@ -0,0 +1,51 @@
1
+ import * as Gtk from "@gtkx/gi/gtk";
2
+ import { getWidgetTypeName } from "../widget-getters.js";
3
+ import { applyHeaderClick } from "./column-header-click.js";
4
+ import { applyRowClick } from "./list-row-click.js";
5
+ import { applyTabClick, isNotebookTab } from "./notebook-tab-click.js";
6
+
7
+ type NativeClick = (widget: Gtk.Widget, nPress: number) => void;
8
+
9
+ const COLUMN_ROW_TYPE_NAME = "GtkColumnViewRowWidget";
10
+ const ROW_TYPE_NAMES: Set<string> = new Set([COLUMN_ROW_TYPE_NAME, "GtkListItemWidget"]);
11
+ const CELL_TYPE_NAME = "GtkColumnViewCellWidget";
12
+ const TITLE_TYPE_NAME = "GtkColumnViewTitle";
13
+ const TOGGLE_EXPAND_ACTION = "listitem.toggle-expand";
14
+
15
+ const isHeaderRow = (widget: Gtk.Widget): boolean =>
16
+ getWidgetTypeName(widget) === COLUMN_ROW_TYPE_NAME && widget.getParent() instanceof Gtk.ColumnView;
17
+
18
+ const isClickTransparent = (widget: Gtk.Widget): boolean =>
19
+ getWidgetTypeName(widget) === CELL_TYPE_NAME || isHeaderRow(widget);
20
+
21
+ const isExpanderToggling = (widget: Gtk.Widget): boolean =>
22
+ widget instanceof Gtk.TreeExpander &&
23
+ widget.getListRow()?.isExpandable() === true &&
24
+ !widget.getHideExpander();
25
+
26
+ const isColumnHeader = (widget: Gtk.Widget): boolean => getWidgetTypeName(widget) === TITLE_TYPE_NAME;
27
+ const isFactoryRow = (widget: Gtk.Widget): boolean => ROW_TYPE_NAMES.has(getWidgetTypeName(widget) ?? "");
28
+
29
+ const toggleExpander = (widget: Gtk.Widget, nPress: number): void => {
30
+ for (let press = 1; press <= nPress; press++) {
31
+ widget.activateAction(TOGGLE_EXPAND_ACTION, null);
32
+ }
33
+ };
34
+
35
+ const nativeClickFor = (widget: Gtk.Widget, isClicked: boolean): NativeClick | null => {
36
+ if (isClicked && isExpanderToggling(widget)) {
37
+ return toggleExpander;
38
+ }
39
+
40
+ if (isColumnHeader(widget)) {
41
+ return applyHeaderClick;
42
+ }
43
+
44
+ if (isNotebookTab(widget)) {
45
+ return applyTabClick;
46
+ }
47
+
48
+ return isFactoryRow(widget) ? applyRowClick : null;
49
+ };
50
+
51
+ export { isClickTransparent, type NativeClick, nativeClickFor };
@@ -0,0 +1,57 @@
1
+ import * as Gtk from "@gtkx/gi/gtk";
2
+ import { ancestorFor } from "../traversal.js";
3
+
4
+ type NotebookTab = { notebook: Gtk.Notebook; index: number };
5
+
6
+ const tabLabelsFor = (notebook: Gtk.Notebook): (Gtk.Widget | null)[] => {
7
+ const labels: (Gtk.Widget | null)[] = [];
8
+
9
+ for (let index = 0; index < notebook.getNPages(); index++) {
10
+ const child = notebook.getNthPage(index);
11
+ labels.push(child === null ? null : notebook.getPage(child).tab);
12
+ }
13
+
14
+ return labels;
15
+ };
16
+
17
+ const isNested = (outer: Gtk.Widget, inner: Gtk.Widget): boolean => outer === inner || inner.isAncestor(outer);
18
+
19
+ const isSameTab = (tab: Gtk.Widget, label: Gtk.Widget | null): boolean =>
20
+ label !== null && (isNested(tab, label) || isNested(label, tab));
21
+
22
+ const tabIndexFor = (notebook: Gtk.Notebook, tab: Gtk.Widget): number => {
23
+ const matches = tabLabelsFor(notebook)
24
+ .map((label, index) => (isSameTab(tab, label) ? index : -1))
25
+ .filter((index) => index >= 0);
26
+
27
+ return matches.length === 1 ? matches[0] ?? -1 : -1;
28
+ };
29
+
30
+ const notebookTabFor = (widget: Gtk.Widget): NotebookTab | null => {
31
+ if (widget.getAccessibleRole() !== Gtk.AccessibleRole.TAB) {
32
+ return null;
33
+ }
34
+
35
+ const notebook = ancestorFor(widget, Gtk.Notebook);
36
+ const index = notebook === null ? -1 : tabIndexFor(notebook, widget);
37
+
38
+ return notebook === null || index < 0 ? null : { notebook, index };
39
+ };
40
+
41
+ const isNotebookTab = (widget: Gtk.Widget): boolean => notebookTabFor(widget) !== null;
42
+
43
+ const applyTabClick = (widget: Gtk.Widget): void => {
44
+ const tab = notebookTabFor(widget);
45
+
46
+ if (tab === null) {
47
+ return;
48
+ }
49
+
50
+ if (tab.notebook.getFocusOnClick()) {
51
+ tab.notebook.grabFocus();
52
+ }
53
+
54
+ tab.notebook.setCurrentPage(tab.index);
55
+ };
56
+
57
+ export { applyTabClick, isNotebookTab };
@@ -1,39 +1,78 @@
1
- import * as Gtk from "@gtkx/gi/gtk";
1
+ import type * as Gtk from "@gtkx/gi/gtk";
2
+ import type { NativeClick } from "./native-click.js";
2
3
  import type { UserEventState } from "./state.js";
3
- import { emitPress, emitRelease } from "./click.js";
4
- import { getOrCreateControllers } from "./controller.js";
4
+ import { clickGestures, emitClickPhase, getAuthoredClickGestures } from "./click.js";
5
5
  import { wrapEvent } from "./event-wrapper.js";
6
+ import { isClickTransparent, nativeClickFor } from "./native-click.js";
6
7
 
7
8
  /**
8
9
  * A pointer action token: a full click (`click`, `[MouseLeft]`), a button press (`down`,
9
10
  * `[MouseLeft>]`), or a button release (`up`, `[/MouseLeft]`).
10
11
  */
11
12
  type PointerInput = "click" | "down" | "up" | "[MouseLeft]" | "[MouseLeft>]" | "[/MouseLeft]";
13
+ type PointerTarget = { widget: Gtk.Widget; gestures: Gtk.GestureClick[]; native: NativeClick | null };
12
14
 
13
15
  const PRESS_INPUTS: Set<PointerInput> = new Set(["[MouseLeft>]", "down"]);
14
16
  const RELEASE_INPUTS: Set<PointerInput> = new Set(["[/MouseLeft]", "up"]);
15
17
  const CLICK_INPUTS: Set<PointerInput> = new Set(["[MouseLeft]", "click"]);
16
18
 
19
+ const pointerWidgetFor = (widget: Gtk.Widget): Gtk.Widget => {
20
+ let current = widget;
21
+
22
+ while (isClickTransparent(current)) {
23
+ const parent = current.getParent();
24
+
25
+ if (parent === null) {
26
+ return current;
27
+ }
28
+
29
+ current = parent;
30
+ }
31
+
32
+ return current;
33
+ };
34
+
35
+ const pointerGesturesFor = (widget: Gtk.Widget, native: NativeClick | null): Gtk.GestureClick[] =>
36
+ native === null ? clickGestures(widget) : getAuthoredClickGestures(widget);
37
+
38
+ const pointerTargetFor = (widget: Gtk.Widget): PointerTarget => {
39
+ const target = pointerWidgetFor(widget);
40
+ const native = nativeClickFor(target, true);
41
+
42
+ return { widget: target, gestures: pointerGesturesFor(target, native), native };
43
+ };
44
+
45
+ const pressPointer = ({ widget, gestures }: PointerTarget): void => {
46
+ emitClickPhase(widget, gestures, 1, "pressed");
47
+ };
48
+
49
+ const releasePointer = ({ widget, gestures, native }: PointerTarget): void => {
50
+ emitClickPhase(widget, gestures, 1, "released");
51
+ native?.(widget, 1);
52
+ };
53
+
54
+ const isPointerPressable = (target: PointerTarget): boolean => target.native !== null || target.gestures.length > 0;
55
+
17
56
  const applyPointerInput = (widget: Gtk.Widget, state: UserEventState, input: PointerInput): void => {
18
- const controllers = getOrCreateControllers(widget, Gtk.GestureClick);
57
+ const target = pointerTargetFor(widget);
19
58
 
20
59
  if (CLICK_INPUTS.has(input)) {
21
- emitPress(widget, controllers, 1);
22
- emitRelease(widget, controllers, 1);
60
+ pressPointer(target);
61
+ releasePointer(target);
23
62
  state.isMouseLeftDown = false;
24
63
 
25
64
  return;
26
65
  }
27
66
 
28
- if (PRESS_INPUTS.has(input) && !state.isMouseLeftDown) {
29
- emitPress(widget, controllers, 1);
67
+ if (PRESS_INPUTS.has(input) && !state.isMouseLeftDown && isPointerPressable(target)) {
68
+ pressPointer(target);
30
69
  state.isMouseLeftDown = true;
31
70
 
32
71
  return;
33
72
  }
34
73
 
35
74
  if (RELEASE_INPUTS.has(input) && state.isMouseLeftDown) {
36
- emitRelease(widget, controllers, 1);
75
+ releasePointer(target);
37
76
  state.isMouseLeftDown = false;
38
77
  }
39
78
  };