summaryrefslogtreecommitdiffstats
path: root/src/Authoring/Qt3DStudio/Application/ProjectFile.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'src/Authoring/Qt3DStudio/Application/ProjectFile.cpp')
-rw-r--r--src/Authoring/Qt3DStudio/Application/ProjectFile.cpp1556
1 files changed, 1556 insertions, 0 deletions
diff --git a/src/Authoring/Qt3DStudio/Application/ProjectFile.cpp b/src/Authoring/Qt3DStudio/Application/ProjectFile.cpp
new file mode 100644
index 00000000..1ad0b5c0
--- /dev/null
+++ b/src/Authoring/Qt3DStudio/Application/ProjectFile.cpp
@@ -0,0 +1,1556 @@
+/****************************************************************************
+**
+** Copyright (C) 2018 The Qt Company Ltd.
+** Contact: https://www.qt.io/licensing/
+**
+** This file is part of Qt 3D Studio.
+**
+** $QT_BEGIN_LICENSE:GPL-EXCEPT$
+** 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 as published by the Free Software
+** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT
+** 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 "ProjectFile.h"
+#include "Qt3DSFileTools.h"
+#include "Exceptions.h"
+#include "DataInputDlg.h"
+#include "StudioApp.h"
+#include "Qt3DSDMStudioSystem.h"
+#include "ClientDataModelBridge.h"
+#include "Core.h"
+#include "Doc.h"
+#include "IDocumentEditor.h"
+#include "PresentationFile.h"
+#include "IStudioRenderer.h"
+#include "StudioUtils.h"
+#include "Dispatch.h"
+#include "MainFrm.h"
+#include <QtCore/qdiriterator.h>
+#include <QtCore/qsavefile.h>
+#include <QtCore/qtimer.h>
+#include <QtWidgets/qmessagebox.h>
+
+ProjectFile::ProjectFile()
+{
+
+}
+
+// find the 1st .uia file in the current or parent directories and assume this is the project file,
+// as a project should have only 1 .uia file
+void ProjectFile::ensureProjectFile()
+{
+ if (!m_fileInfo.exists()) {
+ QFileInfo uipInfo(g_StudioApp.GetCore()->GetDoc()->GetDocumentPath());
+ QString uiaPath(PresentationFile::findProjectFile(uipInfo.absoluteFilePath()));
+
+ if (uiaPath.isEmpty()) {
+ // .uia not found, create a new one in the same folder as uip. Creation sets file info.
+ create(uipInfo.absoluteFilePath().replace(QLatin1String(".uip"),
+ QLatin1String(".uia")));
+ addPresentationNode(uipInfo.absoluteFilePath());
+ updateDocPresentationId();
+ } else {
+ // .uia found, set project file info
+ m_fileInfo.setFile(uiaPath);
+ }
+ }
+}
+
+void ProjectFile::initProjectFile(const QString &presPath)
+{
+ QFileInfo uipFile(presPath);
+ QString uiaPath(PresentationFile::findProjectFile(uipFile.absoluteFilePath()));
+
+ if (uiaPath.isEmpty()) {
+ // .uia not found, clear project file info
+ m_fileInfo = QFileInfo();
+ } else {
+ // .uia found, set project file info
+ m_fileInfo.setFile(uiaPath);
+ }
+}
+
+/**
+ * Add a presentation or presentation-qml node to the project file
+ *
+ * @param pPath the absolute path to the presentation file, it will be saved as relative
+ * @param pId presentation Id
+ */
+void ProjectFile::addPresentationNode(const QString &pPath, const QString &pId)
+{
+ addPresentationNodes({{pPath, pId}});
+}
+
+// Add a list of presentation or presentation-qml nodes to the project file
+void ProjectFile::addPresentationNodes(const QHash<QString, QString> &nodeList)
+{
+ ensureProjectFile();
+
+ QDomDocument doc;
+ QSaveFile file(getProjectFilePath());
+ if (!StudioUtils::openDomDocumentSave(file, doc))
+ return;
+
+ QDomElement rootElem = doc.documentElement();
+ QDomElement assetsElem = rootElem.firstChildElement(QStringLiteral("assets"));
+
+ // create the <assets> node if it doesn't exist
+ bool initial = false;
+ if (assetsElem.isNull()) {
+ assetsElem = doc.createElement(QStringLiteral("assets"));
+ rootElem.insertBefore(assetsElem, {});
+ initial = true;
+ }
+
+ QHash<QString, QString> changesList;
+ QHashIterator<QString, QString> nodesIt(nodeList);
+ while (nodesIt.hasNext()) {
+ nodesIt.next();
+ const QString presPath = nodesIt.key();
+ const QString presId = nodesIt.value();
+ QString relativePresentationPath
+ = QDir(getProjectPath()).relativeFilePath(presPath);
+
+ // make sure the node doesn't already exist
+ bool nodeExists = false;
+ for (QDomElement p = assetsElem.firstChild().toElement(); !p.isNull();
+ p = p.nextSibling().toElement()) {
+ if ((p.nodeName() == QLatin1String("presentation")
+ || p.nodeName() == QLatin1String("presentation-qml"))
+ && p.attribute(QStringLiteral("src")) == relativePresentationPath) {
+ nodeExists = true;
+ break;
+ }
+ }
+
+ if (!nodeExists) {
+ const QString presentationId
+ = ensureUniquePresentationId(presId.isEmpty()
+ ? QFileInfo(presPath).completeBaseName()
+ : presId);
+
+ if (assetsElem.attribute(QStringLiteral("initial")).isEmpty()) {
+ assetsElem.setAttribute(QStringLiteral("initial"), presentationId);
+ m_initialPresentation = presentationId;
+ }
+
+ // add the presentation node
+ bool isQml = presPath.endsWith(QLatin1String(".qml"));
+ QDomElement pElem = isQml ? doc.createElement(QStringLiteral("presentation-qml"))
+ : doc.createElement(QStringLiteral("presentation"));
+ pElem.setAttribute(QStringLiteral("id"), presentationId);
+ pElem.setAttribute(isQml ? QStringLiteral("args") : QStringLiteral("src"),
+ relativePresentationPath);
+ assetsElem.appendChild(pElem);
+ changesList.insert(relativePresentationPath, presentationId);
+
+ if (!initial) {
+ g_StudioApp.m_subpresentations.push_back(
+ SubPresentationRecord(isQml ? QStringLiteral("presentation-qml")
+ : QStringLiteral("presentation"),
+ presentationId, relativePresentationPath));
+ }
+ }
+ }
+
+ if (initial || changesList.size() > 0)
+ StudioUtils::commitDomDocumentSave(file, doc);
+
+ if (changesList.size() > 0) {
+ g_StudioApp.getRenderer().RegisterSubpresentations(g_StudioApp.m_subpresentations);
+
+ QHashIterator<QString, QString> changesIt(changesList);
+ while (changesIt.hasNext()) {
+ changesIt.next();
+ Q_EMIT presentationIdChanged(changesIt.key(), changesIt.value());
+ }
+ }
+}
+
+// Get the src attribute (relative path) to the initial presentation in a uia file, if no initial
+// presentation exists, the first one is returned. Returns empty string if file cannot be read.
+QString ProjectFile::getInitialPresentationSrc(const QString &uiaPath)
+{
+ QDomDocument domDoc;
+ if (!StudioUtils::readFileToDomDocument(uiaPath, domDoc))
+ return {};
+
+ QString firstPresentationSrc;
+ QDomElement assetsElem = domDoc.documentElement().firstChildElement(QStringLiteral("assets"));
+ if (!assetsElem.isNull()) {
+ QString initialId = assetsElem.attribute(QStringLiteral("initial"));
+ if (!initialId.isEmpty()) {
+ QDomNodeList pNodes = assetsElem.elementsByTagName(QStringLiteral("presentation"));
+ for (int i = 0; i < pNodes.count(); ++i) {
+ QDomElement pElem = pNodes.at(i).toElement();
+ if (pElem.attribute(QStringLiteral("id")) == initialId)
+ return pElem.attribute(QStringLiteral("src"));
+
+ if (i == 0)
+ firstPresentationSrc = pElem.attribute(QStringLiteral("src"));
+ }
+ }
+ }
+
+ return firstPresentationSrc;
+}
+
+/**
+ * Write a presentation id to the project file.
+ * If the presentation id doesn't exist yet in project, it's added.
+ *
+ * This also updates the Doc presentation Id if the src param is empty
+ * or same as current presentation.
+ *
+ * @param id presentation Id
+ * @param src source node, if empty the current document node is used
+ */
+void ProjectFile::writePresentationId(const QString &id, const QString &src)
+{
+ ensureProjectFile();
+
+ CDoc *doc = g_StudioApp.GetCore()->GetDoc();
+ QString theSrc = src.isEmpty() ? doc->getRelativePath() : src;
+ QString theId = id.isEmpty() ? doc->getPresentationId() : id;
+ bool isQml = theSrc.endsWith(QLatin1String(".qml"));
+
+ if (theSrc == doc->getRelativePath())
+ doc->setPresentationId(id);
+
+ QDomDocument domDoc;
+ QSaveFile file(getProjectFilePath());
+ if (!StudioUtils::openDomDocumentSave(file, domDoc))
+ return;
+
+ QDomElement assetsElem = domDoc.documentElement().firstChildElement(QStringLiteral("assets"));
+ QDomNodeList pqNodes = isQml ? assetsElem.elementsByTagName(QStringLiteral("presentation-qml"))
+ : assetsElem.elementsByTagName(QStringLiteral("presentation"));
+
+ QString oldId;
+ if (!pqNodes.isEmpty()) {
+ for (int i = 0; i < pqNodes.count(); ++i) {
+ QDomElement pqElem = pqNodes.at(i).toElement();
+ QString srcOrArgs = isQml ? pqElem.attribute(QStringLiteral("args"))
+ : pqElem.attribute(QStringLiteral("src"));
+ if (srcOrArgs == theSrc) {
+ oldId = pqElem.attribute(QStringLiteral("id"));
+ pqElem.setAttribute(QStringLiteral("id"), theId);
+
+ if (assetsElem.attribute(QStringLiteral("initial")) == oldId) {
+ assetsElem.setAttribute(QStringLiteral("initial"), theId);
+ m_initialPresentation = theId;
+ }
+ break;
+ }
+ }
+ }
+
+ if (!src.isEmpty() && oldId.isEmpty()) { // new presentation, add it
+ StudioUtils::commitDomDocumentSave(file, domDoc);
+ QDir projectDir(getProjectPath());
+ addPresentationNode(QDir::cleanPath(projectDir.absoluteFilePath(theSrc)), theId);
+ } else if (!oldId.isEmpty()) { // the presentation id changed
+ StudioUtils::commitDomDocumentSave(file, domDoc);
+
+ // update m_subpresentations
+ auto *sp = std::find_if(g_StudioApp.m_subpresentations.begin(),
+ g_StudioApp.m_subpresentations.end(),
+ [&theSrc](const SubPresentationRecord &spr) -> bool {
+ return spr.m_argsOrSrc == theSrc;
+ });
+ if (sp != g_StudioApp.m_subpresentations.end())
+ sp->m_id = theId;
+
+ // update current doc instances (layers and images) that are using this presentation Id
+ qt3dsdm::TInstanceHandleList instancesToRefresh;
+ auto *bridge = doc->GetStudioSystem()->GetClientDataModelBridge();
+ qt3dsdm::IPropertySystem *propSystem = doc->GetStudioSystem()->GetPropertySystem();
+ std::function<void(qt3dsdm::Qt3DSDMInstanceHandle)>
+ parseChildren = [&](qt3dsdm::Qt3DSDMInstanceHandle instance) {
+ Q3DStudio::CGraphIterator iter;
+ GetAssetChildren(doc, instance, iter);
+
+ while (!iter.IsDone()) {
+ qt3dsdm::Qt3DSDMInstanceHandle child = iter.GetCurrent();
+ if (bridge->GetObjectType(child) & (OBJTYPE_LAYER | OBJTYPE_IMAGE)) {
+ bool add = false;
+ if (bridge->GetSourcePath(child) == oldId) {
+ propSystem->SetInstancePropertyValue(child, bridge->GetSourcePathProperty(),
+ qt3dsdm::SValue(QVariant(theId)));
+ add = true;
+ }
+ if (bridge->getSubpresentation(child).toQString() == oldId) {
+ propSystem->SetInstancePropertyValue(child,
+ bridge->getSubpresentationProperty(),
+ qt3dsdm::SValue(QVariant(theId)));
+ add = true;
+ }
+ if (add)
+ instancesToRefresh.push_back(child);
+ }
+ parseChildren(child);
+ ++iter;
+ }
+ };
+ parseChildren(doc->GetSceneInstance());
+
+ // update changed presentation Id in all .uip files if in-use
+ QDomNodeList pNodes = assetsElem.elementsByTagName(QStringLiteral("presentation"));
+ for (int i = 0; i < pNodes.count(); ++i) {
+ QDomElement pElem = pNodes.at(i).toElement();
+ QString path = QDir(getProjectPath())
+ .absoluteFilePath(pElem.attribute(QStringLiteral("src")));
+ PresentationFile::updatePresentationId(path, oldId, theId);
+ }
+ Q_EMIT presentationIdChanged(theSrc, theId);
+
+ g_StudioApp.getRenderer().RegisterSubpresentations(g_StudioApp.m_subpresentations);
+ if (instancesToRefresh.size() > 0) {
+ g_StudioApp.GetCore()->GetDispatch()->FireImmediateRefreshInstance(
+ &(instancesToRefresh[0]), long(instancesToRefresh.size()));
+ for (auto &instance : instancesToRefresh)
+ doc->getSceneEditor()->saveIfMaterial(instance);
+ }
+ }
+}
+
+// Set the doc PresentationId from the project file, this is called after a document is loaded.
+// If there is no project file, it simply clears the id.
+void ProjectFile::updateDocPresentationId()
+{
+ CDoc *doc = g_StudioApp.GetCore()->GetDoc();
+ doc->setPresentationId({});
+
+ if (!m_fileInfo.exists())
+ return;
+
+ QFile file(getProjectFilePath());
+ if (!file.open(QFile::Text | QFile::ReadOnly)) {
+ qWarning() << file.errorString();
+ return;
+ }
+
+ QXmlStreamReader reader(&file);
+ reader.setNamespaceProcessing(false);
+
+ while (!reader.atEnd()) {
+ if (reader.readNextStartElement() && reader.name() == QLatin1String("presentation")) {
+ const auto attrs = reader.attributes();
+ if (attrs.value(QLatin1String("src")) == doc->getRelativePath()) {
+ // current presentation node
+ doc->setPresentationId(attrs.value(QLatin1String("id")).toString());
+ return;
+ }
+ }
+ }
+}
+
+// get a presentationId that match a given src attribute
+QString ProjectFile::getPresentationId(const QString &src) const
+{
+ if (!m_fileInfo.exists())
+ return {};
+
+ if (src == g_StudioApp.GetCore()->GetDoc()->getRelativePath()) {
+ return g_StudioApp.GetCore()->GetDoc()->getPresentationId();
+ } else {
+ auto *sp = std::find_if(g_StudioApp.m_subpresentations.begin(),
+ g_StudioApp.m_subpresentations.end(),
+ [&src](const SubPresentationRecord &spr) -> bool {
+ return spr.m_argsOrSrc == src;
+ });
+ if (sp != g_StudioApp.m_subpresentations.end())
+ return sp->m_id;
+ }
+
+ return {};
+}
+
+// create the project .uia file
+void ProjectFile::create(const QString &uiaPath)
+{
+ QDomDocument domDoc;
+ domDoc.setContent(QStringLiteral("<?xml version=\"1.0\" encoding=\"utf-8\"?>"
+ "<application xmlns=\"http://qt.io/qt3dstudio/uia\">"
+ "</application>"));
+
+ QSaveFile file(uiaPath);
+ if (StudioUtils::openTextSave(file)) {
+ StudioUtils::commitDomDocumentSave(file, domDoc);
+ m_fileInfo.setFile(uiaPath);
+ }
+}
+
+/**
+ * Clone the project file with a preview suffix and set the initial attribute to the currently
+ * open document
+ *
+ * @return path to the preview project file. Return path to .uip or preview .uip file if there
+ * is no project file.
+ */
+QString ProjectFile::createPreview()
+{
+ CDoc *doc = g_StudioApp.GetCore()->GetDoc();
+ QString uipPrvPath = doc->GetDocumentPath();
+
+ // Commit all open transactions
+ doc->forceCloseTransaction();
+
+ // create a preview uip if doc modified
+ if (doc->isModified()) {
+ uipPrvPath.replace(QLatin1String(".uip"), QLatin1String("_@preview@.uip"));
+ g_StudioApp.GetCore()->OnSaveDocument(uipPrvPath, true);
+ }
+
+ // if no project file exist (.uia) just return the preview uip path
+ if (!m_fileInfo.exists())
+ return uipPrvPath;
+
+ // create a preview project file
+ QString prvPath = getProjectFilePath();
+ prvPath.replace(QLatin1String(".uia"), QLatin1String("_@preview@.uia"));
+
+ if (QFile::exists(prvPath))
+ QFile::remove(prvPath);
+
+ if (QFile::copy(getProjectFilePath(), prvPath)) {
+ QDomDocument domDoc;
+ QSaveFile file(prvPath);
+ if (StudioUtils::openDomDocumentSave(file, domDoc)) {
+ QDomElement assetsElem = domDoc.documentElement()
+ .firstChildElement(QStringLiteral("assets"));
+ assetsElem.setAttribute(QStringLiteral("initial"), doc->getPresentationId());
+
+ if (doc->isModified()) {
+ // Set the preview uip path in the uia file
+ QDomNodeList pNodes = assetsElem.elementsByTagName(QStringLiteral("presentation"));
+ for (int i = 0; i < pNodes.count(); ++i) {
+ QDomElement pElem = pNodes.at(i).toElement();
+ if (pElem.attribute(QStringLiteral("id")) == doc->getPresentationId()) {
+ QString src = QDir(getProjectPath()).relativeFilePath(uipPrvPath);
+ pElem.setAttribute(QStringLiteral("src"), src);
+ break;
+ }
+ }
+ }
+ StudioUtils::commitDomDocumentSave(file, domDoc);
+ }
+
+ return prvPath;
+ } else {
+ qWarning() << "Couldn't clone project file";
+ }
+
+ return {};
+}
+
+void ProjectFile::parseDataInputElem(const QDomElement &elem,
+ QMap<QString, CDataInputDialogItem *> &dataInputs)
+{
+ if (elem.nodeName() == QLatin1String("dataInput")) {
+ CDataInputDialogItem *item = new CDataInputDialogItem();
+ item->name = elem.attribute(QStringLiteral("name"));
+ QString type = elem.attribute(QStringLiteral("type"));
+ if (type == QLatin1String("Ranged Number")) {
+ item->type = EDataType::DataTypeRangedNumber;
+ item->minValue = elem.attribute(QStringLiteral("min")).toFloat();
+ item->maxValue = elem.attribute(QStringLiteral("max")).toFloat();
+ } else if (type == QLatin1String("String")) {
+ item->type = EDataType::DataTypeString;
+ } else if (type == QLatin1String("Float")) {
+ item->type = EDataType::DataTypeFloat;
+ } else if (type == QLatin1String("Boolean")) {
+ item->type = EDataType::DataTypeBoolean;
+ } else if (type == QLatin1String("Vector4")) {
+ item->type = EDataType::DataTypeVector4;
+ } else if (type == QLatin1String("Vector3")) {
+ item->type = EDataType::DataTypeVector3;
+ } else if (type == QLatin1String("Vector2")) {
+ item->type = EDataType::DataTypeVector2;
+ } else if (type == QLatin1String("Variant")) {
+ item->type = EDataType::DataTypeVariant;
+ }
+
+ auto metadata = elem.attribute(QStringLiteral("metadata"));
+ if (!metadata.isEmpty()) {
+ auto metadataList = metadata.split(QLatin1Char('$'));
+
+ if (metadataList.size() & 1) {
+ qWarning("Malformed datainput metadata for datainput %s, cannot parse key"
+ " - value pairs. Stop parsing metadata.", qUtf8Printable(item->name));
+ } else {
+ for (int i = 0; i < metadataList.size(); i += 2) {
+ if (metadataList[i].isEmpty()) {
+ qWarning("Malformed datainput metadata for datainput %s - metadata"
+ " key empty. Stop parsing metadata.", qUtf8Printable(item->name));
+ break;
+ }
+ item->metadata.insert(metadataList[i], metadataList[i+1]);
+ }
+ }
+ }
+ dataInputs.insert(item->name, item);
+ }
+}
+
+void ProjectFile::loadDataInputs(const QString &projFile,
+ QMap<QString, CDataInputDialogItem *> &dataInputs)
+{
+ QFileInfo fi(projFile);
+ if (fi.exists()) {
+ QDomDocument doc;
+ if (!StudioUtils::readFileToDomDocument(projFile, doc))
+ return;
+ QDomElement assetsElem = doc.documentElement().firstChildElement(QStringLiteral("assets"));
+ if (!assetsElem.isNull()) {
+ for (QDomElement p = assetsElem.firstChild().toElement(); !p.isNull();
+ p = p.nextSibling().toElement()) {
+ parseDataInputElem(p, dataInputs);
+ }
+ }
+ }
+}
+
+void ProjectFile::loadSubpresentationsAndDatainputs(
+ QVector<SubPresentationRecord> &subpresentations,
+ QMap<QString, CDataInputDialogItem *> &datainputs)
+{
+ if (!m_fileInfo.exists())
+ return;
+
+ subpresentations.clear();
+ datainputs.clear();
+
+ m_initialPresentation = g_StudioApp.GetCore()->GetDoc()->getPresentationId();
+
+ QDomDocument doc;
+ QSaveFile projectFile(getProjectFilePath());
+ if (!StudioUtils::openDomDocumentSave(projectFile, doc))
+ return;
+
+ QVector<QDomElement> removedElements;
+ QDomElement assetsElem = doc.documentElement().firstChildElement(QStringLiteral("assets"));
+ if (!assetsElem.isNull()) {
+ QString initial = assetsElem.attribute(QStringLiteral("initial"));
+ if (!initial.isEmpty())
+ m_initialPresentation = initial;
+ for (QDomElement p = assetsElem.firstChild().toElement(); !p.isNull();
+ p = p.nextSibling().toElement()) {
+ if ((p.nodeName() == QLatin1String("presentation")
+ || p.nodeName() == QLatin1String("presentation-qml"))
+ && p.attribute(QStringLiteral("id"))
+ != g_StudioApp.GetCore()->GetDoc()->getPresentationId()) {
+ QString argsOrSrc = p.attribute(QStringLiteral("src"));
+ if (argsOrSrc.isNull())
+ argsOrSrc = p.attribute(QStringLiteral("args"));
+ // Skip non-existent presentations (they have been manually deleted)
+ if (QFileInfo().exists(getAbsoluteFilePathTo(argsOrSrc))) {
+ subpresentations.push_back(
+ SubPresentationRecord(p.nodeName(), p.attribute("id"), argsOrSrc));
+ } else {
+ removedElements.append(p);
+ }
+ } else {
+ parseDataInputElem(p, datainputs);
+ }
+ }
+ }
+ if (removedElements.size()) {
+ for (auto &elem : qAsConst(removedElements))
+ assetsElem.removeChild(elem);
+ StudioUtils::commitDomDocumentSave(projectFile, doc);
+ }
+
+ g_StudioApp.GetCore()->GetDoc()->UpdateDatainputMap();
+}
+
+/**
+ * Check that a given presentation's or Qml stream's id is unique
+ *
+ * @param id presentation's or Qml stream's Id
+ * @param src source node to exclude from the check. Defaults to empty.
+ */
+bool ProjectFile::isUniquePresentationId(const QString &id, const QString &src) const
+{
+ if (!m_fileInfo.exists())
+ return true;
+
+ bool isCurrDoc = src == g_StudioApp.GetCore()->GetDoc()->getRelativePath();
+
+ if (!isCurrDoc && id == g_StudioApp.GetCore()->GetDoc()->getPresentationId())
+ return false;
+
+ auto *sp = std::find_if(g_StudioApp.m_subpresentations.begin(),
+ g_StudioApp.m_subpresentations.end(),
+ [&id, &src](const SubPresentationRecord &spr) -> bool {
+ return spr.m_id == id && spr.m_argsOrSrc != src;
+ });
+ return sp == g_StudioApp.m_subpresentations.end();
+}
+
+// Returns unique presentation name based on given relative presentation path
+// Only the file name base is returned, no path or suffix.
+QString ProjectFile::getUniquePresentationName(const QString &presSrc) const
+{
+ if (!m_fileInfo.exists())
+ return {};
+
+ const QString fullPresSrc = getAbsoluteFilePathTo(presSrc);
+ QFileInfo fi(fullPresSrc);
+ const QStringList files = fi.dir().entryList(QDir::Files);
+ QString checkName = fi.fileName();
+ if (files.contains(checkName)) {
+ const QString nameTemplate = QStringLiteral("%1%2.%3");
+ const QString suffix = fi.suffix();
+ QString base = fi.completeBaseName();
+ int counter = 0;
+ int checkIndex = base.size();
+ while (checkIndex > 1 && base.at(checkIndex - 1).isDigit())
+ --checkIndex;
+ if (checkIndex < base.size())
+ counter = base.mid(checkIndex).toInt();
+
+ if (counter > 0)
+ base = base.left(checkIndex);
+
+ while (files.contains(checkName))
+ checkName = nameTemplate.arg(base).arg(++counter).arg(suffix);
+ }
+
+ return QFileInfo(checkName).completeBaseName();
+}
+
+QString ProjectFile::ensureUniquePresentationId(const QString &id) const
+{
+ if (!m_fileInfo.exists())
+ return id;
+
+ QDomDocument doc;
+ if (!StudioUtils::readFileToDomDocument(m_fileInfo.filePath(), doc))
+ return id;
+
+ QString newId = id;
+ QDomElement assetsElem = doc.documentElement().firstChildElement(QStringLiteral("assets"));
+ if (!assetsElem.isNull()) {
+ bool unique;
+ int n = 1;
+ do {
+ unique = true;
+ for (QDomElement p = assetsElem.firstChild().toElement(); !p.isNull();
+ p = p.nextSibling().toElement()) {
+ if ((p.nodeName() == QLatin1String("presentation")
+ || p.nodeName() == QLatin1String("presentation-qml"))
+ && p.attribute(QStringLiteral("id")) == newId) {
+ newId = id + QString::number(n++);
+ unique = false;
+ break;
+ }
+ }
+ } while (!unique);
+ }
+
+ return newId;
+}
+
+// Get the path to the project root. If .uia doesn't exist, return path to current presentation.
+QString ProjectFile::getProjectPath() const
+{
+ if (m_fileInfo.exists())
+ return m_fileInfo.path();
+ else
+ return QFileInfo(g_StudioApp.GetCore()->GetDoc()->GetDocumentPath()).absolutePath();
+}
+
+// Get the path to the project's .uia file. If .uia doesn't exist, return empty string.
+QString ProjectFile::getProjectFilePath() const
+{
+ if (m_fileInfo.exists())
+ return m_fileInfo.filePath();
+ else
+ return {};
+}
+
+// Returns current project name or empty string if there is no .uia file
+QString ProjectFile::getProjectName() const
+{
+ if (m_fileInfo.exists())
+ return m_fileInfo.completeBaseName();
+ else
+ return {};
+}
+
+/**
+ * Get presentations out of a uia file
+ *
+ * @param inUiaPath uia file path
+ * @param outSubpresentations list of collected presentations
+ * @param excludePresentationSrc execluded presentation, (commonly the current presentation)
+ */
+// static
+void ProjectFile::getPresentations(const QString &inUiaPath,
+ QVector<SubPresentationRecord> &outSubpresentations,
+ const QString &excludePresentationSrc)
+{
+ QFile file(inUiaPath);
+ if (!file.open(QFile::Text | QFile::ReadOnly)) {
+ qWarning() << file.errorString();
+ return;
+ }
+
+ QXmlStreamReader reader(&file);
+ reader.setNamespaceProcessing(false);
+
+ while (!reader.atEnd()) {
+ if (reader.readNextStartElement()
+ && (reader.name() == QLatin1String("presentation")
+ || reader.name() == QLatin1String("presentation-qml"))) {
+ const auto attrs = reader.attributes();
+ QString argsOrSrc = attrs.value(QLatin1String("src")).toString();
+ if (excludePresentationSrc == argsOrSrc)
+ continue;
+ if (argsOrSrc.isNull())
+ argsOrSrc = attrs.value(QLatin1String("args")).toString();
+
+ outSubpresentations.push_back(
+ SubPresentationRecord(reader.name().toString(),
+ attrs.value(QLatin1String("id")).toString(),
+ argsOrSrc));
+ } else if (reader.name() == QLatin1String("assets") && !reader.isStartElement()) {
+ break; // reached end of <assets>
+ }
+ }
+}
+
+void ProjectFile::setInitialPresentation(const QString &initialId)
+{
+ if (!initialId.isEmpty() && m_initialPresentation != initialId) {
+ m_initialPresentation = initialId;
+
+ ensureProjectFile();
+
+ QDomDocument domDoc;
+ QSaveFile file(getProjectFilePath());
+ if (!StudioUtils::openDomDocumentSave(file, domDoc))
+ return;
+
+ QDomElement assetsElem
+ = domDoc.documentElement().firstChildElement(QStringLiteral("assets"));
+ if (!assetsElem.isNull() && assetsElem.attribute(QStringLiteral("initial"))
+ != m_initialPresentation) {
+ assetsElem.setAttribute(QStringLiteral("initial"), m_initialPresentation);
+
+ StudioUtils::commitDomDocumentSave(file, domDoc);
+ }
+ }
+}
+
+// Returns true if file rename was successful. The parameters are relative to project root.
+bool ProjectFile::renamePresentationFile(const QString &oldName, const QString &newName)
+{
+ const QString fullOldPath = getAbsoluteFilePathTo(oldName);
+ const QString fullNewPath = getAbsoluteFilePathTo(newName);
+ QFile presFile(fullOldPath);
+ const bool success = presFile.rename(fullNewPath);
+
+ if (success) {
+ // Update assets in .uia
+ ensureProjectFile();
+
+ const bool isQml = oldName.endsWith(QLatin1String(".qml"));
+
+ if (isQml && g_StudioApp.m_qmlStreamMap.contains(fullOldPath)) {
+ // Update Qml stream type cache
+ g_StudioApp.m_qmlStreamMap.remove(fullOldPath);
+ g_StudioApp.m_qmlStreamMap.insert(fullNewPath, true);
+ }
+
+ QDomDocument domDoc;
+ QSaveFile file(getProjectFilePath());
+ if (!StudioUtils::openDomDocumentSave(file, domDoc))
+ return false;
+
+ QDomElement assetsElem
+ = domDoc.documentElement().firstChildElement(QStringLiteral("assets"));
+ if (!assetsElem.isNull()) {
+ QDomNodeList pqNodes
+ = isQml ? assetsElem.elementsByTagName(QStringLiteral("presentation-qml"))
+ : assetsElem.elementsByTagName(QStringLiteral("presentation"));
+ if (!pqNodes.isEmpty()) {
+ CDoc *doc = g_StudioApp.GetCore()->GetDoc();
+ for (int i = 0; i < pqNodes.count(); ++i) {
+ QDomElement pqElem = pqNodes.at(i).toElement();
+ const QString attTag = isQml ? QStringLiteral("args") : QStringLiteral("src");
+ const QString srcOrArgs = pqElem.attribute(attTag);
+ if (srcOrArgs == oldName) {
+ pqElem.setAttribute(attTag, newName);
+
+ if (pqElem.attribute(QStringLiteral("id")) != doc->getPresentationId()) {
+ // update m_subpresentations
+ auto *sp = std::find_if(
+ g_StudioApp.m_subpresentations.begin(),
+ g_StudioApp.m_subpresentations.end(),
+ [&oldName](const SubPresentationRecord &spr) -> bool {
+ return spr.m_argsOrSrc == oldName;
+ });
+ if (sp != g_StudioApp.m_subpresentations.end())
+ sp->m_argsOrSrc = newName;
+ } else {
+ // If renaming current presentation, need to update the doc path, too
+ doc->SetDocumentPath(fullNewPath);
+ }
+
+ StudioUtils::commitDomDocumentSave(file, domDoc);
+
+ Q_EMIT assetNameChanged();
+ break;
+ }
+ }
+ }
+ }
+ }
+
+ return success;
+}
+
+/**
+ * Delete a presentation (or qml-stream) file and remove references to it from the project file.
+ * This function assumes the removed presentation is not referenced by any presentation
+ * in the project and is not the current presentation.
+ *
+ * @param filePath Absolute file path to presentation to delete
+ */
+void ProjectFile::deletePresentationFile(const QString &filePath)
+{
+ QFile(filePath).remove();
+
+ if (m_fileInfo.exists()) {
+ const QString relPath = getRelativeFilePathTo(filePath);
+ const bool isQml = relPath.endsWith(QLatin1String(".qml"));
+
+ // Update records and caches
+ if (isQml && g_StudioApp.m_qmlStreamMap.contains(filePath))
+ g_StudioApp.m_qmlStreamMap.remove(filePath);
+ for (int i = 0, count = g_StudioApp.m_subpresentations.size(); i < count; ++i) {
+ SubPresentationRecord &rec = g_StudioApp.m_subpresentations[i];
+ if (rec.m_argsOrSrc == relPath) {
+ g_StudioApp.m_subpresentations.remove(i);
+ break;
+ }
+ }
+
+ // Update project file
+ QDomDocument domDoc;
+ QSaveFile projectFile(getProjectFilePath());
+ if (!StudioUtils::openDomDocumentSave(projectFile, domDoc))
+ return;
+
+ QDomElement assetsElem
+ = domDoc.documentElement().firstChildElement(QStringLiteral("assets"));
+ if (!assetsElem.isNull()) {
+ QDomNodeList pqNodes
+ = isQml ? assetsElem.elementsByTagName(QStringLiteral("presentation-qml"))
+ : assetsElem.elementsByTagName(QStringLiteral("presentation"));
+ if (!pqNodes.isEmpty()) {
+ for (int i = 0; i < pqNodes.count(); ++i) {
+ QDomElement pqElem = pqNodes.at(i).toElement();
+ const QString attTag = isQml ? QStringLiteral("args") : QStringLiteral("src");
+ const QString srcOrArgs = pqElem.attribute(attTag);
+ if (srcOrArgs == relPath) {
+ const QString id = pqElem.attribute(QStringLiteral("id"));
+ // If initial presentation is deleted, change current to initial
+ if (assetsElem.attribute(QStringLiteral("initial")) == id) {
+ m_initialPresentation
+ = g_StudioApp.GetCore()->GetDoc()->getPresentationId();
+ assetsElem.setAttribute(QStringLiteral("initial"),
+ m_initialPresentation);
+ }
+ assetsElem.removeChild(pqNodes.at(i));
+ StudioUtils::commitDomDocumentSave(projectFile, domDoc);
+ break;
+ }
+ }
+ }
+ }
+ // Update registrations asynchronously, as it messes with event processing, which can
+ // cause issues with file models elsewhere in the editor unless file removal is fully
+ // handled.
+ QTimer::singleShot(0, []() {
+ g_StudioApp.getRenderer().RegisterSubpresentations(g_StudioApp.m_subpresentations);
+ });
+ }
+}
+
+void ProjectFile::renameMaterial(const QString &oldName, const QString &newName)
+{
+ for (auto &pres : qAsConst(g_StudioApp.m_subpresentations)) {
+ if (pres.m_type == QLatin1String("presentation")) {
+ PresentationFile::renameMaterial(getAbsoluteFilePathTo(pres.m_argsOrSrc),
+ oldName, newName);
+ }
+ }
+ Q_EMIT assetNameChanged();
+}
+
+// Copies oldPres presentation as newPres. The id for newPres will be autogenerated.
+bool ProjectFile::duplicatePresentation(const QString &oldPres, const QString &newPres)
+{
+ const QString fullOldPath = getAbsoluteFilePathTo(oldPres);
+ const QString fullNewPath = getAbsoluteFilePathTo(newPres);
+ const bool success = QFile::copy(fullOldPath, fullNewPath);
+
+ if (success)
+ addPresentationNode(fullNewPath, {});
+
+ return success;
+}
+
+/**
+ * Returns an absolute file path for a given relative file path.
+ *
+ * @param relFilePath A file path relative to project root to convert.
+ */
+QString ProjectFile::getAbsoluteFilePathTo(const QString &relFilePath) const
+{
+ auto projectPath = QDir(getProjectPath()).absoluteFilePath(relFilePath);
+ return QDir::cleanPath(projectPath);
+}
+
+/**
+ * Returns a file path relative to the project root for given absolute file path.
+ *
+ * @param absFilePath An absolute file path to convert.
+ */
+QString ProjectFile::getRelativeFilePathTo(const QString &absFilePath) const
+{
+ return QDir(getProjectPath()).relativeFilePath(absFilePath);
+}
+
+// Return multimap of type subpresentationid - DataInputOutputBinding
+QMultiHash<QString, ProjectFile::DataInputOutputBinding>
+ProjectFile::getDiBindingtypesFromSubpresentations() const
+{
+ QMultiHash<QString, DataInputOutputBinding> map;
+ for (auto sp : qAsConst(g_StudioApp.m_subpresentations))
+ PresentationFile::getDataInputBindings(sp, map);
+
+ return map;
+}
+
+/**
+ * Load variants data to m_variantsDef
+ *
+ * @param filePath the file path to load the variants from. If empty, variants are loaded from the
+ * project file and replace m_variantsDef. If a filePath is specified, the loaded
+ * variants are merged with m_variantsDef.
+ */
+void ProjectFile::loadVariants(const QString &filePath)
+{
+ if (!m_fileInfo.exists())
+ return;
+
+ bool isProj = filePath.isEmpty() || filePath == getProjectFilePath();
+ QFile file(isProj ? getProjectFilePath() : filePath);
+ if (!file.open(QFile::Text | QFile::ReadOnly)) {
+ qWarning() << file.errorString();
+ return;
+ }
+
+ if (isProj) {
+ m_variantsDef.clear();
+ m_variantsDefKeys.clear();
+ }
+
+ QXmlStreamReader reader(&file);
+ reader.setNamespaceProcessing(false);
+
+ VariantGroup *currentGroup = nullptr;
+ while (!reader.atEnd()) {
+ if (reader.readNextStartElement()) {
+ if (reader.name() == QLatin1String("variantgroup")) {
+ QString groupId = reader.attributes().value(QLatin1String("id")).toString();
+ currentGroup = &m_variantsDef[groupId];
+ if (!m_variantsDefKeys.contains(groupId)) {
+ // Only update colors for new groups
+ currentGroup->m_color = reader.attributes().value(
+ QLatin1String("color")).toString();
+ m_variantsDefKeys.append(groupId);
+ }
+ } else if (reader.name() == QLatin1String("variant")) {
+ if (currentGroup) {
+ QString tagId = reader.attributes().value(QLatin1String("id")).toString();
+ if (!currentGroup->m_tags.contains(tagId))
+ currentGroup->m_tags.append(tagId);
+ } else {
+ qWarning() << "Error parsing variant tags.";
+ }
+ } else if (currentGroup) {
+ break;
+ }
+ }
+ }
+
+ if (!isProj) {
+ // if loading variants from a file, update the uia
+ QDomDocument domDoc;
+ QSaveFile fileProj(getProjectFilePath());
+ if (!StudioUtils::openDomDocumentSave(fileProj, domDoc))
+ return;
+
+ QDomElement vElem = domDoc.documentElement().firstChildElement(QStringLiteral("variants"));
+ if (!vElem.isNull())
+ domDoc.documentElement().removeChild(vElem);
+
+ vElem = domDoc.createElement(QStringLiteral("variants"));
+ domDoc.documentElement().appendChild(vElem);
+
+ for (auto &g : qAsConst(m_variantsDefKeys)) {
+ QDomElement gElem = domDoc.createElement(QStringLiteral("variantgroup"));
+ gElem.setAttribute(QStringLiteral("id"), g);
+ gElem.setAttribute(QStringLiteral("color"), m_variantsDef[g].m_color);
+ vElem.appendChild(gElem);
+
+ for (auto &t : qAsConst(m_variantsDef[g].m_tags)) {
+ QDomElement tElem = domDoc.createElement(QStringLiteral("variant"));
+ tElem.setAttribute(QStringLiteral("id"), t);
+ gElem.appendChild(tElem);
+ }
+ }
+
+ StudioUtils::commitDomDocumentSave(fileProj, domDoc);
+ }
+
+ g_StudioApp.m_pMainWnd->updateActionFilterEnableState();
+}
+
+// Add a new tag to a variants group
+void ProjectFile::addVariantTag(const QString &group, const QString &newTag)
+{
+ QDomDocument domDoc;
+ QSaveFile file(getProjectFilePath());
+ if (!StudioUtils::openDomDocumentSave(file, domDoc))
+ return;
+
+ QDomElement newTagElem = domDoc.createElement(QStringLiteral("variant"));
+ newTagElem.setAttribute(QStringLiteral("id"), newTag);
+
+ QDomNodeList groupsElems = domDoc.documentElement()
+ .firstChildElement(QStringLiteral("variants"))
+ .elementsByTagName(QStringLiteral("variantgroup"));
+
+ // update and save the uia
+ for (int i = 0; i < groupsElems.count(); ++i) {
+ QDomElement gElem = groupsElems.at(i).toElement();
+ if (gElem.attribute(QStringLiteral("id")) == group) {
+ gElem.appendChild(newTagElem);
+ StudioUtils::commitDomDocumentSave(file, domDoc);
+ break;
+ }
+ }
+
+ // update m_variantsDef
+ m_variantsDef[group].m_tags.append(newTag);
+}
+
+// Add a new group, it is assumes that the new group name is unique
+void ProjectFile::addVariantGroup(const QString &newGroup)
+{
+ QDomDocument domDoc;
+ QSaveFile file(getProjectFilePath());
+ if (!StudioUtils::openDomDocumentSave(file, domDoc))
+ return;
+
+ QDomElement variantsElem = domDoc.documentElement()
+ .firstChildElement(QStringLiteral("variants"));
+
+ if (variantsElem.isNull()) {
+ QDomElement newVariantsElem = domDoc.createElement(QStringLiteral("variants"));
+ domDoc.documentElement().appendChild(newVariantsElem);
+ variantsElem = newVariantsElem;
+ }
+
+ // a set of predefined variant colors to assign to newly created groups
+ static const QStringList VARIANT_COLORS {
+ QStringLiteral("#06c4f4"), QStringLiteral("#f7752a"),
+ QStringLiteral("#d6c605"), QStringLiteral("#ff00ff"),
+ QStringLiteral("#725de8"), QStringLiteral("#8cc63f"),
+ QStringLiteral("#0071bc"), QStringLiteral("#ed1e79"),
+ QStringLiteral("#f9b406"), QStringLiteral("#74c905"),
+ QStringLiteral("#93278f"), QStringLiteral("#d9e021"),
+ QStringLiteral("#00a99d"), QStringLiteral("#c1272d"),
+ QStringLiteral("#f7931e"), QStringLiteral("#f45d85"),
+ QStringLiteral("#682e7a"), QStringLiteral("#05e2d6"),
+ QStringLiteral("#0000ff"), QStringLiteral("#ff0000")
+ };
+
+ if (m_variantColorsIter == -1) { // initialize m_variantColorsIter
+ m_variantColorsIter = 0;
+ if (!m_variantsDefKeys.isEmpty()) {
+ QString lastGroup = m_variantsDefKeys[m_variantsDefKeys.size() - 1];
+ QString lastGroupColor = m_variantsDef[lastGroup].m_color;
+ for (int i = VARIANT_COLORS.length() - 1; i >= 0; --i) {
+ if (VARIANT_COLORS[i] == lastGroupColor) {
+ m_variantColorsIter = i + 1;
+ break;
+ }
+ }
+ }
+ }
+
+ QString newColor = VARIANT_COLORS[m_variantColorsIter++ % VARIANT_COLORS.size()];
+ QDomElement newGroupElem = domDoc.createElement(QStringLiteral("variantgroup"));
+ newGroupElem.setAttribute(QStringLiteral("id"), newGroup);
+ newGroupElem.setAttribute(QStringLiteral("color"), newColor);
+ variantsElem.appendChild(newGroupElem);
+ StudioUtils::commitDomDocumentSave(file, domDoc);
+
+ // update m_variantsDef
+ VariantGroup g;
+ g.m_color = newColor;
+ m_variantsDef[newGroup] = g;
+ m_variantsDefKeys.append(newGroup);
+}
+
+void ProjectFile::renameVariantTag(const QString &group, const QString &oldTag,
+ const QString &newTag)
+{
+ QDomDocument domDoc;
+ QSaveFile file(getProjectFilePath());
+ if (!StudioUtils::openDomDocumentSave(file, domDoc))
+ return;
+
+ // rename the tag in all uip files
+ QDomNodeList presElems = domDoc.documentElement()
+ .firstChildElement(QStringLiteral("assets"))
+ .elementsByTagName(QStringLiteral("presentation"));
+ for (int i = 0; i < presElems.count(); ++i) {
+ QString pPath = m_fileInfo.path() + QLatin1Char('/')
+ + presElems.at(i).toElement().attribute(QStringLiteral("src"));
+ updateVariantsInUip(pPath, VariantsUpdateMode::Rename, group, oldTag, newTag);
+ }
+
+ // update and save the uia
+ QDomNodeList groupsElems = domDoc.documentElement()
+ .firstChildElement(QStringLiteral("variants"))
+ .elementsByTagName(QStringLiteral("variantgroup"));
+
+ bool renamed = false;
+ for (int i = 0; i < groupsElems.count(); ++i) {
+ QDomElement gElem = groupsElems.at(i).toElement();
+ if (gElem.attribute(QStringLiteral("id")) == group) {
+ QDomNodeList tagsElems = gElem.childNodes();
+ for (int j = 0; j < tagsElems.count(); ++j) {
+ QDomElement tElem = tagsElems.at(j).toElement();
+ if (tElem.attribute(QStringLiteral("id")) == oldTag) {
+ tElem.setAttribute(QStringLiteral("id"), newTag);
+ StudioUtils::commitDomDocumentSave(file, domDoc);
+ renamed = true;
+ break;
+ }
+ }
+ if (renamed)
+ break;
+ }
+ }
+
+ // update the property
+ CDoc *doc = g_StudioApp.GetCore()->GetDoc();
+ const auto propertySystem = doc->GetStudioSystem()->GetPropertySystem();
+ const auto bridge = doc->GetStudioSystem()->GetClientDataModelBridge();
+ const QVector<int> instances = doc->getVariantInstances();
+ for (auto instance : instances) {
+ auto property = bridge->getVariantsProperty(instance);
+ qt3dsdm::SValue sValue;
+ if (propertySystem->GetInstancePropertyValue(instance, property, sValue)) {
+ QString propVal = qt3dsdm::get<qt3dsdm::TDataStrPtr>(sValue)->toQString();
+ QString oldGroupTagPair = QStringLiteral("%1:%2").arg(group).arg(oldTag);
+ if (propVal.contains(oldGroupTagPair)) {
+ propVal.replace(oldGroupTagPair, QStringLiteral("%1:%2").arg(group).arg(newTag));
+ propertySystem->SetInstancePropertyValue(instance, property, QVariant(propVal));
+ }
+ }
+ }
+
+ // update m_variantsDef
+ for (auto &t : m_variantsDef[group].m_tags) {
+ if (t == oldTag) {
+ t = newTag;
+ renamed = true;
+ break;
+ }
+ }
+}
+
+// rename a variant group, newGroup is assumed to be unique
+void ProjectFile::renameVariantGroup(const QString &oldGroup, const QString &newGroup)
+{
+ QDomDocument domDoc;
+ QSaveFile file(getProjectFilePath());
+ if (!StudioUtils::openDomDocumentSave(file, domDoc))
+ return;
+
+ // rename the group in all uip files
+ QDomNodeList presElems = domDoc.documentElement()
+ .firstChildElement(QStringLiteral("assets"))
+ .elementsByTagName(QStringLiteral("presentation"));
+ for (int i = 0; i < presElems.count(); ++i) {
+ QString pPath = m_fileInfo.path() + QLatin1Char('/')
+ + presElems.at(i).toElement().attribute(QStringLiteral("src"));
+ updateVariantsInUip(pPath, VariantsUpdateMode::Rename, oldGroup, {}, newGroup);
+ }
+
+ // update and save the uia
+ QDomNodeList groupsElems = domDoc.documentElement()
+ .firstChildElement(QStringLiteral("variants"))
+ .elementsByTagName(QStringLiteral("variantgroup"));
+
+ for (int i = 0; i < groupsElems.count(); ++i) {
+ QDomElement gElem = groupsElems.at(i).toElement();
+ if (gElem.attribute(QStringLiteral("id")) == oldGroup) {
+ gElem.setAttribute(QStringLiteral("id"), newGroup);
+ StudioUtils::commitDomDocumentSave(file, domDoc);
+ break;
+ }
+ }
+
+ // update the property
+ CDoc *doc = g_StudioApp.GetCore()->GetDoc();
+ const auto propertySystem = doc->GetStudioSystem()->GetPropertySystem();
+ const auto bridge = doc->GetStudioSystem()->GetClientDataModelBridge();
+ const QVector<int> instances = doc->getVariantInstances();
+ for (auto instance : instances) {
+ auto property = bridge->getVariantsProperty(instance);
+ qt3dsdm::SValue sValue;
+ if (propertySystem->GetInstancePropertyValue(instance, property, sValue)) {
+ QString propVal = qt3dsdm::get<qt3dsdm::TDataStrPtr>(sValue)->toQString();
+ QString oldGroupWithColon = QStringLiteral("%1:").arg(oldGroup);
+ if (propVal.contains(oldGroupWithColon)) {
+ propVal.replace(oldGroupWithColon, QStringLiteral("%1:").arg(newGroup));
+ propertySystem->SetInstancePropertyValue(instance, property, QVariant(propVal));
+ }
+ }
+ }
+
+ // update m_variantsDef
+ m_variantsDef[newGroup] = m_variantsDef[oldGroup];
+ m_variantsDef.remove(oldGroup);
+ for (auto &g : m_variantsDefKeys) {
+ if (g == oldGroup) {
+ g = newGroup;
+ break;
+ }
+ }
+}
+
+void ProjectFile::deleteVariantGroup(const QString &group)
+{
+ CDoc *doc = g_StudioApp.GetCore()->GetDoc();
+
+ QDomDocument domDoc;
+ QSaveFile file(getProjectFilePath());
+ if (!StudioUtils::openDomDocumentSave(file, domDoc))
+ return;
+
+ // check if group is in use in other presentations in the porject
+ int inUseIdx = -1; // index of first presentation that has the group in-use
+ QDomNodeList presElems = domDoc.documentElement()
+ .firstChildElement(QStringLiteral("assets"))
+ .elementsByTagName(QStringLiteral("presentation"));
+ for (int i = 0; i < presElems.count(); ++i) {
+ QString pPath = m_fileInfo.path() + QLatin1Char('/')
+ + presElems.at(i).toElement().attribute(QStringLiteral("src"));
+ if (pPath != doc->GetDocumentPath() && tagExistsInUip(pPath, group)) {
+ inUseIdx = i;
+ break;
+ }
+ }
+
+ if (inUseIdx != -1) {
+ QMessageBox box;
+ box.setWindowTitle(tr("Group tags in use"));
+ box.setText(tr("Some tags in the Group '%1' are in use in the project, are you sure you"
+ " want to delete the group?").arg(group));
+ box.setIcon(QMessageBox::Warning);
+ box.setStandardButtons(QMessageBox::Yes | QMessageBox::Cancel);
+ box.setButtonText(QMessageBox::Yes, tr("Delete"));
+ switch (box.exec()) {
+ case QMessageBox::Yes:
+ // delete the group from all uips that use it
+ for (int i = inUseIdx; i < presElems.count(); ++i) {
+ QString pPath = m_fileInfo.path() + QLatin1Char('/')
+ + presElems.at(i).toElement().attribute(QStringLiteral("src"));
+ if (pPath != doc->GetDocumentPath())
+ updateVariantsInUip(pPath, VariantsUpdateMode::Delete, group);
+ }
+ break;
+
+ default:
+ // abort deletion
+ return;
+ }
+ }
+
+ // delete the group from current uip, if exists
+ updateVariantsInUip(doc->GetDocumentPath(), VariantsUpdateMode::Delete, group);
+
+ // delete the group from the property (if set)
+ const auto propertySystem = doc->GetStudioSystem()->GetPropertySystem();
+ const auto bridge = doc->GetStudioSystem()->GetClientDataModelBridge();
+ const QVector<int> instances = doc->getVariantInstances();
+ for (auto instance : instances) {
+ auto property = bridge->getVariantsProperty(instance);
+ qt3dsdm::SValue sValue;
+ if (propertySystem->GetInstancePropertyValue(instance, property, sValue)) {
+ QString propVal = qt3dsdm::get<qt3dsdm::TDataStrPtr>(sValue)->toQString();
+ if (propVal.contains(QStringLiteral("%1:").arg(group))) {
+ // property has the deleted group, need to update it, else the deleted group
+ // will be saved the uip if the user saves the presentation.
+ QRegExp rgx(QStringLiteral("%1:\\w*,*|,%1:\\w*").arg(group));
+ propVal.replace(rgx, {});
+ propertySystem->SetInstancePropertyValue(instance, property, QVariant(propVal));
+ }
+ }
+ }
+
+ // update and save the uia
+ QDomElement variantsElem = domDoc.documentElement()
+ .firstChildElement(QStringLiteral("variants"));
+ QDomNodeList groupsElems = variantsElem.elementsByTagName(QStringLiteral("variantgroup"));
+
+ for (int i = 0; i < groupsElems.count(); ++i) {
+ QDomElement gElem = groupsElems.at(i).toElement();
+ if (gElem.attribute(QStringLiteral("id")) == group) {
+ variantsElem.removeChild(gElem);
+ StudioUtils::commitDomDocumentSave(file, domDoc);
+ break;
+ }
+ }
+
+ // update m_variantsDef
+ m_variantsDef.remove(group);
+ m_variantsDefKeys.removeOne(group);
+}
+
+void ProjectFile::changeVariantGroupColor(const QString &group, const QString &newColor)
+{
+ QDomDocument domDoc;
+ QSaveFile file(getProjectFilePath());
+ if (!StudioUtils::openDomDocumentSave(file, domDoc))
+ return;
+
+ // update and save the uia
+ QDomNodeList groupsElems = domDoc.documentElement()
+ .firstChildElement(QStringLiteral("variants"))
+ .elementsByTagName(QStringLiteral("variantgroup"));
+
+ for (int i = 0; i < groupsElems.count(); ++i) {
+ QDomElement gElem = groupsElems.at(i).toElement();
+ if (gElem.attribute(QStringLiteral("id")) == group) {
+ gElem.setAttribute(QStringLiteral("color"), newColor);
+ StudioUtils::commitDomDocumentSave(file, domDoc);
+ break;
+ }
+ }
+
+ // update m_variantsDef
+ m_variantsDef[group].m_color = newColor;
+}
+
+void ProjectFile::deleteVariantTag(const QString &group, const QString &tag)
+{
+ CDoc *doc = g_StudioApp.GetCore()->GetDoc();
+ QDomDocument domDoc;
+ QSaveFile file(getProjectFilePath());
+ if (!StudioUtils::openDomDocumentSave(file, domDoc))
+ return;
+
+ // check if tag is in use in other presentations in the porject
+ int inUseIdx = -1; // list of presentations that has the tag in use
+ QDomNodeList presElems = domDoc.documentElement()
+ .firstChildElement(QStringLiteral("assets"))
+ .elementsByTagName(QStringLiteral("presentation"));
+ for (int i = 0; i < presElems.count(); ++i) {
+ QString pPath = m_fileInfo.path() + QLatin1Char('/')
+ + presElems.at(i).toElement().attribute(QStringLiteral("src"));
+ if (pPath != doc->GetDocumentPath()
+ && tagExistsInUip(pPath, group, tag)) {
+ inUseIdx = i;
+ break;
+ }
+ }
+
+ if (inUseIdx != -1) {
+ QMessageBox box;
+ box.setWindowTitle(tr("Tag in use"));
+ box.setText(tr("The tag '%1' is in use in another presentation, are you sure you want to"
+ " delete it?").arg(tag));
+ box.setIcon(QMessageBox::Warning);
+ box.setStandardButtons(QMessageBox::Yes | QMessageBox::Cancel);
+ box.setButtonText(QMessageBox::Yes, tr("Delete"));
+ switch (box.exec()) {
+ case QMessageBox::Yes:
+ // delete the tag from all uips that use it
+ for (int i = inUseIdx; i < presElems.count(); ++i) {
+ QString pPath = m_fileInfo.path() + QLatin1Char('/')
+ + presElems.at(i).toElement().attribute(QStringLiteral("src"));
+ if (pPath != doc->GetDocumentPath())
+ updateVariantsInUip(pPath, VariantsUpdateMode::Delete, group, tag);
+ }
+ break;
+
+ default:
+ // abort deletion
+ return;
+ }
+ }
+
+ // delete the tag from current doc, if exists
+ updateVariantsInUip(doc->GetDocumentPath(), VariantsUpdateMode::Delete, group, tag);
+
+ QDomNodeList groupsElems = domDoc.documentElement()
+ .firstChildElement(QStringLiteral("variants"))
+ .elementsByTagName(QStringLiteral("variantgroup"));
+
+ // delete the tag from the property (if set)
+ const auto propertySystem = doc->GetStudioSystem()->GetPropertySystem();
+ const auto bridge = doc->GetStudioSystem()->GetClientDataModelBridge();
+ const QVector<int> instances = doc->getVariantInstances();
+ for (auto instance : instances) {
+ auto property = bridge->getVariantsProperty(instance);
+ qt3dsdm::SValue sValue;
+ if (propertySystem->GetInstancePropertyValue(instance, property, sValue)) {
+ QString propVal = qt3dsdm::get<qt3dsdm::TDataStrPtr>(sValue)->toQString();
+ if (propVal.contains(QStringLiteral("%1:%2").arg(group).arg(tag))) {
+ // property has the deleted tag, need to update it, else the deleted tag will be
+ // saved in the uip if the user saves the presentation.
+ QRegExp rgx(QStringLiteral("%1:%2,*|,%1:%2").arg(group).arg(tag));
+ propVal.replace(rgx, {});
+ propertySystem->SetInstancePropertyValue(instance, property, QVariant(propVal));
+ }
+ }
+ }
+
+ // update and save the uia
+ bool deleted = false;
+ for (int i = 0; i < groupsElems.count(); ++i) {
+ QDomElement gElem = groupsElems.at(i).toElement();
+ if (gElem.attribute(QStringLiteral("id")) == group) {
+ QDomNodeList tagsElems = gElem.childNodes();
+ for (int j = 0; j < tagsElems.count(); ++j) {
+ QDomElement tElem = tagsElems.at(j).toElement();
+ if (tElem.attribute(QStringLiteral("id")) == tag) {
+ gElem.removeChild(tElem);
+ StudioUtils::commitDomDocumentSave(file, domDoc);
+ deleted = true;
+ break;
+ }
+ }
+ if (deleted)
+ break;
+ }
+ }
+
+ // update m_variantsDef
+ m_variantsDef[group].m_tags.removeOne(tag);
+}
+
+// rename or delete a tag or group in a uip file
+void ProjectFile::updateVariantsInUip(const QString &src, const VariantsUpdateMode &updateType,
+ const QString &group, const QString &tag,
+ const QString &newName)
+{
+ QDomDocument domDoc;
+ QSaveFile file(src);
+ if (!StudioUtils::openDomDocumentSave(file, domDoc))
+ return;
+
+ QDomElement graphElem = domDoc.documentElement().firstChild()
+ .firstChildElement(QStringLiteral("Graph"));
+ QDomElement sceneElem = graphElem.firstChildElement(QStringLiteral("Scene"));
+
+ bool needsSave = false;
+ QString sceneStr;
+ QTextStream stream(&sceneStr);
+ sceneElem.save(stream, 4);
+
+ // if tag isEmpty() update group, else update tag
+
+ if (updateType == VariantsUpdateMode::Rename) { // rename a tag or a group
+ QString oldPair = group + QLatin1Char(':') + tag;
+ QString newPair = QStringLiteral("%1:%2").arg(tag.isEmpty() ? newName : group)
+ .arg(tag.isEmpty() ? tag : newName);
+ if (sceneStr.contains(oldPair)) {
+ sceneStr.replace(oldPair, newPair);
+ needsSave = true;
+ }
+ } else if (updateType == VariantsUpdateMode::Delete) { // delete a tag or a group
+ QRegExp rgx(tag.isEmpty() ? QStringLiteral("%1:\\w*,*|,%1:\\w*").arg(group)
+ : QStringLiteral("%1:%2,*|,%1:%2").arg(group).arg(tag));
+ if (rgx.indexIn(sceneStr) != -1) {
+ sceneStr.replace(rgx, "");
+ needsSave = true;
+ }
+ }
+
+ if (needsSave) {
+ QDomDocument sceneDom;
+ sceneDom.setContent(sceneStr);
+ graphElem.replaceChild(sceneDom, sceneElem);
+ StudioUtils::commitDomDocumentSave(file, domDoc);
+ }
+}
+
+// if tag param is empty, the method checks if a group exists
+bool ProjectFile::tagExistsInUip(const QString &src, const QString &group, const QString &tag) const
+{
+ QFile file(src);
+ if (!file.open(QFile::Text | QFile::ReadOnly)) {
+ qWarning() << file.errorString();
+ return false;
+ }
+
+ QXmlStreamReader reader(&file);
+ reader.setNamespaceProcessing(false);
+
+ while (!reader.atEnd()) {
+ if (reader.readNextStartElement()) {
+ if (reader.attributes().hasAttribute(QLatin1String("variants"))) {
+ QStringRef v = reader.attributes().value(QLatin1String("variants"));
+ if (v.contains(group + QLatin1Char(':') + tag))
+ return true;
+ } else if (reader.name() == QLatin1String("Logic")) {
+ break;
+ }
+ }
+ }
+
+ return false;
+}
+
+bool ProjectFile::isVariantGroupUnique(const QString &group) const
+{
+ return !m_variantsDef.contains(group);
+}
+
+bool ProjectFile::isVariantTagUnique(const QString &group, const QString &tag) const
+{
+ if (!m_variantsDef.contains(group))
+ return true;
+
+ return !m_variantsDef[group].m_tags.contains(tag);
+}