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 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
98QTuioHandler::~QTuioHandler()
99{
100}
101
102void 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
189void 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
205void 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
256void 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
297QWindowSystemInterface::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
325void 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
354void 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
370void 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
421void 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
474QWindowSystemInterface::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
498void 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
528QT_END_NAMESPACE
529
530#include "moc_qtuiohandler_p.cpp"
531
532

Provided by KDAB

Privacy Policy
Learn Advanced QML with KDAB
Find out more

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