EffectMaker: Open saved compositions

- Also fixing issues related to image paths
- Composition name is shown in save dialog when re-save
- Clear current composition for reset or open a new one

Task-number: QDS-11192
Change-Id: I97aad4b5216e6b116343bb274db0f9abd1275fec
Reviewed-by: Mahmoud Badri <mahmoud.badri@qt.io>
This commit is contained in:
Amr Essam
2023-11-14 14:49:42 +02:00
committed by Amr Elsayed
parent de4c871655
commit 306ce4ab35
10 changed files with 181 additions and 99 deletions

View File

@@ -18,6 +18,7 @@ Item {
SaveDialog { SaveDialog {
id: saveDialog id: saveDialog
compositionName: EffectMakerBackend.effectMakerModel.currentComposition
anchors.centerIn: parent anchors.centerIn: parent
onAccepted: { onAccepted: {
let name = saveDialog.compositionName let name = saveDialog.compositionName

View File

@@ -21,7 +21,7 @@ StudioControls.Dialog {
property string compositionName: "" property string compositionName: ""
onOpened: { onOpened: {
nameText.text = "" //TODO: Generate unique name nameText.text = compositionName //TODO: Generate unique name
emptyText.text = "" emptyText.text = ""
nameText.forceActiveFocus() nameText.forceActiveFocus()
} }

View File

@@ -11,13 +11,36 @@
#include <QFile> #include <QFile>
#include <QJsonArray> #include <QJsonArray>
#include <QJsonDocument> #include <QJsonDocument>
#include <QJsonObject>
namespace EffectMaker { namespace EffectMaker {
CompositionNode::CompositionNode(const QString &qenPath) CompositionNode::CompositionNode(const QString &effectName, const QString &qenPath, const QJsonObject &jsonObject)
{ {
parse(qenPath); QJsonObject json;
if (jsonObject.isEmpty()) {
QFile qenFile(qenPath);
if (!qenFile.open(QIODevice::ReadOnly)) {
qWarning("Couldn't open effect file.");
return;
}
QByteArray loadData = qenFile.readAll();
QJsonParseError parseError;
QJsonDocument jsonDoc(QJsonDocument::fromJson(loadData, &parseError));
if (parseError.error != QJsonParseError::NoError) {
QString error = QString("Error parsing effect node");
QString errorDetails = QString("%1: %2").arg(parseError.offset).arg(parseError.errorString());
qWarning() << error;
qWarning() << errorDetails;
return;
}
json = jsonDoc.object().value("QEN").toObject();
parse(effectName, qenPath, json);
}
else {
parse(effectName, "", jsonObject);
}
} }
QString CompositionNode::fragmentCode() const QString CompositionNode::fragmentCode() const
@@ -63,28 +86,8 @@ CompositionNode::NodeType CompositionNode::type() const
return m_type; return m_type;
} }
void CompositionNode::parse(const QString &qenPath) void CompositionNode::parse(const QString &effectName, const QString &qenPath, const QJsonObject &json)
{ {
QFile qenFile(qenPath);
if (!qenFile.open(QIODevice::ReadOnly)) {
qWarning("Couldn't open effect file.");
return;
}
QByteArray loadData = qenFile.readAll();
QJsonParseError parseError;
QJsonDocument jsonDoc(QJsonDocument::fromJson(loadData, &parseError));
if (parseError.error != QJsonParseError::NoError) {
QString error = QString("Error parsing the effect node: %1:").arg(qenPath);
QString errorDetails = QString("%1: %2").arg(parseError.offset).arg(parseError.errorString());
qWarning() << qPrintable(error);
qWarning() << qPrintable(errorDetails);
return;
}
QJsonObject json = jsonDoc.object().value("QEN").toObject();
int version = -1; int version = -1;
if (json.contains("version")) if (json.contains("version"))
version = json["version"].toInt(-1); version = json["version"].toInt(-1);
@@ -102,7 +105,7 @@ void CompositionNode::parse(const QString &qenPath)
// parse properties // parse properties
QJsonArray jsonProps = json.value("properties").toArray(); QJsonArray jsonProps = json.value("properties").toArray();
for (const auto /*QJsonValueRef*/ &prop : jsonProps) { for (const auto /*QJsonValueRef*/ &prop : jsonProps) {
const auto uniform = new Uniform(prop.toObject(), qenPath); const auto uniform = new Uniform(effectName, prop.toObject(), qenPath);
m_unifomrsModel.addUniform(uniform); m_unifomrsModel.addUniform(uniform);
m_uniforms.append(uniform); m_uniforms.append(uniform);
g_propertyData.insert(uniform->name(), uniform->value()); g_propertyData.insert(uniform->name(), uniform->value());

View File

@@ -5,6 +5,7 @@
#include "effectmakeruniformsmodel.h" #include "effectmakeruniformsmodel.h"
#include <QJsonObject>
#include <QObject> #include <QObject>
namespace EffectMaker { namespace EffectMaker {
@@ -24,7 +25,7 @@ public:
CustomNode CustomNode
}; };
CompositionNode(const QString &qenPath); CompositionNode(const QString &effectName, const QString &qenPath, const QJsonObject &json = {});
QString fragmentCode() const; QString fragmentCode() const;
QString vertexCode() const; QString vertexCode() const;
@@ -48,7 +49,7 @@ signals:
void isEnabledChanged(); void isEnabledChanged();
private: private:
void parse(const QString &qenPath); void parse(const QString &effectName, const QString &qenPath, const QJsonObject &json);
QString m_name; QString m_name;
NodeType m_type = CustomNode; NodeType m_type = CustomNode;

View File

@@ -49,19 +49,6 @@ static bool writeToFile(const QByteArray &buf, const QString &filename, FileType
EffectMakerModel::EffectMakerModel(QObject *parent) EffectMakerModel::EffectMakerModel(QObject *parent)
: QAbstractListModel{parent} : QAbstractListModel{parent}
{ {
connect(&m_fileWatcher, &Utils::FileSystemWatcher::fileChanged, this, [this]() {
// Update component with images not set.
m_loadComponentImages = false;
updateQmlComponent();
// Then enable component images with a longer delay than
// the component updating delay. This way Image elements
// will reload the changed image files.
const int enableImagesDelay = 200;
QTimer::singleShot(enableImagesDelay, this, [this]() {
m_loadComponentImages = true;
updateQmlComponent();
} );
});
} }
QHash<int, QByteArray> EffectMakerModel::roleNames() const QHash<int, QByteArray> EffectMakerModel::roleNames() const
@@ -117,7 +104,7 @@ void EffectMakerModel::setIsEmpty(bool val)
void EffectMakerModel::addNode(const QString &nodeQenPath) void EffectMakerModel::addNode(const QString &nodeQenPath)
{ {
beginInsertRows({}, m_nodes.size(), m_nodes.size()); beginInsertRows({}, m_nodes.size(), m_nodes.size());
auto *node = new CompositionNode(nodeQenPath); auto *node = new CompositionNode("", nodeQenPath);
m_nodes.append(node); m_nodes.append(node);
endInsertRows(); endInsertRows();
@@ -189,12 +176,17 @@ void EffectMakerModel::clear()
if (m_nodes.isEmpty()) if (m_nodes.isEmpty())
return; return;
beginRemoveRows({}, 0, m_nodes.count());
for (CompositionNode *node : std::as_const(m_nodes)) for (CompositionNode *node : std::as_const(m_nodes))
delete node; delete node;
m_nodes.clear(); m_nodes.clear();
endRemoveRows();
setIsEmpty(true); setIsEmpty(true);
bakeShaders();
} }
const QList<Uniform *> EffectMakerModel::allUniforms() const QList<Uniform *> EffectMakerModel::allUniforms()
@@ -571,6 +563,69 @@ void EffectMakerModel::exportComposition(const QString &name)
saveFile.close(); saveFile.close();
} }
void EffectMakerModel::openComposition(const QString &path)
{
clear();
QFile compFile(path);
if (!compFile.open(QIODevice::ReadOnly)) {
QString error = QString("Couldn't open composition file: '%1'").arg(path);
qWarning() << qPrintable(error);
setEffectError(error);
return;
}
QByteArray data = compFile.readAll();
QJsonParseError parseError;
QJsonDocument jsonDoc(QJsonDocument::fromJson(data, &parseError));
if (parseError.error != QJsonParseError::NoError) {
QString error = QString("Error parsing the project file: %1").arg(parseError.errorString());
qWarning() << error;
setEffectError(error);
return;
}
QJsonObject rootJson = jsonDoc.object();
if (!rootJson.contains("QEP")) {
QString error = QStringLiteral("Error: Invalid project file");
qWarning() << error;
setEffectError(error);
return;
}
QJsonObject json = rootJson["QEP"].toObject();
int version = -1;
if (json.contains("version"))
version = json["version"].toInt(-1);
if (version != 1) {
QString error = QString("Error: Unknown project version (%1)").arg(version);
qWarning() << error;
setEffectError(error);
return;
}
// Get effects dir
const QString effectName = QFileInfo(path).baseName();
const Utils::FilePath effectsResDir = QmlDesigner::ModelNodeOperations::getEffectsImportDirectory();
const QString effectsResPath = effectsResDir.pathAppended(effectName).toString();
if (json.contains("nodes") && json["nodes"].isArray()) {
const QJsonArray nodesArray = json["nodes"].toArray();
for (const auto &nodeElement : nodesArray) {
beginInsertRows({}, m_nodes.size(), m_nodes.size());
auto *node = new CompositionNode(effectName, "", nodeElement.toObject());
m_nodes.append(node);
endInsertRows();
}
setIsEmpty(m_nodes.isEmpty());
bakeShaders();
}
setCurrentComposition(effectName);
}
void EffectMakerModel::exportResources(const QString &name) void EffectMakerModel::exportResources(const QString &name)
{ {
// Make sure that uniforms are up-to-date // Make sure that uniforms are up-to-date
@@ -647,18 +702,15 @@ void EffectMakerModel::exportResources(const QString &name)
QString imagePath = uniform->value().toString(); QString imagePath = uniform->value().toString();
QFileInfo fi(imagePath); QFileInfo fi(imagePath);
QString imageFilename = fi.fileName(); QString imageFilename = fi.fileName();
sources.append(imagePath); sources.append(imagePath.remove(0, 7)); // Removes "file://"
dests.append(imageFilename); dests.append(imageFilename);
} }
} }
//TODO: Copy source files if requested in future versions
// Copy files
for (int i = 0; i < sources.count(); ++i) { for (int i = 0; i < sources.count(); ++i) {
Utils::FilePath source = Utils::FilePath::fromString(sources[i]); Utils::FilePath source = Utils::FilePath::fromString(sources[i]);
Utils::FilePath target = Utils::FilePath::fromString(effectsResPath + dests[i]); Utils::FilePath target = Utils::FilePath::fromString(effectsResPath + dests[i]);
if (target.exists()) if (target.exists() && source.fileName() != target.fileName())
target.removeFile(); // Remove existing file for update target.removeFile(); // Remove existing file for update
if (!source.copyFile(target)) if (!source.copyFile(target))
@@ -1254,15 +1306,16 @@ QString EffectMakerModel::getQmlImagesString(bool localFiles)
if (localFiles) { if (localFiles) {
QFileInfo fi(imagePath); QFileInfo fi(imagePath);
imagePath = fi.fileName(); imagePath = fi.fileName();
} imagesString += QString(" source: %1\n").arg(uniform->name());
if (m_loadComponentImages) } else {
imagesString += QString(" source: g_propertyData.%1\n").arg(uniform->name()); imagesString += QString(" source: g_propertyData.%1\n").arg(uniform->name());
if (!localFiles) {
QString mipmapProperty = mipmapPropertyName(uniform->name()); if (uniform->enableMipmap())
imagesString += QString(" mipmap: g_propertyData.%1\n").arg(mipmapProperty);
} else if (uniform->enableMipmap()) {
imagesString += " mipmap: true\n"; imagesString += " mipmap: true\n";
else
QString mipmapProperty = mipmapPropertyName(uniform->name());
} }
imagesString += " visible: false\n"; imagesString += " visible: false\n";
imagesString += " }\n"; imagesString += " }\n";
} }
@@ -1336,6 +1389,19 @@ QString EffectMakerModel::getQmlComponentString(bool localFiles)
return s; return s;
} }
QString EffectMakerModel::currentComposition() const
{
return m_currentComposition;
}
void EffectMakerModel::setCurrentComposition(const QString &newCurrentComposition)
{
if (m_currentComposition == newCurrentComposition)
return;
m_currentComposition = newCurrentComposition;
emit currentCompositionChanged();
}
void EffectMakerModel::updateQmlComponent() void EffectMakerModel::updateQmlComponent()
{ {
// Clear possible QML runtime errors // Clear possible QML runtime errors
@@ -1352,25 +1418,4 @@ QString EffectMakerModel::stripFileFromURL(const QString &urlString) const
return filePath; return filePath;
} }
void EffectMakerModel::updateImageWatchers()
{
const QList<Uniform *> uniforms = allUniforms();
for (Uniform *uniform : uniforms) {
if (uniform->type() == Uniform::Type::Sampler) {
// Watch all image properties files
QString imagePath = stripFileFromURL(uniform->value().toString());
if (imagePath.isEmpty())
continue;
m_fileWatcher.addFile(imagePath, Utils::FileSystemWatcher::WatchAllChanges);
}
}
}
void EffectMakerModel::clearImageWatchers()
{
const auto watchedFiles = m_fileWatcher.files();
if (!watchedFiles.isEmpty())
m_fileWatcher.removeFiles(watchedFiles);
}
} // namespace EffectMaker } // namespace EffectMaker

View File

@@ -6,7 +6,6 @@
#include "shaderfeatures.h" #include "shaderfeatures.h"
#include <utils/filepath.h> #include <utils/filepath.h>
#include <utils/filesystemwatcher.h>
#include <QFileSystemWatcher> #include <QFileSystemWatcher>
#include <QMap> #include <QMap>
@@ -47,7 +46,7 @@ class EffectMakerModel : public QAbstractListModel
Q_PROPERTY(int selectedIndex MEMBER m_selectedIndex NOTIFY selectedIndexChanged) Q_PROPERTY(int selectedIndex MEMBER m_selectedIndex NOTIFY selectedIndexChanged)
Q_PROPERTY(bool shadersUpToDate READ shadersUpToDate WRITE setShadersUpToDate NOTIFY shadersUpToDateChanged) Q_PROPERTY(bool shadersUpToDate READ shadersUpToDate WRITE setShadersUpToDate NOTIFY shadersUpToDateChanged)
Q_PROPERTY(QString qmlComponentString READ qmlComponentString) Q_PROPERTY(QString qmlComponentString READ qmlComponentString)
Q_PROPERTY(QString currentComposition READ currentComposition WRITE setCurrentComposition NOTIFY currentCompositionChanged)
public: public:
EffectMakerModel(QObject *parent = nullptr); EffectMakerModel(QObject *parent = nullptr);
@@ -86,6 +85,11 @@ public:
Q_INVOKABLE void exportComposition(const QString &name); Q_INVOKABLE void exportComposition(const QString &name);
Q_INVOKABLE void exportResources(const QString &name); Q_INVOKABLE void exportResources(const QString &name);
void openComposition(const QString &path);
QString currentComposition() const;
void setCurrentComposition(const QString &newCurrentComposition);
signals: signals:
void isEmptyChanged(); void isEmptyChanged();
void selectedIndexChanged(int idx); void selectedIndexChanged(int idx);
@@ -93,6 +97,8 @@ signals:
void shadersUpToDateChanged(); void shadersUpToDateChanged();
void shadersBaked(); void shadersBaked();
void currentCompositionChanged();
private: private:
enum Roles { enum Roles {
NameRole = Qt::UserRole + 1, NameRole = Qt::UserRole + 1,
@@ -138,8 +144,6 @@ private:
QString generateFragmentShader(bool includeUniforms = true); QString generateFragmentShader(bool includeUniforms = true);
void handleQsbProcessExit(Utils::Process *qsbProcess, const QString &shader); void handleQsbProcessExit(Utils::Process *qsbProcess, const QString &shader);
QString stripFileFromURL(const QString &urlString) const; QString stripFileFromURL(const QString &urlString) const;
void updateImageWatchers();
void clearImageWatchers();
QString getQmlEffectString(); QString getQmlEffectString();
void updateCustomUniforms(); void updateCustomUniforms();
@@ -179,7 +183,7 @@ private:
QString m_previewEffectPropertiesString; QString m_previewEffectPropertiesString;
QString m_qmlComponentString; QString m_qmlComponentString;
bool m_loadComponentImages = true; bool m_loadComponentImages = true;
Utils::FileSystemWatcher m_fileWatcher; QString m_currentComposition;
const QRegularExpression m_spaceReg = QRegularExpression("\\s+"); const QRegularExpression m_spaceReg = QRegularExpression("\\s+");
}; };

View File

@@ -59,12 +59,15 @@ QmlDesigner::WidgetInfo EffectMakerView::widgetInfo()
QmlDesigner::WidgetInfo::LeftPane, 0, tr("Effect Maker")); QmlDesigner::WidgetInfo::LeftPane, 0, tr("Effect Maker"));
} }
void EffectMakerView::customNotification(const AbstractView * /*view*/, void EffectMakerView::customNotification([[maybe_unused]] const AbstractView *view,
const QString & /*identifier*/, const QString &identifier,
const QList<QmlDesigner::ModelNode> & /*nodeList*/, [[maybe_unused]] const QList<QmlDesigner::ModelNode> &nodeList,
const QList<QVariant> & /*data*/) const QList<QVariant> &data)
{ {
// TODO if (identifier == "open_effectmaker_composition" && data.count() > 0) {
const QString compositionPath = data[0].toString();
m_widget->effectMakerModel()->openComposition(compositionPath);
}
} }
void EffectMakerView::modelAttached(QmlDesigner::Model *model) void EffectMakerView::modelAttached(QmlDesigner::Model *model)

