blob: 6ee51aac4935fa5b7e32524080e3a4f3d471ab8e [file] [log] [blame]
/****************************************************************************
**
** Copyright (C) 2019 The Qt Company Ltd.
** Contact: https://www.qt.io/licensing/
**
** This file is part of Qt Quick 3D.
**
** $QT_BEGIN_LICENSE:GPL$
** 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 The Qt Company. For licensing terms
** and conditions see https://www.qt.io/terms-conditions. For further
** information use the contact form at https://www.qt.io/contact-us.
**
** GNU General Public License Usage
** Alternatively, this file may be used under the terms of the GNU
** General Public License version 3 or (at your option) any later version
** approved by the KDE Free Qt Foundation. The licenses are as published by
** the Free Software Foundation and appearing in the file LICENSE.GPL3
** included in the packaging of this file. Please review the following
** information to ensure the GNU General Public License requirements will
** be met: https://www.gnu.org/licenses/gpl-3.0.html.
**
** $QT_END_LICENSE$
**
****************************************************************************/
#include "uipparser.h"
#include "uippresentation.h"
#include "enummaps.h"
#include "uniqueidmapper.h"
#include <QDebug>
QT_BEGIN_NAMESPACE
UipPresentation *UipParser::parse(const QString &filename, const QString &presentationName)
{
if (!setSource(filename))
return nullptr;
return createPresentation(presentationName);
}
UipPresentation *UipParser::parseData(const QByteArray &data, const QString &presentationName)
{
if (!setSourceData(data))
return nullptr;
return createPresentation(presentationName);
}
namespace {
QString sanitizeFilename(const QString &filename) {
//remove ending
QStringList parts = filename.split(".");
QString name = parts[0];
name.replace(" ", "_");
return name;
}
}
UipPresentation *UipParser::createPresentation(const QString &presentationName)
{
if (m_presentation)
delete m_presentation;
m_presentation = new UipPresentation;
m_presentation->setSourceFile(sourceInfo()->absoluteFilePath());
m_presentation->setName(presentationName.isEmpty() ? sanitizeFilename(sourceInfo()->fileName()) : presentationName);
QXmlStreamReader *r = reader();
if (r->readNextStartElement()) {
if (r->name() == QStringLiteral("UIP")
&& (r->attributes().value(QLatin1String("version")) == QStringLiteral("6")
|| r->attributes().value(QLatin1String("version")) == QStringLiteral("5")
|| r->attributes().value(QLatin1String("version")) == QStringLiteral("4")
|| r->attributes().value(QLatin1String("version")) == QStringLiteral("3"))) {
parseUIP();
} else {
r->raiseError(QObject::tr("UIP version is too low, and is no longer supported."));
}
}
if (r->hasError()) {
qWarning() << readerErrorString();
return nullptr;
}
const quint64 loadTime = elapsedSinceSetSource();
qDebug("Presentation %s loaded in %lld ms", qPrintable(m_presentation->sourceFile()), loadTime);
return m_presentation;
}
void UipParser::parseUIP()
{
QXmlStreamReader *r = reader();
int projectCount = 0;
while (r->readNextStartElement()) {
if (r->name() == QStringLiteral("Project")) {
++projectCount;
if (projectCount == 1)
parseProject();
else
r->raiseError(QObject::tr("Multiple Project elements found."));
} else {
r->skipCurrentElement();
}
}
}
void UipParser::parseProject()
{
QXmlStreamReader *r = reader();
bool foundGraph = false;
while (r->readNextStartElement()) {
if (r->name() == QStringLiteral("ProjectSettings"))
parseProjectSettings();
else if (r->name() == QStringLiteral("Classes"))
parseClasses();
else if (r->name() == QStringLiteral("BufferData"))
parseBufferData();
else if (r->name() == QStringLiteral("Graph")) {
if (!foundGraph) {
foundGraph = true;
parseGraph();
} else {
r->raiseError(QObject::tr("Multiple Graph elements found."));
}
} else if (r->name() == QStringLiteral("Logic")) {
if (foundGraph)
parseLogic();
else
r->raiseError(QObject::tr("Encountered Logic element before Graph."));
} else {
r->skipCurrentElement();
}
}
}
void UipParser::parseProjectSettings()
{
QXmlStreamReader *r = reader();
for (const QXmlStreamAttribute &attr : r->attributes()) {
if (attr.name() == QStringLiteral("author")) {
m_presentation->setAuthor(attr.value().toString());
} else if (attr.name() == QStringLiteral("company")) {
m_presentation->setCompany(attr.value().toString());
} else if (attr.name() == QStringLiteral("presentationWidth")) {
int w;
if (Q3DS::convertToInt(attr.value(), &w, "presentation width", r))
m_presentation->setPresentationWidth(w);
} else if (attr.name() == QStringLiteral("presentationHeight")) {
int h;
if (Q3DS::convertToInt(attr.value(), &h, "presentation height", r))
m_presentation->setPresentationHeight(h);
} else if (attr.name() == QStringLiteral("maintainAspect")) {
bool v;
if (Q3DS::convertToBool(attr.value(), &v, "maintainAspect value", r))
m_presentation->setMaintainAspectRatio(v);
}
}
r->skipCurrentElement();
}
void UipParser::parseClasses()
{
QXmlStreamReader *r = reader();
while (r->readNextStartElement()) {
// if (r->name() == QStringLiteral("CustomMaterial")) {
// parseExternalFileRef(std::bind(&UipPresentation::loadCustomMaterial, m_presentation.data(),
// std::placeholders::_1, std::placeholders::_2));
// } else if (r->name() == QStringLiteral("Effect")) {
// parseExternalFileRef(std::bind(&UipPresentation::loadEffect, m_presentation.data(),
// std::placeholders::_1, std::placeholders::_2));
// } else if (r->name() == QStringLiteral("Behavior")) {
// parseExternalFileRef(std::bind(&UipPresentation::loadBehavior, m_presentation.data(),
// std::placeholders::_1, std::placeholders::_2));
// } else if (r->name() == QStringLiteral("RenderPlugin")) {
// r->raiseError(QObject::tr("RenderPlugin not supported"));
// } else {
r->skipCurrentElement();
// }
}
}
void UipParser::parseBufferData()
{
QXmlStreamReader *r = reader();
while (r->readNextStartElement()) {
if (r->name() == QStringLiteral("ImageBuffer"))
parseImageBuffer();
else
r->skipCurrentElement();
}
}
void UipParser::parseImageBuffer()
{
QXmlStreamReader *r = reader();
auto a = r->attributes();
const QStringRef &sourcePath = a.value(QStringLiteral("sourcepath"));
const QStringRef &hasTransparency = a.value(QStringLiteral("hasTransparency"));
if (!sourcePath.isEmpty() && !hasTransparency.isEmpty())
m_presentation->registerImageBuffer(sourcePath.toString(), hasTransparency.compare(QStringLiteral("True")) == 0);
r->skipCurrentElement();
}
void UipParser::parseGraph()
{
QXmlStreamReader *r = reader();
int sceneCount = 0;
while (r->readNextStartElement()) {
if (r->name() == QStringLiteral("Scene")) {
++sceneCount;
if (sceneCount == 1)
parseScene();
else
r->raiseError(QObject::tr("Multiple Scene elements found."));
} else {
r->skipCurrentElement();
}
}
}
void UipParser::parseScene()
{
QXmlStreamReader *r = reader();
QByteArray id = getId(r->name());
if (id.isEmpty())
return;
auto scene = new Scene;
scene->setProperties(r->attributes(), GraphObject::PropSetDefaults);
m_presentation->registerObject(id, scene);
m_presentation->setScene(scene);
// Parse elements below Layer element
while (r->readNextStartElement())
parseObjects(scene);
}
void UipParser::parseObjects(GraphObject *parent)
{
QXmlStreamReader *r = reader();
Q_ASSERT(parent);
QByteArray id = getId(r->name());
if (id.isEmpty())
return;
GraphObject *obj = nullptr;
if (r->name() == QStringLiteral("Layer"))
obj = new LayerNode;
else if (r->name() == QStringLiteral("Camera"))
obj = new CameraNode;
else if (r->name() == QStringLiteral("Light"))
obj = new LightNode;
else if (r->name() == QStringLiteral("Model"))
obj = new ModelNode;
else if (r->name() == QStringLiteral("Group"))
obj = new GroupNode;
else if (r->name() == QStringLiteral("Component"))
obj = new ComponentNode;
else if (r->name() == QStringLiteral("Text"))
obj = new TextNode;
else if (r->name() == QStringLiteral("Material"))
obj = new DefaultMaterial;
else if (r->name() == QStringLiteral("ReferencedMaterial"))
obj = new ReferencedMaterial;
else if (r->name() == QStringLiteral("CustomMaterial"))
obj = new CustomMaterialInstance;
else if (r->name() == QStringLiteral("Effect"))
obj = new EffectInstance;
else if (r->name() == QStringLiteral("Behavior"))
obj = new BehaviorInstance;
else if (r->name() == QStringLiteral("Image"))
obj = new Image;
else if (r->name() == QStringLiteral("Alias"))
obj = new AliasNode;
// Skip unknown elements in the XML
if (!obj) {
r->skipCurrentElement();
return;
}
obj->setProperties(r->attributes(), GraphObject::PropSetDefaults);
m_presentation->registerObject(id, obj);
parent->appendChildNode(obj);
while (r->readNextStartElement())
parseObjects(obj);
}
void UipParser::parseLogic()
{
QXmlStreamReader *r = reader();
int masterCount = 0;
while (r->readNextStartElement()) {
if (r->name() == QStringLiteral("State")) {
QStringRef compRef = r->attributes().value(QLatin1String("component"));
if (!compRef.startsWith('#')) {
r->raiseError(QObject::tr("Invalid ref '%1' in State").arg(compRef.toString()));
return;
}
QByteArray refId = UniqueIdMapper::instance()->queryId(compRef.mid(1).toUtf8());
GraphObject *slideTarget = m_presentation->object(refId);
if (!slideTarget) {
r->raiseError(
QObject::tr(
"State references unknown object '%1'").arg(
compRef.mid(1).toString()));
return;
}
const QByteArray idPrefix = compRef.mid(1).toUtf8();
if (slideTarget->type() == GraphObject::Scene) {
if (++masterCount == 1) {
auto masterSlide = parseSlide(nullptr, idPrefix);
Q_ASSERT(masterSlide);
m_presentation->setMasterSlide(masterSlide);
} else {
r->raiseError(QObject::tr("Multiple State (master slide) elements found."));
}
} else {
Slide *componentMasterSlide = parseSlide(nullptr, idPrefix);
Q_ASSERT(componentMasterSlide);
static_cast<ComponentNode *>(slideTarget)->m_masterSlide = componentMasterSlide; // transfer ownership
}
} else {
r->raiseError(QObject::tr("Logic can only have State children."));
}
}
}
Slide *UipParser::parseSlide(Slide *parent, const QByteArray &idPrefix)
{
QXmlStreamReader *r = reader();
QByteArray id = getId(r->name(), false);
const bool isMaster = !parent;
if (isMaster) {
// The master slide may not have an id.
if (id.isEmpty())
id = idPrefix + QByteArrayLiteral("-Master");
}
if (id.isEmpty())
return nullptr;
Slide *slide = new Slide;
slide->setProperties(r->attributes(), GraphObject::PropSetDefaults);
m_presentation->registerObject(id, slide);
if (parent)
parent->appendChildNode(slide);
while (r->readNextStartElement()) {
if (r->name() == QStringLiteral("State")) {
if (isMaster)
parseSlide(slide);
else
r->raiseError(QObject::tr("Encountered sub-slide in sub-slide."));
} else if (r->name() == QStringLiteral("Add")) {
parseAddSet(slide, false, isMaster);
} else if (r->name() == QStringLiteral("Set")) {
parseAddSet(slide, true, isMaster);
} else {
r->skipCurrentElement();
}
}
return slide;
}
void UipParser::parseAddSet(Slide *slide, bool isSet, bool isMaster)
{
QXmlStreamReader *r = reader();
QStringRef ref = r->attributes().value(QLatin1String("ref"));
if (!ref.startsWith('#')) {
r->raiseError(QObject::tr("Invalid ref '%1' in Add/Set").arg(ref.toString()));
return;
}
QByteArray refId = UniqueIdMapper::instance()->queryId(ref.mid(1).toUtf8());
GraphObject *obj = m_presentation->object(refId);
if (!obj) {
r->raiseError(
QObject::tr("Add/Set references unknown object '%1'").arg(
ref.mid(1).toString()));
return;
}
if (!isSet) {
// Add: register the object for this slide
slide->addObject(obj);
// and set the properties on the object right away.
GraphObject::PropSetFlags flags = 0;
if (isMaster)
flags |= GraphObject::PropSetOnMaster;
obj->setProperties(r->attributes(), flags);
} else {
// Set: store the property changes
QScopedPointer<PropertyChangeList> changeList(new PropertyChangeList);
for (const QXmlStreamAttribute &attr : r->attributes()) {
if (attr.name() == QStringLiteral("ref"))
continue;
if (attr.name() == QStringLiteral("sourcepath")) {
QString absoluteFileName =
m_presentation->assetFileName(attr.value().toString(), nullptr);
changeList->append(PropertyChange(attr.name().toString(), absoluteFileName));
} else {
changeList->append(PropertyChange(attr.name().toString(), attr.value().toString()));
}
}
if (!changeList->isEmpty())
slide->addPropertyChanges(obj, changeList.take());
}
// Store animations and actions.
while (r->readNextStartElement()) {
if (r->name() == QStringLiteral("AnimationTrack")) {
AnimationTrack animTrack;
animTrack.m_target = obj;
for (const QXmlStreamAttribute &attr : r->attributes()) {
if (attr.name() == QStringLiteral("property")) {
animTrack.m_property = attr.value().toString().trimmed();
} else if (attr.name() == QStringLiteral("type")) {
if (!EnumMap::enumFromStr(attr.value(), &animTrack.m_type))
r->raiseError(QObject::tr("Unknown animation type %1").arg(attr.value().toString()));
} else if (attr.name() == QStringLiteral("dynamic")) {
Q3DS::convertToBool(attr.value(), &animTrack.m_dynamic, "'dynamic' attribute value", r);
}
}
parseAnimationKeyFrames(r->readElementText(QXmlStreamReader::SkipChildElements).trimmed(), &animTrack);
if (!animTrack.m_keyFrames.isEmpty())
slide->addAnimation(animTrack);
} else {
r->skipCurrentElement();
}
}
}
void UipParser::parseAnimationKeyFrames(const QString &data, AnimationTrack *animTrack)
{
QXmlStreamReader *r = reader();
QString spaceOnlyData = data;
spaceOnlyData.replace('\n', ' ');
const QStringList values = spaceOnlyData.split(' ', QString::SkipEmptyParts);
if (values.isEmpty() || values.first().isEmpty())
return;
switch (animTrack->m_type) {
case AnimationTrack::Linear:
if (!(values.count() % 2)) {
for (int i = 0; i < values.count() / 2; ++i) {
AnimationTrack::KeyFrame kf;
if (!Q3DS::convertToFloat(&values[i * 2], &kf.time, "keyframe time", r))
continue;
if (!Q3DS::convertToFloat(&values[i * 2 + 1], &kf.value, "keyframe value", r))
continue;
animTrack->m_keyFrames.append(kf);
}
} else {
r->raiseError(QObject::tr("Invalid Linear animation track: %1").arg(spaceOnlyData));
return;
}
break;
case AnimationTrack::EaseInOut:
if (!(values.count() % 4)) {
for (int i = 0; i < values.count() / 4; ++i) {
AnimationTrack::KeyFrame kf;
if (!Q3DS::convertToFloat(&values[i * 4], &kf.time, "keyframe time", r))
continue;
if (!Q3DS::convertToFloat(&values[i * 4 + 1], &kf.value, "keyframe value", r))
continue;
if (!Q3DS::convertToFloat(&values[i * 4 + 2], &kf.easeIn, "keyframe EaseIn", r))
continue;
if (!Q3DS::convertToFloat(&values[i * 4 + 3], &kf.easeOut, "keyframe EaseOut", r))
continue;
animTrack->m_keyFrames.append(kf);
}
} else {
r->raiseError(QObject::tr("Invalid EaseInOut animation track: %1").arg(spaceOnlyData));
return;
}
break;
case AnimationTrack::Bezier:
if (!(values.count() % 6)) {
for (int i = 0; i < values.count() / 6; ++i) {
AnimationTrack::KeyFrame kf;
if (!Q3DS::convertToFloat(&values[i * 6], &kf.time, "keyframe time", r))
continue;
kf.time *= 1000.0f;
if (!Q3DS::convertToFloat(&values[i * 6 + 1], &kf.value, "keyframe value", r))
continue;
if (i < values.count() / 6 - 1) {
if (!Q3DS::convertToFloat(&values[i * 6 + 2], &kf.c2time, "keyframe C2 time", r))
continue;
kf.c2time *= 1000.0f;
if (!Q3DS::convertToFloat(&values[i * 6 + 3], &kf.c2value, "keyframe C2 value", r))
continue;
} else { // last keyframe
kf.c2time = kf.c2value = 0.0f;
}
if (!Q3DS::convertToFloat(&values[i * 6 + 4], &kf.c1time, "keyframe C1 time", r))
continue;
kf.c1time *= 1000.0f;
if (!Q3DS::convertToFloat(&values[i * 6 + 5], &kf.c1value, "keyframe C1 value", r))
continue;
animTrack->m_keyFrames.append(kf);
}
} else {
r->raiseError(QObject::tr("Invalid Bezier animation track: %1").arg(spaceOnlyData));
return;
}
break;
default:
break;
}
}
QByteArray UipParser::getId(const QStringRef &desc, bool required)
{
QByteArray id = reader()->attributes().value(QLatin1String("id")).toUtf8();
if (id.isEmpty() && required)
reader()->raiseError(QObject::tr("Missing %1 id.").arg(desc.toString()));
return UniqueIdMapper::instance()->generateUniqueId(id);
}
void UipParser::parseExternalFileRef(UipParser::ExternalFileLoadCallback callback)
{
QXmlStreamReader *r = reader();
auto a = r->attributes();
const QStringRef id = a.value(QStringLiteral("id"));
const QStringRef sourcePath = a.value(QStringLiteral("sourcepath"));
// custommaterial/effect/behavior all expect ids to be prefixed with #
const QByteArray decoratedId = QByteArrayLiteral("#") + UniqueIdMapper::instance()->queryId(id.toUtf8());
const QString src = m_presentation->assetFileName(sourcePath.toString(), nullptr);
if (!callback(decoratedId, src))
r->raiseError(QObject::tr("Failed to load external file %1").arg(src));
r->skipCurrentElement();
}
QT_END_NAMESPACE