-
-
Notifications
You must be signed in to change notification settings - Fork 27
Expand file tree
/
Copy pathDragGesture.js
More file actions
290 lines (235 loc) · 11.4 KB
/
Copy pathDragGesture.js
File metadata and controls
290 lines (235 loc) · 11.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
//////////////////////////////////////////////////////////////////////////////////////////
// ,-. ,--. ,-. , , ,---. ,-. ;-. ,-. . . ,-. ,--. //
// | \ | ( ` | / | / \ | ) / | | | ) | //
// | | |- `-. |< | | | |-' | | | |-< |- //
// | / | . ) | \ | \ / | \ | | | ) | //
// `-' `--' `-' ' ` ' `-' ' `-' `--` `-' `--' //
//////////////////////////////////////////////////////////////////////////////////////////
// SPDX-FileCopyrightText: Simon Schneegans <code@simonschneegans.de>
// SPDX-License-Identifier: GPL-3.0-or-later
'use strict';
import Meta from 'gi://Meta';
import Clutter from 'gi://Clutter';
import GObject from 'gi://GObject';
import Shell from 'gi://Shell';
import * as Util from 'resource:///org/gnome/shell/misc/util.js';
import * as Main from 'resource:///org/gnome/shell/ui/main.js';
import {ControlsState} from 'resource:///org/gnome/shell/ui/overviewControls.js';
import {Workspace} from 'resource:///org/gnome/shell/ui/workspace.js';
//////////////////////////////////////////////////////////////////////////////////////////
// In GNOME Shell, SwipeTrackers are used all over the place to capture swipe gestures. //
// There's one for entering the overview, one for switching workspaces in desktop mode, //
// one for switching workspaces in overview mode, one for horizontal scrolling in the //
// app drawer, and many more. The ones used for workspace-switching usually do not //
// respond to single-click dragging but only to multi-touch gestures. We want to be //
// able to rotate the cube with the left mouse button, so we add the gesture defined //
// below to these two SwipeTracker instances (this is done by the _addDragGesture() of //
// the extension class). The gesture is loosely based on the gesture defined here: //
// https://gitlab.gnome.org/GNOME/gnome-shell/-/blob/main/js/ui/swipeTracker.js#L89 //
// It behaves the same in the regard that it reports update events for horizontal //
// movements. However, it stores vertical movements as well and makes this accessible //
// via the "pitch" property. This is then used for vertical rotations of the cube. //
//////////////////////////////////////////////////////////////////////////////////////////
const State = {
INACTIVE: 0, // The state will change to PENDING as soon as there is a mouse click.
PENDING: 1, // There was a click, but not enough movement to trigger the gesture.
ACTIVE: 2 // The gesture has been triggered and is in progress.
};
// clang-format off
export var DragGesture =
GObject.registerClass({
Properties: {
'distance': GObject.ParamSpec.double(
'distance', 'distance', 'distance', GObject.ParamFlags.READWRITE, 0, Infinity, 0),
'pitch': GObject.ParamSpec.double(
'pitch', 'pitch', 'pitch', GObject.ParamFlags.READWRITE, 0, 1, 0),
'sensitivity': GObject.ParamSpec.double(
'sensitivity', 'sensitivity', 'sensitivity', GObject.ParamFlags.READWRITE, 1, 10, 1),
},
Signals: {
'begin': {param_types: [GObject.TYPE_UINT, GObject.TYPE_DOUBLE, GObject.TYPE_DOUBLE]},
'update': {param_types: [GObject.TYPE_UINT, GObject.TYPE_DOUBLE, GObject.TYPE_DOUBLE]},
'end': {param_types: [GObject.TYPE_UINT, GObject.TYPE_DOUBLE]},
},
},
class DragGesture extends GObject.Object {
// clang-format on
_init(actor, mode) {
super._init();
this._actor = actor;
this._state = State.INACTIVE;
this._mode = mode;
// We listen to the 'captured-event' to be able to intercept some other actions. The
// main problem is the long-press action of the desktop background actor. This
// swallows any click events preventing us from dragging the desktop background.
// By connecting to 'captured-event', we have to extra-careful to propagate any
// event we are not interested in.
this._actorConnection1 = actor.connect('captured-event', (a, e) => {
return this._handleEvent(e);
});
// Once the input is grabbed, events are delivered directly to the actor, so we have
// also to connect to the normal "event" signal.
this._actorConnection2 = actor.connect('event', (a, e) => {
if (this._lastGrab) {
return this._handleEvent(e);
}
return Clutter.EVENT_PROPAGATE;
});
}
// Disconnects from the actor.
destroy() {
this._actor.disconnect(this._actorConnection1);
this._actor.disconnect(this._actorConnection2);
}
// This is called on every captured event.
_handleEvent(event) {
// Abort if the gesture is not meant for the current action mode (e.g. either
// Shell.ActionMode.OVERVIEW or Shell.ActionMode.NORMAL).
if (this._mode != Main.actionMode) {
return Clutter.EVENT_PROPAGATE;
}
// In the overview, we only want to switch workspaces by dragging when in
// window-picker state.
if (Main.actionMode == Shell.ActionMode.OVERVIEW) {
if (Main.overview._overview.controls._stateAdjustment.value !=
ControlsState.WINDOW_PICKER) {
return Clutter.EVENT_PROPAGATE;
}
}
// Ignore touch events on X11. On X11, we get emulated pointer events.
if (!Meta.is_wayland_compositor() &&
(event.type() == Clutter.EventType.TOUCH_BEGIN ||
event.type() == Clutter.EventType.TOUCH_UPDATE ||
event.type() == Clutter.EventType.TOUCH_END)) {
return Clutter.EVENT_PROPAGATE;
}
// When a mouse button is pressed or a touch event starts, we store the
// corresponding position. The gesture is maybe triggered later, if the pointer was
// moved a little.
if (event.type() == Clutter.EventType.BUTTON_PRESS ||
event.type() == Clutter.EventType.TOUCH_BEGIN) {
const source = global.stage.get_event_actor(event);
if (source) {
// Here's a minor hack: In the overview, there are some draggable things like
// window previews which "compete" with this gesture. Sometimes, the cube is
// dragged, sometimes the window previews. So we make sure that we do only start
// the gesture for events which originate from the given actor or from a
// workspace's background.
if (Main.actionMode != Shell.ActionMode.OVERVIEW || source == this._actor ||
source.get_parent() instanceof Workspace) {
this._clickPos = event.get_coords();
this._state = State.PENDING;
}
}
return Clutter.EVENT_PROPAGATE;
}
// Abort the pending state if the pointer leaves the actor.
if (event.type() == Clutter.EventType.LEAVE && this._state == State.PENDING) {
this._cancel();
return Clutter.EVENT_PROPAGATE;
}
// As soon as the pointer is moved a bit, the drag action becomes active.
if (this._eventIsMotion(event)) {
// If the mouse button is not pressed, we are not interested in the event.
if (this._state != State.INACTIVE && event.type() == Clutter.EventType.MOTION &&
(event.get_state() & Clutter.ModifierType.BUTTON1_MASK) == 0) {
this._cancel();
return Clutter.EVENT_PROPAGATE;
}
const currentPos = event.get_coords();
// If we are in the pending state, the gesture may be triggered as soon as the
// pointer is moved enough.
if (this._state == State.PENDING) {
const threshold = Clutter.Settings.get_default().dnd_drag_threshold;
if (Math.abs(currentPos[0] - this._clickPos[0]) > threshold ||
Math.abs(currentPos[1] - this._clickPos[1]) > threshold) {
// When starting a drag in desktop mode, we grab the input so that we can move
// the pointer across windows without loosing the input events.
if (Main.actionMode == Shell.ActionMode.NORMAL) {
if (!this._grab()) {
return Clutter.EVENT_PROPAGATE;
}
}
this._state = State.ACTIVE;
[this._lastX, this._startY] = currentPos;
this.pitch = 0;
this.emit('begin', event.get_time(), currentPos[0], currentPos[1]);
}
// Even if the gesture started, we propagate the event so that any other
// gestures may wait for long-presses are canceled properly.
return Clutter.EVENT_PROPAGATE;
}
// In the active state, we report updates on each movement.
if (this._state == State.ACTIVE) {
// Compute the horizontal movement relative to the last call.
let deltaX = currentPos[0] - this._lastX;
this._lastX = currentPos[0];
// Compute the accumulated pitch relative to the screen height.
this.pitch = (this._startY - currentPos[1]) / global.screen_height;
// Increase sensitivity.
deltaX *= this.sensitivity;
// Increase horizontal movement if the cube is rotated vertically.
deltaX *= Util.lerp(1.0, global.workspaceManager.get_n_workspaces(),
Math.abs(this.pitch));
this.emit('update', event.get_time(), -deltaX, this.distance);
return Clutter.EVENT_STOP;
}
return Clutter.EVENT_PROPAGATE;
}
// As soon as the mouse button is released or the touch event ends, we quit the
// gesture.
if (this._eventIsRelease(event)) {
// If the gesture was active, report an end event.
if (this._state == State.ACTIVE) {
this._cancel();
this.emit('end', event.get_time(), this.distance);
return Clutter.EVENT_STOP;
}
// If the gesture was in pending state, set it to inactive again.
this._cancel();
return Clutter.EVENT_PROPAGATE;
}
return Clutter.EVENT_PROPAGATE;
}
// This aborts any ongoing grab and resets the current state to inactive.
_cancel() {
if (this._lastGrab) {
this._ungrab();
}
this._state = State.INACTIVE;
}
// Makes sure that all events from the pointing device we received last input from is
// passed to the given actor. This is used to ensure that we do not "loose" the touch
// buttons will dragging them around.
_grab() {
this._lastGrab = global.stage.grab(this._actor);
return this._lastGrab != null;
}
// Releases a grab created with the method above.
_ungrab() {
this._lastGrab.dismiss();
this._lastGrab = null;
}
// This is borrowed from here:
// https://gitlab.gnome.org/GNOME/gnome-shell/-/blob/main/js/ui/dnd.js#L214
_eventIsRelease(event) {
if (event.type() == Clutter.EventType.BUTTON_RELEASE) {
const buttonMask = Clutter.ModifierType.BUTTON1_MASK |
Clutter.ModifierType.BUTTON2_MASK | Clutter.ModifierType.BUTTON3_MASK;
// We only obey the last button release from the device, other buttons may get
// pressed / released during the drag.
return (event.get_state() & buttonMask) == 0;
} else if (event.type() == Clutter.EventType.TOUCH_END) {
// For touch, we only obey the pointer emulating sequence.
return global.display.is_pointer_emulating_sequence(event.get_event_sequence());
}
return false;
}
// This is borrowed from here:
// https://gitlab.gnome.org/GNOME/gnome-shell/-/blob/main/js/ui/dnd.js#L259
_eventIsMotion(event) {
return event.type() == Clutter.EventType.MOTION ||
(event.type() == Clutter.EventType.TOUCH_UPDATE &&
global.display.is_pointer_emulating_sequence(event.get_event_sequence()));
}
});