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

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