Lomiri
Loading...
Searching...
No Matches
VirtualTouchPad.qml
1/*
2 * Copyright (C) 2015 Canonical Ltd.
3 *
4 * This program is free software; you can redistribute it and/or modify
5 * it under the terms of the GNU General Public License as published by
6 * the Free Software Foundation; version 3.
7 *
8 * This program is distributed in the hope that it will be useful,
9 * but WITHOUT ANY WARRANTY; without even the implied warranty of
10 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 * GNU General Public License for more details.
12 *
13 * You should have received a copy of the GNU General Public License
14 * along with this program. If not, see <http://www.gnu.org/licenses/>.
15 */
16
17import QtQuick 2.15
18import QtQuick.Layouts 1.1
19import Lomiri.Components 1.3
20import Qt.labs.settings 1.0
21import Aethercast 1.0
22import UInput 0.1
23import "../Components"
24
25Item {
26 id: root
27
28 property bool oskEnabled: false
29
30 AethercastDisplays {
31 id: aethercastDisplays
32 }
33
34 Component.onCompleted: {
35 UInput.createMouse();
36 if (!settings.touchpadTutorialHasRun) {
37 root.runTutorial()
38 }
39 }
40 Component.onDestruction: UInput.removeMouse()
41
42 function runTutorial() {
43 // If the tutorial animation is started too early, e.g. in Component.onCompleted,
44 // root width & height might be reported as 0x0 still. As animations read their
45 // values at startup and won't update them, lets make sure to only start once
46 // we have some actual size.
47 if (root.width > 0 && root.height > 0) {
48 tutorial.start();
49 } else {
50 tutorialTimer.start();
51 }
52 }
53
54 Timer {
55 id: tutorialTimer
56 interval: 50
57 repeat: false
58 running: false
59 onTriggered: root.runTutorial();
60 }
61
62 readonly property bool pressed: point1.pressed || point2.pressed || leftButton.pressed || rightButton.pressed
63
64 property var settings: Settings {
65 objectName: "virtualTouchPadSettings"
66 property bool touchpadTutorialHasRun: false
67 property bool oskEnabled: true
68 }
69
70 MultiPointTouchArea {
71 objectName: "touchPadArea"
72 anchors.fill: parent
73 enabled: !tutorial.running || tutorial.paused
74
75 // FIXME: Once we have Qt DPR support, this should be Qt.styleHints.startDragDistance
76 readonly property int clickThreshold: internalGu * 0.5
77 property bool isClick: false
78 property bool isDoubleClick: false
79 property bool isDrag: false
80
81 onPressed: {
82 if (tutorial.paused) {
83 tutorial.resume();
84 return;
85 }
86
87 // If double-tapping *really* fast, it could happen that we end up having only point2 pressed
88 // Make sure we check for both combos, only point1 or only point2
89 if (((point1.pressed && !point2.pressed) || (!point1.pressed && point2.pressed))
90 && clickTimer.running) {
91 clickTimer.stop();
92 UInput.pressMouse(UInput.ButtonLeft)
93 isDoubleClick = true;
94 }
95 isClick = true;
96 }
97
98 onUpdated: {
99 switch (touchPoints.length) {
100 case 1:
101 moveMouse(touchPoints);
102 return;
103 case 2:
104 scroll(touchPoints);
105 return;
106 }
107 }
108
109 onReleased: {
110 if (isDoubleClick || isDrag) {
111 UInput.releaseMouse(UInput.ButtonLeft)
112 isDoubleClick = false;
113 }
114 if (isClick) {
115 clickTimer.scheduleClick(point1.pressed ? UInput.ButtonRight : UInput.ButtonLeft)
116 }
117 isClick = false;
118 isDrag = false;
119 }
120
121 Timer {
122 id: clickTimer
123 repeat: false
124 interval: 200
125 property int button: UInput.ButtonLeft
126 onTriggered: {
127 UInput.pressMouse(button);
128 UInput.releaseMouse(button);
129 }
130 function scheduleClick(button) {
131 clickTimer.button = button;
132 clickTimer.start();
133 }
134 }
135
136 function moveMouse(touchPoints) {
137 var tp = touchPoints[0];
138 if (isClick &&
139 (Math.abs(tp.x - tp.startX) > clickThreshold ||
140 Math.abs(tp.y - tp.startY) > clickThreshold)) {
141 isClick = false;
142 isDrag = true;
143 }
144
145 UInput.moveMouse(tp.x - tp.previousX, tp.y - tp.previousY);
146 }
147
148 function scroll(touchPoints) {
149 var dh = 0;
150 var dv = 0;
151 var tp = touchPoints[0];
152 if (isClick &&
153 (Math.abs(tp.x - tp.startX) > clickThreshold ||
154 Math.abs(tp.y - tp.startY) > clickThreshold)) {
155 isClick = false;
156 }
157 dh += tp.x - tp.previousX;
158 dv += tp.y - tp.previousY;
159
160 tp = touchPoints[1];
161 if (isClick &&
162 (Math.abs(tp.x - tp.startX) > clickThreshold ||
163 Math.abs(tp.y - tp.startY) > clickThreshold)) {
164 isClick = false;
165 }
166 dh += tp.x - tp.previousX;
167 dv += tp.y - tp.previousY;
168
169 // As we added up the movement of the two fingers, let's divide it again by 2
170 dh /= 2;
171 dv /= 2;
172
173 UInput.scrollMouse(dh, dv);
174 }
175
176 touchPoints: [
177 TouchPoint {
178 id: point1
179 },
180 TouchPoint {
181 id: point2
182 }
183 ]
184 }
185
186 RowLayout {
187 anchors { left: parent.left; right: parent.right; bottom: parent.bottom; margins: -internalGu * 1 }
188 height: internalGu * 10
189 spacing: internalGu * 1
190
191 MouseArea {
192 id: leftButton
193 objectName: "leftButton"
194 Layout.fillWidth: true
195 Layout.fillHeight: true
196 onPressed: UInput.pressMouse(UInput.ButtonLeft);
197 onReleased: UInput.releaseMouse(UInput.ButtonLeft);
198 property bool highlight: false
199 LomiriShape {
200 anchors.fill: parent
201 backgroundColor: leftButton.highlight || leftButton.pressed ? LomiriColors.ash : LomiriColors.inkstone
202 Behavior on backgroundColor { ColorAnimation { duration: LomiriAnimation.FastDuration } }
203 }
204 }
205
206 MouseArea {
207 id: rightButton
208 objectName: "rightButton"
209 Layout.fillWidth: true
210 Layout.fillHeight: true
211 onPressed: UInput.pressMouse(UInput.ButtonRight);
212 onReleased: UInput.releaseMouse(UInput.ButtonRight);
213 property bool highlight: false
214 LomiriShape {
215 anchors.fill: parent
216 backgroundColor: rightButton.highlight || rightButton.pressed ? LomiriColors.ash : LomiriColors.inkstone
217 Behavior on backgroundColor { ColorAnimation { duration: LomiriAnimation.FastDuration } }
218 }
219 }
220 }
221
222 AbstractButton {
223 id: disconnectButton
224 objectName: "disconnectButton"
225 anchors { right: parent.right; top: parent.top; margins: internalGu * 2 }
226 height: internalGu * 6
227 width: visible ? height : 0
228 visible: aethercastDisplays.state === "connected"
229
230 onClicked: {
231 aethercastDisplays.enabled = false
232 }
233
234 Rectangle {
235 anchors.fill: parent
236 radius: width / 2
237 color: LomiriColors.inkstone
238 }
239
240 Icon {
241 anchors.fill: parent
242 anchors.margins: internalGu * 1.5
243 name: "close"
244 color: "red"
245 }
246 }
247
248 AbstractButton {
249 id: oskButton
250 objectName: "oskButton"
251 anchors { right: disconnectButton.left; top: parent.top; margins: internalGu * 2 }
252 height: internalGu * 6
253 width: height
254
255 onClicked: {
256 settings.oskEnabled = !settings.oskEnabled
257 }
258
259 Rectangle {
260 anchors.fill: parent
261 radius: width / 2
262 color: LomiriColors.inkstone
263 }
264
265 Icon {
266 anchors.fill: parent
267 anchors.margins: internalGu * 1.5
268 name: "input-keyboard-symbolic"
269 color: settings.oskEnabled ? LomiriColors.porcelain : LomiriColors.red
270 }
271 }
272
273 InputMethod {
274 id: inputMethod
275 // Don't resize when there is only one screen to avoid resize clashing with the InputMethod in the Shell.
276 enabled: root.oskEnabled && settings.oskEnabled && !tutorial.running
277 objectName: "inputMethod"
278 anchors.fill: parent
279 }
280
281 Label {
282 id: tutorialLabel
283 objectName: "tutorialLabel"
284 anchors { left: parent.left; top: parent.top; right: parent.right; margins: internalGu * 4; topMargin: internalGu * 10 }
285 opacity: 0
286 visible: opacity > 0
287 font.pixelSize: 2 * internalGu
288 color: "white"
289 wrapMode: Text.WordWrap
290 }
291
292 Icon {
293 id: tutorialImage
294 objectName: "tutorialImage"
295 height: internalGu * 8
296 width: height
297 name: "input-touchpad-symbolic"
298 color: "white"
299 opacity: 0
300 visible: opacity > 0
301 anchors { top: tutorialLabel.bottom; horizontalCenter: parent.horizontalCenter; margins: internalGu * 2 }
302 }
303
304 Item {
305 id: tutorialFinger1
306 objectName: "tutorialFinger1"
307 width: internalGu * 5
308 height: width
309 property real scale: 1
310 opacity: 0
311 visible: opacity > 0
312 Rectangle {
313 width: parent.width * parent.scale
314 height: width
315 anchors.centerIn: parent
316 radius: width / 2
317 color: LomiriColors.inkstone
318 }
319 }
320
321 Item {
322 id: tutorialFinger2
323 objectName: "tutorialFinger2"
324 width: internalGu * 5
325 height: width
326 property real scale: 1
327 opacity: 0
328 visible: opacity > 0
329 Rectangle {
330 width: parent.width * parent.scale
331 height: width
332 anchors.centerIn: parent
333 radius: width / 2
334 color: LomiriColors.inkstone
335 }
336 }
337
338 SequentialAnimation {
339 id: tutorial
340 objectName: "tutorialAnimation"
341
342 PropertyAction { targets: [leftButton, rightButton, oskButton]; property: "enabled"; value: false }
343 PropertyAction { targets: [leftButton, rightButton, oskButton]; property: "opacity"; value: 0 }
344 PropertyAction { target: tutorialLabel; property: "text"; value: i18n.tr("Your device is now connected to an external display. Use this screen as a touch pad to interact with the pointer.") }
345 LomiriNumberAnimation { targets: [tutorialLabel, tutorialImage]; property: "opacity"; to: 1; duration: LomiriAnimation.FastDuration }
346 PropertyAction { target: tutorial; property: "paused"; value: true }
347 PauseAnimation { duration: 500 } // it takes a bit until pausing actually takes effect
348 LomiriNumberAnimation { targets: [tutorialLabel, tutorialImage]; property: "opacity"; to: 0; duration: LomiriAnimation.FastDuration }
349
350 LomiriNumberAnimation { target: leftButton; property: "opacity"; to: 1 }
351 LomiriNumberAnimation { target: rightButton; property: "opacity"; to: 1 }
352
353 PauseAnimation { duration: LomiriAnimation.SleepyDuration }
354 PropertyAction { target: tutorialLabel; property: "text"; value: i18n.tr("Tap left button to click.") }
355 LomiriNumberAnimation { target: tutorialLabel; property: "opacity"; to: 1; duration: LomiriAnimation.FastDuration }
356 SequentialAnimation {
357 loops: 2
358 PropertyAction { target: leftButton; property: "highlight"; value: true }
359 PauseAnimation { duration: LomiriAnimation.FastDuration }
360 PropertyAction { target: leftButton; property: "highlight"; value: false }
361 PauseAnimation { duration: LomiriAnimation.SleepyDuration }
362 }
363 LomiriNumberAnimation { target: tutorialLabel; property: "opacity"; to: 0; duration: LomiriAnimation.FastDuration }
364
365 PauseAnimation { duration: LomiriAnimation.SleepyDuration }
366 PropertyAction { target: tutorialLabel; property: "text"; value: i18n.tr("Tap right button to right click.") }
367 LomiriNumberAnimation { target: tutorialLabel; property: "opacity"; to: 1; duration: LomiriAnimation.FastDuration }
368 SequentialAnimation {
369 loops: 2
370 PropertyAction { target: rightButton; property: "highlight"; value: true }
371 PauseAnimation { duration: LomiriAnimation.FastDuration }
372 PropertyAction { target: rightButton; property: "highlight"; value: false }
373 PauseAnimation { duration: LomiriAnimation.SleepyDuration }
374 }
375 LomiriNumberAnimation { target: tutorialLabel; property: "opacity"; to: 0; duration: LomiriAnimation.FastDuration }
376
377 PauseAnimation { duration: LomiriAnimation.SleepyDuration }
378 PropertyAction { target: tutorialLabel; property: "text"; value: i18n.tr("Swipe with two fingers to scroll.") }
379 LomiriNumberAnimation { target: tutorialLabel; property: "opacity"; to: 1; duration: LomiriAnimation.FastDuration }
380 PropertyAction { target: tutorialFinger1; property: "x"; value: root.width / 2 - tutorialFinger1.width - internalGu * 1 }
381 PropertyAction { target: tutorialFinger2; property: "x"; value: root.width / 2 + tutorialFinger1.width + internalGu * 1 - tutorialFinger2.width }
382 PropertyAction { target: tutorialFinger1; property: "y"; value: root.height / 2 - internalGu * 10 }
383 PropertyAction { target: tutorialFinger2; property: "y"; value: root.height / 2 - internalGu * 10 }
384 SequentialAnimation {
385 ParallelAnimation {
386 LomiriNumberAnimation { target: tutorialFinger1; property: "opacity"; to: 1; duration: LomiriAnimation.FastDuration }
387 LomiriNumberAnimation { target: tutorialFinger2; property: "opacity"; to: 1; duration: LomiriAnimation.FastDuration }
388 LomiriNumberAnimation { target: tutorialFinger1; property: "scale"; from: 0; to: 1; duration: LomiriAnimation.FastDuration }
389 LomiriNumberAnimation { target: tutorialFinger2; property: "scale"; from: 0; to: 1; duration: LomiriAnimation.FastDuration }
390 }
391 ParallelAnimation {
392 LomiriNumberAnimation { target: tutorialFinger1; property: "y"; to: root.height / 2 + internalGu * 10; duration: LomiriAnimation.SleepyDuration }
393 LomiriNumberAnimation { target: tutorialFinger2; property: "y"; to: root.height / 2 + internalGu * 10; duration: LomiriAnimation.SleepyDuration }
394 }
395 ParallelAnimation {
396 LomiriNumberAnimation { target: tutorialFinger1; property: "opacity"; to: 0; duration: LomiriAnimation.FastDuration }
397 LomiriNumberAnimation { target: tutorialFinger2; property: "opacity"; to: 0; duration: LomiriAnimation.FastDuration }
398 LomiriNumberAnimation { target: tutorialFinger1; property: "scale"; from: 1; to: 0; duration: LomiriAnimation.FastDuration }
399 LomiriNumberAnimation { target: tutorialFinger2; property: "scale"; from: 1; to: 0; duration: LomiriAnimation.FastDuration }
400 }
401 PauseAnimation { duration: LomiriAnimation.SlowDuration }
402 ParallelAnimation {
403 LomiriNumberAnimation { target: tutorialFinger1; property: "opacity"; to: 1; duration: LomiriAnimation.FastDuration }
404 LomiriNumberAnimation { target: tutorialFinger2; property: "opacity"; to: 1; duration: LomiriAnimation.FastDuration }
405 LomiriNumberAnimation { target: tutorialFinger1; property: "scale"; from: 0; to: 1; duration: LomiriAnimation.FastDuration }
406 LomiriNumberAnimation { target: tutorialFinger2; property: "scale"; from: 0; to: 1; duration: LomiriAnimation.FastDuration }
407 }
408 ParallelAnimation {
409 LomiriNumberAnimation { target: tutorialFinger1; property: "y"; to: root.height / 2 - internalGu * 10; duration: LomiriAnimation.SleepyDuration }
410 LomiriNumberAnimation { target: tutorialFinger2; property: "y"; to: root.height / 2 - internalGu * 10; duration: LomiriAnimation.SleepyDuration }
411 }
412 ParallelAnimation {
413 LomiriNumberAnimation { target: tutorialFinger1; property: "opacity"; to: 0; duration: LomiriAnimation.FastDuration }
414 LomiriNumberAnimation { target: tutorialFinger2; property: "opacity"; to: 0; duration: LomiriAnimation.FastDuration }
415 LomiriNumberAnimation { target: tutorialFinger1; property: "scale"; from: 1; to: 0; duration: LomiriAnimation.FastDuration }
416 LomiriNumberAnimation { target: tutorialFinger2; property: "scale"; from: 1; to: 0; duration: LomiriAnimation.FastDuration }
417 }
418 PauseAnimation { duration: LomiriAnimation.SlowDuration }
419 }
420 LomiriNumberAnimation { target: tutorialLabel; property: "opacity"; to: 0; duration: LomiriAnimation.FastDuration }
421
422 PauseAnimation { duration: LomiriAnimation.SleepyDuration }
423 PropertyAction { target: tutorialLabel; property: "text"; value: i18n.tr("Find more settings in the system settings.") }
424 LomiriNumberAnimation { target: tutorialLabel; property: "opacity"; to: 1; duration: LomiriAnimation.FastDuration }
425 PauseAnimation { duration: 2000 }
426 LomiriNumberAnimation { target: tutorialLabel; property: "opacity"; to: 0; duration: LomiriAnimation.FastDuration }
427
428 LomiriNumberAnimation { target: oskButton; property: "opacity"; to: 1 }
429 PropertyAction { targets: [leftButton, rightButton, oskButton]; property: "enabled"; value: true }
430
431 PropertyAction { target: settings; property: "touchpadTutorialHasRun"; value: true }
432 }
433}