From eb2014382be588ea92e172d8b36385b8b86b64de Mon Sep 17 00:00:00 2001 From: Robin Burchell Date: Wed, 26 Nov 2014 22:16:16 -0800 Subject: Long live the TuioTouch input plugin! This is an import of the tuio2qt plugin (https://github.com/rburchell/tuio2qt), as of sha 9b1f163ac52ea440e83f16b3906f9b55e21b87be henceforth to be developed as a part of Qt itself. This plugin offers touch events via QPA interfaces, using data offered over the TUIO protocol (http://www.tuio.org). It is useful for accepting touch input on devices which otherwise don't have touch input (such as desktops) for the purposes of development, as well as accepting input from some hardware which offers up touch events specifically over the TUIO protocol. Known "shortcomings" at this time, as documented in the README: * Multiple TUIO sources sending data at the same time will conflict. This will not cause problems, strictly speaking, but it will not work well (repeated touchpoint release/press events for the same IDs) * TCP transport is not currently supported. I don't see a need for it at this time, but I have left the capability in terms of port acceptance open for it to be made available. Change-Id: I7178f9db13c635268db8460fbe4d4ea6be654c05 Reviewed-by: Shawn Rutledge --- src/plugins/generic/generic.pro | 2 + src/plugins/generic/tuiotouch/README.md | 54 ++++ src/plugins/generic/tuiotouch/main.cpp | 68 +++++ src/plugins/generic/tuiotouch/qoscbundle.cpp | 186 ++++++++++++++ src/plugins/generic/tuiotouch/qoscbundle_p.h | 62 +++++ src/plugins/generic/tuiotouch/qoscmessage.cpp | 138 ++++++++++ src/plugins/generic/tuiotouch/qoscmessage_p.h | 57 +++++ src/plugins/generic/tuiotouch/qtuio_p.h | 62 +++++ src/plugins/generic/tuiotouch/qtuiocursor_p.h | 102 ++++++++ src/plugins/generic/tuiotouch/qtuiohandler.cpp | 337 +++++++++++++++++++++++++ src/plugins/generic/tuiotouch/qtuiohandler_p.h | 79 ++++++ src/plugins/generic/tuiotouch/tuiotouch.json | 3 + src/plugins/generic/tuiotouch/tuiotouch.pro | 26 ++ 13 files changed, 1176 insertions(+) create mode 100644 src/plugins/generic/tuiotouch/README.md create mode 100644 src/plugins/generic/tuiotouch/main.cpp create mode 100644 src/plugins/generic/tuiotouch/qoscbundle.cpp create mode 100644 src/plugins/generic/tuiotouch/qoscbundle_p.h create mode 100644 src/plugins/generic/tuiotouch/qoscmessage.cpp create mode 100644 src/plugins/generic/tuiotouch/qoscmessage_p.h create mode 100644 src/plugins/generic/tuiotouch/qtuio_p.h create mode 100644 src/plugins/generic/tuiotouch/qtuiocursor_p.h create mode 100644 src/plugins/generic/tuiotouch/qtuiohandler.cpp create mode 100644 src/plugins/generic/tuiotouch/qtuiohandler_p.h create mode 100644 src/plugins/generic/tuiotouch/tuiotouch.json create mode 100644 src/plugins/generic/tuiotouch/tuiotouch.pro (limited to 'src') diff --git a/src/plugins/generic/generic.pro b/src/plugins/generic/generic.pro index 767b9a55c1..4cdc7e7a76 100644 --- a/src/plugins/generic/generic.pro +++ b/src/plugins/generic/generic.pro @@ -7,3 +7,5 @@ contains(QT_CONFIG, evdev) { contains(QT_CONFIG, tslib) { SUBDIRS += tslib } + +SUBDIRS += tuiotouch diff --git a/src/plugins/generic/tuiotouch/README.md b/src/plugins/generic/tuiotouch/README.md new file mode 100644 index 0000000000..2675803896 --- /dev/null +++ b/src/plugins/generic/tuiotouch/README.md @@ -0,0 +1,54 @@ +# TuioTouch plugin for Qt 5 + +## Introduction + +This is a QPA-using plugin (meaning, it uses Qt internals) that provides touch +events from TUIO-based sources (such as [TUIOPad](https://code.google.com/p/tuiopad/)). + +[TUIO](http://www.tuio.org/) is a framework for providing touch events over the +network (implemented here using a UDP transport). + +This repository also includes a simple [OSC](http://opensoundcontrol.org/spec-1_0) +parser. OSC is the binary format that TUIO uses for over-the-wire communication. + +## Use + +Run your application with -plugin TuioTouch, e.g. + +`qmlscene foo.qml -plugin TuioTouch` + +Or make sure the plugin is loaded using the QT_QPA_GENERIC_PLUGINS environment +variable. + +By default, you must direct TUIO packets to the IP of the machine the application +is running on, protocol UDP, port 3333. If you want to customize the port, you +may provide a port number like this: + +`qmlscene foo.qml -plugin TuioTouch:udp=3333` + +At present, UDP is the only supported transport mechanism. + +## Advanced use + +If you have the need to invert the X/Y axis, you can do so, by adding an +additional option when loading the plugin. + +For example: + +`qmlscene foo.qml -plugin TuioTouch:udp=4000:invertx:inverty` + +Would invert the X and Y coordinates of all input coming in on port 4000. + +You can also rotate the coordinates directly, using the rotate option: + +`qmlscene foo.qml -plugin TuioTouch:udp=4000:rotate=180` + +Supported rotations are 90, 180, and 270. + +## Further work + +* Support other profiles (we implement 2Dcur, we want 2Dobj, 2Dblb?) +* Support multiple simultaneous sources, generating distinct QTouchEvents + * We'd need to somehow not rely on FSEQ for removing touchpoints, else our + currently minor memory exhaustion problem could become a real issue +* Support TCP transports? diff --git a/src/plugins/generic/tuiotouch/main.cpp b/src/plugins/generic/tuiotouch/main.cpp new file mode 100644 index 0000000000..c750a4f9a0 --- /dev/null +++ b/src/plugins/generic/tuiotouch/main.cpp @@ -0,0 +1,68 @@ +/**************************************************************************** +** +** Copyright (C) 2014 Robin Burchell +** Copyright (C) 2014 Digia Plc and/or its subsidiary(-ies). +** Contact: http://www.qt-project.org/legal +** +** This file is part of the QtCore module of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL21$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and Digia. For licensing terms and +** conditions see http://qt.digia.com/licensing. For further information +** use the contact form at http://qt.digia.com/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 2.1 or version 3 as published by the Free +** Software Foundation and appearing in the file LICENSE.LGPLv21 and +** LICENSE.LGPLv3 included in the packaging of this file. Please review the +** following information to ensure the GNU Lesser General Public License +** requirements will be met: https://www.gnu.org/licenses/lgpl.html and +** http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. +** +** In addition, as a special exception, Digia gives you certain additional +** rights. These rights are described in the Digia Qt LGPL Exception +** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include +#include + +#include "qtuiohandler_p.h" + +QT_BEGIN_NAMESPACE + +class QTuioTouchPlugin : public QGenericPlugin +{ + Q_OBJECT + Q_PLUGIN_METADATA(IID "org.qt-project.Qt.QGenericPluginFactoryInterface" FILE "tuiotouch.json") + +public: + QTuioTouchPlugin(); + + QObject* create(const QString &key, const QString &specification); +}; + +QTuioTouchPlugin::QTuioTouchPlugin() +{ +} + +QObject* QTuioTouchPlugin::create(const QString &key, + const QString &spec) +{ + if (!key.compare(QLatin1String("TuioTouch"), Qt::CaseInsensitive)) + return new QTuioHandler(spec); + + return 0; +} + +QT_END_NAMESPACE + +#include "main.moc" diff --git a/src/plugins/generic/tuiotouch/qoscbundle.cpp b/src/plugins/generic/tuiotouch/qoscbundle.cpp new file mode 100644 index 0000000000..39e1bdcd81 --- /dev/null +++ b/src/plugins/generic/tuiotouch/qoscbundle.cpp @@ -0,0 +1,186 @@ +/**************************************************************************** +** +** Copyright (C) 2014 Robin Burchell +** Copyright (C) 2014 Digia Plc and/or its subsidiary(-ies). +** Contact: http://www.qt-project.org/legal +** +** This file is part of the QtCore module of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL21$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and Digia. For licensing terms and +** conditions see http://qt.digia.com/licensing. For further information +** use the contact form at http://qt.digia.com/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 2.1 or version 3 as published by the Free +** Software Foundation and appearing in the file LICENSE.LGPLv21 and +** LICENSE.LGPLv3 included in the packaging of this file. Please review the +** following information to ensure the GNU Lesser General Public License +** requirements will be met: https://www.gnu.org/licenses/lgpl.html and +** http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. +** +** In addition, as a special exception, Digia gives you certain additional +** rights. These rights are described in the Digia Qt LGPL Exception +** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include +#include +#include + +#include "qoscbundle_p.h" +#include "qtuio_p.h" + +QT_BEGIN_NAMESPACE + +Q_LOGGING_CATEGORY(lcTuioBundle, "qt.qpa.tuio.bundle") + +// TUIO packets are transmitted using the OSC protocol, located at: +// http://opensoundcontrol.org/specification +// Snippets of this specification have been pasted into the source as a means of +// easily communicating requirements. + +QOscBundle::QOscBundle(const QByteArray &data) + : m_isValid(false) + , m_immediate(false) + , m_timeEpoch(0) + , m_timePico(0) +{ + // 8 16 24 32 40 48 56 64 + // # b u n d l e \0 + // 23 62 75 6e 64 6c 65 00 // OSC string bundle identifier + // 00 00 00 00 00 00 00 01 // osc time-tag, "immediately" + // 00 00 00 30 // element length + // => message or bundle(s), preceded by length each time + qCDebug(lcTuioBundle) << data.toHex(); + quint32 parsedBytes = 0; + + // "An OSC Bundle consists of the OSC-string "#bundle"" + QByteArray identifier; + if (!qt_readOscString(data, identifier, parsedBytes) || identifier != "#bundle") + return; + + // "followed by an OSC Time + // Tag, followed by zero or more OSC Bundle Elements. The OSC-timetag is a + // 64-bit fixed point time tag whose semantics are described below." + if (parsedBytes > (quint32)data.size() || data.size() - parsedBytes < sizeof(quint64)) + return; + + // "Time tags are represented by a 64 bit fixed point number. The first 32 + // bits specify the number of seconds since midnight on January 1, 1900, + // and the last 32 bits specify fractional parts of a second to a precision + // of about 200 picoseconds. This is the representation used by Internet NTP + // timestamps." + // + // (editor's note: one may wonder how a 64bit big-endian number can also be + // two 32bit numbers, without specifying in which order they occur or + // anything, and one may indeed continue to wonder.) + quint32 oscTimeEpoch = qFromBigEndian((const uchar*)data.constData() + parsedBytes); + parsedBytes += sizeof(quint32); + quint32 oscTimePico = qFromBigEndian((const uchar*)data.constData() + parsedBytes); + parsedBytes += sizeof(quint32); + + bool isImmediate = false; + + if (oscTimeEpoch == 0 && oscTimePico == 1) { + // "The time tag value consisting of 63 zero bits followed by a + // one in the least signifigant bit is a special case meaning + // "immediately."" + isImmediate = true; + } + + while (parsedBytes < (quint32)data.size()) { + // "An OSC Bundle Element consists of its size and its contents. The size is an + // int32 representing the number of 8-bit bytes in the contents, and will + // always be a multiple of 4." + // + // in practice, a bundle can contain multiple bundles or messages, + // though, and each is prefixed by a size. + if (data.size() - parsedBytes < sizeof(quint32)) + return; + + quint32 size = qFromBigEndian((const uchar*)data.constData() + parsedBytes); + parsedBytes += sizeof(quint32); + + if (data.size() - parsedBytes < size) + return; + + if (size == 0) { + // empty bundle; these are valid, but should they be allowed? the + // spec is unclear on this... + qWarning() << "Empty bundle?"; + m_isValid = true; + m_immediate = isImmediate; + m_timeEpoch = oscTimeEpoch; + m_timePico = oscTimePico; + return; + } + + // "The contents are either an OSC Message or an OSC Bundle. + // Note this recursive definition: bundle may contain bundles." + QByteArray subdata = data.mid(parsedBytes, size); + parsedBytes += size; + + // "The contents of an OSC packet must be either an OSC Message or an OSC Bundle. + // The first byte of the packet's contents unambiguously distinguishes between + // these two alternatives." + // + // we're not dealing with a packet here, but the same trick works just + // the same. + QByteArray bundleIdentifier = QByteArray("#bundle\0", 8); + if (subdata.startsWith('/')) { + // starts with / => address pattern => start of a message + QOscMessage subMessage(subdata); + if (subMessage.isValid()) { + m_isValid = true; + m_immediate = isImmediate; + m_timeEpoch = oscTimeEpoch; + m_timePico = oscTimePico; + m_messages.append(subMessage); + } else { + qWarning() << "Invalid sub-message"; + return; + } + } else if (subdata.startsWith(bundleIdentifier)) { + // bundle identifier start => bundle + QOscBundle subBundle(subdata); + if (subBundle.isValid()) { + m_isValid = true; + m_immediate = isImmediate; + m_timeEpoch = oscTimeEpoch; + m_timePico = oscTimePico; + m_bundles.append(subBundle); + } + } else { + qWarning() << "Malformed sub-data!"; + return; + } + } +} + + +bool QOscBundle::isValid() const +{ + return m_isValid; +} + +QList QOscBundle::bundles() const +{ + return m_bundles; +} + +QList QOscBundle::messages() const +{ + return m_messages; +} + +QT_END_NAMESPACE + diff --git a/src/plugins/generic/tuiotouch/qoscbundle_p.h b/src/plugins/generic/tuiotouch/qoscbundle_p.h new file mode 100644 index 0000000000..b97cd1e761 --- /dev/null +++ b/src/plugins/generic/tuiotouch/qoscbundle_p.h @@ -0,0 +1,62 @@ +/**************************************************************************** +** +** Copyright (C) 2014 Robin Burchell +** Copyright (C) 2014 Digia Plc and/or its subsidiary(-ies). +** Contact: http://www.qt-project.org/legal +** +** This file is part of the QtCore module of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL21$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and Digia. For licensing terms and +** conditions see http://qt.digia.com/licensing. For further information +** use the contact form at http://qt.digia.com/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 2.1 or version 3 as published by the Free +** Software Foundation and appearing in the file LICENSE.LGPLv21 and +** LICENSE.LGPLv3 included in the packaging of this file. Please review the +** following information to ensure the GNU Lesser General Public License +** requirements will be met: https://www.gnu.org/licenses/lgpl.html and +** http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. +** +** In addition, as a special exception, Digia gives you certain additional +** rights. These rights are described in the Digia Qt LGPL Exception +** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#ifndef QOSCBUNDLE_P_H +#define QOSCBUNDLE_P_H + +#include "qoscmessage_p.h" + +QT_BEGIN_NAMESPACE + +class QOscBundle +{ +public: + QOscBundle(const QByteArray &data); + + bool isValid() const; + QList bundles() const; + QList messages() const; + +private: + bool m_isValid; + bool m_immediate; + quint32 m_timeEpoch; + quint32 m_timePico; + QList m_bundles; + QList m_messages; +}; + +QT_END_NAMESPACE + +#endif // QOSCBUNDLE_P_H diff --git a/src/plugins/generic/tuiotouch/qoscmessage.cpp b/src/plugins/generic/tuiotouch/qoscmessage.cpp new file mode 100644 index 0000000000..1ebc4ee434 --- /dev/null +++ b/src/plugins/generic/tuiotouch/qoscmessage.cpp @@ -0,0 +1,138 @@ +/**************************************************************************** +** +** Copyright (C) 2014 Robin Burchell +** Copyright (C) 2014 Digia Plc and/or its subsidiary(-ies). +** Contact: http://www.qt-project.org/legal +** +** This file is part of the QtCore module of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL21$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and Digia. For licensing terms and +** conditions see http://qt.digia.com/licensing. For further information +** use the contact form at http://qt.digia.com/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 2.1 or version 3 as published by the Free +** Software Foundation and appearing in the file LICENSE.LGPLv21 and +** LICENSE.LGPLv3 included in the packaging of this file. Please review the +** following information to ensure the GNU Lesser General Public License +** requirements will be met: https://www.gnu.org/licenses/lgpl.html and +** http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. +** +** In addition, as a special exception, Digia gives you certain additional +** rights. These rights are described in the Digia Qt LGPL Exception +** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include +#include +#include +#include +#include + +#include "qoscmessage_p.h" +#include "qtuio_p.h" + +QT_BEGIN_NAMESPACE + +Q_LOGGING_CATEGORY(lcTuioMessage, "qt.qpa.tuio.message") + +// TUIO packets are transmitted using the OSC protocol, located at: +// http://opensoundcontrol.org/specification +// Snippets of this specification have been pasted into the source as a means of +// easily communicating requirements. + +QOscMessage::QOscMessage(const QByteArray &data) + : m_isValid(false) +{ + qCDebug(lcTuioMessage) << data.toHex(); + quint32 parsedBytes = 0; + + // "An OSC message consists of an OSC Address Pattern" + QByteArray addressPattern; + if (!qt_readOscString(data, addressPattern, parsedBytes) || addressPattern.size() == 0) + return; + + // "followed by an OSC Type Tag String" + QByteArray typeTagString; + if (!qt_readOscString(data, typeTagString, parsedBytes)) + return; + + // "Note: some older implementations of OSC may omit the OSC Type Tag string. + // Until all such implementations are updated, OSC implementations should be + // robust in the case of a missing OSC Type Tag String." + // + // (although, the editor notes one may question how exactly the hell one is + // supposed to be robust when the behavior is unspecified.) + if (typeTagString.size() == 0 || typeTagString.at(0) != ',') + return; + + QList arguments; + + // "followed by zero or more OSC Arguments." + for (int i = 1; i < typeTagString.size(); ++i) { + char typeTag = typeTagString.at(i); + if (typeTag == 's') { // osc-string + QByteArray aString; + if (!qt_readOscString(data, aString, parsedBytes)) + return; + arguments.append(aString); + } else if (typeTag == 'i') { // int32 + if (parsedBytes > (quint32)data.size() || data.size() - parsedBytes < sizeof(quint32)) + return; + + quint32 anInt = qFromBigEndian((const uchar*)data.constData() + parsedBytes); + parsedBytes += sizeof(quint32); + + // TODO: is int32 in OSC signed, or unsigned? + arguments.append((int)anInt); + } else if (typeTag == 'f') { // float32 + if (parsedBytes > (quint32)data.size() || data.size() - parsedBytes < sizeof(quint32)) + return; + + Q_STATIC_ASSERT(sizeof(float) == sizeof(quint32)); + union { + quint32 u; + float f; + } value; + value.u = qFromBigEndian((const uchar*)data.constData() + parsedBytes); + parsedBytes += sizeof(quint32); + arguments.append(value.f); + } else { + qWarning() << "Reading argument of unknown type " << typeTag; + return; + } + } + + m_isValid = true; + m_addressPattern = addressPattern; + m_arguments = arguments; + + qCDebug(lcTuioMessage) << "Message with address pattern: " << addressPattern << " arguments: " << arguments; +} + +bool QOscMessage::isValid() const +{ + return m_isValid; +} + +QByteArray QOscMessage::addressPattern() const +{ + return m_addressPattern; +} + +QList QOscMessage::arguments() const +{ + return m_arguments; +} + +QT_END_NAMESPACE + diff --git a/src/plugins/generic/tuiotouch/qoscmessage_p.h b/src/plugins/generic/tuiotouch/qoscmessage_p.h new file mode 100644 index 0000000000..4282dce9d4 --- /dev/null +++ b/src/plugins/generic/tuiotouch/qoscmessage_p.h @@ -0,0 +1,57 @@ +/**************************************************************************** +** +** Copyright (C) 2014 Robin Burchell +** Copyright (C) 2014 Digia Plc and/or its subsidiary(-ies). +** Contact: http://www.qt-project.org/legal +** +** This file is part of the QtCore module of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL21$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and Digia. For licensing terms and +** conditions see http://qt.digia.com/licensing. For further information +** use the contact form at http://qt.digia.com/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 2.1 or version 3 as published by the Free +** Software Foundation and appearing in the file LICENSE.LGPLv21 and +** LICENSE.LGPLv3 included in the packaging of this file. Please review the +** following information to ensure the GNU Lesser General Public License +** requirements will be met: https://www.gnu.org/licenses/lgpl.html and +** http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. +** +** In addition, as a special exception, Digia gives you certain additional +** rights. These rights are described in the Digia Qt LGPL Exception +** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#ifndef QOSCMESSAGE_P_H +#define QOSCMESSAGE_P_H + +QT_BEGIN_NAMESPACE + +class QOscMessage +{ +public: + QOscMessage(const QByteArray &data); + bool isValid() const; + + QByteArray addressPattern() const; + QList arguments() const; + +private: + bool m_isValid; + QByteArray m_addressPattern; + QList m_arguments; +}; + +QT_END_NAMESPACE + +#endif diff --git a/src/plugins/generic/tuiotouch/qtuio_p.h b/src/plugins/generic/tuiotouch/qtuio_p.h new file mode 100644 index 0000000000..ac8e5a128c --- /dev/null +++ b/src/plugins/generic/tuiotouch/qtuio_p.h @@ -0,0 +1,62 @@ +/**************************************************************************** +** +** Copyright (C) 2014 Robin Burchell +** Copyright (C) 2014 Digia Plc and/or its subsidiary(-ies). +** Contact: http://www.qt-project.org/legal +** +** This file is part of the QtCore module of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL21$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and Digia. For licensing terms and +** conditions see http://qt.digia.com/licensing. For further information +** use the contact form at http://qt.digia.com/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 2.1 or version 3 as published by the Free +** Software Foundation and appearing in the file LICENSE.LGPLv21 and +** LICENSE.LGPLv3 included in the packaging of this file. Please review the +** following information to ensure the GNU Lesser General Public License +** requirements will be met: https://www.gnu.org/licenses/lgpl.html and +** http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. +** +** In addition, as a special exception, Digia gives you certain additional +** rights. These rights are described in the Digia Qt LGPL Exception +** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#ifndef QTUIO_P_H +#define QTUIO_P_H + +QT_BEGIN_NAMESPACE + +inline bool qt_readOscString(const QByteArray &source, QByteArray &dest, quint32 &pos) +{ + int end = source.indexOf('\0', pos); + if (end < 0) { + pos = source.size(); + dest = QByteArray(); + return false; + } + + dest = source.mid(pos, end - pos); + + // Skip additional NULL bytes at the end of the string to make sure the + // total number of bits a multiple of 32 bits ("OSC-string" in the + // specification). + end += 4 - ((end - pos) % 4); + + pos = end; + return true; +} + +QT_END_NAMESPACE + +#endif diff --git a/src/plugins/generic/tuiotouch/qtuiocursor_p.h b/src/plugins/generic/tuiotouch/qtuiocursor_p.h new file mode 100644 index 0000000000..b58ab0fbae --- /dev/null +++ b/src/plugins/generic/tuiotouch/qtuiocursor_p.h @@ -0,0 +1,102 @@ +/**************************************************************************** +** +** Copyright (C) 2014 Robin Burchell +** Copyright (C) 2014 Digia Plc and/or its subsidiary(-ies). +** Contact: http://www.qt-project.org/legal +** +** This file is part of the QtCore module of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL21$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and Digia. For licensing terms and +** conditions see http://qt.digia.com/licensing. For further information +** use the contact form at http://qt.digia.com/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 2.1 or version 3 as published by the Free +** Software Foundation and appearing in the file LICENSE.LGPLv21 and +** LICENSE.LGPLv3 included in the packaging of this file. Please review the +** following information to ensure the GNU Lesser General Public License +** requirements will be met: https://www.gnu.org/licenses/lgpl.html and +** http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. +** +** In addition, as a special exception, Digia gives you certain additional +** rights. These rights are described in the Digia Qt LGPL Exception +** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#ifndef QTUIOCURSOR_P_H +#define QTUIOCURSOR_P_H + +#include + +QT_BEGIN_NAMESPACE + +class QTuioCursor +{ +public: + QTuioCursor(int id = -1) + : m_id(id) + , m_x(0) + , m_y(0) + , m_vx(0) + , m_vy(0) + , m_acceleration(0) + , m_state(Qt::TouchPointPressed) + { + } + + int id() const { return m_id; } + + void setX(float x) + { + if (state() == Qt::TouchPointStationary && + !qFuzzyCompare(m_x + 2.0, x + 2.0)) { // +2 because 1 is a valid value, and qFuzzyCompare can't cope with 0.0 + setState(Qt::TouchPointMoved); + } + m_x = x; + } + float x() const { return m_x; } + + void setY(float y) + { + if (state() == Qt::TouchPointStationary && + !qFuzzyCompare(m_y + 2.0, y + 2.0)) { // +2 because 1 is a valid value, and qFuzzyCompare can't cope with 0.0 + setState(Qt::TouchPointMoved); + } + m_y = y; + } + float y() const { return m_y; } + + void setVX(float vx) { m_vx = vx; } + float vx() const { return m_vx; } + + void setVY(float vy) { m_vy = vy; } + float vy() const { return m_vy; } + + void setAcceleration(float acceleration) { m_acceleration = acceleration; } + float acceleration() const { return m_acceleration; } + + void setState(const Qt::TouchPointState &state) { m_state = state; } + Qt::TouchPointState state() const { return m_state; } + +private: + int m_id; + float m_x; + float m_y; + float m_vx; + float m_vy; + float m_acceleration; + Qt::TouchPointState m_state; +}; + +QT_END_NAMESPACE + +#endif // QTUIOCURSOR_P_H diff --git a/src/plugins/generic/tuiotouch/qtuiohandler.cpp b/src/plugins/generic/tuiotouch/qtuiohandler.cpp new file mode 100644 index 0000000000..cf588afebc --- /dev/null +++ b/src/plugins/generic/tuiotouch/qtuiohandler.cpp @@ -0,0 +1,337 @@ +/**************************************************************************** +** +** Copyright (C) 2014 Robin Burchell +** Copyright (C) 2014 Digia Plc and/or its subsidiary(-ies). +** Contact: http://www.qt-project.org/legal +** +** This file is part of the QtCore module of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL21$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and Digia. For licensing terms and +** conditions see http://qt.digia.com/licensing. For further information +** use the contact form at http://qt.digia.com/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 2.1 or version 3 as published by the Free +** Software Foundation and appearing in the file LICENSE.LGPLv21 and +** LICENSE.LGPLv3 included in the packaging of this file. Please review the +** following information to ensure the GNU Lesser General Public License +** requirements will be met: https://www.gnu.org/licenses/lgpl.html and +** http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. +** +** In addition, as a special exception, Digia gives you certain additional +** rights. These rights are described in the Digia Qt LGPL Exception +** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include +#include +#include +#include + +#include + +#include "qtuiocursor_p.h" +#include "qtuiohandler_p.h" +#include "qoscbundle_p.h" + +QT_BEGIN_NAMESPACE + +Q_LOGGING_CATEGORY(lcTuioSource, "qt.qpa.tuio.source") +Q_LOGGING_CATEGORY(lcTuioSet, "qt.qpa.tuio.set") + +QTuioHandler::QTuioHandler(const QString &specification) + : m_device(new QTouchDevice) // not leaked, QTouchDevice cleans up registered devices itself +{ + QStringList args = specification.split(':'); + int portNumber = 3333; + int rotationAngle = 0; + bool invertx = false; + bool inverty = false; + + for (int i = 0; i < args.count(); ++i) { + if (args.at(i).startsWith("udp=")) { + QString portString = args.at(i).section('=', 1, 1); + portNumber = portString.toInt(); + } else if (args.at(i).startsWith("tcp=")) { + QString portString = args.at(i).section('=', 1, 1); + portNumber = portString.toInt(); + qWarning() << "TCP is not yet supported. Falling back to UDP on " << portNumber; + } else if (args.at(i) == "invertx") { + invertx = true; + } else if (args.at(i) == "inverty") { + inverty = true; + } else if (args.at(i).startsWith("rotate=")) { + QString rotateArg = args.at(i).section('=', 1, 1); + int argValue = rotateArg.toInt(); + switch (argValue) { + case 90: + case 180: + case 270: + rotationAngle = argValue; + default: + break; + } + } + } + + if (rotationAngle) + m_transform = QTransform::fromTranslate(0.5, 0.5).rotate(rotationAngle).translate(-0.5, -0.5); + + if (invertx) + m_transform *= QTransform::fromTranslate(0.5, 0.5).scale(-1.0, 1.0).translate(-0.5, -0.5); + + if (inverty) + m_transform *= QTransform::fromTranslate(0.5, 0.5).scale(1.0, -1.0).translate(-0.5, -0.5); + + m_device->setName("TUIO"); // TODO: multiple based on SOURCE? + m_device->setType(QTouchDevice::TouchScreen); + m_device->setCapabilities(QTouchDevice::Position | + QTouchDevice::Area | + QTouchDevice::Velocity | + QTouchDevice::NormalizedPosition); + QWindowSystemInterface::registerTouchDevice(m_device); + + if (!m_socket.bind(QHostAddress::Any, portNumber)) { + qWarning() << "Failed to bind TUIO socket: " << m_socket.errorString(); + return; + } + + connect(&m_socket, &QUdpSocket::readyRead, this, &QTuioHandler::processPackets); +} + +QTuioHandler::~QTuioHandler() +{ +} + +void QTuioHandler::processPackets() +{ + while (m_socket.hasPendingDatagrams()) { + QByteArray datagram; + datagram.resize(m_socket.pendingDatagramSize()); + QHostAddress sender; + quint16 senderPort; + + qint64 size = m_socket.readDatagram(datagram.data(), datagram.size(), + &sender, &senderPort); + + if (size == -1) + continue; + + if (size != datagram.size()) + datagram.resize(size); + + QOscBundle bundle(datagram); + if (!bundle.isValid()) + continue; + + // "A typical TUIO bundle will contain an initial ALIVE message, + // followed by an arbitrary number of SET messages that can fit into the + // actual bundle capacity and a concluding FSEQ message. A minimal TUIO + // bundle needs to contain at least the compulsory ALIVE and FSEQ + // messages. The FSEQ frame ID is incremented for each delivered bundle, + // while redundant bundles can be marked using the frame sequence ID + // -1." + QList messages = bundle.messages(); + + foreach (const QOscMessage &message, messages) { + if (message.addressPattern() != "/tuio/2Dcur") { + qWarning() << "Ignoring unknown address pattern " << message.addressPattern(); + continue; + } + + QList arguments = message.arguments(); + if (arguments.count() == 0) { + qWarning() << "Ignoring TUIO message with no arguments"; + continue; + } + + QByteArray messageType = arguments.at(0).toByteArray(); + if (messageType == "source") { + process2DCurSource(message); + } else if (messageType == "alive") { + process2DCurAlive(message); + } else if (messageType == "set") { + process2DCurSet(message); + } else if (messageType == "fseq") { + process2DCurFseq(message); + } else { + qWarning() << "Ignoring unknown TUIO message type: " << messageType; + continue; + } + } + } +} + +void QTuioHandler::process2DCurSource(const QOscMessage &message) +{ + QList arguments = message.arguments(); + if (arguments.count() != 2) { + qWarning() << "Ignoring malformed TUIO source message: " << arguments.count(); + return; + } + + if (QMetaType::Type(arguments.at(1).type()) != QMetaType::QByteArray) { + qWarning() << "Ignoring malformed TUIO source message (bad argument type)"; + return; + } + + qCDebug(lcTuioSource) << "Got TUIO source message from: " << arguments.at(1).toByteArray(); +} + +void QTuioHandler::process2DCurAlive(const QOscMessage &message) +{ + QList arguments = message.arguments(); + + // delta the notified cursors that are active, against the ones we already + // know of. + // + // TBD: right now we're assuming one 2Dcur alive message corresponds to a + // new data source from the input. is this correct, or do we need to store + // changes and only process the deltas on fseq? + QMap oldActiveCursors = m_activeCursors; + QMap newActiveCursors; + + for (int i = 1; i < arguments.count(); ++i) { + if (QMetaType::Type(arguments.at(i).type()) != QMetaType::Int) { + qWarning() << "Ignoring malformed TUIO alive message (bad argument on position" << i << arguments << ")"; + return; + } + + int cursorId = arguments.at(i).toInt(); + if (!oldActiveCursors.contains(cursorId)) { + // newly active + QTuioCursor cursor(cursorId); + cursor.setState(Qt::TouchPointPressed); + newActiveCursors.insert(cursorId, cursor); + } else { + // we already know about it, remove it so it isn't marked as released + QTuioCursor cursor = oldActiveCursors.value(cursorId); + cursor.setState(Qt::TouchPointStationary); // position change in SET will update if needed + newActiveCursors.insert(cursorId, cursor); + oldActiveCursors.remove(cursorId); + } + } + + // anything left is dead now + QMap::ConstIterator it = oldActiveCursors.constBegin(); + + // deadCursors should be cleared from the last FSEQ now + m_deadCursors.reserve(oldActiveCursors.size()); + + // TODO: there could be an issue of resource exhaustion here if FSEQ isn't + // sent in a timely fashion. we should probably track message counts and + // force-flush if we get too many built up. + while (it != oldActiveCursors.constEnd()) { + m_deadCursors.append(it.value()); + ++it; + } + + m_activeCursors = newActiveCursors; +} + +void QTuioHandler::process2DCurSet(const QOscMessage &message) +{ + QList arguments = message.arguments(); + if (arguments.count() < 7) { + qWarning() << "Ignoring malformed TUIO set message with too few arguments: " << arguments.count(); + return; + } + + if (QMetaType::Type(arguments.at(1).type()) != QMetaType::Int || + QMetaType::Type(arguments.at(2).type()) != QMetaType::Float || + QMetaType::Type(arguments.at(3).type()) != QMetaType::Float || + QMetaType::Type(arguments.at(4).type()) != QMetaType::Float || + QMetaType::Type(arguments.at(5).type()) != QMetaType::Float || + QMetaType::Type(arguments.at(6).type()) != QMetaType::Float + ) { + qWarning() << "Ignoring malformed TUIO set message with bad types: " << arguments; + return; + } + + int cursorId = arguments.at(1).toInt(); + float x = arguments.at(2).toFloat(); + float y = arguments.at(3).toFloat(); + float vx = arguments.at(4).toFloat(); + float vy = arguments.at(5).toFloat(); + float acceleration = arguments.at(6).toFloat(); + + QMap::Iterator it = m_activeCursors.find(cursorId); + if (it == m_activeCursors.end()) { + qWarning() << "Ignoring malformed TUIO set for nonexistent cursor " << cursorId; + return; + } + + qCDebug(lcTuioSet) << "Processing SET for " << cursorId << " x: " << x << y << vx << vy << acceleration; + QTuioCursor &cur = *it; + cur.setX(x); + cur.setY(y); + cur.setVX(vx); + cur.setVY(vy); + cur.setAcceleration(acceleration); +} + +QWindowSystemInterface::TouchPoint QTuioHandler::cursorToTouchPoint(const QTuioCursor &tc, QWindow *win) +{ + QWindowSystemInterface::TouchPoint tp; + tp.id = tc.id(); + tp.pressure = 1.0f; + + tp.normalPosition = QPointF(tc.x(), tc.y()); + + if (!m_transform.isIdentity()) + tp.normalPosition = m_transform.map(tp.normalPosition); + + tp.state = tc.state(); + tp.area = QRectF(0, 0, 1, 1); + + // we map the touch to the size of the window. we do this, because frankly, + // trying to figure out which part of the screen to hit in order to press an + // element on the UI is pretty tricky when one is not using an overlay-style + // TUIO device. + // + // in the future, it might make sense to make this choice optional, + // dependent on the spec. + QPointF relPos = QPointF(win->size().width() * tp.normalPosition.x(), win->size().height() * tp.normalPosition.y()); + QPointF delta = relPos - relPos.toPoint(); + tp.area.moveCenter(win->mapToGlobal(relPos.toPoint()) + delta); + tp.velocity = QVector2D(win->size().width() * tc.vx(), win->size().height() * tc.vy()); + return tp; +} + + +void QTuioHandler::process2DCurFseq(const QOscMessage &message) +{ + Q_UNUSED(message); // TODO: do we need to do anything with the frame id? + + QWindow *win = QGuiApplication::focusWindow(); + if (!win) + return; + + QList tpl; + + foreach (const QTuioCursor &tc, m_activeCursors) { + QWindowSystemInterface::TouchPoint tp = cursorToTouchPoint(tc, win); + tpl.append(tp); + } + + foreach (const QTuioCursor &tc, m_deadCursors) { + QWindowSystemInterface::TouchPoint tp = cursorToTouchPoint(tc, win); + tp.state = Qt::TouchPointReleased; + tpl.append(tp); + } + QWindowSystemInterface::handleTouchEvent(win, m_device, tpl); + + m_deadCursors.clear(); +} + +QT_END_NAMESPACE + diff --git a/src/plugins/generic/tuiotouch/qtuiohandler_p.h b/src/plugins/generic/tuiotouch/qtuiohandler_p.h new file mode 100644 index 0000000000..99111fec8b --- /dev/null +++ b/src/plugins/generic/tuiotouch/qtuiohandler_p.h @@ -0,0 +1,79 @@ +/**************************************************************************** +** +** Copyright (C) 2014 Robin Burchell +** Copyright (C) 2014 Digia Plc and/or its subsidiary(-ies). +** Contact: http://www.qt-project.org/legal +** +** This file is part of the QtCore module of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL21$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and Digia. For licensing terms and +** conditions see http://qt.digia.com/licensing. For further information +** use the contact form at http://qt.digia.com/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 2.1 or version 3 as published by the Free +** Software Foundation and appearing in the file LICENSE.LGPLv21 and +** LICENSE.LGPLv3 included in the packaging of this file. Please review the +** following information to ensure the GNU Lesser General Public License +** requirements will be met: https://www.gnu.org/licenses/lgpl.html and +** http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. +** +** In addition, as a special exception, Digia gives you certain additional +** rights. These rights are described in the Digia Qt LGPL Exception +** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#ifndef QTUIOHANDLER_P_H +#define QTUIOHANDLER_P_H + +#include +#include +#include +#include +#include + +#include + +QT_BEGIN_NAMESPACE + +class QTouchDevice; +class QOscMessage; +class QTuioCursor; + +class QTuioHandler : public QObject +{ + Q_OBJECT + +public: + explicit QTuioHandler(const QString &specification); + virtual ~QTuioHandler(); + +private slots: + void processPackets(); + void process2DCurSource(const QOscMessage &message); + void process2DCurAlive(const QOscMessage &message); + void process2DCurSet(const QOscMessage &message); + void process2DCurFseq(const QOscMessage &message); + +private: + QWindowSystemInterface::TouchPoint cursorToTouchPoint(const QTuioCursor &tc, QWindow *win); + + QTouchDevice *m_device; + QUdpSocket m_socket; + QMap m_activeCursors; + QVector m_deadCursors; + QTransform m_transform; +}; + +QT_END_NAMESPACE + +#endif // QTUIOHANDLER_P_H diff --git a/src/plugins/generic/tuiotouch/tuiotouch.json b/src/plugins/generic/tuiotouch/tuiotouch.json new file mode 100644 index 0000000000..bdbbcd203a --- /dev/null +++ b/src/plugins/generic/tuiotouch/tuiotouch.json @@ -0,0 +1,3 @@ +{ + "Keys": [ "TuioTouch" ] +} diff --git a/src/plugins/generic/tuiotouch/tuiotouch.pro b/src/plugins/generic/tuiotouch/tuiotouch.pro new file mode 100644 index 0000000000..5e53403f5b --- /dev/null +++ b/src/plugins/generic/tuiotouch/tuiotouch.pro @@ -0,0 +1,26 @@ +TARGET = qtuiotouchplugin + +PLUGIN_TYPE = generic +PLUGIN_EXTENDS = - +PLUGIN_CLASS_NAME = QTuioTouchPlugin +load(qt_plugin) + +QT += \ + core-private \ + gui-private \ + network + +SOURCES += \ + main.cpp \ + qoscbundle.cpp \ + qoscmessage.cpp \ + qtuiohandler.cpp + +HEADERS += \ + qoscbundle_p.h \ + qoscmessage_p.h \ + qtuiohandler_p.h \ + qtuiocursor_p.h + +OTHER_FILES += \ + tuiotouch.json -- cgit v1.2.3