@gtkx/testing 0.20.0 → 1.0.0-rc.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.
- package/README.md +122 -55
- package/dist/act.d.ts +19 -0
- package/dist/act.d.ts.map +1 -0
- package/dist/act.js +48 -0
- package/dist/act.js.map +1 -0
- package/dist/bound-queries.d.ts +32 -0
- package/dist/bound-queries.d.ts.map +1 -0
- package/dist/bound-queries.js +2 -0
- package/dist/bound-queries.js.map +1 -0
- package/dist/build-queries.d.ts +16 -0
- package/dist/build-queries.d.ts.map +1 -0
- package/dist/build-queries.js +103 -0
- package/dist/build-queries.js.map +1 -0
- package/dist/cleanup-registry.d.ts +4 -0
- package/dist/cleanup-registry.d.ts.map +1 -0
- package/dist/cleanup-registry.js +11 -0
- package/dist/cleanup-registry.js.map +1 -0
- package/dist/config.d.ts +18 -51
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +9 -40
- package/dist/config.js.map +1 -1
- package/dist/editable.d.ts +7 -0
- package/dist/editable.d.ts.map +1 -0
- package/dist/editable.js +24 -0
- package/dist/editable.js.map +1 -0
- package/dist/errors.d.ts +29 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +119 -0
- package/dist/errors.js.map +1 -0
- package/dist/fire-event.d.ts +7 -25
- package/dist/fire-event.d.ts.map +1 -1
- package/dist/fire-event.js +9 -29
- package/dist/fire-event.js.map +1 -1
- package/dist/frame-sync.d.ts +3 -0
- package/dist/frame-sync.d.ts.map +1 -0
- package/dist/frame-sync.js +30 -0
- package/dist/frame-sync.js.map +1 -0
- package/dist/index.d.ts +16 -11
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +10 -6
- package/dist/index.js.map +1 -1
- package/dist/matchers.d.ts +55 -0
- package/dist/matchers.d.ts.map +1 -0
- package/dist/matchers.js +103 -0
- package/dist/matchers.js.map +1 -0
- package/dist/pretty-widget.d.ts +21 -25
- package/dist/pretty-widget.d.ts.map +1 -1
- package/dist/pretty-widget.js +69 -115
- package/dist/pretty-widget.js.map +1 -1
- package/dist/queries.d.ts +130 -177
- package/dist/queries.d.ts.map +1 -1
- package/dist/queries.js +212 -325
- package/dist/queries.js.map +1 -1
- package/dist/register-test-hooks.d.ts +2 -0
- package/dist/register-test-hooks.d.ts.map +1 -0
- package/dist/register-test-hooks.js +15 -0
- package/dist/register-test-hooks.js.map +1 -0
- package/dist/render-hook.d.ts +14 -35
- package/dist/render-hook.d.ts.map +1 -1
- package/dist/render-hook.js +8 -45
- package/dist/render-hook.js.map +1 -1
- package/dist/render.d.ts +11 -41
- package/dist/render.d.ts.map +1 -1
- package/dist/render.js +129 -118
- package/dist/render.js.map +1 -1
- package/dist/role-helpers.d.ts +16 -51
- package/dist/role-helpers.d.ts.map +1 -1
- package/dist/role-helpers.js +39 -58
- package/dist/role-helpers.js.map +1 -1
- package/dist/screen.d.ts +6 -63
- package/dist/screen.d.ts.map +1 -1
- package/dist/screen.js +21 -109
- package/dist/screen.js.map +1 -1
- package/dist/screenshot.d.ts +20 -23
- package/dist/screenshot.d.ts.map +1 -1
- package/dist/screenshot.js +89 -48
- package/dist/screenshot.js.map +1 -1
- package/dist/suggestions.d.ts +22 -0
- package/dist/suggestions.d.ts.map +1 -0
- package/dist/suggestions.js +56 -0
- package/dist/suggestions.js.map +1 -0
- package/dist/traversal.d.ts +10 -8
- package/dist/traversal.d.ts.map +1 -1
- package/dist/traversal.js +28 -13
- package/dist/traversal.js.map +1 -1
- package/dist/types.d.ts +103 -172
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/dist/user-event/adjustment.d.ts +8 -0
- package/dist/user-event/adjustment.d.ts.map +1 -0
- package/dist/user-event/adjustment.js +33 -0
- package/dist/user-event/adjustment.js.map +1 -0
- package/dist/user-event/click.d.ts +7 -0
- package/dist/user-event/click.d.ts.map +1 -0
- package/dist/user-event/click.js +65 -0
- package/dist/user-event/click.js.map +1 -0
- package/dist/user-event/controller.d.ts +7 -0
- package/dist/user-event/controller.d.ts.map +1 -0
- package/dist/user-event/controller.js +28 -0
- package/dist/user-event/controller.js.map +1 -0
- package/dist/user-event/dispatch.d.ts +7 -0
- package/dist/user-event/dispatch.d.ts.map +1 -0
- package/dist/user-event/dispatch.js +10 -0
- package/dist/user-event/dispatch.js.map +1 -0
- package/dist/user-event/event-wrapper.d.ts +3 -0
- package/dist/user-event/event-wrapper.d.ts.map +1 -0
- package/dist/user-event/event-wrapper.js +48 -0
- package/dist/user-event/event-wrapper.js.map +1 -0
- package/dist/user-event/gesture.d.ts +31 -0
- package/dist/user-event/gesture.d.ts.map +1 -0
- package/dist/user-event/gesture.js +105 -0
- package/dist/user-event/gesture.js.map +1 -0
- package/dist/user-event/index.d.ts +44 -0
- package/dist/user-event/index.d.ts.map +1 -0
- package/dist/user-event/index.js +38 -0
- package/dist/user-event/index.js.map +1 -0
- package/dist/user-event/keyboard.d.ts +9 -0
- package/dist/user-event/keyboard.d.ts.map +1 -0
- package/dist/user-event/keyboard.js +173 -0
- package/dist/user-event/keyboard.js.map +1 -0
- package/dist/user-event/pointer.d.ts +6 -0
- package/dist/user-event/pointer.d.ts.map +1 -0
- package/dist/user-event/pointer.js +27 -0
- package/dist/user-event/pointer.js.map +1 -0
- package/dist/user-event/selection.d.ts +4 -0
- package/dist/user-event/selection.d.ts.map +1 -0
- package/dist/user-event/selection.js +106 -0
- package/dist/user-event/selection.js.map +1 -0
- package/dist/user-event/state.d.ts +6 -0
- package/dist/user-event/state.d.ts.map +1 -0
- package/dist/user-event/state.js +2 -0
- package/dist/user-event/state.js.map +1 -0
- package/dist/user-event/text.d.ts +17 -0
- package/dist/user-event/text.d.ts.map +1 -0
- package/dist/user-event/text.js +107 -0
- package/dist/user-event/text.js.map +1 -0
- package/dist/wait-for.d.ts +13 -32
- package/dist/wait-for.d.ts.map +1 -1
- package/dist/wait-for.js +62 -73
- package/dist/wait-for.js.map +1 -1
- package/dist/widget-accessible-properties.d.ts +33 -0
- package/dist/widget-accessible-properties.d.ts.map +1 -0
- package/dist/widget-accessible-properties.js +215 -0
- package/dist/widget-accessible-properties.js.map +1 -0
- package/dist/within.d.ts +9 -26
- package/dist/within.d.ts.map +1 -1
- package/dist/within.js +15 -25
- package/dist/within.js.map +1 -1
- package/package.json +21 -12
- package/src/act.ts +68 -0
- package/src/bound-queries.ts +36 -0
- package/src/build-queries.ts +185 -0
- package/src/cleanup-registry.ts +14 -0
- package/src/config.ts +28 -59
- package/src/editable.ts +30 -0
- package/src/errors.ts +130 -0
- package/src/fire-event.ts +11 -38
- package/src/frame-sync.ts +34 -0
- package/src/index.ts +60 -18
- package/src/matchers.ts +200 -0
- package/src/pretty-widget.ts +99 -138
- package/src/queries.ts +321 -425
- package/src/register-test-hooks.ts +16 -0
- package/src/render-hook.tsx +31 -44
- package/src/render.tsx +161 -149
- package/src/role-helpers.ts +47 -67
- package/src/screen.ts +21 -114
- package/src/screenshot.ts +104 -55
- package/src/suggestions.ts +87 -0
- package/src/traversal.ts +32 -18
- package/src/types.ts +115 -176
- package/src/user-event/adjustment.ts +49 -0
- package/src/user-event/click.ts +70 -0
- package/src/user-event/controller.ts +44 -0
- package/src/user-event/dispatch.ts +23 -0
- package/src/user-event/event-wrapper.ts +54 -0
- package/src/user-event/gesture.ts +154 -0
- package/src/user-event/index.ts +74 -0
- package/src/user-event/keyboard.ts +204 -0
- package/src/user-event/pointer.ts +34 -0
- package/src/user-event/selection.ts +122 -0
- package/src/user-event/state.ts +6 -0
- package/src/user-event/text.ts +137 -0
- package/src/wait-for.ts +79 -80
- package/src/widget-accessible-properties.ts +244 -0
- package/src/within.ts +26 -27
- package/dist/bind-queries.d.ts +0 -14
- package/dist/bind-queries.d.ts.map +0 -1
- package/dist/bind-queries.js +0 -29
- package/dist/bind-queries.js.map +0 -1
- package/dist/error-builder.d.ts +0 -28
- package/dist/error-builder.d.ts.map +0 -1
- package/dist/error-builder.js +0 -81
- package/dist/error-builder.js.map +0 -1
- package/dist/timing.d.ts +0 -18
- package/dist/timing.d.ts.map +0 -1
- package/dist/timing.js +0 -18
- package/dist/timing.js.map +0 -1
- package/dist/user-event.d.ts +0 -138
- package/dist/user-event.d.ts.map +0 -1
- package/dist/user-event.js +0 -411
- package/dist/user-event.js.map +0 -1
- package/dist/widget-text.d.ts +0 -69
- package/dist/widget-text.d.ts.map +0 -1
- package/dist/widget-text.js +0 -177
- package/dist/widget-text.js.map +0 -1
- package/dist/widget.d.ts +0 -3
- package/dist/widget.d.ts.map +0 -1
- package/dist/widget.js +0 -13
- package/dist/widget.js.map +0 -1
- package/src/bind-queries.ts +0 -52
- package/src/error-builder.ts +0 -102
- package/src/timing.ts +0 -17
- package/src/user-event.ts +0 -492
- package/src/widget-text.ts +0 -202
- package/src/widget.ts +0 -15
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import * as GObject from "@gtkx/gi/gobject";
|
|
2
|
+
import * as Gtk from "@gtkx/gi/gtk";
|
|
3
|
+
import { getAllControllers } from "./controller.js";
|
|
4
|
+
import { dispatchOnControllers, dispatchOnOrCreateControllers } from "./dispatch.js";
|
|
5
|
+
import { wrapEvent } from "./event-wrapper.js";
|
|
6
|
+
|
|
7
|
+
/** The value delivered to a drop target: a primitive (converted to a GObject.Value) or an explicit GObject.Value. */
|
|
8
|
+
export type DropContent = string | number | boolean | GObject.Value;
|
|
9
|
+
|
|
10
|
+
/** Options for a drop: the drop coordinates within the target. */
|
|
11
|
+
export type DropOptions = {
|
|
12
|
+
x?: number;
|
|
13
|
+
y?: number;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
/** A drag offset relative to the drag start point. */
|
|
17
|
+
export type DragOffset = {
|
|
18
|
+
x: number;
|
|
19
|
+
y: number;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
/** Options for a drag gesture: the starting point, the number of interpolated drag updates, or explicit intermediate offsets emitted before the final one. */
|
|
23
|
+
export type DragOptions = {
|
|
24
|
+
startX?: number;
|
|
25
|
+
startY?: number;
|
|
26
|
+
steps?: number;
|
|
27
|
+
offsets?: DragOffset[];
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const buildDropValue = (content: DropContent): GObject.Value => {
|
|
31
|
+
if (content instanceof GObject.Value) return content;
|
|
32
|
+
if (typeof content === "string") {
|
|
33
|
+
return GObject.buildValue(GObject.TYPE_STRING, (v) => v.setString(content));
|
|
34
|
+
}
|
|
35
|
+
if (typeof content === "boolean") {
|
|
36
|
+
return GObject.buildValue(GObject.TYPE_BOOLEAN, (v) => v.setBoolean(content));
|
|
37
|
+
}
|
|
38
|
+
return GObject.buildValue(GObject.TYPE_DOUBLE, (v) => v.setDouble(content));
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
export const hover = (widget: Gtk.Widget): Promise<void> =>
|
|
42
|
+
dispatchOnOrCreateControllers(widget, Gtk.EventControllerMotion, (controller) => controller.emit("enter", 0, 0));
|
|
43
|
+
|
|
44
|
+
export const unhover = (widget: Gtk.Widget): Promise<void> =>
|
|
45
|
+
dispatchOnOrCreateControllers(widget, Gtk.EventControllerMotion, (controller) => controller.emit("leave"));
|
|
46
|
+
|
|
47
|
+
export const rotate = (widget: Gtk.Widget, angle: number, deltaAngle: number = angle): Promise<void> =>
|
|
48
|
+
dispatchOnControllers(widget, Gtk.GestureRotate, (controller) =>
|
|
49
|
+
controller.emit("angle-changed", angle, deltaAngle),
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
export const zoom = (widget: Gtk.Widget, scale: number): Promise<void> =>
|
|
53
|
+
dispatchOnControllers(widget, Gtk.GestureZoom, (controller) => controller.emit("scale-changed", scale));
|
|
54
|
+
|
|
55
|
+
export const swipe = (widget: Gtk.Widget, velocityX: number, velocityY: number): Promise<void> =>
|
|
56
|
+
dispatchOnControllers(widget, Gtk.GestureSwipe, (controller) => controller.emit("swipe", velocityX, velocityY));
|
|
57
|
+
|
|
58
|
+
export const longPress = (widget: Gtk.Widget, x: number = 0, y: number = 0): Promise<void> =>
|
|
59
|
+
dispatchOnControllers(widget, Gtk.GestureLongPress, (controller) => controller.emit("pressed", x, y));
|
|
60
|
+
|
|
61
|
+
type DragInstancePatch = {
|
|
62
|
+
getStartPoint?: Gtk.GestureDrag["getStartPoint"] | undefined;
|
|
63
|
+
getOffset?: Gtk.GestureDrag["getOffset"] | undefined;
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
const patchDragState = (controller: Gtk.GestureDrag, start: DragOffset, offset: () => DragOffset): (() => void) => {
|
|
67
|
+
const instance: DragInstancePatch = controller;
|
|
68
|
+
const ownsStartPoint = Object.hasOwn(instance, "getStartPoint");
|
|
69
|
+
const ownsOffset = Object.hasOwn(instance, "getOffset");
|
|
70
|
+
const previousStartPoint = instance.getStartPoint;
|
|
71
|
+
const previousOffset = instance.getOffset;
|
|
72
|
+
instance.getStartPoint = () => [true, start.x, start.y];
|
|
73
|
+
instance.getOffset = () => {
|
|
74
|
+
const current = offset();
|
|
75
|
+
return [true, current.x, current.y];
|
|
76
|
+
};
|
|
77
|
+
return () => {
|
|
78
|
+
if (ownsStartPoint) instance.getStartPoint = previousStartPoint;
|
|
79
|
+
else delete instance.getStartPoint;
|
|
80
|
+
if (ownsOffset) instance.getOffset = previousOffset;
|
|
81
|
+
else delete instance.getOffset;
|
|
82
|
+
};
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
const runDragSequence = (controllers: Gtk.GestureDrag[], start: DragOffset, updates: DragOffset[]): void => {
|
|
86
|
+
let current: DragOffset = { x: 0, y: 0 };
|
|
87
|
+
const restores = controllers.map((controller) => patchDragState(controller, start, () => current));
|
|
88
|
+
try {
|
|
89
|
+
for (const controller of controllers) {
|
|
90
|
+
controller.emit("drag-begin", start.x, start.y);
|
|
91
|
+
}
|
|
92
|
+
for (const update of updates) {
|
|
93
|
+
current = update;
|
|
94
|
+
for (const controller of controllers) {
|
|
95
|
+
controller.emit("drag-update", update.x, update.y);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
for (const controller of controllers) {
|
|
99
|
+
controller.emit("drag-end", current.x, current.y);
|
|
100
|
+
}
|
|
101
|
+
} finally {
|
|
102
|
+
for (const restore of restores) restore();
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
const resolveDragUpdates = (dx: number, dy: number, options: DragOptions): DragOffset[] => {
|
|
107
|
+
if (options.offsets) return [...options.offsets, { x: dx, y: dy }];
|
|
108
|
+
const steps = Math.max(1, Math.floor(options.steps ?? 2));
|
|
109
|
+
const updates: DragOffset[] = [];
|
|
110
|
+
for (let i = 1; i <= steps; i++) {
|
|
111
|
+
updates.push({ x: (dx * i) / steps, y: (dy * i) / steps });
|
|
112
|
+
}
|
|
113
|
+
return updates;
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
export const drag = async (widget: Gtk.Widget, dx: number, dy: number, options: DragOptions = {}): Promise<void> => {
|
|
117
|
+
if (widget instanceof Gtk.Range) {
|
|
118
|
+
throw new Error(
|
|
119
|
+
"userEvent.drag cannot drive a Gtk.Range's built-in slider (its drag reads pointer coordinates from the display); " +
|
|
120
|
+
"use userEvent.slide(range, value) or userEvent.keyboard for sliders",
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
const start = { x: options.startX ?? 0, y: options.startY ?? 0 };
|
|
124
|
+
const updates = resolveDragUpdates(dx, dy, options);
|
|
125
|
+
await wrapEvent(widget, () => {
|
|
126
|
+
runDragSequence(getAllControllers(widget, Gtk.GestureDrag), start, updates);
|
|
127
|
+
});
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
const emitDrop = (target: Gtk.Widget, content: DropContent, options: DropOptions): void => {
|
|
131
|
+
const dropTargets = getAllControllers(target, Gtk.DropTarget);
|
|
132
|
+
for (const dropTarget of dropTargets) {
|
|
133
|
+
dropTarget.emit("drop", buildDropValue(content), options.x ?? 0, options.y ?? 0);
|
|
134
|
+
}
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
export const drop = (widget: Gtk.Widget, content: DropContent, options: DropOptions = {}): Promise<void> =>
|
|
138
|
+
wrapEvent(widget, () => {
|
|
139
|
+
emitDrop(widget, content, options);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
export const dragAndDrop = async (
|
|
143
|
+
source: Gtk.Widget,
|
|
144
|
+
target: Gtk.Widget,
|
|
145
|
+
content: DropContent,
|
|
146
|
+
options: DropOptions = {},
|
|
147
|
+
): Promise<void> => {
|
|
148
|
+
await wrapEvent(source, () => {
|
|
149
|
+
getAllControllers(source, Gtk.DragSource);
|
|
150
|
+
});
|
|
151
|
+
await wrapEvent(target, () => {
|
|
152
|
+
emitDrop(target, content, options);
|
|
153
|
+
});
|
|
154
|
+
};
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import type * as Gtk from "@gtkx/gi/gtk";
|
|
2
|
+
import { scroll, slide } from "./adjustment.js";
|
|
3
|
+
import { click, dblClick, tripleClick } from "./click.js";
|
|
4
|
+
import { drag, dragAndDrop, drop, hover, longPress, rotate, swipe, unhover, zoom } from "./gesture.js";
|
|
5
|
+
import { keyboard, tab } from "./keyboard.js";
|
|
6
|
+
import { type PointerInput, pointer } from "./pointer.js";
|
|
7
|
+
import { deselectOptions, selectOptions } from "./selection.js";
|
|
8
|
+
import { createInitialState } from "./state.js";
|
|
9
|
+
import { clear, copy, cut, paste, type } from "./text.js";
|
|
10
|
+
|
|
11
|
+
export type { ScrollDelta } from "./adjustment.js";
|
|
12
|
+
export type { DragOffset, DragOptions, DropContent, DropOptions } from "./gesture.js";
|
|
13
|
+
export type { TabOptions } from "./keyboard.js";
|
|
14
|
+
export type { PointerInput } from "./pointer.js";
|
|
15
|
+
export type { TypeOptions } from "./text.js";
|
|
16
|
+
export { resetClipboard } from "./text.js";
|
|
17
|
+
|
|
18
|
+
/** The set of user interaction helpers exposed by {@link userEvent}, covering clicks, typing, keyboard, pointer, gestures, selection, and scrolling. */
|
|
19
|
+
export type UserEvent = {
|
|
20
|
+
click: typeof click;
|
|
21
|
+
dblClick: typeof dblClick;
|
|
22
|
+
tripleClick: typeof tripleClick;
|
|
23
|
+
tab: typeof tab;
|
|
24
|
+
type: typeof type;
|
|
25
|
+
clear: typeof clear;
|
|
26
|
+
copy: typeof copy;
|
|
27
|
+
cut: typeof cut;
|
|
28
|
+
paste: typeof paste;
|
|
29
|
+
selectOptions: typeof selectOptions;
|
|
30
|
+
deselectOptions: typeof deselectOptions;
|
|
31
|
+
hover: typeof hover;
|
|
32
|
+
unhover: typeof unhover;
|
|
33
|
+
rotate: typeof rotate;
|
|
34
|
+
zoom: typeof zoom;
|
|
35
|
+
swipe: typeof swipe;
|
|
36
|
+
longPress: typeof longPress;
|
|
37
|
+
drag: typeof drag;
|
|
38
|
+
drop: typeof drop;
|
|
39
|
+
dragAndDrop: typeof dragAndDrop;
|
|
40
|
+
slide: typeof slide;
|
|
41
|
+
scroll: typeof scroll;
|
|
42
|
+
keyboard: (widget: Gtk.Widget, input: string) => Promise<void>;
|
|
43
|
+
pointer: (widget: Gtk.Widget, input: PointerInput) => Promise<void>;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const state = createInitialState();
|
|
47
|
+
|
|
48
|
+
/** High-level helpers that drive widgets by dispatching the same events and gestures GTK4 delivers when someone clicks, types, or drags. */
|
|
49
|
+
export const userEvent: UserEvent = {
|
|
50
|
+
click,
|
|
51
|
+
dblClick,
|
|
52
|
+
tripleClick,
|
|
53
|
+
tab,
|
|
54
|
+
type,
|
|
55
|
+
clear,
|
|
56
|
+
copy,
|
|
57
|
+
cut,
|
|
58
|
+
paste,
|
|
59
|
+
selectOptions,
|
|
60
|
+
deselectOptions,
|
|
61
|
+
hover,
|
|
62
|
+
unhover,
|
|
63
|
+
rotate,
|
|
64
|
+
zoom,
|
|
65
|
+
swipe,
|
|
66
|
+
longPress,
|
|
67
|
+
drag,
|
|
68
|
+
drop,
|
|
69
|
+
dragAndDrop,
|
|
70
|
+
slide,
|
|
71
|
+
scroll,
|
|
72
|
+
keyboard: (widget: Gtk.Widget, input: string): Promise<void> => keyboard(state, widget, input),
|
|
73
|
+
pointer: (widget: Gtk.Widget, input: PointerInput): Promise<void> => pointer(state, widget, input),
|
|
74
|
+
};
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import * as Gdk from "@gtkx/gi/gdk";
|
|
2
|
+
import * as Gtk from "@gtkx/gi/gtk";
|
|
3
|
+
import { getEditableDelegate } from "../editable.js";
|
|
4
|
+
import { fireEvent } from "../fire-event.js";
|
|
5
|
+
import { getOrCreateControllers } from "./controller.js";
|
|
6
|
+
import { wrapEvent } from "./event-wrapper.js";
|
|
7
|
+
import type { UserEventState } from "./state.js";
|
|
8
|
+
|
|
9
|
+
/** Options for {@link tab}: when `shift` is set, move focus backward instead of forward. */
|
|
10
|
+
export type TabOptions = {
|
|
11
|
+
shift?: boolean;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export const tab = (widget: Gtk.Widget, options?: TabOptions): Promise<void> =>
|
|
15
|
+
wrapEvent(widget, () => {
|
|
16
|
+
const direction = options?.shift ? Gtk.DirectionType.TAB_BACKWARD : Gtk.DirectionType.TAB_FORWARD;
|
|
17
|
+
const root = widget.getRoot();
|
|
18
|
+
|
|
19
|
+
if (root) {
|
|
20
|
+
root.childFocus(direction);
|
|
21
|
+
}
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
const KEY_MAP: Record<string, number> = {
|
|
25
|
+
Enter: Gdk.KEY_Return,
|
|
26
|
+
Tab: Gdk.KEY_Tab,
|
|
27
|
+
Escape: Gdk.KEY_Escape,
|
|
28
|
+
Backspace: Gdk.KEY_BackSpace,
|
|
29
|
+
Delete: Gdk.KEY_Delete,
|
|
30
|
+
ArrowUp: Gdk.KEY_Up,
|
|
31
|
+
ArrowDown: Gdk.KEY_Down,
|
|
32
|
+
ArrowLeft: Gdk.KEY_Left,
|
|
33
|
+
ArrowRight: Gdk.KEY_Right,
|
|
34
|
+
Home: Gdk.KEY_Home,
|
|
35
|
+
End: Gdk.KEY_End,
|
|
36
|
+
PageUp: Gdk.KEY_Page_Up,
|
|
37
|
+
PageDown: Gdk.KEY_Page_Down,
|
|
38
|
+
Space: Gdk.KEY_space,
|
|
39
|
+
Shift: Gdk.KEY_Shift_L,
|
|
40
|
+
Control: Gdk.KEY_Control_L,
|
|
41
|
+
Alt: Gdk.KEY_Alt_L,
|
|
42
|
+
Meta: Gdk.KEY_Meta_L,
|
|
43
|
+
F1: Gdk.KEY_F1,
|
|
44
|
+
F2: Gdk.KEY_F2,
|
|
45
|
+
F3: Gdk.KEY_F3,
|
|
46
|
+
F4: Gdk.KEY_F4,
|
|
47
|
+
F5: Gdk.KEY_F5,
|
|
48
|
+
F6: Gdk.KEY_F6,
|
|
49
|
+
F7: Gdk.KEY_F7,
|
|
50
|
+
F8: Gdk.KEY_F8,
|
|
51
|
+
F9: Gdk.KEY_F9,
|
|
52
|
+
F10: Gdk.KEY_F10,
|
|
53
|
+
F11: Gdk.KEY_F11,
|
|
54
|
+
F12: Gdk.KEY_F12,
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
type KeyAction = { keyval: number; press: boolean };
|
|
58
|
+
|
|
59
|
+
const parseKeyToken = (token: string): { keyval: number; press: boolean; release: boolean } => {
|
|
60
|
+
let keyName = token;
|
|
61
|
+
let press = true;
|
|
62
|
+
let release = true;
|
|
63
|
+
|
|
64
|
+
if (keyName.startsWith("/")) {
|
|
65
|
+
keyName = keyName.slice(1);
|
|
66
|
+
press = false;
|
|
67
|
+
} else if (keyName.endsWith(">")) {
|
|
68
|
+
keyName = keyName.slice(0, -1);
|
|
69
|
+
release = false;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const keyval = KEY_MAP[keyName];
|
|
73
|
+
if (keyval === undefined) {
|
|
74
|
+
throw new Error(`Unknown key: {${keyName}}`);
|
|
75
|
+
}
|
|
76
|
+
return { keyval, press, release };
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
const parseKeyboardInput = (input: string): KeyAction[] => {
|
|
80
|
+
const actions: KeyAction[] = [];
|
|
81
|
+
let i = 0;
|
|
82
|
+
|
|
83
|
+
while (i < input.length) {
|
|
84
|
+
if (input[i] !== "{") {
|
|
85
|
+
const keyval = input.codePointAt(i) ?? 0;
|
|
86
|
+
actions.push({ keyval, press: true }, { keyval, press: false });
|
|
87
|
+
i++;
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const endBrace = input.indexOf("}", i);
|
|
92
|
+
if (endBrace === -1) break;
|
|
93
|
+
|
|
94
|
+
const { keyval, press, release } = parseKeyToken(input.slice(i + 1, endBrace));
|
|
95
|
+
if (press) actions.push({ keyval, press: true });
|
|
96
|
+
if (release) actions.push({ keyval, press: false });
|
|
97
|
+
|
|
98
|
+
i = endBrace + 1;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return actions;
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
const MODIFIER_KEYVAL_TO_MASK: Record<number, number> = {
|
|
105
|
+
[Gdk.KEY_Shift_L]: Gdk.ModifierType.SHIFT_MASK,
|
|
106
|
+
[Gdk.KEY_Shift_R]: Gdk.ModifierType.SHIFT_MASK,
|
|
107
|
+
[Gdk.KEY_Control_L]: Gdk.ModifierType.CONTROL_MASK,
|
|
108
|
+
[Gdk.KEY_Control_R]: Gdk.ModifierType.CONTROL_MASK,
|
|
109
|
+
[Gdk.KEY_Alt_L]: Gdk.ModifierType.ALT_MASK,
|
|
110
|
+
[Gdk.KEY_Alt_R]: Gdk.ModifierType.ALT_MASK,
|
|
111
|
+
[Gdk.KEY_Meta_L]: Gdk.ModifierType.META_MASK,
|
|
112
|
+
[Gdk.KEY_Meta_R]: Gdk.ModifierType.META_MASK,
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
const updateModifierState = (state: UserEventState, action: KeyAction): void => {
|
|
116
|
+
const mask = MODIFIER_KEYVAL_TO_MASK[action.keyval];
|
|
117
|
+
if (!mask) return;
|
|
118
|
+
if (action.press) {
|
|
119
|
+
state.modifierState |= mask;
|
|
120
|
+
} else {
|
|
121
|
+
state.modifierState &= ~mask;
|
|
122
|
+
}
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
const matchesTrigger = (trigger: Gtk.ShortcutTrigger | null, keyval: number, modifiers: number): boolean => {
|
|
126
|
+
if (trigger instanceof Gtk.KeyvalTrigger) {
|
|
127
|
+
return trigger.getKeyval() === keyval && trigger.getModifiers() === modifiers;
|
|
128
|
+
}
|
|
129
|
+
if (trigger instanceof Gtk.AlternativeTrigger) {
|
|
130
|
+
return (
|
|
131
|
+
matchesTrigger(trigger.getFirst(), keyval, modifiers) ||
|
|
132
|
+
matchesTrigger(trigger.getSecond(), keyval, modifiers)
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
return false;
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
const activateMatchingShortcut = (
|
|
139
|
+
controller: Gtk.ShortcutController,
|
|
140
|
+
widget: Gtk.Widget,
|
|
141
|
+
keyval: number,
|
|
142
|
+
modifiers: number,
|
|
143
|
+
): boolean => {
|
|
144
|
+
const count = controller.getNItems();
|
|
145
|
+
for (let j = 0; j < count; j++) {
|
|
146
|
+
const shortcut = controller.getItem(j);
|
|
147
|
+
if (!(shortcut instanceof Gtk.Shortcut)) continue;
|
|
148
|
+
if (!matchesTrigger(shortcut.getTrigger(), keyval, modifiers)) continue;
|
|
149
|
+
const action = shortcut.getAction();
|
|
150
|
+
if (action instanceof Gtk.SignalAction && action.getSignalName() === "move-focus") continue;
|
|
151
|
+
if (action?.activate(0 as Gtk.ShortcutActionFlags, widget, shortcut.getArguments())) return true;
|
|
152
|
+
}
|
|
153
|
+
return false;
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
const dispatchShortcutsOnWidget = (widget: Gtk.Widget, keyval: number, modifiers: number): boolean => {
|
|
157
|
+
const controllers = widget.observeControllers();
|
|
158
|
+
for (let i = 0; i < controllers.getNItems(); i++) {
|
|
159
|
+
const controller = controllers.getItem(i);
|
|
160
|
+
if (
|
|
161
|
+
controller instanceof Gtk.ShortcutController &&
|
|
162
|
+
activateMatchingShortcut(controller, widget, keyval, modifiers)
|
|
163
|
+
) {
|
|
164
|
+
return true;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
return false;
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
const dispatchShortcuts = (widget: Gtk.Widget, keyval: number, modifiers: number): boolean => {
|
|
171
|
+
const delegate = getEditableDelegate(widget);
|
|
172
|
+
if (delegate && dispatchShortcutsOnWidget(delegate, keyval, modifiers)) return true;
|
|
173
|
+
for (let current: Gtk.Widget | null = widget; current; current = current.getParent()) {
|
|
174
|
+
if (dispatchShortcutsOnWidget(current, keyval, modifiers)) return true;
|
|
175
|
+
}
|
|
176
|
+
return false;
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
const applyKeyAction = async (
|
|
180
|
+
widget: Gtk.Widget,
|
|
181
|
+
controllers: Gtk.EventControllerKey[],
|
|
182
|
+
state: UserEventState,
|
|
183
|
+
action: KeyAction,
|
|
184
|
+
): Promise<void> => {
|
|
185
|
+
updateModifierState(state, action);
|
|
186
|
+
const signalName = action.press ? "key-pressed" : "key-released";
|
|
187
|
+
for (const controller of controllers) {
|
|
188
|
+
controller.emit(signalName, action.keyval, 0, state.modifierState);
|
|
189
|
+
}
|
|
190
|
+
if (action.press) {
|
|
191
|
+
const handled = dispatchShortcuts(widget, action.keyval, state.modifierState);
|
|
192
|
+
if (!handled && action.keyval === Gdk.KEY_Return && widget instanceof Gtk.Editable) {
|
|
193
|
+
await fireEvent(widget, "activate");
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
export const keyboard = (state: UserEventState, widget: Gtk.Widget, input: string): Promise<void> =>
|
|
199
|
+
wrapEvent(widget, async () => {
|
|
200
|
+
const controllers = getOrCreateControllers(widget, Gtk.EventControllerKey);
|
|
201
|
+
for (const action of parseKeyboardInput(input)) {
|
|
202
|
+
await applyKeyAction(widget, controllers, state, action);
|
|
203
|
+
}
|
|
204
|
+
});
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import * as Gtk from "@gtkx/gi/gtk";
|
|
2
|
+
import { emitPress, emitRelease } from "./click.js";
|
|
3
|
+
import { getOrCreateControllers } from "./controller.js";
|
|
4
|
+
import { wrapEvent } from "./event-wrapper.js";
|
|
5
|
+
import type { UserEventState } from "./state.js";
|
|
6
|
+
|
|
7
|
+
/** A pointer action token: a full click (`click`, `[MouseLeft]`), a button press (`down`, `[MouseLeft>]`), or a button release (`up`, `[/MouseLeft]`). */
|
|
8
|
+
export type PointerInput = "click" | "down" | "up" | "[MouseLeft]" | "[MouseLeft>]" | "[/MouseLeft]";
|
|
9
|
+
|
|
10
|
+
const PRESS_INPUTS = new Set<PointerInput>(["[MouseLeft>]", "down"]);
|
|
11
|
+
const RELEASE_INPUTS = new Set<PointerInput>(["[/MouseLeft]", "up"]);
|
|
12
|
+
const CLICK_INPUTS = new Set<PointerInput>(["[MouseLeft]", "click"]);
|
|
13
|
+
|
|
14
|
+
const applyPointerInput = (widget: Gtk.Widget, state: UserEventState, input: PointerInput): void => {
|
|
15
|
+
const controllers = getOrCreateControllers(widget, Gtk.GestureClick);
|
|
16
|
+
if (CLICK_INPUTS.has(input)) {
|
|
17
|
+
emitPress(widget, controllers, 1);
|
|
18
|
+
emitRelease(widget, controllers, 1);
|
|
19
|
+
state.mouseLeftDown = false;
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
if (PRESS_INPUTS.has(input) && !state.mouseLeftDown) {
|
|
23
|
+
emitPress(widget, controllers, 1);
|
|
24
|
+
state.mouseLeftDown = true;
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
if (RELEASE_INPUTS.has(input) && state.mouseLeftDown) {
|
|
28
|
+
emitRelease(widget, controllers, 1);
|
|
29
|
+
state.mouseLeftDown = false;
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export const pointer = (state: UserEventState, widget: Gtk.Widget, input: PointerInput): Promise<void> =>
|
|
34
|
+
wrapEvent(widget, () => applyPointerInput(widget, state, input));
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import * as Gtk from "@gtkx/gi/gtk";
|
|
2
|
+
import { formatRoleList } from "../role-helpers.js";
|
|
3
|
+
import { wrapEvent } from "./event-wrapper.js";
|
|
4
|
+
|
|
5
|
+
const SELECTABLE_ROLES = new Set<Gtk.AccessibleRole>([Gtk.AccessibleRole.COMBO_BOX, Gtk.AccessibleRole.LIST]);
|
|
6
|
+
|
|
7
|
+
const isSelectable = (widget: Gtk.Widget): boolean => {
|
|
8
|
+
return SELECTABLE_ROLES.has(widget.getAccessibleRole());
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
const selectListViewItems = (selectionModel: Gtk.SelectionModel, positions: number[], exclusive: boolean): void => {
|
|
12
|
+
if (positions.length === 0) {
|
|
13
|
+
selectionModel.unselectRange(0, selectionModel.getNItems());
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const [first] = positions;
|
|
18
|
+
if (exclusive && positions.length === 1 && first !== undefined) {
|
|
19
|
+
selectionModel.selectItem(first, true);
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const nItems = selectionModel.getNItems();
|
|
24
|
+
const selected = Gtk.Bitset.newEmpty();
|
|
25
|
+
const mask = Gtk.Bitset.newRange(0, nItems);
|
|
26
|
+
|
|
27
|
+
for (const pos of positions) {
|
|
28
|
+
selected.add(pos);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
selectionModel.setSelection(selected, mask);
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const isListView = (widget: Gtk.Widget): widget is Gtk.ListView | Gtk.GridView | Gtk.ColumnView => {
|
|
35
|
+
return widget instanceof Gtk.ListView || widget instanceof Gtk.GridView || widget instanceof Gtk.ColumnView;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const selectComboBoxOption = (widget: Gtk.Widget, valueArray: number[]): void => {
|
|
39
|
+
if (valueArray.length > 1) {
|
|
40
|
+
throw new Error("Cannot select multiple options: ComboBox only supports single selection");
|
|
41
|
+
}
|
|
42
|
+
const [selection] = valueArray;
|
|
43
|
+
if (selection === undefined) return;
|
|
44
|
+
if (widget instanceof Gtk.DropDown) {
|
|
45
|
+
widget.setSelected(selection);
|
|
46
|
+
} else if (widget instanceof Gtk.ComboBox) {
|
|
47
|
+
widget.setActive(selection);
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const applyListBoxRows = (listBox: Gtk.ListBox, valueArray: number[], select: boolean): void => {
|
|
52
|
+
for (const value of valueArray) {
|
|
53
|
+
const row = listBox.getRowAtIndex(value);
|
|
54
|
+
if (!row) continue;
|
|
55
|
+
if (select) {
|
|
56
|
+
listBox.selectRow(row);
|
|
57
|
+
row.activate();
|
|
58
|
+
} else {
|
|
59
|
+
listBox.unselectRow(row);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
const requireSelectionModel = (
|
|
65
|
+
widget: Gtk.ListView | Gtk.GridView | Gtk.ColumnView,
|
|
66
|
+
verb: string,
|
|
67
|
+
): Gtk.SelectionModel => {
|
|
68
|
+
const selectionModel = widget.getModel();
|
|
69
|
+
if (selectionModel === null) {
|
|
70
|
+
throw new Error(`Cannot ${verb} options: list view has no selection model`);
|
|
71
|
+
}
|
|
72
|
+
return selectionModel;
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
const selectInListView = (widget: Gtk.ListView | Gtk.GridView | Gtk.ColumnView, valueArray: number[]): void => {
|
|
76
|
+
const selectionModel = requireSelectionModel(widget, "select");
|
|
77
|
+
const isMultiSelection = selectionModel instanceof Gtk.MultiSelection;
|
|
78
|
+
selectListViewItems(selectionModel, valueArray, !isMultiSelection);
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
const selectByRole = (widget: Gtk.Widget, valueArray: number[]): void => {
|
|
82
|
+
if (!isSelectable(widget)) {
|
|
83
|
+
throw new Error(`Cannot select options: expected selectable widget (${formatRoleList(SELECTABLE_ROLES)})`);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const role = widget.getAccessibleRole();
|
|
87
|
+
if (role === Gtk.AccessibleRole.COMBO_BOX) {
|
|
88
|
+
selectComboBoxOption(widget, valueArray);
|
|
89
|
+
} else if (widget instanceof Gtk.ListBox) {
|
|
90
|
+
applyListBoxRows(widget, valueArray, true);
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
export const selectOptions = (widget: Gtk.Widget, values: number | number[]): Promise<void> =>
|
|
95
|
+
wrapEvent(widget, () => {
|
|
96
|
+
const valueArray = Array.isArray(values) ? values : [values];
|
|
97
|
+
if (isListView(widget)) {
|
|
98
|
+
selectInListView(widget, valueArray);
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
selectByRole(widget, valueArray);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
const deselectInListView = (widget: Gtk.ListView | Gtk.GridView | Gtk.ColumnView, valueArray: number[]): void => {
|
|
105
|
+
const selectionModel = requireSelectionModel(widget, "deselect");
|
|
106
|
+
for (const pos of valueArray) {
|
|
107
|
+
selectionModel.unselectItem(pos);
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
export const deselectOptions = (widget: Gtk.Widget, values: number | number[]): Promise<void> =>
|
|
112
|
+
wrapEvent(widget, () => {
|
|
113
|
+
const valueArray = Array.isArray(values) ? values : [values];
|
|
114
|
+
if (isListView(widget)) {
|
|
115
|
+
deselectInListView(widget, valueArray);
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
if (!(widget instanceof Gtk.ListBox)) {
|
|
119
|
+
throw new Error("Cannot deselect options: only ListBox supports deselection");
|
|
120
|
+
}
|
|
121
|
+
applyListBoxRows(widget, valueArray, false);
|
|
122
|
+
});
|