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