View File

@@ -6,13 +6,15 @@
#include "propertyhandler.h" #include "propertyhandler.h"
#include <modelnodeoperations.h>
#include <QColor> #include <QColor>
#include <QJsonObject> #include <QJsonObject>
#include <QVector2D> #include <QVector2D>
namespace EffectMaker { namespace EffectMaker {
Uniform::Uniform(const QJsonObject &propObj, const QString &qenPath) Uniform::Uniform(const QString &effectName, const QJsonObject &propObj, const QString &qenPath)
: m_qenPath(qenPath) : m_qenPath(qenPath)
{ {
QString value, defaultValue, minValue, maxValue; QString value, defaultValue, minValue, maxValue;
@@ -26,9 +28,11 @@ Uniform::Uniform(const QJsonObject &propObj, const QString &qenPath)
if (m_displayName.isEmpty()) if (m_displayName.isEmpty())
m_displayName = m_name; m_displayName = m_name;
QString resPath;
if (m_type == Type::Sampler) { if (m_type == Type::Sampler) {
resPath = getResourcePath(effectName, defaultValue, qenPath);
if (!defaultValue.isEmpty()) if (!defaultValue.isEmpty())
defaultValue = getResourcePath(defaultValue); defaultValue = resPath;
if (propObj.contains("enableMipmap")) if (propObj.contains("enableMipmap"))
m_enableMipmap = getBoolValue(propObj.value("enableMipmap"), false); m_enableMipmap = getBoolValue(propObj.value("enableMipmap"), false);
// Update the mipmap property // Update the mipmap property
@@ -39,7 +43,7 @@ Uniform::Uniform(const QJsonObject &propObj, const QString &qenPath)
if (propObj.contains("value")) { if (propObj.contains("value")) {
value = propObj.value("value").toString(); value = propObj.value("value").toString();
if (m_type == Type::Sampler) if (m_type == Type::Sampler)
value = getResourcePath(value); value = resPath;
} else { } else {
// QEN files don't store the current value, so with those use default value // QEN files don't store the current value, so with those use default value
value = defaultValue; value = defaultValue;
@@ -166,9 +170,13 @@ bool Uniform::getBoolValue(const QJsonValue &jsonValue, bool defaultValue)
// Returns the path for a shader resource // Returns the path for a shader resource
// Used with sampler types // Used with sampler types
QString Uniform::getResourcePath(const QString &value) const QString Uniform::getResourcePath(const QString &effectName, const QString &value, const QString &qenPath) const
{ {
QString filePath = value; QString filePath = value;
if (qenPath.isEmpty()) {
const Utils::FilePath effectsResDir = QmlDesigner::ModelNodeOperations::getEffectsImportDirectory();
return effectsResDir.pathAppended(effectName).pathAppended(value).toString();
} else {
QDir dir(m_qenPath); QDir dir(m_qenPath);
dir.cdUp(); dir.cdUp();
QString absPath = dir.absoluteFilePath(filePath); QString absPath = dir.absoluteFilePath(filePath);
@@ -176,6 +184,7 @@ QString Uniform::getResourcePath(const QString &value) const
absPath = QUrl::fromLocalFile(absPath).toString(); absPath = QUrl::fromLocalFile(absPath).toString();
return absPath; return absPath;
} }
}
// Validation and setting values // Validation and setting values
void Uniform::setValueData(const QString &value, const QString &defaultValue, void Uniform::setValueData(const QString &value, const QString &defaultValue,
@@ -300,7 +309,7 @@ Uniform::Type Uniform::typeFromString(const QString &typeString)
return Uniform::Type::Vec4; return Uniform::Type::Vec4;
else if (typeString == "color") else if (typeString == "color")
return Uniform::Type::Color; return Uniform::Type::Color;
else if (typeString == "image") else if (typeString == "sampler2D" || typeString == "image") //TODO: change image to sample2D in all QENs
return Uniform::Type::Sampler; return Uniform::Type::Sampler;
else if (typeString == "define") else if (typeString == "define")
return Uniform::Type::Define; return Uniform::Type::Define;

View File

@@ -40,7 +40,7 @@ public:
Define Define
}; };
Uniform(const QJsonObject &props, const QString &qenPath); Uniform(const QString &effectName, const QJsonObject &props, const QString &qenPath);
Type type() const; Type type() const;
QString typeName() const; QString typeName() const;
@@ -78,7 +78,7 @@ signals:
private: private:
QString mipmapPropertyName(const QString &name) const; QString mipmapPropertyName(const QString &name) const;
bool getBoolValue(const QJsonValue &jsonValue, bool defaultValue); bool getBoolValue(const QJsonValue &jsonValue, bool defaultValue);
QString getResourcePath(const QString &value) const; QString getResourcePath(const QString &effectName, const QString &value, const QString &qenPath) const;
void setValueData(const QString &value, const QString &defaultValue, void setValueData(const QString &value, const QString &defaultValue,
const QString &minValue, const QString &maxValue); const QString &minValue, const QString &maxValue);

View File

@@ -13,6 +13,9 @@
#include "qmldesignerplugin.h" #include "qmldesignerplugin.h"
#include "theme.h" #include "theme.h"
#include <extensionsystem/pluginmanager.h>
#include <extensionsystem/pluginspec.h>
#include <studioquickwidget.h> #include <studioquickwidget.h>
#include <coreplugin/fileutils.h> #include <coreplugin/fileutils.h>
@@ -364,8 +367,21 @@ QSet<QString> AssetsLibraryWidget::supportedAssetSuffixes(bool complex)
return suffixes; return suffixes;
} }
bool isEffectMakerActivated()
{
const QVector<ExtensionSystem::PluginSpec *> specs = ExtensionSystem::PluginManager::plugins();
return std::find_if(specs.begin(), specs.end(),
[](ExtensionSystem::PluginSpec *spec) {
return spec->name() == "EffectMakerNew" && spec->isEffectivelyEnabled();
})
!= specs.end();
}
void AssetsLibraryWidget::openEffectMaker(const QString &filePath) void AssetsLibraryWidget::openEffectMaker(const QString &filePath)
{ {
if (isEffectMakerActivated())
m_assetsView->emitCustomNotification("open_effectmaker_composition", {}, {filePath});
else
ModelNodeOperations::openEffectMaker(filePath); ModelNodeOperations::openEffectMaker(filePath);
} }