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