1 | // Copyright (C) 2014 Robin Burchell <robin.burchell@viroteck.net> |
2 | // Copyright (C) 2016 The Qt Company Ltd. |
3 | // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only |
4 | |
5 | #include "qtuiohandler_p.h" |
6 | |
7 | #include "qtuiocursor_p.h" |
8 | #include "qtuiotoken_p.h" |
9 | #include "qoscbundle_p.h" |
10 | #include "qoscmessage_p.h" |
11 | |
12 | #include <qpa/qwindowsysteminterface.h> |
13 | |
14 | #include <QPointingDevice> |
15 | #include <QWindow> |
16 | #include <QGuiApplication> |
17 | |
18 | #include <QLoggingCategory> |
19 | #include <QRect> |
20 | #include <qmath.h> |
21 | |
22 | QT_BEGIN_NAMESPACE |
23 | |
24 | Q_LOGGING_CATEGORY(lcTuioHandler, "qt.qpa.tuio.handler" ) |
25 | Q_LOGGING_CATEGORY(lcTuioSource, "qt.qpa.tuio.source" ) |
26 | Q_LOGGING_CATEGORY(lcTuioSet, "qt.qpa.tuio.set" ) |
27 | |
28 | // With TUIO the first application takes exclusive ownership of the "device" |
29 | // we cannot attach more than one application to the same port anyway. |
30 | // Forcing delivery makes it easy to use simulators in the same machine |
31 | // and forget about headaches about unfocused TUIO windows. |
32 | static bool forceDelivery = qEnvironmentVariableIsSet(varName: "QT_TUIOTOUCH_DELIVER_WITHOUT_FOCUS" ); |
33 | |
34 | QTuioHandler::QTuioHandler(const QString &specification) |
35 | { |
36 | QStringList args = specification.split(sep: ':'); |
37 | int portNumber = 3333; |
38 | int rotationAngle = 0; |
39 | bool invertx = false; |
40 | bool inverty = false; |
41 | |
42 | for (int i = 0; i < args.size(); ++i) { |
43 | if (args.at(i).startsWith(s: "udp=" )) { |
44 | QString portString = args.at(i).section(asep: '=', astart: 1, aend: 1); |
45 | portNumber = portString.toInt(); |
46 | } else if (args.at(i).startsWith(s: "tcp=" )) { |
47 | QString portString = args.at(i).section(asep: '=', astart: 1, aend: 1); |
48 | portNumber = portString.toInt(); |
49 | qCWarning(lcTuioHandler) << "TCP is not yet supported. Falling back to UDP on " << portNumber; |
50 | } else if (args.at(i) == "invertx" ) { |
51 | invertx = true; |
52 | } else if (args.at(i) == "inverty" ) { |
53 | inverty = true; |
54 | } else if (args.at(i).startsWith(s: "rotate=" )) { |
55 | QString rotateArg = args.at(i).section(asep: '=', astart: 1, aend: 1); |
56 | int argValue = rotateArg.toInt(); |
57 | switch (argValue) { |
58 | case 90: |
59 | case 180: |
60 | case 270: |
61 | rotationAngle = argValue; |
62 | break; |
63 | default: |
64 | break; |
65 | } |
66 | } |
67 | } |
68 | |
69 | if (rotationAngle) |
70 | m_transform = QTransform::fromTranslate(dx: 0.5, dy: 0.5).rotate(a: rotationAngle).translate(dx: -0.5, dy: -0.5); |
71 | |
72 | if (invertx) |
73 | m_transform *= QTransform::fromTranslate(dx: 0.5, dy: 0.5).scale(sx: -1.0, sy: 1.0).translate(dx: -0.5, dy: -0.5); |
74 | |
75 | if (inverty) |
76 | m_transform *= QTransform::fromTranslate(dx: 0.5, dy: 0.5).scale(sx: 1.0, sy: -1.0).translate(dx: -0.5, dy: -0.5); |
77 | |
78 | // not leaked, QPointingDevice cleans up registered devices itself |
79 | // TODO register each device based on SOURCE, not just an all-purpose generic touchscreen |
80 | // TODO define seats when multiple connections occur |
81 | m_device = new QPointingDevice(QLatin1String("TUIO" ), 1, QInputDevice::DeviceType::TouchScreen, |
82 | QPointingDevice::PointerType::Finger, |
83 | QInputDevice::Capability::Position | |
84 | QInputDevice::Capability::Area | |
85 | QInputDevice::Capability::Velocity | |
86 | QInputDevice::Capability::NormalizedPosition, |
87 | 16, 0); |
88 | QWindowSystemInterface::registerInputDevice(device: m_device); |
89 | |
90 | if (!m_socket.bind(addr: QHostAddress::Any, port: portNumber)) { |
91 | qCWarning(lcTuioHandler) << "Failed to bind TUIO socket: " << m_socket.errorString(); |
92 | return; |
93 | } |
94 | |
95 | connect(sender: &m_socket, signal: &QUdpSocket::readyRead, context: this, slot: &QTuioHandler::processPackets); |
96 | } |
97 | |
98 | QTuioHandler::~QTuioHandler() |
99 | { |
100 | } |
101 | |
102 | void QTuioHandler::processPackets() |
103 | { |
104 | while (m_socket.hasPendingDatagrams()) { |
105 | QByteArray datagram; |
106 | datagram.resize(size: m_socket.pendingDatagramSize()); |
107 | QHostAddress sender; |
108 | quint16 senderPort; |
109 | |
110 | qint64 size = m_socket.readDatagram(data: datagram.data(), maxlen: datagram.size(), |
111 | host: &sender, port: &senderPort); |
112 | |
113 | if (size == -1) |
114 | continue; |
115 | |
116 | if (size != datagram.size()) |
117 | datagram.resize(size); |
118 | |
119 | // "A typical TUIO bundle will contain an initial ALIVE message, |
120 | // followed by an arbitrary number of SET messages that can fit into the |
121 | // actual bundle capacity and a concluding FSEQ message. A minimal TUIO |
122 | // bundle needs to contain at least the compulsory ALIVE and FSEQ |
123 | // messages. The FSEQ frame ID is incremented for each delivered bundle, |
124 | // while redundant bundles can be marked using the frame sequence ID |
125 | // -1." |
126 | QList<QOscMessage> messages; |
127 | |
128 | QOscBundle bundle(datagram); |
129 | if (bundle.isValid()) { |
130 | messages = bundle.messages(); |
131 | } else { |
132 | QOscMessage msg(datagram); |
133 | if (!msg.isValid()) { |
134 | qCWarning(lcTuioSet) << "Got invalid datagram." ; |
135 | continue; |
136 | } |
137 | messages.push_back(t: msg); |
138 | } |
139 | |
140 | for (const QOscMessage &message : std::as_const(t&: messages)) { |
141 | if (message.addressPattern() == "/tuio/2Dcur" ) { |
142 | QList<QVariant> arguments = message.arguments(); |
143 | if (arguments.size() == 0) { |
144 | qCWarning(lcTuioHandler, "Ignoring TUIO message with no arguments" ); |
145 | continue; |
146 | } |
147 | |
148 | QByteArray messageType = arguments.at(i: 0).toByteArray(); |
149 | if (messageType == "source" ) { |
150 | process2DCurSource(message); |
151 | } else if (messageType == "alive" ) { |
152 | process2DCurAlive(message); |
153 | } else if (messageType == "set" ) { |
154 | process2DCurSet(message); |
155 | } else if (messageType == "fseq" ) { |
156 | process2DCurFseq(message); |
157 | } else { |
158 | qCWarning(lcTuioHandler) << "Ignoring unknown TUIO message type: " << messageType; |
159 | continue; |
160 | } |
161 | } else if (message.addressPattern() == "/tuio/2Dobj" ) { |
162 | QList<QVariant> arguments = message.arguments(); |
163 | if (arguments.size() == 0) { |
164 | qCWarning(lcTuioHandler, "Ignoring TUIO message with no arguments" ); |
165 | continue; |
166 | } |
167 | |
168 | QByteArray messageType = arguments.at(i: 0).toByteArray(); |
169 | if (messageType == "source" ) { |
170 | process2DObjSource(message); |
171 | } else if (messageType == "alive" ) { |
172 | process2DObjAlive(message); |
173 | } else if (messageType == "set" ) { |
174 | process2DObjSet(message); |
175 | } else if (messageType == "fseq" ) { |
176 | process2DObjFseq(message); |
177 | } else { |
178 | qCWarning(lcTuioHandler) << "Ignoring unknown TUIO message type: " << messageType; |
179 | continue; |
180 | } |
181 | } else { |
182 | qCWarning(lcTuioHandler) << "Ignoring unknown address pattern " << message.addressPattern(); |
183 | continue; |
184 | } |
185 | } |
186 | } |
187 | } |
188 | |
189 | void QTuioHandler::process2DCurSource(const QOscMessage &message) |
190 | { |
191 | QList<QVariant> arguments = message.arguments(); |
192 | if (arguments.size() != 2) { |
193 | qCWarning(lcTuioSource) << "Ignoring malformed TUIO source message: " << arguments.size(); |
194 | return; |
195 | } |
196 | |
197 | if (QMetaType::Type(arguments.at(i: 1).userType()) != QMetaType::QByteArray) { |
198 | qCWarning(lcTuioSource, "Ignoring malformed TUIO source message (bad argument type)" ); |
199 | return; |
200 | } |
201 | |
202 | qCDebug(lcTuioSource) << "Got TUIO source message from: " << arguments.at(i: 1).toByteArray(); |
203 | } |
204 | |
205 | void QTuioHandler::process2DCurAlive(const QOscMessage &message) |
206 | { |
207 | QList<QVariant> arguments = message.arguments(); |
208 | |
209 | // delta the notified cursors that are active, against the ones we already |
210 | // know of. |
211 | // |
212 | // TBD: right now we're assuming one 2Dcur alive message corresponds to a |
213 | // new data source from the input. is this correct, or do we need to store |
214 | // changes and only process the deltas on fseq? |
215 | QMap<int, QTuioCursor> oldActiveCursors = m_activeCursors; |
216 | QMap<int, QTuioCursor> newActiveCursors; |
217 | |
218 | for (int i = 1; i < arguments.size(); ++i) { |
219 | if (QMetaType::Type(arguments.at(i).userType()) != QMetaType::Int) { |
220 | qCWarning(lcTuioHandler) << "Ignoring malformed TUIO alive message (bad argument on position" << i << arguments << ')'; |
221 | return; |
222 | } |
223 | |
224 | int cursorId = arguments.at(i).toInt(); |
225 | if (!oldActiveCursors.contains(key: cursorId)) { |
226 | // newly active |
227 | QTuioCursor cursor(cursorId); |
228 | cursor.setState(QEventPoint::State::Pressed); |
229 | newActiveCursors.insert(key: cursorId, value: cursor); |
230 | } else { |
231 | // we already know about it, remove it so it isn't marked as released |
232 | QTuioCursor cursor = oldActiveCursors.value(key: cursorId); |
233 | cursor.setState(QEventPoint::State::Stationary); // position change in SET will update if needed |
234 | newActiveCursors.insert(key: cursorId, value: cursor); |
235 | oldActiveCursors.remove(key: cursorId); |
236 | } |
237 | } |
238 | |
239 | // anything left is dead now |
240 | QMap<int, QTuioCursor>::ConstIterator it = oldActiveCursors.constBegin(); |
241 | |
242 | // deadCursors should be cleared from the last FSEQ now |
243 | m_deadCursors.reserve(asize: oldActiveCursors.size()); |
244 | |
245 | // TODO: there could be an issue of resource exhaustion here if FSEQ isn't |
246 | // sent in a timely fashion. we should probably track message counts and |
247 | // force-flush if we get too many built up. |
248 | while (it != oldActiveCursors.constEnd()) { |
249 | m_deadCursors.append(t: it.value()); |
250 | ++it; |
251 | } |
252 | |
253 | m_activeCursors = newActiveCursors; |
254 | } |
255 | |
256 | void QTuioHandler::process2DCurSet(const QOscMessage &message) |
257 | { |
258 | QList<QVariant> arguments = message.arguments(); |
259 | if (arguments.size() < 7) { |
260 | qCWarning(lcTuioSet) << "Ignoring malformed TUIO set message with too few arguments: " << arguments.size(); |
261 | return; |
262 | } |
263 | |
264 | if (QMetaType::Type(arguments.at(i: 1).userType()) != QMetaType::Int || |
265 | QMetaType::Type(arguments.at(i: 2).userType()) != QMetaType::Float || |
266 | QMetaType::Type(arguments.at(i: 3).userType()) != QMetaType::Float || |
267 | QMetaType::Type(arguments.at(i: 4).userType()) != QMetaType::Float || |
268 | QMetaType::Type(arguments.at(i: 5).userType()) != QMetaType::Float || |
269 | QMetaType::Type(arguments.at(i: 6).userType()) != QMetaType::Float |
270 | ) { |
271 | qCWarning(lcTuioSet) << "Ignoring malformed TUIO set message with bad types: " << arguments; |
272 | return; |
273 | } |
274 | |
275 | int cursorId = arguments.at(i: 1).toInt(); |
276 | float x = arguments.at(i: 2).toFloat(); |
277 | float y = arguments.at(i: 3).toFloat(); |
278 | float vx = arguments.at(i: 4).toFloat(); |
279 | float vy = arguments.at(i: 5).toFloat(); |
280 | float acceleration = arguments.at(i: 6).toFloat(); |
281 | |
282 | QMap<int, QTuioCursor>::Iterator it = m_activeCursors.find(key: cursorId); |
283 | if (it == m_activeCursors.end()) { |
284 | qCWarning(lcTuioSet) << "Ignoring malformed TUIO set for nonexistent cursor " << cursorId; |
285 | return; |
286 | } |
287 | |
288 | qCDebug(lcTuioSet) << "Processing SET for " << cursorId << " x: " << x << y << vx << vy << acceleration; |
289 | QTuioCursor &cur = *it; |
290 | cur.setX(x); |
291 | cur.setY(y); |
292 | cur.setVX(vx); |
293 | cur.setVY(vy); |
294 | cur.setAcceleration(acceleration); |
295 | } |
296 | |
297 | QWindowSystemInterface::TouchPoint QTuioHandler::cursorToTouchPoint(const QTuioCursor &tc, QWindow *win) |
298 | { |
299 | QWindowSystemInterface::TouchPoint tp; |
300 | tp.id = tc.id(); |
301 | tp.pressure = 1.0f; |
302 | |
303 | tp.normalPosition = QPointF(tc.x(), tc.y()); |
304 | |
305 | if (!m_transform.isIdentity()) |
306 | tp.normalPosition = m_transform.map(p: tp.normalPosition); |
307 | |
308 | tp.state = tc.state(); |
309 | |
310 | // we map the touch to the size of the window. we do this, because frankly, |
311 | // trying to figure out which part of the screen to hit in order to press an |
312 | // element on the UI is pretty tricky when one is not using an overlay-style |
313 | // TUIO device. |
314 | // |
315 | // in the future, it might make sense to make this choice optional, |
316 | // dependent on the spec. |
317 | QPointF relPos = QPointF(win->size().width() * tp.normalPosition.x(), win->size().height() * tp.normalPosition.y()); |
318 | QPointF delta = relPos - relPos.toPoint(); |
319 | tp.area.moveCenter(p: win->mapToGlobal(pos: relPos.toPoint()) + delta); |
320 | tp.velocity = QVector2D(win->size().width() * tc.vx(), win->size().height() * tc.vy()); |
321 | return tp; |
322 | } |
323 | |
324 | |
325 | void QTuioHandler::process2DCurFseq(const QOscMessage &message) |
326 | { |
327 | Q_UNUSED(message); // TODO: do we need to do anything with the frame id? |
328 | |
329 | QWindow *win = QGuiApplication::focusWindow(); |
330 | if (!win && QGuiApplication::topLevelWindows().size() > 0 && forceDelivery) |
331 | win = QGuiApplication::topLevelWindows().at(i: 0); |
332 | |
333 | if (!win) |
334 | return; |
335 | |
336 | QList<QWindowSystemInterface::TouchPoint> tpl; |
337 | tpl.reserve(asize: m_activeCursors.size() + m_deadCursors.size()); |
338 | |
339 | for (const QTuioCursor &tc : std::as_const(t&: m_activeCursors)) { |
340 | QWindowSystemInterface::TouchPoint tp = cursorToTouchPoint(tc, win); |
341 | tpl.append(t: tp); |
342 | } |
343 | |
344 | for (const QTuioCursor &tc : std::as_const(t&: m_deadCursors)) { |
345 | QWindowSystemInterface::TouchPoint tp = cursorToTouchPoint(tc, win); |
346 | tp.state = QEventPoint::State::Released; |
347 | tpl.append(t: tp); |
348 | } |
349 | QWindowSystemInterface::handleTouchEvent(window: win, device: m_device, points: tpl); |
350 | |
351 | m_deadCursors.clear(); |
352 | } |
353 | |
354 | void QTuioHandler::process2DObjSource(const QOscMessage &message) |
355 | { |
356 | QList<QVariant> arguments = message.arguments(); |
357 | if (arguments.size() != 2) { |
358 | qCWarning(lcTuioSource ) << "Ignoring malformed TUIO source message: " << arguments.size(); |
359 | return; |
360 | } |
361 | |
362 | if (QMetaType::Type(arguments.at(i: 1).userType()) != QMetaType::QByteArray) { |
363 | qCWarning(lcTuioSource, "Ignoring malformed TUIO source message (bad argument type)" ); |
364 | return; |
365 | } |
366 | |
367 | qCDebug(lcTuioSource) << "Got TUIO source message from: " << arguments.at(i: 1).toByteArray(); |
368 | } |
369 | |
370 | void QTuioHandler::process2DObjAlive(const QOscMessage &message) |
371 | { |
372 | QList<QVariant> arguments = message.arguments(); |
373 | |
374 | // delta the notified tokens that are active, against the ones we already |
375 | // know of. |
376 | // |
377 | // TBD: right now we're assuming one 2DObj alive message corresponds to a |
378 | // new data source from the input. is this correct, or do we need to store |
379 | // changes and only process the deltas on fseq? |
380 | QMap<int, QTuioToken> oldActiveTokens = m_activeTokens; |
381 | QMap<int, QTuioToken> newActiveTokens; |
382 | |
383 | for (int i = 1; i < arguments.size(); ++i) { |
384 | if (QMetaType::Type(arguments.at(i).userType()) != QMetaType::Int) { |
385 | qCWarning(lcTuioHandler) << "Ignoring malformed TUIO alive message (bad argument on position" << i << arguments << ')'; |
386 | return; |
387 | } |
388 | |
389 | int sessionId = arguments.at(i).toInt(); |
390 | if (!oldActiveTokens.contains(key: sessionId)) { |
391 | // newly active |
392 | QTuioToken token(sessionId); |
393 | token.setState(QEventPoint::State::Pressed); |
394 | newActiveTokens.insert(key: sessionId, value: token); |
395 | } else { |
396 | // we already know about it, remove it so it isn't marked as released |
397 | QTuioToken token = oldActiveTokens.value(key: sessionId); |
398 | token.setState(QEventPoint::State::Stationary); // position change in SET will update if needed |
399 | newActiveTokens.insert(key: sessionId, value: token); |
400 | oldActiveTokens.remove(key: sessionId); |
401 | } |
402 | } |
403 | |
404 | // anything left is dead now |
405 | QMap<int, QTuioToken>::ConstIterator it = oldActiveTokens.constBegin(); |
406 | |
407 | // deadTokens should be cleared from the last FSEQ now |
408 | m_deadTokens.reserve(asize: oldActiveTokens.size()); |
409 | |
410 | // TODO: there could be an issue of resource exhaustion here if FSEQ isn't |
411 | // sent in a timely fashion. we should probably track message counts and |
412 | // force-flush if we get too many built up. |
413 | while (it != oldActiveTokens.constEnd()) { |
414 | m_deadTokens.append(t: it.value()); |
415 | ++it; |
416 | } |
417 | |
418 | m_activeTokens = newActiveTokens; |
419 | } |
420 | |
421 | void QTuioHandler::process2DObjSet(const QOscMessage &message) |
422 | { |
423 | QList<QVariant> arguments = message.arguments(); |
424 | if (arguments.size() < 7) { |
425 | qCWarning(lcTuioSet) << "Ignoring malformed TUIO set message with too few arguments: " << arguments.size(); |
426 | return; |
427 | } |
428 | |
429 | if (QMetaType::Type(arguments.at(i: 1).userType()) != QMetaType::Int || |
430 | QMetaType::Type(arguments.at(i: 2).userType()) != QMetaType::Int || |
431 | QMetaType::Type(arguments.at(i: 3).userType()) != QMetaType::Float || |
432 | QMetaType::Type(arguments.at(i: 4).userType()) != QMetaType::Float || |
433 | QMetaType::Type(arguments.at(i: 5).userType()) != QMetaType::Float || |
434 | QMetaType::Type(arguments.at(i: 6).userType()) != QMetaType::Float || |
435 | QMetaType::Type(arguments.at(i: 7).userType()) != QMetaType::Float || |
436 | QMetaType::Type(arguments.at(i: 8).userType()) != QMetaType::Float || |
437 | QMetaType::Type(arguments.at(i: 9).userType()) != QMetaType::Float || |
438 | QMetaType::Type(arguments.at(i: 10).userType()) != QMetaType::Float) { |
439 | qCWarning(lcTuioSet) << "Ignoring malformed TUIO set message with bad types: " << arguments; |
440 | return; |
441 | } |
442 | |
443 | int id = arguments.at(i: 1).toInt(); |
444 | int classId = arguments.at(i: 2).toInt(); |
445 | float x = arguments.at(i: 3).toFloat(); |
446 | float y = arguments.at(i: 4).toFloat(); |
447 | float angle = arguments.at(i: 5).toFloat(); |
448 | float vx = arguments.at(i: 6).toFloat(); |
449 | float vy = arguments.at(i: 7).toFloat(); |
450 | float angularVelocity = arguments.at(i: 8).toFloat(); |
451 | float acceleration = arguments.at(i: 9).toFloat(); |
452 | float angularAcceleration = arguments.at(i: 10).toFloat(); |
453 | |
454 | QMap<int, QTuioToken>::Iterator it = m_activeTokens.find(key: id); |
455 | if (it == m_activeTokens.end()) { |
456 | qCWarning(lcTuioSet) << "Ignoring malformed TUIO set for nonexistent token " << classId; |
457 | return; |
458 | } |
459 | |
460 | qCDebug(lcTuioSet) << "Processing SET for token " << classId << id << " @ " << x << y << " angle: " << angle << |
461 | "vel" << vx << vy << angularVelocity << "acc" << acceleration << angularAcceleration; |
462 | QTuioToken &tok = *it; |
463 | tok.setClassId(classId); |
464 | tok.setX(x); |
465 | tok.setY(y); |
466 | tok.setVX(vx); |
467 | tok.setVY(vy); |
468 | tok.setAcceleration(acceleration); |
469 | tok.setAngle(angle); |
470 | tok.setAngularVelocity(angularAcceleration); |
471 | tok.setAngularAcceleration(angularAcceleration); |
472 | } |
473 | |
474 | QWindowSystemInterface::TouchPoint QTuioHandler::tokenToTouchPoint(const QTuioToken &tc, QWindow *win) |
475 | { |
476 | QWindowSystemInterface::TouchPoint tp; |
477 | tp.id = tc.id(); |
478 | tp.uniqueId = tc.classId(); // TODO TUIO 2.0: populate a QVariant, and register the mapping from int to arbitrary UID data |
479 | tp.pressure = 1.0f; |
480 | |
481 | tp.normalPosition = QPointF(tc.x(), tc.y()); |
482 | |
483 | if (!m_transform.isIdentity()) |
484 | tp.normalPosition = m_transform.map(p: tp.normalPosition); |
485 | |
486 | tp.state = tc.state(); |
487 | |
488 | // We map the token position to the size of the window. |
489 | QPointF relPos = QPointF(win->size().width() * tp.normalPosition.x(), win->size().height() * tp.normalPosition.y()); |
490 | QPointF delta = relPos - relPos.toPoint(); |
491 | tp.area.moveCenter(p: win->mapToGlobal(pos: relPos.toPoint()) + delta); |
492 | tp.velocity = QVector2D(win->size().width() * tc.vx(), win->size().height() * tc.vy()); |
493 | tp.rotation = qRadiansToDegrees(radians: tc.angle()); |
494 | return tp; |
495 | } |
496 | |
497 | |
498 | void QTuioHandler::process2DObjFseq(const QOscMessage &message) |
499 | { |
500 | Q_UNUSED(message); // TODO: do we need to do anything with the frame id? |
501 | |
502 | QWindow *win = QGuiApplication::focusWindow(); |
503 | if (!win && QGuiApplication::topLevelWindows().size() > 0 && forceDelivery) |
504 | win = QGuiApplication::topLevelWindows().at(i: 0); |
505 | |
506 | if (!win) |
507 | return; |
508 | |
509 | QList<QWindowSystemInterface::TouchPoint> tpl; |
510 | tpl.reserve(asize: m_activeTokens.size() + m_deadTokens.size()); |
511 | |
512 | for (const QTuioToken & t : std::as_const(t&: m_activeTokens)) { |
513 | QWindowSystemInterface::TouchPoint tp = tokenToTouchPoint(tc: t, win); |
514 | tpl.append(t: tp); |
515 | } |
516 | |
517 | for (const QTuioToken & t : std::as_const(t&: m_deadTokens)) { |
518 | QWindowSystemInterface::TouchPoint tp = tokenToTouchPoint(tc: t, win); |
519 | tp.state = QEventPoint::State::Released; |
520 | tp.velocity = QVector2D(); |
521 | tpl.append(t: tp); |
522 | } |
523 | QWindowSystemInterface::handleTouchEvent(window: win, device: m_device, points: tpl); |
524 | |
525 | m_deadTokens.clear(); |
526 | } |
527 | |
528 | QT_END_NAMESPACE |
529 | |
530 | #include "moc_qtuiohandler_p.cpp" |
531 | |
532 | |