Unity 8
TouchDispatcher.cpp
1 /*
2  * Copyright (C) 2014-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 #include "TouchDispatcher.h"
18 
19 #include <QGuiApplication>
20 #include <QScopedPointer>
21 #include <QStyleHints>
22 
23 #pragma GCC diagnostic push
24 #pragma GCC diagnostic ignored "-pedantic"
25 #include <private/qquickitem_p.h>
26 #pragma GCC diagnostic pop
27 
28 #define TOUCHDISPATCHER_DEBUG 0
29 
30 #if TOUCHDISPATCHER_DEBUG
31 #define ugDebug(params) qDebug().nospace() << "[TouchDispatcher(" << this << ")] " << params
32 #include <DebugHelpers.h>
33 #else // TOUCHDISPATCHER_DEBUG
34 #define ugDebug(params) ((void)0)
35 #endif // TOUCHDISPATCHER_DEBUG
36 
37 TouchDispatcher::TouchDispatcher()
38  : m_status(NoActiveTouch)
39  , m_touchMouseId(-1)
40  , m_touchMousePressTimestamp(0)
41 {
42 }
43 
44 void TouchDispatcher::setTargetItem(QQuickItem *target)
45 {
46  if (target != m_targetItem) {
47  m_targetItem = target;
48  if (m_status != NoActiveTouch) {
49  qWarning("[TouchDispatcher] Changing target item in the middle of a touch stream");
50  setStatus(TargetRejectedTouches);
51  }
52  }
53 }
54 
55 void TouchDispatcher::dispatch(QTouchDevice *device,
56  Qt::KeyboardModifiers modifiers,
57  const QList<QTouchEvent::TouchPoint> &touchPoints,
58  QWindow *window,
59  ulong timestamp)
60 {
61  if (m_targetItem.isNull()) {
62  qWarning("[TouchDispatcher] Cannot dispatch touch event because target item is null");
63  return;
64  }
65 
66  QEvent::Type eventType = resolveEventType(touchPoints);
67 
68  if (eventType == QEvent::TouchBegin) {
69  dispatchTouchBegin(device, modifiers, touchPoints, window, timestamp);
70 
71  } else if (eventType == QEvent::TouchUpdate || eventType == QEvent::TouchEnd) {
72 
73  if (m_status == DeliveringTouchEvents) {
74  dispatchAsTouch(eventType, device, modifiers, touchPoints, window, timestamp);
75  } else if (m_status == DeliveringMouseEvents) {
76  dispatchAsMouse(device, modifiers, touchPoints, timestamp);
77  } else {
78  Q_ASSERT(m_status == TargetRejectedTouches);
79  ugDebug("Not dispatching touch event to " << m_targetItem.data()
80  << "because it already rejected the touch stream.");
81  // Do nothing
82  }
83 
84  if (eventType == QEvent::TouchEnd) {
85  setStatus(NoActiveTouch);
86  m_touchMouseId = -1;
87  }
88 
89  } else {
90  // Should never happen
91  qCritical() << "[TouchDispatcher] Unexpected event type" << eventType;
92  Q_ASSERT(false);
93  return;
94  }
95 }
96 
97 void TouchDispatcher::dispatchTouchBegin(
98  QTouchDevice *device,
99  Qt::KeyboardModifiers modifiers,
100  const QList<QTouchEvent::TouchPoint> &touchPoints,
101  QWindow *window,
102  ulong timestamp)
103 {
104  Q_ASSERT(m_status == NoActiveTouch);
105  QQuickItem *targetItem = m_targetItem.data();
106 
107  if (!targetItem->isEnabled() || !targetItem->isVisible()) {
108  ugDebug("Cannot dispatch touch event to " << targetItem << " because it's disabled or invisible.");
109  return;
110  }
111 
112  // Map touch points to targetItem coordinates
113  QList<QTouchEvent::TouchPoint> targetTouchPoints = touchPoints;
114  transformTouchPoints(targetTouchPoints, QQuickItemPrivate::get(targetItem)->windowToItemTransform());
115 
116  QScopedPointer<QTouchEvent> touchEvent(
117  createQTouchEvent(QEvent::TouchBegin, device, modifiers, targetTouchPoints, window, timestamp));
118 
119 
120  ugDebug("dispatching " << qPrintable(touchEventToString(touchEvent.data()))
121  << " to " << targetItem);
122  QCoreApplication::sendEvent(targetItem, touchEvent.data());
123 
124 
125  if (touchEvent->isAccepted()) {
126  ugDebug("Item accepted the touch event.");
127  setStatus(DeliveringTouchEvents);
128  } else if (targetItem->acceptedMouseButtons() & Qt::LeftButton) {
129  ugDebug("Item rejected the touch event. Trying a QMouseEvent");
130  // NB: Arbitrarily chose the first touch point to emulate the mouse pointer
131  QScopedPointer<QMouseEvent> mouseEvent(
132  touchToMouseEvent(QEvent::MouseButtonPress, targetTouchPoints.at(0), timestamp,
133  modifiers, false /* transformNeeded */));
134  Q_ASSERT(targetTouchPoints.at(0).state() == Qt::TouchPointPressed);
135 
136  ugDebug("dispatching " << qPrintable(mouseEventToString(mouseEvent.data()))
137  << " to " << m_targetItem.data());
138  QCoreApplication::sendEvent(targetItem, mouseEvent.data());
139  if (mouseEvent->isAccepted()) {
140  ugDebug("Item accepted the QMouseEvent.");
141  setStatus(DeliveringMouseEvents);
142  m_touchMouseId = targetTouchPoints.at(0).id();
143 
144  if (checkIfDoubleClicked(timestamp)) {
145  QScopedPointer<QMouseEvent> doubleClickEvent(
146  touchToMouseEvent(QEvent::MouseButtonDblClick, targetTouchPoints.at(0), timestamp,
147  modifiers, false /* transformNeeded */));
148  ugDebug("dispatching " << qPrintable(mouseEventToString(doubleClickEvent.data()))
149  << " to " << m_targetItem.data());
150  QCoreApplication::sendEvent(targetItem, doubleClickEvent.data());
151  }
152 
153  } else {
154  ugDebug("Item rejected the QMouseEvent.");
155  setStatus(TargetRejectedTouches);
156  }
157  } else {
158  ugDebug("Item rejected the touch event and does not accept mouse buttons.");
159  setStatus(TargetRejectedTouches);
160  }
161 }
162 
163 void TouchDispatcher::dispatchAsTouch(QEvent::Type eventType,
164  QTouchDevice *device,
165  Qt::KeyboardModifiers modifiers,
166  const QList<QTouchEvent::TouchPoint> &touchPoints,
167  QWindow *window,
168  ulong timestamp)
169 {
170  QQuickItem *targetItem = m_targetItem.data();
171 
172  // Map touch points to targetItem coordinates
173  QList<QTouchEvent::TouchPoint> targetTouchPoints = touchPoints;
174  transformTouchPoints(targetTouchPoints, QQuickItemPrivate::get(targetItem)->windowToItemTransform());
175 
176  QScopedPointer<QTouchEvent> eventForTargetItem(
177  createQTouchEvent(eventType, device, modifiers, targetTouchPoints, window, timestamp));
178 
179 
180  ugDebug("dispatching " << qPrintable(touchEventToString(eventForTargetItem.data()))
181  << " to " << targetItem);
182  QCoreApplication::sendEvent(targetItem, eventForTargetItem.data());
183 }
184 
185 void TouchDispatcher::dispatchAsMouse(
186  QTouchDevice * /*device*/,
187  Qt::KeyboardModifiers modifiers,
188  const QList<QTouchEvent::TouchPoint> &touchPoints,
189  ulong timestamp)
190 {
191  // TODO: Detect double clicks in order to synthesize QEvent::MouseButtonDblClick events accordingly
192 
193  Q_ASSERT(!touchPoints.isEmpty());
194 
195  const QTouchEvent::TouchPoint *touchMouse = nullptr;
196 
197  if (m_touchMouseId != -1) {
198  for (int i = 0; i < touchPoints.count() && !touchMouse; ++i) {
199  const auto &touchPoint = touchPoints.at(i);
200  if (touchPoint.id() == m_touchMouseId) {
201  touchMouse = &touchPoint;
202  }
203  }
204 
205  Q_ASSERT(touchMouse);
206  if (!touchMouse) {
207  // should not happen, but deal with it just in case.
208  qWarning("[TouchDispatcher] Didn't find touch with id %d, used for mouse pointer emulation.",
209  m_touchMouseId);
210  m_touchMouseId = touchPoints.at(0).id();
211  touchMouse = &touchPoints.at(0);
212  }
213  } else {
214  // Try to find a new touch for mouse emulation
215  for (int i = 0; i < touchPoints.count() && !touchMouse; ++i) {
216  const auto &touchPoint = touchPoints.at(i);
217  if (touchPoint.state() == Qt::TouchPointPressed) {
218  touchMouse = &touchPoint;
219  m_touchMouseId = touchMouse->id();
220  }
221  }
222  }
223 
224  if (touchMouse) {
225  QEvent::Type eventType;
226  if (touchMouse->state() == Qt::TouchPointPressed) {
227  eventType = QEvent::MouseButtonPress;
228  } if (touchMouse->state() == Qt::TouchPointReleased) {
229  eventType = QEvent::MouseButtonRelease;
230  m_touchMouseId = -1;
231  } else {
232  eventType = QEvent::MouseMove;
233  }
234 
235  QScopedPointer<QMouseEvent> mouseEvent(touchToMouseEvent(eventType, *touchMouse, timestamp, modifiers,
236  true /* transformNeeded */));
237 
238  ugDebug("dispatching " << qPrintable(mouseEventToString(mouseEvent.data()))
239  << " to " << m_targetItem.data());
240  QCoreApplication::sendEvent(m_targetItem.data(), mouseEvent.data());
241  }
242 }
243 
244 QTouchEvent *TouchDispatcher::createQTouchEvent(QEvent::Type eventType,
245  QTouchDevice *device,
246  Qt::KeyboardModifiers modifiers,
247  const QList<QTouchEvent::TouchPoint> &touchPoints,
248  QWindow *window,
249  ulong timestamp)
250 {
251  Qt::TouchPointStates eventStates = 0;
252  for (int i = 0; i < touchPoints.count(); i++)
253  eventStates |= touchPoints[i].state();
254  // if all points have the same state, set the event type accordingly
255  switch (eventStates) {
256  case Qt::TouchPointPressed:
257  eventType = QEvent::TouchBegin;
258  break;
259  case Qt::TouchPointReleased:
260  eventType = QEvent::TouchEnd;
261  break;
262  default:
263  eventType = QEvent::TouchUpdate;
264  break;
265  }
266 
267  QTouchEvent *touchEvent = new QTouchEvent(eventType);
268  touchEvent->setWindow(window);
269  touchEvent->setTarget(m_targetItem.data());
270  touchEvent->setDevice(device);
271  touchEvent->setModifiers(modifiers);
272  touchEvent->setTouchPoints(touchPoints);
273  touchEvent->setTouchPointStates(eventStates);
274  touchEvent->setTimestamp(timestamp);
275  touchEvent->accept();
276  return touchEvent;
277 }
278 
279 // NB: From QQuickWindow
280 void TouchDispatcher::transformTouchPoints(QList<QTouchEvent::TouchPoint> &touchPoints, const QTransform &transform)
281 {
282  QMatrix4x4 transformMatrix(transform);
283  for (int i=0; i<touchPoints.count(); i++) {
284  QTouchEvent::TouchPoint &touchPoint = touchPoints[i];
285  touchPoint.setRect(transform.mapRect(touchPoint.sceneRect()));
286  touchPoint.setStartPos(transform.map(touchPoint.startScenePos()));
287  touchPoint.setLastPos(transform.map(touchPoint.lastScenePos()));
288  touchPoint.setVelocity(transformMatrix.mapVector(touchPoint.velocity()).toVector2D());
289  }
290 }
291 
292 // Copied with minor modifications from qtdeclarative/src/quick/items/qquickwindow.cpp
293 QMouseEvent *TouchDispatcher::touchToMouseEvent(
294  QEvent::Type type, const QTouchEvent::TouchPoint &p,
295  ulong timestamp, Qt::KeyboardModifiers modifiers,
296  bool transformNeeded)
297 {
298  QQuickItem *item = m_targetItem.data();
299 
300  // The touch point local position and velocity are not yet transformed.
301  QMouseEvent *me = new QMouseEvent(type, transformNeeded ? item->mapFromScene(p.scenePos()) : p.pos(),
302  p.scenePos(), p.screenPos(), Qt::LeftButton,
303  (type == QEvent::MouseButtonRelease ? Qt::NoButton : Qt::LeftButton),
304  modifiers);
305  me->setAccepted(true);
306  me->setTimestamp(timestamp);
307  QVector2D transformedVelocity = p.velocity();
308  if (transformNeeded) {
309  QQuickItemPrivate *itemPrivate = QQuickItemPrivate::get(item);
310  QMatrix4x4 transformMatrix(itemPrivate->windowToItemTransform());
311  transformedVelocity = transformMatrix.mapVector(p.velocity()).toVector2D();
312  }
313 
314  // Add these later if needed:
315  //QGuiApplicationPrivate::setMouseEventCapsAndVelocity(me, event->device()->capabilities(), transformedVelocity);
316  //QGuiApplicationPrivate::setMouseEventSource(me, Qt::MouseEventSynthesizedByQt);
317  return me;
318 }
319 
320 /*
321  Copied from qquickwindow.cpp which has:
322  Copyright (C) 2013 Digia Plc and/or its subsidiary(-ies)
323  Under GPL 3.0 license.
324 */
325 bool TouchDispatcher::checkIfDoubleClicked(ulong newPressEventTimestamp)
326 {
327  bool doubleClicked;
328 
329  if (m_touchMousePressTimestamp == 0) {
330  // just initialize the variable
331  m_touchMousePressTimestamp = newPressEventTimestamp;
332  doubleClicked = false;
333  } else {
334  ulong timeBetweenPresses = newPressEventTimestamp - m_touchMousePressTimestamp;
335  ulong doubleClickInterval = static_cast<ulong>(qApp->styleHints()->
336  mouseDoubleClickInterval());
337  doubleClicked = timeBetweenPresses < doubleClickInterval;
338  if (doubleClicked) {
339  m_touchMousePressTimestamp = 0;
340  } else {
341  m_touchMousePressTimestamp = newPressEventTimestamp;
342  }
343  }
344 
345  return doubleClicked;
346 }
347 
348 void TouchDispatcher::setStatus(Status status)
349 {
350  if (status != m_status) {
351  #if TOUCHDISPATCHER_DEBUG
352  switch (status) {
353  case NoActiveTouch:
354  ugDebug("status = NoActiveTouch");
355  break;
356  case DeliveringTouchEvents:
357  ugDebug("status = DeliveringTouchEvents");
358  break;
359  case DeliveringMouseEvents:
360  ugDebug("status = DeliveringMouseEvents");
361  break;
362  case TargetRejectedTouches:
363  ugDebug("status = TargetRejectedTouches");
364  break;
365  default:
366  ugDebug("status = " << status);
367  break;
368  }
369  #endif
370  m_status = status;
371  }
372 }
373 
374 void TouchDispatcher::reset()
375 {
376  setStatus(NoActiveTouch);
377  m_touchMouseId = -1;
378  m_touchMousePressTimestamp =0;
379 }
380 
381 QEvent::Type TouchDispatcher::resolveEventType(const QList<QTouchEvent::TouchPoint> &touchPoints)
382 {
383  QEvent::Type eventType;
384 
385  Qt::TouchPointStates eventStates = 0;
386  for (int i = 0; i < touchPoints.count(); i++)
387  eventStates |= touchPoints[i].state();
388 
389  switch (eventStates) {
390  case Qt::TouchPointPressed:
391  eventType = QEvent::TouchBegin;
392  break;
393  case Qt::TouchPointReleased:
394  eventType = QEvent::TouchEnd;
395  break;
396  default:
397  eventType = QEvent::TouchUpdate;
398  break;
399  }
400 
401  return eventType;
402 }