Qt
Internal/Contributor docs for the Qt SDK. Note: These are NOT official API docs; those are found at https://doc.qt.io/
Loading...
Searching...
No Matches
qtuiohandler.cpp
Go to the documentation of this file.
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
6
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
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("QT_TUIOTOUCH_DELIVER_WITHOUT_FOCUS");
33
34QTuioHandler::QTuioHandler(const QString &specification)
35{
36 QStringList args = specification.split(':');
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("udp=")) {
44 QString portString = args.at(i).section('=', 1, 1);
45 portNumber = portString.toInt();
46 } else if (args.at(i).startsWith("tcp=")) {
47 QString portString = args.at(i).section('=', 1, 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("rotate=")) {
55 QString rotateArg = args.at(i).section('=', 1, 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(0.5, 0.5).rotate(rotationAngle).translate(-0.5, -0.5);
71
72 if (invertx)
73 m_transform *= QTransform::fromTranslate(0.5, 0.5).scale(-1.0, 1.0).translate(-0.5, -0.5);
74
75 if (inverty)
76 m_transform *= QTransform::fromTranslate(0.5, 0.5).scale(1.0, -1.0).translate(-0.5, -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(m_device);
89
90 if (!m_socket.bind(QHostAddress::Any, portNumber)) {
91 qCWarning(lcTuioHandler) << "Failed to bind TUIO socket: " << m_socket.errorString();
92 return;
93 }
94
95 connect(&m_socket, &QUdpSocket::readyRead, this, &QTuioHandler::processPackets);
96}
97
101
102void QTuioHandler::processPackets()
103{
104 while (m_socket.hasPendingDatagrams()) {
105 QByteArray datagram;
106 datagram.resize(m_socket.pendingDatagramSize());
107 QHostAddress sender;
108 quint16 senderPort;
109
110 qint64 size = m_socket.readDatagram(datagram.data(), datagram.size(),
111 &sender, &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(msg);
138 }
139
140 for (const QOscMessage &message : std::as_const(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(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(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(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(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(cursorId)) {
226 // newly active
227 QTuioCursor cursor(cursorId);
228 cursor.setState(QEventPoint::State::Pressed);
229 newActiveCursors.insert(cursorId, cursor);
230 } else {
231 // we already know about it, remove it so it isn't marked as released
232 QTuioCursor cursor = oldActiveCursors.value(cursorId);
233 cursor.setState(QEventPoint::State::Stationary); // position change in SET will update if needed
234 newActiveCursors.insert(cursorId, cursor);
235 oldActiveCursors.remove(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(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(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(1).userType()) != QMetaType::Int ||
265 QMetaType::Type(arguments.at(2).userType()) != QMetaType::Float ||
266 QMetaType::Type(arguments.at(3).userType()) != QMetaType::Float ||
267 QMetaType::Type(arguments.at(4).userType()) != QMetaType::Float ||
268 QMetaType::Type(arguments.at(5).userType()) != QMetaType::Float ||
269 QMetaType::Type(arguments.at(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(1).toInt();
276 float x = arguments.at(2).toFloat();
277 float y = arguments.at(3).toFloat();
278 float vx = arguments.at(4).toFloat();
279 float vy = arguments.at(5).toFloat();
280 float acceleration = arguments.at(6).toFloat();
281
282 QMap<int, QTuioCursor>::Iterator it = m_activeCursors.find(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(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(win->mapToGlobal(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(0);
332
333 if (!win)
334 return;
335
336 QList<QWindowSystemInterface::TouchPoint> tpl;
337 tpl.reserve(m_activeCursors.size() + m_deadCursors.size());
338
339 for (const QTuioCursor &tc : std::as_const(m_activeCursors)) {
340 QWindowSystemInterface::TouchPoint tp = cursorToTouchPoint(tc, win);
341 tpl.append(tp);
342 }
343
344 for (const QTuioCursor &tc : std::as_const(m_deadCursors)) {
345 QWindowSystemInterface::TouchPoint tp = cursorToTouchPoint(tc, win);
346 tp.state = QEventPoint::State::Released;
347 tpl.append(tp);
348 }
349 QWindowSystemInterface::handleTouchEvent(win, m_device, 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(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(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(sessionId)) {
391 // newly active
392 QTuioToken token(sessionId);
393 token.setState(QEventPoint::State::Pressed);
394 newActiveTokens.insert(sessionId, token);
395 } else {
396 // we already know about it, remove it so it isn't marked as released
397 QTuioToken token = oldActiveTokens.value(sessionId);
398 token.setState(QEventPoint::State::Stationary); // position change in SET will update if needed
399 newActiveTokens.insert(sessionId, token);
400 oldActiveTokens.remove(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(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(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(1).userType()) != QMetaType::Int ||
430 QMetaType::Type(arguments.at(2).userType()) != QMetaType::Int ||
431 QMetaType::Type(arguments.at(3).userType()) != QMetaType::Float ||
432 QMetaType::Type(arguments.at(4).userType()) != QMetaType::Float ||
433 QMetaType::Type(arguments.at(5).userType()) != QMetaType::Float ||
434 QMetaType::Type(arguments.at(6).userType()) != QMetaType::Float ||
435 QMetaType::Type(arguments.at(7).userType()) != QMetaType::Float ||
436 QMetaType::Type(arguments.at(8).userType()) != QMetaType::Float ||
437 QMetaType::Type(arguments.at(9).userType()) != QMetaType::Float ||
438 QMetaType::Type(arguments.at(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(1).toInt();
444 int classId = arguments.at(2).toInt();
445 float x = arguments.at(3).toFloat();
446 float y = arguments.at(4).toFloat();
447 float angle = arguments.at(5).toFloat();
448 float vx = arguments.at(6).toFloat();
449 float vy = arguments.at(7).toFloat();
450 float angularVelocity = arguments.at(8).toFloat();
451 float acceleration = arguments.at(9).toFloat();
452 float angularAcceleration = arguments.at(10).toFloat();
453
454 QMap<int, QTuioToken>::Iterator it = m_activeTokens.find(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(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(win->mapToGlobal(relPos.toPoint()) + delta);
492 tp.velocity = QVector2D(win->size().width() * tc.vx(), win->size().height() * tc.vy());
493 tp.rotation = qRadiansToDegrees(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(0);
505
506 if (!win)
507 return;
508
509 QList<QWindowSystemInterface::TouchPoint> tpl;
510 tpl.reserve(m_activeTokens.size() + m_deadTokens.size());
511
512 for (const QTuioToken & t : std::as_const(m_activeTokens)) {
513 QWindowSystemInterface::TouchPoint tp = tokenToTouchPoint(t, win);
514 tpl.append(tp);
515 }
516
517 for (const QTuioToken & t : std::as_const(m_deadTokens)) {
518 QWindowSystemInterface::TouchPoint tp = tokenToTouchPoint(t, win);
519 tp.state = QEventPoint::State::Released;
520 tp.velocity = QVector2D();
521 tpl.append(tp);
522 }
523 QWindowSystemInterface::handleTouchEvent(win, m_device, tpl);
524
525 m_deadTokens.clear();
526}
527
528QT_END_NAMESPACE
529
530#include "moc_qtuiohandler_p.cpp"
bool isValid() const
void setX(float x)
void setVY(float vy)
void setVX(float vx)
float vx() const
void setAcceleration(float acceleration)
float vy() const
int id() const
float x() const
void setY(float y)
float y() const
QTuioCursor(int id=-1)
virtual ~QTuioHandler()
QTuioToken(int id=-1)
void setAngularAcceleration(float angularAcceleration)
void setX(float x)
void setAcceleration(float acceleration)
float y() const
float vy() const
int id() const
int classId() const
void setVY(float vy)
void setAngle(float angle)
void setClassId(int classId)
void setY(float y)
float angle() const
void setAngularVelocity(float angularVelocity)
float x() const
float vx() const
void setVX(float vx)
constexpr float qRadiansToDegrees(float radians)
Definition qmath.h:272