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

source code of qtbase/src/plugins/generic/tuiotouch/qtuiohandler.cpp