@gtkx/testing 1.0.0-rc.4 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -5
- package/dist/accessible-native.d.ts +16 -0
- package/dist/accessible-native.d.ts.map +1 -0
- package/dist/accessible-native.js +206 -0
- package/dist/accessible-native.js.map +1 -0
- package/dist/build-queries.d.ts +27 -10
- package/dist/build-queries.d.ts.map +1 -1
- package/dist/build-queries.js +22 -24
- package/dist/build-queries.js.map +1 -1
- package/dist/cleanup-registry.d.ts +1 -1
- package/dist/cleanup-registry.d.ts.map +1 -1
- package/dist/cleanup-registry.js.map +1 -1
- package/dist/errors.d.ts +10 -1
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +10 -1
- package/dist/errors.js.map +1 -1
- package/dist/fire-event.d.ts +35 -6
- package/dist/fire-event.d.ts.map +1 -1
- package/dist/fire-event.js +23 -7
- package/dist/fire-event.js.map +1 -1
- package/dist/frame-sync.d.ts +2 -1
- package/dist/frame-sync.d.ts.map +1 -1
- package/dist/frame-sync.js +15 -5
- package/dist/frame-sync.js.map +1 -1
- package/dist/harness-window.d.ts +4 -0
- package/dist/harness-window.d.ts.map +1 -0
- package/dist/harness-window.js +6 -0
- package/dist/harness-window.js.map +1 -0
- package/dist/index.d.ts +14 -7
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +9 -5
- package/dist/index.js.map +1 -1
- package/dist/internal.d.ts +4 -0
- package/dist/internal.d.ts.map +1 -0
- package/dist/internal.js +4 -0
- package/dist/internal.js.map +1 -0
- package/dist/matchers.d.ts +88 -46
- package/dist/matchers.d.ts.map +1 -1
- package/dist/matchers.js +156 -18
- package/dist/matchers.js.map +1 -1
- package/dist/pretty-format.d.ts +2 -0
- package/dist/pretty-format.d.ts.map +1 -0
- package/dist/pretty-format.js +2 -0
- package/dist/pretty-format.js.map +1 -0
- package/dist/pretty-widget.d.ts +5 -2
- package/dist/pretty-widget.d.ts.map +1 -1
- package/dist/pretty-widget.js +73 -56
- package/dist/pretty-widget.js.map +1 -1
- package/dist/production.d.ts +8 -0
- package/dist/production.d.ts.map +1 -0
- package/dist/production.js +34 -0
- package/dist/production.js.map +1 -0
- package/dist/queries.d.ts +98 -56
- package/dist/queries.d.ts.map +1 -1
- package/dist/queries.js +123 -83
- package/dist/queries.js.map +1 -1
- package/dist/query-helpers.d.ts +44 -0
- package/dist/query-helpers.d.ts.map +1 -0
- package/dist/query-helpers.js +57 -0
- package/dist/query-helpers.js.map +1 -0
- package/dist/render-children.d.ts +9 -0
- package/dist/render-children.d.ts.map +1 -0
- package/dist/render-children.js +11 -0
- package/dist/render-children.js.map +1 -0
- package/dist/render.d.ts.map +1 -1
- package/dist/render.js +11 -9
- package/dist/render.js.map +1 -1
- package/dist/role-helpers.d.ts +12 -3
- package/dist/role-helpers.d.ts.map +1 -1
- package/dist/role-helpers.js +13 -4
- package/dist/role-helpers.js.map +1 -1
- package/dist/role-naming.d.ts +5 -0
- package/dist/role-naming.d.ts.map +1 -0
- package/dist/role-naming.js +96 -0
- package/dist/role-naming.js.map +1 -0
- package/dist/screenshot.d.ts +12 -20
- package/dist/screenshot.d.ts.map +1 -1
- package/dist/screenshot.js +183 -91
- package/dist/screenshot.js.map +1 -1
- package/dist/traversal.d.ts +4 -1
- package/dist/traversal.d.ts.map +1 -1
- package/dist/traversal.js +25 -6
- package/dist/traversal.js.map +1 -1
- package/dist/types.d.ts +26 -14
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/dist/user-event/adjustment.d.ts +3 -0
- package/dist/user-event/adjustment.d.ts.map +1 -1
- package/dist/user-event/adjustment.js +23 -2
- package/dist/user-event/adjustment.js.map +1 -1
- package/dist/user-event/click.d.ts +43 -8
- package/dist/user-event/click.d.ts.map +1 -1
- package/dist/user-event/click.js +175 -44
- package/dist/user-event/click.js.map +1 -1
- package/dist/user-event/column-header-click.d.ts +4 -0
- package/dist/user-event/column-header-click.d.ts.map +1 -0
- package/dist/user-event/column-header-click.js +65 -0
- package/dist/user-event/column-header-click.js.map +1 -0
- package/dist/user-event/event-wrapper.d.ts.map +1 -1
- package/dist/user-event/event-wrapper.js +2 -1
- package/dist/user-event/event-wrapper.js.map +1 -1
- package/dist/user-event/index.d.ts +51 -10
- package/dist/user-event/index.d.ts.map +1 -1
- package/dist/user-event/index.js +43 -26
- package/dist/user-event/index.js.map +1 -1
- package/dist/user-event/indexed-children.d.ts +10 -0
- package/dist/user-event/indexed-children.d.ts.map +1 -0
- package/dist/user-event/indexed-children.js +50 -0
- package/dist/user-event/indexed-children.js.map +1 -0
- package/dist/user-event/keyboard.d.ts.map +1 -1
- package/dist/user-event/keyboard.js +33 -4
- package/dist/user-event/keyboard.js.map +1 -1
- package/dist/user-event/list-row-click.d.ts +4 -0
- package/dist/user-event/list-row-click.d.ts.map +1 -0
- package/dist/user-event/list-row-click.js +39 -0
- package/dist/user-event/list-row-click.js.map +1 -0
- package/dist/user-event/native-click.d.ts +6 -0
- package/dist/user-event/native-click.d.ts.map +1 -0
- package/dist/user-event/native-click.js +32 -0
- package/dist/user-event/native-click.js.map +1 -0
- package/dist/user-event/pointer.d.ts +1 -1
- package/dist/user-event/pointer.d.ts.map +1 -1
- package/dist/user-event/pointer.js +33 -9
- package/dist/user-event/pointer.js.map +1 -1
- package/dist/user-event/selection.d.ts +11 -8
- package/dist/user-event/selection.d.ts.map +1 -1
- package/dist/user-event/selection.js +49 -38
- package/dist/user-event/selection.js.map +1 -1
- package/dist/user-event/text.d.ts +6 -2
- package/dist/user-event/text.d.ts.map +1 -1
- package/dist/user-event/text.js +68 -18
- package/dist/user-event/text.js.map +1 -1
- package/dist/widget-accessible-properties.d.ts +14 -26
- package/dist/widget-accessible-properties.d.ts.map +1 -1
- package/dist/widget-accessible-properties.js +135 -190
- package/dist/widget-accessible-properties.js.map +1 -1
- package/dist/widget-getters.d.ts +8 -0
- package/dist/widget-getters.d.ts.map +1 -0
- package/dist/widget-getters.js +19 -0
- package/dist/widget-getters.js.map +1 -0
- package/dist/within.d.ts +3 -2
- package/dist/within.d.ts.map +1 -1
- package/dist/within.js +3 -2
- package/dist/within.js.map +1 -1
- package/package.json +12 -7
- package/src/accessible-native.ts +333 -0
- package/src/build-queries.ts +47 -41
- package/src/cleanup-registry.ts +1 -1
- package/src/errors.ts +12 -1
- package/src/fire-event.ts +61 -8
- package/src/frame-sync.ts +21 -5
- package/src/harness-window.ts +9 -0
- package/src/index.ts +18 -11
- package/src/internal.ts +11 -0
- package/src/matchers.ts +428 -70
- package/src/pretty-format.ts +1 -0
- package/src/pretty-widget.ts +97 -70
- package/src/production.ts +52 -0
- package/src/queries.ts +127 -89
- package/src/query-helpers.ts +97 -0
- package/src/render-children.ts +25 -0
- package/src/render.tsx +16 -11
- package/src/role-helpers.ts +14 -4
- package/src/role-naming.ts +99 -0
- package/src/screenshot.ts +272 -103
- package/src/traversal.ts +44 -6
- package/src/types.ts +28 -16
- package/src/user-event/adjustment.ts +29 -2
- package/src/user-event/click.ts +247 -52
- package/src/user-event/column-header-click.ts +90 -0
- package/src/user-event/event-wrapper.ts +3 -1
- package/src/user-event/index.ts +100 -37
- package/src/user-event/indexed-children.ts +75 -0
- package/src/user-event/keyboard.ts +52 -4
- package/src/user-event/list-row-click.ts +57 -0
- package/src/user-event/native-click.ts +46 -0
- package/src/user-event/pointer.ts +48 -9
- package/src/user-event/selection.ts +67 -41
- package/src/user-event/text.ts +92 -21
- package/src/widget-accessible-properties.ts +183 -230
- package/src/widget-getters.ts +32 -0
- package/src/within.ts +3 -2
package/src/types.ts
CHANGED
|
@@ -55,13 +55,19 @@ type MatcherOptions<T extends Gtk.Widget = Gtk.Widget> = {
|
|
|
55
55
|
as?: WidgetType<T> | undefined;
|
|
56
56
|
} & WaitForOptions;
|
|
57
57
|
|
|
58
|
-
/**
|
|
58
|
+
/**
|
|
59
|
+
* Constraints on a widget's numeric range value used by role queries. The numbers come from the
|
|
60
|
+
* accessibility tree and match within 0.001, the resolution GTK keeps them to.
|
|
61
|
+
*/
|
|
59
62
|
type ByRoleValue = {
|
|
60
63
|
/** The current value. */
|
|
61
64
|
now?: number | undefined;
|
|
62
65
|
/** The lower bound of the value's range. */
|
|
63
66
|
min?: number | undefined;
|
|
64
|
-
/**
|
|
67
|
+
/**
|
|
68
|
+
* The highest value the widget can reach, which on a paged widget such as a scrollbar is its
|
|
69
|
+
* adjustment's upper bound less one page.
|
|
70
|
+
*/
|
|
65
71
|
max?: number | undefined;
|
|
66
72
|
/** Matcher for the value's textual representation. */
|
|
67
73
|
text?: Matcher | undefined;
|
|
@@ -87,7 +93,7 @@ type ByRoleOptions<T extends Gtk.Widget = Gtk.Widget> = MatcherOptions<T> & {
|
|
|
87
93
|
description?: Matcher | undefined;
|
|
88
94
|
/** Constraints on the widget's range value, each checked only when given. */
|
|
89
95
|
value?: ByRoleValue | undefined;
|
|
90
|
-
/** When true, include widgets excluded from the accessibility tree. */
|
|
96
|
+
/** When true, include widgets excluded from the accessibility tree. Widgets that are not mapped stay excluded. */
|
|
91
97
|
hidden?: boolean | undefined;
|
|
92
98
|
};
|
|
93
99
|
|
|
@@ -187,8 +193,11 @@ type DebugUtilities = {
|
|
|
187
193
|
debug: (element?: Container | Container[], options?: PrettyWidgetOptions) => void;
|
|
188
194
|
/** Prints the accessible roles found in the scope, with the widgets carrying each one. */
|
|
189
195
|
logRoles: () => void;
|
|
190
|
-
/**
|
|
191
|
-
|
|
196
|
+
/**
|
|
197
|
+
* Captures what is on screen, which is the active toplevel window: GTK4 exposes no position for
|
|
198
|
+
* a toplevel, so windows cannot be composited into a single image.
|
|
199
|
+
*/
|
|
200
|
+
screenshot: (options?: ScreenshotOptions) => Promise<ScreenshotResult>;
|
|
192
201
|
};
|
|
193
202
|
|
|
194
203
|
/** A captured screenshot: base64-encoded image data, its MIME type, and pixel dimensions. */
|
|
@@ -203,26 +212,27 @@ type ScreenshotResult = {
|
|
|
203
212
|
height: number;
|
|
204
213
|
};
|
|
205
214
|
|
|
206
|
-
/**
|
|
215
|
+
/**
|
|
216
|
+
* Options for capturing a screenshot: the poll timeout and interval, a rendering scale factor, and
|
|
217
|
+
* a file to write the PNG to.
|
|
218
|
+
*/
|
|
207
219
|
type ScreenshotOptions = Pick<WaitForOptions, "timeout" | "interval"> & {
|
|
208
220
|
/** Device scale factor applied when rendering. */
|
|
209
|
-
scale?: number;
|
|
221
|
+
scale?: number | undefined;
|
|
222
|
+
/** File the PNG is written to, with its parent directories created as needed. */
|
|
223
|
+
path?: string | undefined;
|
|
210
224
|
};
|
|
211
225
|
|
|
212
|
-
/**
|
|
213
|
-
* Selects the window to screenshot by index, or by title (exact string or regular expression);
|
|
214
|
-
* undefined targets the default window.
|
|
215
|
-
*/
|
|
216
|
-
type WindowSelector = number | string | RegExp | undefined;
|
|
217
|
-
|
|
218
226
|
/**
|
|
219
227
|
* Options for {@link renderHook}: an optional wrapper and the initial props (required unless the
|
|
220
228
|
* props type permits undefined).
|
|
221
229
|
*/
|
|
222
230
|
type RenderHookOptions<Props> = {
|
|
231
|
+
/** Component wrapped around the one calling the hook, such as a context provider. */
|
|
223
232
|
wrapper?: WrapperComponent;
|
|
224
233
|
} & (undefined extends Props
|
|
225
234
|
? {
|
|
235
|
+
/** Props the hook is invoked with until `rerender` is given new ones. */
|
|
226
236
|
initialProps?: Props;
|
|
227
237
|
}
|
|
228
238
|
: {
|
|
@@ -234,8 +244,11 @@ type RenderHookOptions<Props> = {
|
|
|
234
244
|
* props and to unmount.
|
|
235
245
|
*/
|
|
236
246
|
type RenderHookResult<Result, Props> = {
|
|
237
|
-
/**
|
|
238
|
-
result: {
|
|
247
|
+
/** Stable object the hook's return value is written to on every render. */
|
|
248
|
+
result: {
|
|
249
|
+
/** The hook's latest return value. */
|
|
250
|
+
current: Result;
|
|
251
|
+
};
|
|
239
252
|
/** Re-invokes the hook, keeping the previous props when none are given. */
|
|
240
253
|
rerender: (newProps?: Props) => Promise<void>;
|
|
241
254
|
/** Unmounts the component that calls the hook. */
|
|
@@ -260,7 +273,6 @@ export {
|
|
|
260
273
|
type DebugUtilities,
|
|
261
274
|
type ScreenshotResult,
|
|
262
275
|
type ScreenshotOptions,
|
|
263
|
-
type WindowSelector,
|
|
264
276
|
type RenderHookOptions,
|
|
265
277
|
type RenderHookResult,
|
|
266
278
|
};
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import * as Gtk from "@gtkx/gi/gtk";
|
|
2
|
+
import { scheduleNextFrame } from "../frame-sync.js";
|
|
2
3
|
import { wrapEvent } from "./event-wrapper.js";
|
|
3
4
|
|
|
4
5
|
/** A scroll distance in pixels along each axis. */
|
|
@@ -44,22 +45,47 @@ const resolveScrollAdjustments = (widget: Gtk.Widget): ScrollAdjustments | null
|
|
|
44
45
|
return null;
|
|
45
46
|
};
|
|
46
47
|
|
|
48
|
+
const stepToward = (adjustment: Gtk.Adjustment, target: number, step: number): number => {
|
|
49
|
+
const value = adjustment.getValue();
|
|
50
|
+
adjustment.setValue(value < target ? Math.min(value + step, target) : Math.max(value - step, target));
|
|
51
|
+
|
|
52
|
+
return adjustment.getValue();
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const rampTo = (adjustment: Gtk.Adjustment, target: number, step: number): void => {
|
|
56
|
+
let value = adjustment.getValue();
|
|
57
|
+
|
|
58
|
+
while (value !== target) {
|
|
59
|
+
const moved = stepToward(adjustment, target, step);
|
|
60
|
+
|
|
61
|
+
if (moved === value) {
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
value = moved;
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
|
|
47
69
|
const applyScrollDelta = (adjustment: Gtk.Adjustment | null, delta: number): void => {
|
|
48
70
|
if (!adjustment || delta === 0) {
|
|
49
71
|
return;
|
|
50
72
|
}
|
|
51
73
|
|
|
52
|
-
adjustment.
|
|
74
|
+
const pageSize = adjustment.getPageSize();
|
|
75
|
+
rampTo(adjustment, adjustment.getValue() + delta, pageSize > 0 ? pageSize : Math.abs(delta));
|
|
53
76
|
};
|
|
54
77
|
|
|
55
78
|
/**
|
|
56
79
|
* Adds the delta to the adjustments of the widget itself, or of its nearest Gtk.ScrolledWindow or
|
|
57
80
|
* Gtk.Scrollable ancestor.
|
|
58
81
|
*
|
|
82
|
+
* Each adjustment advances in viewport-sized steps rather than one jump, so virtualized views such
|
|
83
|
+
* as Gtk.ListView, Gtk.GridView and Gtk.ColumnView re-anchor onto the rows the new offset shows.
|
|
84
|
+
*
|
|
59
85
|
* @throws When neither the widget nor any of its ancestors is scrollable.
|
|
60
86
|
*/
|
|
61
87
|
const scroll = (widget: Gtk.Widget, delta: ScrollDelta): Promise<void> =>
|
|
62
|
-
wrapEvent(widget, () => {
|
|
88
|
+
wrapEvent(widget, async () => {
|
|
63
89
|
const adjustments = resolveScrollAdjustments(widget);
|
|
64
90
|
|
|
65
91
|
if (!adjustments) {
|
|
@@ -70,6 +96,7 @@ const scroll = (widget: Gtk.Widget, delta: ScrollDelta): Promise<void> =>
|
|
|
70
96
|
|
|
71
97
|
applyScrollDelta(adjustments.horizontal, delta.x ?? 0);
|
|
72
98
|
applyScrollDelta(adjustments.vertical, delta.y ?? 0);
|
|
99
|
+
await scheduleNextFrame(widget);
|
|
73
100
|
});
|
|
74
101
|
|
|
75
102
|
export { slide, scroll, type ScrollDelta };
|
package/src/user-event/click.ts
CHANGED
|
@@ -1,8 +1,29 @@
|
|
|
1
|
+
import * as Gdk from "@gtkx/gi/gdk";
|
|
1
2
|
import * as Gtk from "@gtkx/gi/gtk";
|
|
2
|
-
import {
|
|
3
|
+
import { hasSignalListener } from "@gtkx/runtime/internal";
|
|
4
|
+
import { callBooleanGetter, getCallableMethod, hasWidgetMethod } from "../widget-getters.js";
|
|
5
|
+
import { queryAllControllers } from "./controller.js";
|
|
3
6
|
import { wrapEvent } from "./event-wrapper.js";
|
|
7
|
+
import { hasIndexedChildren, selectContainerChild, SELECTED_PROBE, unselectOtherChildren } from "./indexed-children.js";
|
|
8
|
+
import { isClickTransparent, type NativeClick, nativeClickFor } from "./native-click.js";
|
|
4
9
|
|
|
5
|
-
|
|
10
|
+
type PressPoint = { x: number; y: number };
|
|
11
|
+
type ClickPhase = "pressed" | "released";
|
|
12
|
+
type ClickSite = { gestures: Gtk.GestureClick[]; point: PressPoint };
|
|
13
|
+
type ClickScope = { isClicked: boolean; isInternalAllowed: boolean };
|
|
14
|
+
|
|
15
|
+
type ClickTarget = {
|
|
16
|
+
widget: Gtk.Widget;
|
|
17
|
+
container: Gtk.Widget | null;
|
|
18
|
+
gestures: Gtk.GestureClick[];
|
|
19
|
+
isClaiming: boolean;
|
|
20
|
+
native: NativeClick | null;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const CLICK_SIGNALS = ["pressed", "released"];
|
|
24
|
+
const PRIMARY_BUTTONS: Set<number> = new Set([0, Gdk.BUTTON_PRIMARY]);
|
|
25
|
+
|
|
26
|
+
const getCenterPoint = (widget: Gtk.Widget): PressPoint => {
|
|
6
27
|
const width = widget.getWidth();
|
|
7
28
|
const height = widget.getHeight();
|
|
8
29
|
|
|
@@ -13,46 +34,205 @@ const getPressPoint = (widget: Gtk.Widget): { x: number; y: number } => {
|
|
|
13
34
|
return { x: 0, y: 0 };
|
|
14
35
|
};
|
|
15
36
|
|
|
16
|
-
const
|
|
17
|
-
|
|
37
|
+
const getClickPoint = (clicked: Gtk.Widget, carrier: Gtk.Widget): PressPoint => {
|
|
38
|
+
if (clicked === carrier) {
|
|
39
|
+
return getCenterPoint(clicked);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const [isComputed, bounds] = clicked.computeBounds(carrier);
|
|
18
43
|
|
|
44
|
+
if (!isComputed) {
|
|
45
|
+
return getCenterPoint(carrier);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return { x: bounds.getX() + bounds.getWidth() / 2, y: bounds.getY() + bounds.getHeight() / 2 };
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const emitGesture = (point: PressPoint, controllers: Gtk.GestureClick[], nPress: number, signal: string): void => {
|
|
19
52
|
for (const controller of controllers) {
|
|
20
|
-
controller.emit(signal, nPress, x, y);
|
|
53
|
+
controller.emit(signal, nPress, point.x, point.y);
|
|
21
54
|
}
|
|
22
55
|
};
|
|
23
56
|
|
|
24
|
-
const
|
|
25
|
-
|
|
57
|
+
const emitClickPhase = (
|
|
58
|
+
widget: Gtk.Widget,
|
|
59
|
+
controllers: Gtk.GestureClick[],
|
|
60
|
+
nPress: number,
|
|
61
|
+
phase: ClickPhase,
|
|
62
|
+
): void => {
|
|
63
|
+
emitGesture(getCenterPoint(widget), controllers, nPress, phase);
|
|
26
64
|
};
|
|
27
65
|
|
|
28
|
-
const
|
|
29
|
-
|
|
66
|
+
const emitSitePhase = (sites: ClickSite[], nPress: number, phase: ClickPhase): void => {
|
|
67
|
+
for (const site of sites) {
|
|
68
|
+
emitGesture(site.point, site.gestures, nPress, phase);
|
|
69
|
+
}
|
|
30
70
|
};
|
|
31
71
|
|
|
32
|
-
const emitClickSequence = (
|
|
33
|
-
|
|
34
|
-
|
|
72
|
+
const emitClickSequence = (sites: ClickSite[], nPress: number): void => {
|
|
73
|
+
for (let i = 1; i <= nPress; i++) {
|
|
74
|
+
emitSitePhase(sites, i, "pressed");
|
|
75
|
+
emitSitePhase(sites, i, "released");
|
|
76
|
+
}
|
|
77
|
+
};
|
|
35
78
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
emitRelease(target, controllers, i);
|
|
39
|
-
}
|
|
40
|
-
});
|
|
79
|
+
const containerFor = (widget: Gtk.Widget): Gtk.Widget | null => {
|
|
80
|
+
const parent = widget.getParent();
|
|
41
81
|
|
|
42
|
-
|
|
43
|
-
|
|
82
|
+
return parent !== null && hasWidgetMethod(widget, SELECTED_PROBE) && hasIndexedChildren(parent) ? parent : null;
|
|
83
|
+
};
|
|
44
84
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
85
|
+
const hasClickHandlers = (gesture: Gtk.GestureClick): boolean => hasSignalListener(gesture, CLICK_SIGNALS);
|
|
86
|
+
const isInternalGesture = (gesture: Gtk.GestureClick): boolean => !hasSignalListener(gesture);
|
|
87
|
+
const isPrimaryGesture = (gesture: Gtk.GestureClick): boolean => PRIMARY_BUTTONS.has(gesture.getButton());
|
|
88
|
+
|
|
89
|
+
const clickGestures = (widget: Gtk.Widget): Gtk.GestureClick[] =>
|
|
90
|
+
queryAllControllers(widget, Gtk.GestureClick).filter((gesture) => isPrimaryGesture(gesture));
|
|
91
|
+
|
|
92
|
+
const getAuthoredClickGestures = (widget: Gtk.Widget): Gtk.GestureClick[] =>
|
|
93
|
+
clickGestures(widget).filter((gesture) => hasClickHandlers(gesture));
|
|
94
|
+
|
|
95
|
+
const isClaimingTarget = (widget: Gtk.Widget): boolean =>
|
|
96
|
+
widget instanceof Gtk.Button || containerFor(widget) !== null;
|
|
97
|
+
|
|
98
|
+
const gestureTargetFor = (widget: Gtk.Widget, isInternalAllowed: boolean): ClickTarget | null => {
|
|
99
|
+
const gestures = clickGestures(widget);
|
|
100
|
+
|
|
101
|
+
if (isClaimingTarget(widget)) {
|
|
102
|
+
return { widget, container: containerFor(widget), gestures, isClaiming: true, native: null };
|
|
103
|
+
}
|
|
49
104
|
|
|
50
|
-
|
|
105
|
+
const authored = gestures.filter((gesture) => hasClickHandlers(gesture));
|
|
106
|
+
|
|
107
|
+
if (authored.length > 0) {
|
|
108
|
+
return { widget, container: null, gestures: authored, isClaiming: false, native: null };
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (isInternalAllowed && gestures.some((gesture) => isInternalGesture(gesture))) {
|
|
112
|
+
return { widget, container: null, gestures, isClaiming: true, native: null };
|
|
51
113
|
}
|
|
52
114
|
|
|
53
115
|
return null;
|
|
54
116
|
};
|
|
55
117
|
|
|
118
|
+
const clickTargetFor = (widget: Gtk.Widget, scope: ClickScope): ClickTarget | null => {
|
|
119
|
+
if (isClickTransparent(widget)) {
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const native = nativeClickFor(widget, scope.isClicked);
|
|
124
|
+
|
|
125
|
+
if (native !== null) {
|
|
126
|
+
return { widget, container: null, gestures: getAuthoredClickGestures(widget), isClaiming: true, native };
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return gestureTargetFor(widget, scope.isInternalAllowed);
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
const isSelfClickTarget = (widget: Gtk.Widget): boolean =>
|
|
133
|
+
isClickTransparent(widget) || clickTargetFor(widget, { isClicked: true, isInternalAllowed: false }) !== null;
|
|
134
|
+
|
|
135
|
+
const nextClickWidget = (current: Gtk.Widget, target: ClickTarget | null): Gtk.Widget | null =>
|
|
136
|
+
target?.isClaiming === true ? null : current.getParent();
|
|
137
|
+
|
|
138
|
+
const collectClickTargets = (widget: Gtk.Widget): ClickTarget[] => {
|
|
139
|
+
const targets: ClickTarget[] = [];
|
|
140
|
+
let current: Gtk.Widget | null = widget;
|
|
141
|
+
|
|
142
|
+
while (current !== null) {
|
|
143
|
+
const isClicked = current === widget;
|
|
144
|
+
const scope = { isClicked, isInternalAllowed: !isClicked && targets.length === 0 };
|
|
145
|
+
const target = clickTargetFor(current, scope);
|
|
146
|
+
const next = nextClickWidget(current, target);
|
|
147
|
+
|
|
148
|
+
if (target !== null) {
|
|
149
|
+
targets.push(target);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
current = next;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return targets;
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
const targetSites = (clicked: Gtk.Widget, target: ClickTarget): ClickSite[] => {
|
|
159
|
+
const { container } = target;
|
|
160
|
+
const site = { gestures: target.gestures, point: getClickPoint(clicked, target.widget) };
|
|
161
|
+
|
|
162
|
+
if (container === null) {
|
|
163
|
+
return [site];
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return [site, { gestures: getAuthoredClickGestures(container), point: getClickPoint(clicked, container) }];
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
const isSingleClickActivating = (container: Gtk.Widget): boolean =>
|
|
170
|
+
callBooleanGetter(container, "getActivateOnSingleClick") ?? true;
|
|
171
|
+
|
|
172
|
+
const isMultipleSelection = (container: Gtk.Widget): boolean =>
|
|
173
|
+
getCallableMethod<[], Gtk.SelectionMode>(container, "getSelectionMode")?.() === Gtk.SelectionMode.MULTIPLE;
|
|
174
|
+
|
|
175
|
+
const isChildSelectable = (child: Gtk.Widget): boolean => callBooleanGetter(child, "getSelectable") ?? true;
|
|
176
|
+
|
|
177
|
+
const replaceContainerSelection = (container: Gtk.Widget, child: Gtk.Widget): void => {
|
|
178
|
+
if (!isChildSelectable(child)) {
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
selectContainerChild(container, child);
|
|
183
|
+
|
|
184
|
+
if (isMultipleSelection(container)) {
|
|
185
|
+
unselectOtherChildren(container, child);
|
|
186
|
+
}
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
const isActivatedByClick = (container: Gtk.Widget, nPress: number): boolean =>
|
|
190
|
+
isSingleClickActivating(container) || nPress > 1;
|
|
191
|
+
|
|
192
|
+
const isSelectionReplacedByClick = (container: Gtk.Widget, nPress: number): boolean =>
|
|
193
|
+
nPress > 1 || !isSingleClickActivating(container);
|
|
194
|
+
|
|
195
|
+
const applyContainerOutcome = (target: ClickTarget, nPress: number): void => {
|
|
196
|
+
const { container, widget } = target;
|
|
197
|
+
|
|
198
|
+
if (container === null || widget.getParent() !== container) {
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
if (isActivatedByClick(container, nPress)) {
|
|
203
|
+
widget.activate();
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
if (isSelectionReplacedByClick(container, nPress)) {
|
|
207
|
+
replaceContainerSelection(container, widget);
|
|
208
|
+
}
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
const applyClickOutcome = (target: ClickTarget, nPress: number): void => {
|
|
212
|
+
if (target.native === null) {
|
|
213
|
+
applyContainerOutcome(target, nPress);
|
|
214
|
+
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
target.native(target.widget, nPress);
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
const applyClickOutcomes = (targets: ClickTarget[], nPress: number): void => {
|
|
222
|
+
for (const target of targets) {
|
|
223
|
+
applyClickOutcome(target, nPress);
|
|
224
|
+
}
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
const deliverClick = (widget: Gtk.Widget, nPress: number): Promise<void> =>
|
|
228
|
+
wrapEvent(widget, () => {
|
|
229
|
+
const targets = collectClickTargets(widget);
|
|
230
|
+
const sites = targets.flatMap((target) => targetSites(widget, target));
|
|
231
|
+
emitClickSequence(sites, nPress);
|
|
232
|
+
applyClickOutcomes(targets, nPress);
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
/* eslint-disable-next-line unicorn/consistent-boolean-name -- the boolean reports whether activation succeeded */
|
|
56
236
|
const tryActivate = async (widget: Gtk.Widget): Promise<boolean> => {
|
|
57
237
|
if (widget.getAccessibleRole() === Gtk.AccessibleRole.LABEL) {
|
|
58
238
|
return false;
|
|
@@ -68,39 +248,54 @@ const tryActivate = async (widget: Gtk.Widget): Promise<boolean> => {
|
|
|
68
248
|
};
|
|
69
249
|
|
|
70
250
|
/**
|
|
71
|
-
*
|
|
72
|
-
*
|
|
73
|
-
*
|
|
251
|
+
* Activates a widget that neither claims the click nor carries a click gesture of its own, and
|
|
252
|
+
* otherwise delivers a press and release. The press travels outwards from the clicked widget
|
|
253
|
+
* through every widget carrying a click gesture with a pressed or released handler, the way GTK
|
|
254
|
+
* hands the same press to each of them, and stops at the first Gtk.Button or indexed child of a
|
|
255
|
+
* list box or flow box, which claims it. A gesture GTK attached itself takes the press only when no
|
|
256
|
+
* widget below it handles one, and stops it there, so an expander or a notebook tab nested in a row
|
|
257
|
+
* opens instead of activating the row. Coordinates are the clicked widget's position in each
|
|
258
|
+
* carrier, so a gesture the container of an indexed child carries reads the child's position rather
|
|
259
|
+
* than the container's center. An indexed child that the press reaches is then activated, or
|
|
260
|
+
* exclusively selected when its container does not activate on a single click. A widget with the
|
|
261
|
+
* label role is never activated, but does consume the click when it carries a gesture of its own.
|
|
262
|
+
*
|
|
263
|
+
* The press also stops at a widget whose click GTK4 implements in C on a gesture it attached
|
|
264
|
+
* itself, reading a GdkEvent that off-screen synthesis cannot produce. Gestures that widget carries
|
|
265
|
+
* of its own still take the press and release, the way GTK hands them the press before its own
|
|
266
|
+
* gesture claims it, and the same outcome is then applied through the public action GTK's own
|
|
267
|
+
* handler invokes: a list, grid, or column-view row is focused and selected, and activated as well
|
|
268
|
+
* on a second press or when its view activates on a single click; an expandable tree expander
|
|
269
|
+
* toggles its expansion, once per press, and only when it is the widget clicked, so a click on its
|
|
270
|
+
* child falls through to the enclosing row; a column header sorts by its column. Sorting goes
|
|
271
|
+
* through Gtk.ColumnView.sortByColumn, so the primary sort column and order match what a pointer
|
|
272
|
+
* produces while previously sorted columns are dropped rather than kept as secondary keys. A
|
|
273
|
+
* column-view cell and the row that carries the column headers stand in the way of the click rather
|
|
274
|
+
* than taking it, so a click on either reaches the row or the view behind it.
|
|
74
275
|
*/
|
|
75
276
|
const click = async (widget: Gtk.Widget): Promise<void> => {
|
|
76
|
-
if (widget
|
|
77
|
-
await emitClickSequence(widget, widget, 1);
|
|
78
|
-
|
|
79
|
-
return;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
if (widget instanceof Gtk.Switch) {
|
|
83
|
-
await wrapEvent(widget, () => {
|
|
84
|
-
widget.setActive(!widget.getActive());
|
|
85
|
-
});
|
|
86
|
-
|
|
87
|
-
return;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
if (await tryActivate(widget)) {
|
|
277
|
+
if (!isSelfClickTarget(widget) && (await tryActivate(widget))) {
|
|
91
278
|
return;
|
|
92
279
|
}
|
|
93
280
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
if (target) {
|
|
97
|
-
await emitClickSequence(widget, target, 1);
|
|
98
|
-
}
|
|
281
|
+
await deliverClick(widget, 1);
|
|
99
282
|
};
|
|
100
283
|
|
|
101
|
-
/**
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
284
|
+
/**
|
|
285
|
+
* Delivers a two-press click gesture the way {@link click} delivers a single press, without trying
|
|
286
|
+
* activation first. A list box row or flow box child the presses reach, clicked directly or through
|
|
287
|
+
* a descendant, is activated and exclusively selected, as GTK's double-click path does whether or
|
|
288
|
+
* not the container activates on a single click. A widget whose click GTK4 implements itself
|
|
289
|
+
* receives that outcome once per press, so a list, grid, or column-view row is selected and then
|
|
290
|
+
* activated by the second press, a tree expander ends back where it started, and a column header
|
|
291
|
+
* sorts and then inverts its order.
|
|
292
|
+
*/
|
|
293
|
+
const dblClick = (widget: Gtk.Widget): Promise<void> => deliverClick(widget, 2);
|
|
294
|
+
/**
|
|
295
|
+
* Delivers a three-press click gesture the same way a double click is delivered, applying the same
|
|
296
|
+
* outcome to a list box row or flow box child the presses reach, and applying the outcome GTK4
|
|
297
|
+
* implements itself once per press to a row, tree expander, or column header.
|
|
298
|
+
*/
|
|
299
|
+
const tripleClick = (widget: Gtk.Widget): Promise<void> => deliverClick(widget, 3);
|
|
105
300
|
|
|
106
|
-
export {
|
|
301
|
+
export { clickGestures, emitClickPhase, getAuthoredClickGestures, click, dblClick, tripleClick };
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import * as Gtk from "@gtkx/gi/gtk";
|
|
2
|
+
import { children } from "../traversal.js";
|
|
3
|
+
import { getWidgetTextContent } from "../widget-accessible-properties.js";
|
|
4
|
+
|
|
5
|
+
const columnViewFor = (header: Gtk.Widget): Gtk.ColumnView | null => {
|
|
6
|
+
let current: Gtk.Widget | null = header.getParent();
|
|
7
|
+
|
|
8
|
+
while (current !== null) {
|
|
9
|
+
if (current instanceof Gtk.ColumnView) {
|
|
10
|
+
return current;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
current = current.getParent();
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
return null;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const columnsFor = (view: Gtk.ColumnView): Gtk.ColumnViewColumn[] => {
|
|
20
|
+
const model = view.getColumns();
|
|
21
|
+
const columns: Gtk.ColumnViewColumn[] = [];
|
|
22
|
+
|
|
23
|
+
for (let index = 0; index < model.getNItems(); index++) {
|
|
24
|
+
const column = model.getItem(index);
|
|
25
|
+
|
|
26
|
+
if (column instanceof Gtk.ColumnViewColumn) {
|
|
27
|
+
columns.push(column);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return columns;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const headerIndexFor = (header: Gtk.Widget): number => {
|
|
35
|
+
const parent = header.getParent();
|
|
36
|
+
|
|
37
|
+
return parent === null ? -1 : [...children(parent)].indexOf(header);
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const titledColumnFor = (columns: Gtk.ColumnViewColumn[], title: string | null): Gtk.ColumnViewColumn | null => {
|
|
41
|
+
const matches = columns.filter((column) => column.getTitle() === title);
|
|
42
|
+
|
|
43
|
+
return matches.length === 1 ? matches[0] ?? null : null;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const columnFor = (header: Gtk.Widget, view: Gtk.ColumnView): Gtk.ColumnViewColumn | null => {
|
|
47
|
+
const columns = columnsFor(view);
|
|
48
|
+
const picked = columns[headerIndexFor(header)] ?? null;
|
|
49
|
+
const title = getWidgetTextContent(header);
|
|
50
|
+
|
|
51
|
+
if (picked !== null && picked.getTitle() === title) {
|
|
52
|
+
return picked;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return titledColumnFor(columns, title) ?? picked;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const sortableColumnFor = (header: Gtk.Widget, view: Gtk.ColumnView): Gtk.ColumnViewColumn | null => {
|
|
59
|
+
const column = columnFor(header, view);
|
|
60
|
+
|
|
61
|
+
return column !== null && column.getSorter() !== null ? column : null;
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
const invertedSortDirection = (order: Gtk.SortType): Gtk.SortType =>
|
|
65
|
+
order === Gtk.SortType.ASCENDING ? Gtk.SortType.DESCENDING : Gtk.SortType.ASCENDING;
|
|
66
|
+
|
|
67
|
+
const nextSortDirection = (view: Gtk.ColumnView, column: Gtk.ColumnViewColumn): Gtk.SortType => {
|
|
68
|
+
const sorter = view.getSorter();
|
|
69
|
+
|
|
70
|
+
if (!(sorter instanceof Gtk.ColumnViewSorter) || sorter.getPrimarySortColumn() !== column) {
|
|
71
|
+
return Gtk.SortType.ASCENDING;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return invertedSortDirection(sorter.getPrimarySortOrder());
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
const applyHeaderClick = (header: Gtk.Widget, nPress: number): void => {
|
|
78
|
+
const view = columnViewFor(header);
|
|
79
|
+
const column = view === null ? null : sortableColumnFor(header, view);
|
|
80
|
+
|
|
81
|
+
if (view === null || column === null) {
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
for (let press = 1; press <= nPress; press++) {
|
|
86
|
+
view.sortByColumn(column, nextSortDirection(view, column));
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
export { applyHeaderClick };
|
|
@@ -14,7 +14,9 @@ const canDisplayDeliverActivation = (window: Gtk.Window): boolean =>
|
|
|
14
14
|
window.getDisplay().getDefaultSeat() !== null;
|
|
15
15
|
|
|
16
16
|
const findWindowActionabilityFailure = (widget: Gtk.Widget, root: Gtk.Window): string | null => {
|
|
17
|
-
|
|
17
|
+
const [isComputed, allocation] = root.computeBounds(root);
|
|
18
|
+
|
|
19
|
+
if (!isComputed || allocation.getWidth() === 0) {
|
|
18
20
|
return WINDOW_NOT_ALLOCATED;
|
|
19
21
|
}
|
|
20
22
|
|