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