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 <shawn.rutledge@digia.com>
This commit is contained in:
Robin Burchell 2014-11-26 22:16:16 -08:00
parent e0a8b5ce88
commit eb2014382b
13 changed files with 1176 additions and 0 deletions

View File

@ -7,3 +7,5 @@ contains(QT_CONFIG, evdev) {
contains(QT_CONFIG, tslib) {
SUBDIRS += tslib
}
SUBDIRS += tuiotouch

View File

@ -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?

View File

@ -0,0 +1,68 @@
/****************************************************************************
**
** Copyright (C) 2014 Robin Burchell <robin.burchell@viroteck.net>
** 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 <QtGui/qgenericplugin.h>
#include <QCoreApplication>
#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"

View File

@ -0,0 +1,186 @@
/****************************************************************************
**
** Copyright (C) 2014 Robin Burchell <robin.burchell@viroteck.net>
** 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 <QtEndian>
#include <QDebug>
#include <QLoggingCategory>
#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<quint32>((const uchar*)data.constData() + parsedBytes);
parsedBytes += sizeof(quint32);
quint32 oscTimePico = qFromBigEndian<quint32>((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<quint32>((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> QOscBundle::bundles() const
{
return m_bundles;
}
QList<QOscMessage> QOscBundle::messages() const
{
return m_messages;
}
QT_END_NAMESPACE

View File

@ -0,0 +1,62 @@
/****************************************************************************
**
** Copyright (C) 2014 Robin Burchell <robin.burchell@viroteck.net>
** 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<QOscBundle> bundles() const;
QList<QOscMessage> messages() const;
private:
bool m_isValid;
bool m_immediate;
quint32 m_timeEpoch;
quint32 m_timePico;
QList<QOscBundle> m_bundles;
QList<QOscMessage> m_messages;
};
QT_END_NAMESPACE
#endif // QOSCBUNDLE_P_H

View File

@ -0,0 +1,138 @@
/****************************************************************************
**
** Copyright (C) 2014 Robin Burchell <robin.burchell@viroteck.net>
** 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 <QByteArray>
#include <QDebug>
#include <QtEndian>
#include <QVariant>
#include <QLoggingCategory>
#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<QVariant> 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<quint32>((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<quint32>((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<QVariant> QOscMessage::arguments() const
{
return m_arguments;
}
QT_END_NAMESPACE

View File

@ -0,0 +1,57 @@
/****************************************************************************
**
** Copyright (C) 2014 Robin Burchell <robin.burchell@viroteck.net>
** 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<QVariant> arguments() const;
private:
bool m_isValid;
QByteArray m_addressPattern;
QList<QVariant> m_arguments;
};
QT_END_NAMESPACE
#endif

View File

@ -0,0 +1,62 @@
/****************************************************************************
**
** Copyright (C) 2014 Robin Burchell <robin.burchell@viroteck.net>
** 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

View File

@ -0,0 +1,102 @@
/****************************************************************************
**
** Copyright (C) 2014 Robin Burchell <robin.burchell@viroteck.net>
** 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>
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

View File

@ -0,0 +1,337 @@
/****************************************************************************
**
** Copyright (C) 2014 Robin Burchell <robin.burchell@viroteck.net>
** 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 <QLoggingCategory>
#include <QRect>
#include <QWindow>
#include <QGuiApplication>
#include <qpa/qwindowsysteminterface.h>
#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<QOscMessage> messages = bundle.messages();
foreach (const QOscMessage &message, messages) {
if (message.addressPattern() != "/tuio/2Dcur") {
qWarning() << "Ignoring unknown address pattern " << message.addressPattern();
continue;
}
QList<QVariant> 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<QVariant> 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<QVariant> 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<int, QTuioCursor> oldActiveCursors = m_activeCursors;
QMap<int, QTuioCursor> 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<int, QTuioCursor>::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<QVariant> 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<int, QTuioCursor>::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<QWindowSystemInterface::TouchPoint> 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

View File

@ -0,0 +1,79 @@
/****************************************************************************
**
** Copyright (C) 2014 Robin Burchell <robin.burchell@viroteck.net>
** 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 <QObject>
#include <QMap>
#include <QUdpSocket>
#include <QVector>
#include <QTransform>
#include <qpa/qwindowsysteminterface.h>
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<int, QTuioCursor> m_activeCursors;
QVector<QTuioCursor> m_deadCursors;
QTransform m_transform;
};
QT_END_NAMESPACE
#endif // QTUIOHANDLER_P_H

View File

@ -0,0 +1,3 @@
{
"Keys": [ "TuioTouch" ]
}

View File

@ -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