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