Source code

Revision control

Copy as Markdown

// Copyright (C) 2022 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
// Qt-Security score:critical reason:data-parser
#include "effectmanager.h"
#include "propertyhandler.h"
#include "syntaxhighlighterdata.h"
#include <QDir>
#include <QFile>
#include <QJsonDocument>
#include <QJsonObject>
#include <QJsonArray>
#include <QImageReader>
#include <QQmlContext>
#include <QtQml/qqmlfile.h>
#include <QXmlStreamWriter>
QQmlPropertyMap g_argData;
enum class FileType
{
Binary,
Text
};
static bool writeToFile(const QByteArray &buf, const QString &filename, FileType fileType)
{
QDir().mkpath(QFileInfo(filename).path());
QFile f(filename);
QIODevice::OpenMode flags = QIODevice::WriteOnly | QIODevice::Truncate;
if (fileType == FileType::Text)
flags |= QIODevice::Text;
if (!f.open(flags)) {
qWarning() << "Failed to open file for writing:" << filename;
return false;
}
f.write(buf);
return true;
}
static void removeIfExists(const QString &filePath)
{
QFile file(filePath);
if (file.exists())
file.remove();
}
// Returns the boolean value of QJsonValue. It can be either boolean
// (true, false) or string ("true", "false"). Returns the defaultValue
// if QJsonValue is undefined, empty, or some other type.
static bool getBoolValue(const QJsonValue &jsonValue, bool defaultValue)
{
bool returnValue = defaultValue;
if (jsonValue.isBool()) {
returnValue = jsonValue.toBool();
} else if (jsonValue.isString()) {
QString s = jsonValue.toString().toLower();
if (s == QStringLiteral("true"))
returnValue = true;
else if (s == QStringLiteral("false"))
returnValue = false;
}
return returnValue;
}
EffectManager::EffectManager(QObject *parent) : QObject(parent)
{
m_settings = new ApplicationSettings(this);
setUniformModel(new UniformModel(this));
m_addNodeModel = new AddNodeModel(this);
m_codeHelper = new CodeHelper(this);
m_vertexShaderFile.setFileTemplate(QDir::tempPath() + "/qqem_XXXXXX.vert.qsb");
m_fragmentShaderFile.setFileTemplate(QDir::tempPath() + "/qqem_XXXXXX.frag.qsb");
if (m_vertexShaderFile.open()) {
m_vertexShaderFilename = m_vertexShaderFile.fileName();
qInfo() << "Using temporary vs file:" << m_vertexShaderFilename;
}
if (m_fragmentShaderFile.open()) {
m_fragmentShaderFilename = m_fragmentShaderFile.fileName();
qInfo() << "Using temporary fs file:" << m_fragmentShaderFilename;
}
// Prepare baker
m_baker.setGeneratedShaderVariants({ QShader::StandardShader });
updateBakedShaderVersions();
m_shaderBakerTimer.setInterval(1000);
m_shaderBakerTimer.setSingleShot(true);
connect(&m_shaderBakerTimer, &QTimer::timeout, this, &EffectManager::doBakeShaders);
m_settings->updateRecentProjectsModel();
connect(m_uniformModel, &UniformModel::qmlComponentChanged, this, [this]() {
updateCustomUniforms();
updateQmlComponent();
// Also update the features as QML e.g. might have started using iTime
m_shaderFeatures.update(generateVertexShader(false), generateFragmentShader(false), m_previewEffectPropertiesString);
});
connect(m_uniformModel, &UniformModel::uniformsChanged, this, [this]() {
updateImageWatchers();
bakeShaders();
});
connect(m_uniformModel, &UniformModel::addFSCode, this, [this](const QString &code) {
if (auto selectedNode = m_nodeView->m_nodesModel->m_selectedNode) {
selectedNode->fragmentCode += code;
Q_EMIT m_nodeView->selectedNodeFragmentCodeChanged();
}
});
connect(&m_fileWatcher, &QFileSystemWatcher::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 relaod the changed image files.
const int enableImagesDelay = effectUpdateDelay() + 100;
QTimer::singleShot(enableImagesDelay, this, [this]() {
m_loadComponentImages = true;
updateQmlComponent();
} );
});
}
UniformModel *EffectManager::uniformModel() const
{
return m_uniformModel;
}
void EffectManager::setUniformModel(UniformModel *newUniformModel)
{
m_uniformModel = newUniformModel;
if (m_uniformModel) {
m_uniformModel->m_effectManager = this;
m_uniformModel->setModelData(&m_uniformTable);
}
emit uniformModelChanged();
}
CodeHelper *EffectManager::codeHelper() const
{
return m_codeHelper;
}
QString EffectManager::fragmentShader() const
{
return m_fragmentShader;
}
void EffectManager::setFragmentShader(const QString &newFragmentShader)
{
if (m_fragmentShader == newFragmentShader)
return;
m_fragmentShader = newFragmentShader;
emit fragmentShaderChanged();
setUnsavedChanges(true);
}
QString EffectManager::vertexShader() const
{
return m_vertexShader;
}
void EffectManager::setVertexShader(const QString &newVertexShader)
{
if (m_vertexShader == newVertexShader)
return;
m_vertexShader = newVertexShader;
emit vertexShaderChanged();
setUnsavedChanges(true);
}
bool EffectManager::unsavedChanges() const
{
return m_unsavedChanges;
}
void EffectManager::setUnsavedChanges(bool newUnsavedChanges)
{
if (m_unsavedChanges == newUnsavedChanges || m_firstBake)
return;
m_unsavedChanges = newUnsavedChanges;
emit unsavedChangesChanged();
}
bool EffectManager::hasProjectFilename() const
{
return !m_projectFilename.isEmpty();
}
QString EffectManager::projectFilename() const
{
return m_projectFilename.toString();
}
QString EffectManager::exportFilename() const
{
return m_exportFilename;
}
QString EffectManager::projectName() const
{
return m_projectName;
}
void EffectManager::setProjectName(const QString &name)
{
if (m_projectName == name)
return;
m_projectName = name;
Q_EMIT projectNameChanged();
}
QString EffectManager::projectDirectory() const
{
return m_projectDirectory;
}
QString EffectManager::exportDirectory() const
{
return m_exportDirectory;
}
int EffectManager::exportFlags() const
{
return m_exportFlags;
}
QStringList EffectManager::getDefaultRootVertexShader()
{
if (m_defaultRootVertexShader.isEmpty()) {
m_defaultRootVertexShader << "void main() {";
m_defaultRootVertexShader << " texCoord = qt_MultiTexCoord0;";
m_defaultRootVertexShader << " fragCoord = qt_Vertex.xy;";
m_defaultRootVertexShader << " vec2 vertCoord = qt_Vertex.xy;";
m_defaultRootVertexShader << " @nodes";
m_defaultRootVertexShader << " gl_Position = qt_Matrix * vec4(vertCoord, 0.0, 1.0);";
m_defaultRootVertexShader << "}";
}
return m_defaultRootVertexShader;
}
QStringList EffectManager::getDefaultRootFragmentShader()
{
if (m_defaultRootFragmentShader.isEmpty()) {
m_defaultRootFragmentShader << "void main() {";
m_defaultRootFragmentShader << " fragColor = texture(iSource, texCoord);";
m_defaultRootFragmentShader << " @nodes";
m_defaultRootFragmentShader << " fragColor = fragColor * qt_Opacity;";
m_defaultRootFragmentShader << "}";
}
return m_defaultRootFragmentShader;
}
QString EffectManager::processVertexRootLine(const QString &line)
{
QString output;
static QRegularExpression spaceReg("\\s+");
QStringList lineList = line.split(spaceReg, Qt::SkipEmptyParts);
if (lineList.length() > 1 && lineList.at(0) == QStringLiteral("out")) {
lineList.removeFirst();
QString outLine = lineList.join(' ');
m_shaderVaryingVariables << outLine;
} else {
output = line + '\n';
}
return output;
}
QString EffectManager::processFragmentRootLine(const QString &line)
{
QString output;
static QRegularExpression spaceReg("\\s+");
QStringList lineList = line.split(spaceReg, Qt::SkipEmptyParts);
// Just skip all "in" variables. It is enough to have "out" variable in vertex.
if (lineList.length() > 1 && lineList.at(0) == QStringLiteral("in"))
return QString();
output = line + '\n';
return output;
}
// Outputs the custom varying variables.
// When outState is true, output vertex (out) version, when false output fragment (in) version.
QString EffectManager::getCustomShaderVaryings(bool outState)
{
QString output;
QString direction = outState ? QStringLiteral("out") : QStringLiteral("in");
int varLocation = m_shaderFeatures.enabled(ShaderFeatures::FragCoord) ? 2 : 1;
for (const auto &var : m_shaderVaryingVariables) {
output += QString("layout(location = %1) %2 %3\n").arg(QString::number(varLocation), direction, var);
varLocation++;
}
return output;
}
// Remove all post-processing tags ("@tag") from the code.
// Except "@nodes" tag as that is handled later.
QStringList EffectManager::removeTagsFromCode(const QStringList &codeLines) {
QStringList s;
for (const auto &line : codeLines) {
const auto trimmedLine = line.trimmed();
if (!trimmedLine.startsWith('@') || trimmedLine.startsWith("@nodes")) {
s << line;
} else {
// Check if the tag is known
bool validTag = false;
auto tags = SyntaxHighlighterData::reservedTagNames();
static QRegularExpression spaceReg("\\s+");
QString firstWord = trimmedLine.split(spaceReg, Qt::SkipEmptyParts).first();
for (const auto &tag : tags) {
if (firstWord == QString::fromUtf8(tag)) {
validTag = true;
break;
}
}
if (!validTag)
setEffectError(QString("Unknown tag: %1").arg(trimmedLine), ErrorPreprocessor);
}
}
return s;
}
QString EffectManager::removeTagsFromCode(const QString &code) {
QStringList codeLines = removeTagsFromCode(code.split('\n'));
return codeLines.join('\n');
}
QString EffectManager::generateVertexShader(bool includeUniforms) {
QString s;
if (includeUniforms)
s += getVSUniforms();
// Remove tags when not generating for features check
const bool removeTags = includeUniforms;
s += getDefineProperties();
s += getConstVariables();
// When the node is complete, add shader code in correct nodes order
// split to root and main parts
QString s_root;
QString s_main;
QStringList s_sourceCode;
m_shaderVaryingVariables.clear();
if (m_nodeView->nodeGraphComplete()) {
for (auto n : m_nodeView->m_activeNodesList) {
if (!n->vertexCode.isEmpty() && !n->disabled) {
if (n->type == 0) {
s_sourceCode = n->vertexCode.split('\n');
} else if (n->type == 2) {
QStringList vertexCode = n->vertexCode.split('\n');
int mainIndex = getTagIndex(vertexCode, QStringLiteral("main"));
int line = 0;
for (const auto &ss : vertexCode) {
if (mainIndex == -1 || line > mainIndex)
s_main += QStringLiteral(" ") + ss + '\n';
else if (line < mainIndex)
s_root += processVertexRootLine(ss);
line++;
}
}
}
}
}
if (s_sourceCode.isEmpty()) {
// If source nodes doesn't contain any code, fail to the default one
s_sourceCode << getDefaultRootVertexShader();
}
if (removeTags) {
s_sourceCode = removeTagsFromCode(s_sourceCode);
s_root = removeTagsFromCode(s_root);
s_main = removeTagsFromCode(s_main);
}
s += getCustomShaderVaryings(true);
s += s_root + '\n';
int nodesIndex = getTagIndex(s_sourceCode, QStringLiteral("nodes"));
int line = 0;
for (const auto &ss : s_sourceCode) {
if (line == nodesIndex)
s += s_main;
else
s += ss + '\n';
line++;
}
return s;
}
QString EffectManager::generateFragmentShader(bool includeUniforms) {
QString s;
if (includeUniforms)
s += getFSUniforms();
// Remove tags when not generating for features check
const bool removeTags = includeUniforms;
s += getDefineProperties();
s += getConstVariables();
// When the node is complete, add shader code in correct nodes order
// split to root and main parts
QString s_root;
QString s_main;
QStringList s_sourceCode;
if (m_nodeView->nodeGraphComplete()) {
for (auto n : m_nodeView->m_activeNodesList) {
if (!n->fragmentCode.isEmpty() && !n->disabled) {
if (n->type == 0) {
s_sourceCode = n->fragmentCode.split('\n');
} else if (n->type == 2) {
QStringList fragmentCode = n->fragmentCode.split('\n');
int mainIndex = getTagIndex(fragmentCode, QStringLiteral("main"));
int line = 0;
for (const auto &ss : fragmentCode) {
if (mainIndex == -1 || line > mainIndex)
s_main += QStringLiteral(" ") + ss + '\n';
else if (line < mainIndex)
s_root += processFragmentRootLine(ss);
line++;
}
}
}
}
}
if (s_sourceCode.isEmpty()) {
// If source nodes doesn't contain any code, fail to the default one
s_sourceCode << getDefaultRootFragmentShader();
}
if (removeTags) {
s_sourceCode = removeTagsFromCode(s_sourceCode);
s_root = removeTagsFromCode(s_root);
s_main = removeTagsFromCode(s_main);
}
s += getCustomShaderVaryings(false);
s += s_root + '\n';
int nodesIndex = getTagIndex(s_sourceCode, QStringLiteral("nodes"));
int line = 0;
for (const auto &ss : s_sourceCode) {
if (line == nodesIndex)
s += s_main;
else
s += ss + '\n';
line++;
}
return s;
}
int EffectManager::getTagIndex(const QStringList &code, const QString &tag)
{
int index = -1;
int line = 0;
const QString tagString = QString("@%1").arg(tag);
for (const auto &s : code) {
auto st = s.trimmed();
// Check if line or first non-space content of the line matches to tag
static auto spaceReg = QRegularExpression("\\s");
auto firstSpace = st.indexOf(spaceReg);
QString firstWord = st;
if (firstSpace > 0)
firstWord = st.sliced(0, firstSpace);
if (firstWord == tagString) {
index = line;
break;
}
line++;
}
return index;
}
void EffectManager::updateBakedShaderVersions()
{
QList<QShaderBaker::GeneratedShader> targets;
targets.append({ QShader::SpirvShader, QShaderVersion(100) }); // Vulkan 1.0
targets.append({ QShader::HlslShader, QShaderVersion(50) }); // Shader Model 5.0
targets.append({ QShader::MslShader, QShaderVersion(12) }); // Metal 1.2
targets.append({ QShader::GlslShader, QShaderVersion(300, QShaderVersion::GlslEs) }); // GLES 3.0+
targets.append({ QShader::GlslShader, QShaderVersion(410) }); // OpenGL 4.1+
targets.append({ QShader::GlslShader, QShaderVersion(330) }); // OpenGL 3.3
targets.append({ QShader::GlslShader, QShaderVersion(140) }); // OpenGL 3.1
if (m_settings->useLegacyShaders()) {
targets.append({ QShader::GlslShader, QShaderVersion(100, QShaderVersion::GlslEs) }); // GLES 2.0
targets.append({ QShader::GlslShader, QShaderVersion(120) }); // OpenGL 2.1
}
m_baker.setGeneratedShaders(targets);
}
// Bake the shaders if they have changed
// When forced is true, will bake even when autoplay is off
void EffectManager::bakeShaders(bool forced)
{
resetEffectError(ErrorPreprocessor);
if (m_vertexShader == generateVertexShader()
&& m_fragmentShader == generateFragmentShader()) {
setShadersUpToDate(true);
return;
}
setShadersUpToDate(false);
if (forced)
doBakeShaders();
else if (m_autoPlayEffect)
m_shaderBakerTimer.start();
}
void EffectManager::doBakeShaders()
{
// First update the features based on shader content
// This will make sure that next calls to generate* will produce correct uniforms.
m_shaderFeatures.update(generateVertexShader(false), generateFragmentShader(false), m_previewEffectPropertiesString);
updateCustomUniforms();
setVertexShader(generateVertexShader());
QString vs = m_vertexShader;
m_baker.setSourceString(vs.toUtf8(), QShader::VertexStage);
QShader vertShader = m_baker.bake();
if (!vertShader.isValid()) {
qWarning() << "Shader baking failed:" << qPrintable(m_baker.errorMessage());
setEffectError(m_baker.errorMessage().split('\n').first(), ErrorVert);
} else {
QString filename = m_vertexShaderFile.fileName();
writeToFile(vertShader.serialized(), filename, FileType::Binary);
resetEffectError(ErrorVert);
}
setFragmentShader(generateFragmentShader());
QString fs = m_fragmentShader;
m_baker.setSourceString(fs.toUtf8(), QShader::FragmentStage);
QShader fragShader = m_baker.bake();
if (!fragShader.isValid()) {
qWarning() << "Shader baking failed:" << qPrintable(m_baker.errorMessage());
setEffectError(m_baker.errorMessage().split('\n').first(), ErrorFrag);
} else {
QString filename = m_fragmentShaderFile.fileName();
writeToFile(fragShader.serialized(), filename, FileType::Binary);
resetEffectError(ErrorFrag);
}
if (vertShader.isValid() && fragShader.isValid()) {
Q_EMIT shadersBaked();
setShadersUpToDate(true);
}
m_firstBake = false;
}
const QString EffectManager::getBufUniform()
{
QString s;
s += "layout(std140, binding = 0) uniform buf {\n";
s += " mat4 qt_Matrix;\n";
s += " float qt_Opacity;\n";
if (m_shaderFeatures.enabled(ShaderFeatures::Time))
s += " float iTime;\n";
if (m_shaderFeatures.enabled(ShaderFeatures::Frame))
s += " int iFrame;\n";
if (m_shaderFeatures.enabled(ShaderFeatures::Resolution))
s += " vec3 iResolution;\n";
if (m_shaderFeatures.enabled(ShaderFeatures::Mouse))
s += " vec4 iMouse;\n";
for (auto &uniform : m_uniformTable) {
if (!m_nodeView->m_activeNodesIds.contains(uniform.nodeId))
continue;
if (uniform.exportProperty &&
uniform.type != UniformModel::Uniform::Type::Sampler &&
uniform.type != UniformModel::Uniform::Type::Define) {
QString type = m_uniformModel->typeToUniform(uniform.type);
QString props = " " + type + " " + uniform.name + ";\n";
s += props;
}
}
s += "};\n";
return s;
}
const QString EffectManager::getVSUniforms()
{
QString s;
s += "#version 440\n";
s += '\n';
s += "layout(location = 0) in vec4 qt_Vertex;\n";
s += "layout(location = 1) in vec2 qt_MultiTexCoord0;\n";
s += "layout(location = 0) out vec2 texCoord;\n";
if (m_shaderFeatures.enabled(ShaderFeatures::FragCoord))
s += "layout(location = 1) out vec2 fragCoord;\n";
s += '\n';
s += getBufUniform();
s += '\n';
s += "out gl_PerVertex { vec4 gl_Position; };\n";
s += '\n';
return s;
}
const QString EffectManager::getFSUniforms()
{
QString s;
s += "#version 440\n";
s += '\n';
s += "layout(location = 0) in vec2 texCoord;\n";
if (m_shaderFeatures.enabled(ShaderFeatures::FragCoord))
s += "layout(location = 1) in vec2 fragCoord;\n";
s += "layout(location = 0) out vec4 fragColor;\n";
s += '\n';
s += getBufUniform();
s += '\n';
bool usesSource = m_shaderFeatures.enabled(ShaderFeatures::Source);
if (usesSource)
s += "layout(binding = 1) uniform sampler2D iSource;\n";
// Add sampler uniforms
int bindingIndex = usesSource ? 2 : 1;
for (auto &uniform : m_uniformTable) {
if (!m_nodeView->m_activeNodesIds.contains(uniform.nodeId))
continue;
if (uniform.type == UniformModel::Uniform::Type::Sampler) {
// Start index from 2, 1 is source item
QString props = QString("layout(binding = %1) uniform sampler2D %2").arg(bindingIndex).arg(uniform.name);
s += props + ";\n";
bindingIndex++;
}
}
s += '\n';
if (m_shaderFeatures.enabled(ShaderFeatures::BlurSources)) {
const int blurItems = 5;
for (int i = 1; i <= blurItems; i++) {
QString props = QString("layout(binding = %1) uniform sampler2D iSourceBlur%2")
.arg(bindingIndex).arg(QString::number(i));
s += props + ";\n";
bindingIndex++;
}
s += '\n';
}
return s;
}
const QString EffectManager::getDefineProperties()
{
QString s;
for (auto &uniform : m_uniformTable) {
if (!m_nodeView->m_activeNodesIds.contains(uniform.nodeId))
continue;
if (uniform.type == UniformModel::Uniform::Type::Define) {
QString defineValue = uniform.value.toString();
s += QString("#define %1 %2\n").arg(uniform.name, defineValue);
}
}
if (!s.isEmpty())
s += '\n';
return s;
}
const QString EffectManager::getConstVariables()
{
QString s;
for (auto &uniform : m_uniformTable) {
if (!m_nodeView->m_activeNodesIds.contains(uniform.nodeId))
continue;
if (!uniform.exportProperty) {
QString constValue = m_uniformModel->valueAsVariable(uniform);
QString type = m_uniformModel->typeToUniform(uniform.type);
s += QString("const %1 %2 = %3;\n").arg(type, uniform.name, constValue);
}
}
if (!s.isEmpty())
s += '\n';
return s;
}
// Returns name for image mipmap property.
// e.g. "myImage" -> "myImageMipmap".
QString EffectManager::mipmapPropertyName(const QString &name) const
{
QString simplifiedName = name.simplified();
simplifiedName = simplifiedName.remove(' ');
simplifiedName += "Mipmap";
return simplifiedName;
}
QString EffectManager::getQmlImagesString(bool localFiles)
{
QString imagesString;
for (auto &uniform : m_uniformTable) {
if (!m_nodeView->m_activeNodesIds.contains(uniform.nodeId))
continue;
if (uniform.type == UniformModel::Uniform::Type::Sampler) {
if (localFiles && !uniform.exportImage)
continue;
QString imagePath = uniform.value.toString();
if (imagePath.isEmpty())
continue;
imagesString += " Image {\n";
QString simplifiedName = UniformModel::getImageElementName(uniform);
imagesString += QString(" id: %1\n").arg(simplifiedName);
imagesString += " anchors.fill: parent\n";
// File paths are absolute, return as local when requested
if (localFiles) {
QFileInfo fi(imagePath);
imagePath = fi.fileName();
}
if (m_loadComponentImages)
imagesString += QString(" source: \"%1\"\n").arg(imagePath);
if (!localFiles) {
QString mipmapProperty = mipmapPropertyName(uniform.name);
imagesString += QString(" mipmap: g_propertyData.%1\n").arg(mipmapProperty);
} else if (uniform.enableMipmap) {
imagesString += " mipmap: true\n";
}
imagesString += " visible: false\n";
imagesString += " }\n";
}
}
return imagesString;
}
// Generates string of the custom properties (uniforms) into ShaderEffect component
// Also generates QML images elements for samplers.
void EffectManager::updateCustomUniforms()
{
QString exportedRootPropertiesString;
QString previewEffectPropertiesString;
QString exportedEffectPropertiesString;
for (auto &uniform : m_uniformTable) {
if (!m_nodeView->m_activeNodesIds.contains(uniform.nodeId))
continue;
if (!uniform.exportProperty)
continue;
const bool isDefine = uniform.type == UniformModel::Uniform::Type::Define;
const bool isImage = uniform.type == UniformModel::Uniform::Type::Sampler;
QString type = m_uniformModel->typeToProperty(uniform.type);
QString value = m_uniformModel->valueAsString(uniform);
QString bindedValue = m_uniformModel->valueAsBinding(uniform);
// When user has set custom uniform value, use it as-is
if (uniform.useCustomValue) {
value = uniform.customValue;
bindedValue = value;
}
// Note: Define type properties appear also as QML properties (in preview) in case QML side
// needs to use them. This is used at least by BlurHelper BLUR_HELPER_MAX_LEVEL.
QString propertyName = isDefine ? uniform.name.toLower() : uniform.name;
if (!uniform.useCustomValue && !isDefine && !uniform.description.isEmpty()) {
// When exporting, add API documentation for properties
QStringList descriptionLines = uniform.description.split('\n');
for (const auto &line: std::as_const(descriptionLines)) {
if (line.trimmed().isEmpty())
exportedRootPropertiesString += QStringLiteral(" //\n");
else
exportedRootPropertiesString += QStringLiteral(" // ") + line + '\n';
}
}
QString valueString = value.isEmpty() ? QString() : QString(": %1").arg(value);
QString bindedValueString = bindedValue.isEmpty() ? QString() : QString(": %1").arg(bindedValue);
// Custom values are not readonly, others inside the effect can be
QString readOnly = uniform.useCustomValue ? QString() : QStringLiteral("readonly ");
previewEffectPropertiesString += " " + readOnly + "property " + type + " " + propertyName + bindedValueString + '\n';
// Define type properties are not added into exports
if (!isDefine) {
if (uniform.useCustomValue) {
// Custom values are only inside the effect, with description comments
if (!uniform.description.isEmpty()) {
QStringList descriptionLines = uniform.description.split('\n');
for (const auto &line: descriptionLines)
exportedEffectPropertiesString += QStringLiteral(" // ") + line + '\n';
}
exportedEffectPropertiesString += QStringLiteral(" ") + readOnly + "property " + type + " " + propertyName + bindedValueString + '\n';
} else {
// Custom values are not added into root
if (isImage && !uniform.exportImage) {
// When exporting image is disabled, remove value from root property
valueString.clear();
}
exportedRootPropertiesString += " property " + type + " " + propertyName + valueString + '\n';
exportedEffectPropertiesString += QStringLiteral(" ") + readOnly + "property alias " + propertyName + ": rootItem." + uniform.name + '\n';
}
}
}
// See if any of the properties changed
if (m_exportedRootPropertiesString != exportedRootPropertiesString
|| m_previewEffectPropertiesString != previewEffectPropertiesString
|| m_exportedEffectPropertiesString != exportedEffectPropertiesString) {
setUnsavedChanges(true);
m_exportedRootPropertiesString = exportedRootPropertiesString;
m_previewEffectPropertiesString = previewEffectPropertiesString;
m_exportedEffectPropertiesString = exportedEffectPropertiesString;
}
}
QString EffectManager::getQmlEffectString()
{
QString s;
if (!m_effectHeadings.isEmpty()) {
s += m_effectHeadings;
s += '\n';
}
s += QString("// Created with Qt Quick Effect Maker (version %1), %2\n\n")
.arg(qApp->applicationVersion(), QDateTime::currentDateTime().toString());
s += "import QtQuick\n";
s += '\n';
s += "Item {\n";
s += " id: rootItem\n";
s += '\n';
if (m_shaderFeatures.enabled(ShaderFeatures::Source)) {
s += " // This is the main source for the effect\n";
s += " property Item source: null\n";
}
if (m_shaderFeatures.enabled(ShaderFeatures::Time) ||
m_shaderFeatures.enabled(ShaderFeatures::Frame)) {
s += " // Enable this to animate iTime property\n";
s += " property bool timeRunning: false\n";
}
if (m_shaderFeatures.enabled(ShaderFeatures::Time)) {
s += " // When timeRunning is false, this can be used to control iTime manually\n";
s += " property real animatedTime: frameAnimation.elapsedTime\n";
}
if (m_shaderFeatures.enabled(ShaderFeatures::Frame)) {
s += " // When timeRunning is false, this can be used to control iFrame manually\n";
s += " property int animatedFrame: frameAnimation.currentFrame\n";
}
s += '\n';
// Custom properties
if (!m_exportedRootPropertiesString.isEmpty()) {
s += m_exportedRootPropertiesString;
s += '\n';
}
if (m_shaderFeatures.enabled(ShaderFeatures::Time) ||
m_shaderFeatures.enabled(ShaderFeatures::Frame)) {
s += " FrameAnimation {\n";
s += " id: frameAnimation\n";
s += " running: rootItem.timeRunning\n";
s += " }\n";
s += '\n';
}
if (m_shaderFeatures.enabled(ShaderFeatures::Mouse)) {
s += " // Mouse handling for iMouse variable\n";
s += " property real _effectMouseX: 0\n";
s += " property real _effectMouseY: 0\n";
s += " property real _effectMouseZ: 0\n";
s += " property real _effectMouseW: 0\n";
s += " MouseArea {\n";
s += " anchors.fill: parent\n";
s += " onPressed: (mouse)=> {\n";
s += " _effectMouseX = mouse.x\n";
s += " _effectMouseY = mouse.y\n";
s += " _effectMouseZ = mouse.x\n";
s += " _effectMouseW = mouse.y\n";
s += " clickTimer.restart();\n";
s += " }\n";
s += " onPositionChanged: (mouse)=> {\n";
s += " _effectMouseX = mouse.x\n";
s += " _effectMouseY = mouse.y\n";
s += " }\n";
s += " onReleased: (mouse)=> {\n";
s += " _effectMouseZ = -(_effectMouseZ)\n";
s += " }\n";
s += " Timer {\n";
s += " id: clickTimer\n";
s += " interval: 20\n";
s += " onTriggered: {\n";
s += " _effectMouseW = -(_effectMouseW)\n";
s += " }\n";
s += " }\n";
s += " }\n";
s += '\n';
}
if (m_shaderFeatures.enabled(ShaderFeatures::BlurSources)) {
s += " BlurHelper {\n";
s += " id: blurHelper\n";
s += " anchors.fill: parent\n";
int blurMax = 32;
if (g_propertyData.contains("BLUR_HELPER_MAX_LEVEL"))
blurMax = g_propertyData["BLUR_HELPER_MAX_LEVEL"].toInt();
s += QString(" property int blurMax: %1\n").arg(blurMax);
s += " property real blurMultiplier: rootItem.blurMultiplier\n";
s += " }\n";
}
s += getQmlComponentString(true);
s += "}\n";
return s;
}
QString EffectManager::getQmlComponentString(bool localFiles)
{
auto addProperty = [localFiles](const QString &name, const QString &var, const QString &type, bool blurHelper = false)
{
if (localFiles) {
const QString parent = blurHelper ? "blurHelper." : "rootItem.";
return QString("readonly property alias %1: %2%3\n").arg(name, parent, var);
} else {
const QString parent = blurHelper ? "blurHelper." : QString();
return QString("readonly property %1 %2: %3%4\n").arg(type, name, parent, var);
}
};
QString customImagesString = getQmlImagesString(localFiles);
QString vertexShaderFilename = "file:///" + m_vertexShaderFilename;
QString fragmentShaderFilename = "file:///" + m_fragmentShaderFilename;
QString s;
QString l1 = localFiles ? QStringLiteral(" ") : QStringLiteral("");
QString l2 = localFiles ? QStringLiteral(" ") : QStringLiteral(" ");
QString l3 = localFiles ? QStringLiteral(" ") : QStringLiteral(" ");
if (!localFiles)
s += "import QtQuick\n";
s += l1 + "ShaderEffect {\n";
if (m_shaderFeatures.enabled(ShaderFeatures::Source))
s += l2 + addProperty("iSource", "source", "Item");
if (m_shaderFeatures.enabled(ShaderFeatures::Time))
s += l2 + addProperty("iTime", "animatedTime", "real");
if (m_shaderFeatures.enabled(ShaderFeatures::Frame))
s += l2 + addProperty("iFrame", "animatedFrame", "int");
if (m_shaderFeatures.enabled(ShaderFeatures::Resolution)) {
// Note: Pixel ratio is currently always 1.0
s += l2 + "readonly property vector3d iResolution: Qt.vector3d(width, height, 1.0)\n";
}
if (m_shaderFeatures.enabled(ShaderFeatures::Mouse)) {
s += l2 + "readonly property vector4d iMouse: Qt.vector4d(rootItem._effectMouseX, rootItem._effectMouseY,\n";
s += l2 + " rootItem._effectMouseZ, rootItem._effectMouseW)\n";
}
if (m_shaderFeatures.enabled(ShaderFeatures::BlurSources)) {
s += l2 + addProperty("iSourceBlur1", "blurSrc1", "Item", true);
s += l2 + addProperty("iSourceBlur2", "blurSrc2", "Item", true);
s += l2 + addProperty("iSourceBlur3", "blurSrc3", "Item", true);
s += l2 + addProperty("iSourceBlur4", "blurSrc4", "Item", true);
s += l2 + addProperty("iSourceBlur5", "blurSrc5", "Item", true);
}
// When used in editor preview component, we need property with value
// and when in exported component, property with binding to root value.
if (localFiles)
s += m_exportedEffectPropertiesString;
else
s += m_previewEffectPropertiesString;
if (!customImagesString.isEmpty())
s += '\n' + customImagesString;
// Add here all the custom QML code from nodes
if (m_nodeView) {
QString qmlCode;
for (auto n : m_nodeView->m_activeNodesList) {
if (!n->disabled && !n->qmlCode.isEmpty()) {
QString spacing = localFiles ? QStringLiteral(" ") : QStringLiteral(" ");
qmlCode += QStringLiteral("\n");
qmlCode += spacing + QString("//%1\n").arg(n->name);
QStringList qmlLines = n->qmlCode.split("\n");
for (const auto &line: qmlLines)
qmlCode += spacing + line + "\n";
}
}
s += qmlCode;
if (qmlCode != m_qmlCode) {
m_qmlCode = qmlCode;
setUnsavedChanges(true);
}
}
s += '\n';
s += l2 + "vertexShader: '" + vertexShaderFilename + "'\n";
s += l2 + "fragmentShader: '" + fragmentShaderFilename + "'\n";
s += l2 + "anchors.fill: parent\n";
if (m_shaderFeatures.enabled(ShaderFeatures::GridMesh)) {
QString gridSize = QString("%1, %2").arg(m_shaderFeatures.m_gridMeshWidth).arg(m_shaderFeatures.m_gridMeshHeight);
s += l2 + "mesh: GridMesh {\n";
s += l3 + QString("resolution: Qt.size(%1)\n").arg(gridSize);
s += l2 + "}\n";
}
s += l1 + "}\n";
return s;
}
void EffectManager::updateQmlComponent() {
// Clear possible QML runtime errors
resetEffectError(ErrorQMLRuntime);
QString s = getQmlComponentString(false);
setQmlComponentString(s);
}
NodeView *EffectManager::nodeView() const
{
return m_nodeView;
}
void EffectManager::setNodeView(NodeView *newNodeView)
{
if (m_nodeView == newNodeView)
return;
m_nodeView = newNodeView;
emit nodeViewChanged();
if (m_nodeView) {
m_nodeView->m_effectManager = this;
connect(m_nodeView, &NodeView::activeNodesListChanged, this, [this]() {
updateQmlComponent();
bakeShaders(true);
});
connect(m_nodeView, &NodeView::selectedNodeIdChanged, this, [this]() {
// Update visibility of properties based on the selected node
m_uniformModel->beginResetModel();
QList<UniformModel::Uniform>::iterator it = m_uniformModel->m_uniformTable->begin();
while (it != m_uniformModel->m_uniformTable->end()) {
if (m_nodeView->m_selectedNodeId == 0)
(*it).visible = true;
else if ((*it).nodeId == m_nodeView->m_selectedNodeId)
(*it).visible = true;
else
(*it).visible = false;
it++;
}
m_uniformModel->endResetModel();
});
}
}
// This will be called once when nodesview etc. components exist
void EffectManager::initialize()
{
bool projectOpened = false;
QString overrideExportPath;
if (g_argData.contains("export_path")) {
// Set export path first, so it gets saved if new project is created
overrideExportPath = g_argData.value("export_path").toString();
m_exportDirectory = overrideExportPath;
}
if (g_argData.contains("effects_project_path")) {
// Open or create project given as commandline parameter
QString projectFile = g_argData.value("effects_project_path").toString();
QString currentPath = QDir::currentPath();
QString fullFilePath = relativeToAbsolutePath(projectFile, currentPath);
bool createProject = g_argData.contains("create_project");
if (createProject) {
QFileInfo fi(fullFilePath);
projectOpened = newProject(fi.path(), fi.baseName(), true, false);
} else {
projectOpened = loadProject(QUrl{fullFilePath});
}
}
if (!overrideExportPath.isEmpty()) {
// Re-apply export path as loading the project may have changed it
m_exportDirectory = overrideExportPath;
Q_EMIT exportDirectoryChanged();
}
if (!projectOpened) {
// If project not open, reset the node view
cleanupNodeView();
}
QQmlContext *rootContext = QQmlEngine::contextForObject(this);
if (rootContext) {
auto *engine = rootContext->engine();
// Set up QML runtime error handling
connect(engine, &QQmlEngine::warnings, this, [this](const QList<QQmlError> &warnings) {
if (warnings.isEmpty())
return;
QQmlError error = warnings.first();
QString errorMessage = error.toString();
errorMessage.replace(QStringLiteral("<Unknown File>"), QStringLiteral("ERROR: "));
qInfo() << "QML:" << error.line() << ":" << errorMessage;
setEffectError(errorMessage, ErrorQMLRuntime, error.line());
}
);
}
// Select the main node
m_nodeView->selectSingleNode(0);
m_nodeView->updateCodeSelectorModel();
m_nodeView->m_initialized = true;
}
// Detects common GLSL error messages and returns potential
// additional error information related to them.
QString EffectManager::detectErrorMessage(const QString &errorMessage)
{
QHash<QString, QString> nodeErrors {
{ "'BLUR_HELPER_MAX_LEVEL' : undeclared identifier", "BlurHelper"},
{ "'iSourceBlur1' : undeclared identifier", "BlurHelper"},
{ "'hash23' : no matching overloaded function found", "NoiseHelper" },
{ "'HASH_BOX_SIZE' : undeclared identifier", "NoiseHelper" },
{ "'pseudo3dNoise' : no matching overloaded function found", "NoiseHelper" }
};
QString missingNodeError = QStringLiteral("Are you missing a %1 node?\n");
QHash<QString, QString>::const_iterator i = nodeErrors.constBegin();
while (i != nodeErrors.constEnd()) {
if (errorMessage.contains(i.key()))
return missingNodeError.arg(i.value());
++i;
}
return QString();
}
// Return first error message (if any)
EffectError EffectManager::effectError() const
{
for (const auto &e : m_effectErrors) {
if (!e.m_message.isEmpty())
return e;
}
return EffectError();
}
// Set the effect error message with optional type and lineNumber.
// Type comes from ErrorTypes, defaulting to common errors (-1).
// Note that type must match with UI editor tab index.
void EffectManager::setEffectError(const QString &errorMessage, int type, int lineNumber)
{
EffectError error;
error.m_type = type;
if (type == 1 || type == 2) {
// For shaders, get the line number from baker output.
// Which is something like "ERROR: :15: message"
int glslErrorLineNumber = -1;
static QRegularExpression spaceReg("\\s+");
QStringList errorStringList = errorMessage.split(spaceReg, Qt::SkipEmptyParts);
if (errorStringList.size() >= 2) {
QString lineString = errorStringList.at(1).trimmed();
if (lineString.size() >= 3) {
// String is ":[linenumber]:", get only the number.
glslErrorLineNumber = lineString.sliced(1, lineString.size() - 2).toInt();
}
}
error.m_line = glslErrorLineNumber;
} else {
// For QML (and others) use given linenumber
error.m_line = lineNumber;
}
QString additionalErrorInfo = detectErrorMessage(errorMessage);
error.m_message = additionalErrorInfo + errorMessage;
m_effectErrors.insert(type, error);
Q_EMIT effectErrorChanged();
}
void EffectManager::resetEffectError(int type)
{
if (m_effectErrors.contains(type)) {
m_effectErrors.remove(type);
Q_EMIT effectErrorChanged();
}
}
const QString &EffectManager::fragmentShaderFilename() const
{
return m_fragmentShaderFilename;
}
void EffectManager::setFragmentShaderFilename(const QString &newFragmentShaderFilename)
{
if (m_fragmentShaderFilename == newFragmentShaderFilename)
return;
m_fragmentShaderFilename = newFragmentShaderFilename;
emit fragmentShaderFilenameChanged();
}
const QString &EffectManager::vertexShaderFilename() const
{
return m_vertexShaderFilename;
}
void EffectManager::setVertexShaderFilename(const QString &newVertexShaderFilename)
{
if (m_vertexShaderFilename == newVertexShaderFilename)
return;
m_vertexShaderFilename = newVertexShaderFilename;
emit vertexShaderFilenameChanged();
}
const QString &EffectManager::qmlComponentString() const
{
return m_qmlComponentString;
}
void EffectManager::setQmlComponentString(const QString &string)
{
if (m_qmlComponentString == string)
return;
m_qmlComponentString = string;
emit qmlComponentStringChanged();
}
NodesModel::Node EffectManager::loadEffectNode(const QString &filename)
{
QFile loadFile(filename);
NodesModel::Node node;
if (!loadFile.open(QIODevice::ReadOnly)) {
qWarning("Couldn't open node file.");
return node;
}
if (m_nodeView)
m_nodeView->initializeNode(node);
QByteArray loadData = loadFile.readAll();
QJsonParseError parseError;
QJsonDocument jsonDoc(QJsonDocument::fromJson(loadData, &parseError));
if (parseError.error != QJsonParseError::NoError) {
QString error = QString("Error parsing the effect node: %1:").arg(filename);
QString errorDetails = QString("%1: %2").arg(parseError.offset).arg(parseError.errorString());
qWarning() << qPrintable(error);
qWarning() << qPrintable(errorDetails);
return node;
}
QJsonObject json = jsonDoc.object();
QFileInfo fi(loadFile);
createNodeFromJson(json, node, true, fi.absolutePath());
return node;
}
bool EffectManager::addEffectNode(const QString &filename, int startNodeId, int endNodeId)
{
auto node = loadEffectNode(filename);
addNodeIntoView(node, startNodeId, endNodeId);
return true;
}
bool EffectManager::addNodeIntoView(NodesModel::Node &node, int startNodeId, int endNodeId)
{
m_nodeView->m_nodesModel->beginResetModel();
m_nodeView->m_arrowsModel->beginResetModel();
if (startNodeId > -1 && endNodeId > -1) {
auto n1 = m_nodeView->m_nodesModel->getNodeWithId(startNodeId);
auto n2 = m_nodeView->m_nodesModel->getNodeWithId(endNodeId);
if (n1 && n2) {
// Remove already existing arrow
for (auto &arrow : m_nodeView->m_arrowsModel->m_arrowsList) {
if (arrow.endNodeId == endNodeId)
m_nodeView->m_arrowsModel->m_arrowsList.removeAll(arrow);
}
// Update next nodes
n1->nextNodeId = node.nodeId;
node.nextNodeId = n2->nodeId;
// Add new arrow
ArrowsModel::Arrow a1{0, 0, 0, 0, n1->nodeId, node.nodeId};
m_nodeView->m_arrowsModel->m_arrowsList << a1;
ArrowsModel::Arrow a2{0, 0, 0, 0, node.nodeId, n2->nodeId};
m_nodeView->m_arrowsModel->m_arrowsList << a2;
// Position the new node
float centerX = (n1->x + n1->width / 2.0f + n2->x + n2->width / 2.0f) / 2.0f;
float centerY = (n1->y + n1->height / 2.0f + n2->y + n2->height / 2.0f) / 2.0f;
node.x = centerX - node.width / 2.0f;
node.y = centerY - node.height / 2.0f;
}
}
// Add Node uniforms into uniform model
for (const auto &u : node.jsonUniforms)
m_uniformModel->appendUniform(u);
m_nodeView->m_nodesModel->m_nodesList << node;
m_nodeView->m_nodesModel->endResetModel();
m_nodeView->m_arrowsModel->endResetModel();
m_nodeView->updateArrowsPositions();
m_nodeView->updateActiveNodesList();
// Select the newly created node
m_nodeView->selectSingleNode(node.nodeId);
setUnsavedChanges(true);
return true;
}
bool EffectManager::addNodeConnection(int startNodeId, int endNodeId)
{
auto n1 = m_nodeView->m_nodesModel->getNodeWithId(startNodeId);
auto n2 = m_nodeView->m_nodesModel->getNodeWithId(endNodeId);
if (!n1 || !n2) {
qWarning("Can't connect unknown nodes");
return false;
}
if (n1->nodeId == n2->nodeId) {
qWarning("Can't connect node with itself");
return false;
}
// Remove already existing arrow
for (auto &arrow : m_nodeView->m_arrowsModel->m_arrowsList) {
if (arrow.endNodeId == endNodeId)
m_nodeView->m_arrowsModel->m_arrowsList.removeAll(arrow);
}
// Update next node
n1->nextNodeId = n2->nodeId;
// Add new arrow
ArrowsModel::Arrow a1{0, 0, 0, 0, n1->nodeId, n2->nodeId};
m_nodeView->m_arrowsModel->m_arrowsList << a1;
return true;
}
QString EffectManager::codeFromJsonArray(const QJsonArray &codeArray)
{
QString codeString;
for (const auto& element : codeArray) {
codeString += element.toString();
codeString += '\n';
}
codeString.chop(1); // Remove last '\n'
return codeString;
}
// Parameter fullNode means nodes from files with QEN etc.
// Parameter nodePath is the directory where node is loaded from.
bool EffectManager::createNodeFromJson(const QJsonObject &rootJson, NodesModel::Node &node, bool fullNode, const QString &nodePath)
{
QJsonObject json;
if (fullNode) {
if (!rootJson.contains("QEN")) {
qWarning("Invalid Node file");
return false;
}
json = rootJson["QEN"].toObject();
int version = -1;
if (json.contains("version"))
version = json["version"].toInt(-1);
if (version != 1) {
QString error = QString("Error: Unknown effect node version (%1)").arg(version);
qWarning() << qPrintable(error);
setEffectError(error);
return false;
}
} else {
json = rootJson;
}
if (json.contains("name")) {
node.name = json["name"].toString();
} else {
QString error = QString("Error: Node missing a name");
qWarning() << qPrintable(error);
setEffectError(error);
return false;
}
// When loading nodes they contain extra data
if (json.contains("nodeId"))
node.nodeId = json["nodeId"].toInt();
if (json.contains("x"))
node.x = json["x"].toDouble();
if (json.contains("y"))
node.y = json["y"].toDouble();
if (json.contains("disabled"))
node.disabled = getBoolValue(json["disabled"], false);
if (m_nodeView) {
// Update the node size based on its type
m_nodeView->initializeNodeSize(node);
node.name = m_nodeView->getUniqueNodeName(node.name);
}
node.description = json["description"].toString();
if (json.contains("fragmentCode") && json["fragmentCode"].isArray())
node.fragmentCode = codeFromJsonArray(json["fragmentCode"].toArray());
if (json.contains("vertexCode") && json["vertexCode"].isArray())
node.vertexCode = codeFromJsonArray(json["vertexCode"].toArray());
if (json.contains("qmlCode") && json["qmlCode"].isArray())
node.qmlCode = codeFromJsonArray(json["qmlCode"].toArray());
if (json.contains("properties") && json["properties"].isArray()) {
QJsonArray propertiesArray = json["properties"].toArray();
for (const auto& element : propertiesArray) {
auto propertyObject = element.toObject();
UniformModel::Uniform u = {};
u.nodeId = node.nodeId;
u.name = propertyObject["name"].toString().toUtf8();
u.description = propertyObject["description"].toString();
u.type = m_uniformModel->typeFromString(propertyObject["type"].toString());
u.exportProperty = getBoolValue(propertyObject["exported"], true);
QString value, defaultValue, minValue, maxValue;
defaultValue = propertyObject["defaultValue"].toString();
if (u.type == UniformModel::Uniform::Type::Sampler) {
if (!defaultValue.isEmpty())
defaultValue = relativeToAbsolutePath(defaultValue, nodePath);
if (propertyObject.contains("enableMipmap"))
u.enableMipmap = getBoolValue(propertyObject["enableMipmap"], false);
if (propertyObject.contains("exportImage"))
u.exportImage = getBoolValue(propertyObject["exportImage"], true);
// Update the mipmap property
QString mipmapProperty = mipmapPropertyName(u.name);
g_propertyData[mipmapProperty] = u.enableMipmap;
}
if (propertyObject.contains("value")) {
value = propertyObject["value"].toString();
if (u.type == UniformModel::Uniform::Type::Sampler && !value.isEmpty())
value = relativeToAbsolutePath(value, nodePath);
} else {
// QEN files don't store the current value, so with those use default value
value = defaultValue;
}
u.customValue = propertyObject["customValue"].toString();
u.useCustomValue = getBoolValue(propertyObject["useCustomValue"], false);
minValue = propertyObject["minValue"].toString();
maxValue = propertyObject["maxValue"].toString();
m_uniformModel->setUniformValueData(&u, value, defaultValue, minValue, maxValue);
node.jsonUniforms << u;
}
}
return true;
}
bool EffectManager::deleteEffectNodes(QList<int> nodeIds)
{
if (nodeIds.isEmpty())
return false;
m_nodeView->m_nodesModel->beginResetModel();
m_nodeView->m_arrowsModel->beginResetModel();
m_uniformModel->beginResetModel();
for (auto nodeId : nodeIds) {
auto node = m_nodeView->m_nodesModel->getNodeWithId(nodeId);
if (!node)
continue;
if (node->type != NodesModel::NodeType::CustomNode)
continue;
// Remove possibly existing arrows
{
QList<ArrowsModel::Arrow>::iterator it = m_nodeView->m_arrowsModel->m_arrowsList.begin();
while (it != m_nodeView->m_arrowsModel->m_arrowsList.end()) {
if ((*it).startNodeId == node->nodeId) {
// Update nextNode
node->nextNodeId = -1;
it = m_nodeView->m_arrowsModel->m_arrowsList.erase(it);
} else if ((*it).endNodeId == node->nodeId) {
// Update nextNode
if (auto n = m_nodeView->m_nodesModel->getNodeWithId((*it).startNodeId))
n->nextNodeId = -1;
it = m_nodeView->m_arrowsModel->m_arrowsList.erase(it);
} else {
it++;
}
}
}
// Remove properties
{
QList<UniformModel::Uniform>::iterator it = m_uniformModel->m_uniformTable->begin();
while (it != m_uniformModel->m_uniformTable->end()) {
if ((*it).nodeId == nodeId)
it = m_uniformModel->m_uniformTable->erase(it);
else
it++;
}
}
// Remove node
m_nodeView->m_nodesModel->m_nodesList.removeAll(*node);
}
m_nodeView->m_nodesModel->endResetModel();
m_nodeView->m_arrowsModel->endResetModel();
m_uniformModel->endResetModel();
m_nodeView->updateActiveNodesList();
// Select the Main node
m_nodeView->selectMainNode();
return true;
}
QString EffectManager::getSupportedImageFormatsFilter() const
{
auto formats = QImageReader::supportedImageFormats();
QString imageFilter = QStringLiteral("Image files (");
for (const auto &format : std::as_const(formats))
imageFilter += QStringLiteral("*.") + format + QStringLiteral(" ");
imageFilter += QStringLiteral(")");
return imageFilter;
}
void EffectManager::cleanupProject()
{
m_exportFilename.clear();
Q_EMIT exportFilenameChanged();
m_exportDirectory.clear();
Q_EMIT exportDirectoryChanged();
m_exportFlags = QMLComponent | QSBShaders | Images;
Q_EMIT exportFlagsChanged();
// Reset also settings
setEffectPadding(QRect(0, 0, 0, 0));
setEffectHeadings(QString());
clearImageWatchers();
}
QString EffectManager::replaceOldTagsWithNew(const QString &code) {
QString s = code;
s = s.replace("//main", "@main");
s = s.replace("//nodes", "@nodes");
s = s.replace("//mesh", "@mesh");
s = s.replace("//blursources", "@blursources");
return s;
}
bool EffectManager::loadProject(const QUrl &filename)
{
auto loadFile = resolveFileFromUrl(filename);
resetEffectError();
if (!loadFile.open(QIODevice::ReadOnly)) {
QString error = QString("Couldn't open project file: '%1'").arg(filename.toString());
qWarning() << qPrintable(error);
setEffectError(error);
m_settings->removeRecentProjectsModel(filename.toString());
return false;
}
QByteArray data = loadFile.readAll();
QJsonParseError parseError;
QJsonDocument jsonDoc(QJsonDocument::fromJson(data, &parseError));
if (parseError.error != QJsonParseError::NoError) {
QString error = QString("Error parsing the project file: %1: %2").arg(parseError.offset).arg(parseError.errorString());
qWarning() << qPrintable(error);
setEffectError(error);
m_settings->removeRecentProjectsModel(filename.toString());
return false;
}
QJsonObject rootJson = jsonDoc.object();
if (!rootJson.contains("QEP")) {
QString error = QStringLiteral("Error: Invalid project file");
qWarning() << qPrintable(error);
setEffectError(error);
m_settings->removeRecentProjectsModel(filename.toString());
return false;
}
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() << qPrintable(error);
setEffectError(error);
m_settings->removeRecentProjectsModel(filename.toString());
return false;
}
// Get the QQEM version this project was saved with.
// As a number, so we can use it for comparisons.
// Start with 0.4 as QQM 0.41 was the first version which started
// saving these version numbers.
double qqemVersion = 0.4;
if (json.contains("QQEM")) {
QString versionString = json["QQEM"].toString();
bool ok;
double versionNumber = versionString.toDouble(&ok);
if (ok) {
qqemVersion = versionNumber;
} else {
QString error = QString("Warning: Invalid QQEM version (%1)").arg(versionString);
qWarning() << qPrintable(error);
setEffectError(error);
}
}
// At this point we consider project to be OK, so start cleanup & load
cleanupProject();
cleanupNodeView(false);
m_uniformTable.clear();
updateCustomUniforms();
// Update project directory & name
m_projectFilename = filename;
Q_EMIT projectFilenameChanged();
Q_EMIT hasProjectFilenameChanged();
QFileInfo fi(loadFile);
m_projectDirectory = fi.path();
Q_EMIT projectDirectoryChanged();
setProjectName(fi.baseName());
m_settings->updateRecentProjectsModel(m_projectName, m_projectFilename.toString());
// Get export directory & name
if (json.contains("exportName")) {
m_exportFilename = json["exportName"].toString();
Q_EMIT exportFilenameChanged();
}
if (json.contains("exportDirectory")) {
QString exportDirectory = json["exportDirectory"].toString();
m_exportDirectory = relativeToAbsolutePath(exportDirectory, m_projectDirectory);
Q_EMIT exportDirectoryChanged();
}
if (json.contains("exportFlags")) {
m_exportFlags = json["exportFlags"].toInt();
Q_EMIT exportFlagsChanged();
}
if (json.contains("settings") && json["settings"].isObject()) {
QJsonObject settingsObject = json["settings"].toObject();
// Effect item padding
QRect padding(0, 0, 0, 0);
padding.setX(settingsObject["paddingLeft"].toInt());
padding.setY(settingsObject["paddingTop"].toInt());
padding.setWidth(settingsObject["paddingRight"].toInt());
padding.setHeight(settingsObject["paddingBottom"].toInt());
setEffectPadding(padding);
// Effect headings
if (settingsObject.contains("headings") && settingsObject["headings"].isArray())
setEffectHeadings(codeFromJsonArray(settingsObject["headings"].toArray()));
}
if (json.contains("nodes") && json["nodes"].isArray()) {
QJsonArray nodesArray = json["nodes"].toArray();
for (const auto& nodeElement : nodesArray) {
QJsonObject nodeObject = nodeElement.toObject();
int type = NodesModel::CustomNode;
if (nodeObject.contains("type"))
type = nodeObject["type"].toInt();
if (type == NodesModel::CustomNode) {
NodesModel::Node node;
m_nodeView->initializeNode(node);
createNodeFromJson(nodeObject, node, false, m_projectDirectory);
addNodeIntoView(node);
} else {
// Source / Output node
int nodeId = nodeObject["nodeId"].toInt();
auto node = m_nodeView->m_nodesModel->getNodeWithId(nodeId);
if (node) {
node->x = nodeObject["x"].toDouble();
node->y = nodeObject["y"].toDouble();
if (node->type == NodesModel::SourceNode) {
// Source can contain also shaders
if (nodeObject.contains("vertexCode") && nodeObject["vertexCode"].isArray())
node->vertexCode = codeFromJsonArray(nodeObject["vertexCode"].toArray());
else
node->vertexCode = getDefaultRootVertexShader().join('\n');
if (nodeObject.contains("fragmentCode") && nodeObject["fragmentCode"].isArray())
node->fragmentCode = codeFromJsonArray(nodeObject["fragmentCode"].toArray());
else
node->fragmentCode = getDefaultRootFragmentShader().join('\n');
// And QML
if (nodeObject.contains("qmlCode") && nodeObject["qmlCode"].isArray())
node->qmlCode = codeFromJsonArray(nodeObject["qmlCode"].toArray());
Q_EMIT m_nodeView->selectedNodeFragmentCodeChanged();
Q_EMIT m_nodeView->selectedNodeVertexCodeChanged();
Q_EMIT m_nodeView->selectedNodeQmlCodeChanged();
}
}
}
}
}
if (json.contains("connections") && json["connections"].isArray()) {
QJsonArray connectionsArray = json["connections"].toArray();
for (const auto& connectionElement : connectionsArray) {
QJsonObject connectionObject = connectionElement.toObject();
int fromId = connectionObject["fromId"].toInt();
int toId = connectionObject["toId"].toInt();
addNodeConnection(fromId, toId);
}
}
// Replace old tags ("//nodes") with new format ("@nodes")
// Projects saved with version <= 0.40 use the old tags format
if (qqemVersion <= 0.4) {
m_nodeView->m_nodesModel->beginResetModel();
for (auto &node : m_nodeView->m_nodesModel->m_nodesList) {
node.vertexCode = replaceOldTagsWithNew(node.vertexCode);
node.fragmentCode = replaceOldTagsWithNew(node.fragmentCode);
if (node.selected) {
Q_EMIT m_nodeView->selectedNodeVertexCodeChanged();
Q_EMIT m_nodeView->selectedNodeFragmentCodeChanged();
}
}
m_nodeView->m_nodesModel->endResetModel();
}
m_nodeView->updateActiveNodesList();
// Layout nodes automatically to suit current view size
// But wait that we are definitely in the design mode
QTimer::singleShot(1, m_nodeView, [this]() {
m_nodeView->layoutNodes(false);
} );
setUnsavedChanges(false);
return true;
}
bool EffectManager::saveProject(const QUrl &filename)
{
QUrl fileUrl = filename;
// When this is called without a filename, use previous one
if (filename.isEmpty())
fileUrl = m_projectFilename;
auto saveFile = resolveFileFromUrl(fileUrl);
if (!saveFile.open(QIODevice::WriteOnly)) {
QString error = QString("Error: Couldn't save project file: '%1'").arg(fileUrl.toString());
qWarning() << qPrintable(error);
setEffectError(error);
return false;
}
m_projectFilename = fileUrl;
Q_EMIT projectFilenameChanged();
Q_EMIT hasProjectFilenameChanged();
QFileInfo fi(saveFile);
setProjectName(fi.baseName());
QJsonObject json;
// File format version
json.insert("version", 1);
// QQEM version
json.insert("QQEM", qApp->applicationVersion());
// Add project settings
QJsonObject settingsObject;
if (m_effectPadding.x() != 0)
settingsObject.insert("paddingLeft", m_effectPadding.x());
if (m_effectPadding.y() != 0)
settingsObject.insert("paddingTop", m_effectPadding.y());
if (m_effectPadding.width() != 0)
settingsObject.insert("paddingRight", m_effectPadding.width());
if (m_effectPadding.height() != 0)
settingsObject.insert("paddingBottom", m_effectPadding.height());
if (!m_effectHeadings.isEmpty()) {
QJsonArray headingsArray;
QStringList hLines = m_effectHeadings.split('\n');
for (const auto &line: std::as_const(hLines))
headingsArray.append(line);
if (!headingsArray.isEmpty())
settingsObject.insert("headings", headingsArray);
}
if (!settingsObject.isEmpty())
json.insert("settings", settingsObject);
// Add export directory & name
if (!m_exportFilename.isEmpty())
json.insert("exportName", m_exportFilename);
if (!m_exportDirectory.isEmpty()) {
// Export directory is stored as a relative path.
QString relativeExportPath = absoluteToRelativePath(m_exportDirectory, m_projectDirectory);
json.insert("exportDirectory", relativeExportPath);
}
json.insert("exportFlags", m_exportFlags);
// Add nodes
QJsonArray nodesArray;
for (const auto &node : m_nodeView->m_nodesModel->m_nodesList) {
QJsonObject nodeObject = nodeToJson(node, false, fi.absolutePath());
nodesArray.append(nodeObject);
}
if (!nodesArray.isEmpty())
json.insert("nodes", nodesArray);
// Add connections
QJsonArray connectionsArray;
for (const auto &arrow : m_nodeView->m_arrowsModel->m_arrowsList) {
QJsonObject arrowObject;
arrowObject.insert("fromId", arrow.startNodeId);
arrowObject.insert("toId", arrow.endNodeId);
// Add connection into array
connectionsArray.append(arrowObject);
}
if (!connectionsArray.isEmpty())
json.insert("connections", connectionsArray);
QJsonObject rootJson;
rootJson.insert("QEP", json);
QJsonDocument jsonDoc(rootJson);
saveFile.write(jsonDoc.toJson());
setUnsavedChanges(false);
if (!filename.isEmpty()) {
// When called with filename (so initial save or save as),
// add into recent projects list.
m_settings->updateRecentProjectsModel(m_projectName, m_projectFilename.toString());
}
return true;
}
// Takes in absolute path (e.g. "file:///C:/myimages/steel1.jpg") and
// path to convert to (e.g. "C:/qqem/defaultnodes".
// Retuns relative path (e.g. "../myimages/steel1.jpg")
QString EffectManager::absoluteToRelativePath(const QString &path, const QString &toPath) {
if (path.isEmpty())
return QString();
QUrl url(path);
QString filePath = (url.scheme() == QStringLiteral("file")) ? url.toLocalFile() : url.toString();
QDir dir(toPath);
QString localPath = dir.relativeFilePath(filePath);
return localPath;
}
// Takes in path relative to project path (e.g. "../myimages/steel1.jpg") and
// path to convert to (e.g. "C:/qqem/defaultnodes".
// Returns absolute path (e.g. "file:///C:/qqem/myimages/steel1.jpg")
QString EffectManager::relativeToAbsolutePath(const QString &path, const QString &toPath) {
QString filePath = path;
QDir dir(toPath);
QString absPath = dir.absoluteFilePath(filePath);
absPath = QDir::cleanPath(absPath);
absPath = QUrl::fromLocalFile(absPath).toString();
return absPath;
}
// Removes "file:" from the URL path.
// So e.g. "file:///C:/myimages/steel1.jpg" -> "C:/myimages/steel1.jpg"
QString EffectManager::stripFileFromURL(const QString &urlString) const
{
QUrl url(urlString);
QString filePath = (url.scheme() == QStringLiteral("file")) ? url.toLocalFile() : url.toString();
return filePath;
}
// Adds "file:" scheme to the URL path.
// So e.g. "C:/myimages/steel1.jpg" -> "file:///C:/myimages/steel1.jpg"
QString EffectManager::addFileToURL(const QString &urlString) const
{
if (!urlString.startsWith("file:"))
return QStringLiteral("file:///") + urlString;
return urlString;
}
// Returns absolute directory of the path.
// When useFileScheme is true, "file:" scheme is added into result.
QString EffectManager::getDirectory(const QString &path, bool useFileScheme) const
{
QString filePath = stripFileFromURL(path);
QFileInfo fi(filePath);
QString dir = fi.canonicalPath();
if (useFileScheme)
dir = addFileToURL(dir);
return dir;
}
QString EffectManager::getDefaultImagesDirectory(bool useFileScheme) const
{
QString dir = m_settings->defaultResourcePath() + QStringLiteral("/defaultnodes/images");
if (useFileScheme)
dir = addFileToURL(dir);
return dir;
}
// Creates JSON presentation of the \a node.
// When simplified is true, temporary UI data is ignored (position, nodeId etx.)
// For projects these are saved, for node components not.
QJsonObject EffectManager::nodeToJson(const NodesModel::Node &node, bool simplified, const QString &nodePath)
{
QJsonObject nodeObject;
nodeObject.insert("name", node.name);
if (!node.description.isEmpty())
nodeObject.insert("description", node.description);
if (!simplified) {
nodeObject.insert("type", node.type);
nodeObject.insert("nodeId", node.nodeId);
nodeObject.insert("x", node.x);
nodeObject.insert("y", node.y);
if (node.disabled)
nodeObject.insert("disabled", true);
} else {
nodeObject.insert("version", 1);
}
// Add properties
QJsonArray propertiesArray;
for (auto &uniform : m_uniformTable) {
if (uniform.nodeId != node.nodeId)
continue;
QJsonObject uniformObject;
uniformObject.insert("name", QString(uniform.name));
QString type = m_uniformModel->stringFromType(uniform.type);
uniformObject.insert("type", type);
if (!simplified) {
QString value = m_uniformModel->variantAsDataString(uniform.type, uniform.value);
if (uniform.type == UniformModel::Uniform::Type::Sampler)
value = absoluteToRelativePath(value, nodePath);
uniformObject.insert("value", value);
}
QString defaultValue = m_uniformModel->variantAsDataString(uniform.type, uniform.defaultValue);
if (uniform.type == UniformModel::Uniform::Type::Sampler) {
defaultValue = absoluteToRelativePath(defaultValue, nodePath);
if (uniform.enableMipmap)
uniformObject.insert("enableMipmap", uniform.enableMipmap);
if (!uniform.exportImage)
uniformObject.insert("exportImage", false);
}
uniformObject.insert("defaultValue", defaultValue);
if (!uniform.description.isEmpty())
uniformObject.insert("description", uniform.description);
if (uniform.type == UniformModel::Uniform::Type::Float
|| uniform.type == UniformModel::Uniform::Type::Int
|| uniform.type == UniformModel::Uniform::Type::Vec2
|| uniform.type == UniformModel::Uniform::Type::Vec3
|| uniform.type == UniformModel::Uniform::Type::Vec4) {
uniformObject.insert("minValue", m_uniformModel->variantAsDataString(uniform.type, uniform.minValue));
uniformObject.insert("maxValue", m_uniformModel->variantAsDataString(uniform.type, uniform.maxValue));
}
if (!uniform.customValue.isEmpty())
uniformObject.insert("customValue", uniform.customValue);
if (uniform.useCustomValue)
uniformObject.insert("useCustomValue", true);
if (!uniform.exportProperty)
uniformObject.insert("exported", false);
propertiesArray.append(uniformObject);
}
if (!propertiesArray.isEmpty())
nodeObject.insert("properties", propertiesArray);
// Add shaders
if (!node.fragmentCode.trimmed().isEmpty()) {
QJsonArray fragmentCodeArray;
QStringList fsLines = node.fragmentCode.split('\n');
for (const auto &line: fsLines)
fragmentCodeArray.append(line);
if (!fragmentCodeArray.isEmpty())
nodeObject.insert("fragmentCode", fragmentCodeArray);
}
if (!node.vertexCode.trimmed().isEmpty()) {
QJsonArray vertexCodeArray;
QStringList vsLines = node.vertexCode.split('\n');
for (const auto &line: vsLines)
vertexCodeArray.append(line);
if (!vertexCodeArray.isEmpty())
nodeObject.insert("vertexCode", vertexCodeArray);
}
// Add QML code
if (!node.qmlCode.trimmed().isEmpty()) {
QJsonArray qmlCodeArray;
QStringList qmlLines = node.qmlCode.split('\n');
for (const auto &line: qmlLines)
qmlCodeArray.append(line);
if (!qmlCodeArray.isEmpty())
nodeObject.insert("qmlCode", qmlCodeArray);
}
if (simplified) {
QJsonObject rootJson;
rootJson.insert("QEN", nodeObject);
return rootJson;
}
return nodeObject;
}
bool EffectManager::newProject(const QString &filepath, const QString &filename, bool clearNodeView, bool createProjectDir)
{
if (filepath.isEmpty()) {
qWarning("No path");
return false;
}
if (filename.isEmpty()) {
qWarning("No filename");
return false;
}
if (clearNodeView) {
resetEffectError();
cleanupProject();
cleanupNodeView(true);
}
QString dirPath = filepath;
if (createProjectDir)
dirPath += "/" + filename;
// Make sure that dir exists
QDir dir(dirPath);
if (!dir.exists())
dir.mkpath(".");
m_projectDirectory = dirPath;
Q_EMIT projectDirectoryChanged();
// Create project file
QString projectFilename;
if (!dirPath.startsWith("file:"))
projectFilename += "file:///";
projectFilename += dirPath + "/" + filename + ".qep";
m_projectFilename = QUrl{projectFilename};
Q_EMIT projectFilenameChanged();
Q_EMIT hasProjectFilenameChanged();
m_nodeView->updateActiveNodesList();
// Layout nodes automatically to suit current view size
// But wait that we are definitely in the design mode
QTimer::singleShot(1, m_nodeView, [this]() {
m_nodeView->layoutNodes(false);
} );
// Save the new project, with whatever nodes user had at this point
saveProject(m_projectFilename);
return true;
}
void EffectManager::closeProject()
{
cleanupProject();
cleanupNodeView(true);
// Update project directory & name
m_projectFilename.clear();
Q_EMIT projectFilenameChanged();
Q_EMIT hasProjectFilenameChanged();
m_projectDirectory.clear();
Q_EMIT projectDirectoryChanged();
setProjectName(QString());
}
bool EffectManager::exportEffect(const QString &dirPath, const QString &filename, int exportFlags, int qsbVersionIndex)
{
if (dirPath.isEmpty() || filename.isEmpty()) {
QString error = QString("Error: Couldn't export the effect: '%1/%2'").arg(dirPath, filename);
qWarning() << qPrintable(error);
setEffectError(error);
return false;
}
// Update export filename & path
m_exportFilename = filename;
Q_EMIT exportFilenameChanged();
m_exportDirectory = dirPath;
Q_EMIT exportDirectoryChanged();
// Make sure that uniforms are up-to-date
updateCustomUniforms();
// Make sure that dir exists
QDir dir(dirPath);
if (!dir.exists())
dir.mkpath(".");
QString qmlFilename = filename + ".qml";
QString vsFilename = filename + ".vert.qsb";
QString fsFilename = filename + ".frag.qsb";
QString vsSourceFilename = filename + ".vert";
QString fsSourceFilename = filename + ".frag";
QString qrcFilename = filename + ".qrc";
QStringList exportedFilenames;
// Make sure that QML component starts with capital letter
qmlFilename[0] = qmlFilename[0].toUpper();
// Shaders & qrc on the other hand will be all lowercase
vsFilename = vsFilename.toLower();
fsFilename = fsFilename.toLower();
vsSourceFilename = vsSourceFilename.toLower();
fsSourceFilename = fsSourceFilename.toLower();
qrcFilename = qrcFilename.toLower();
// Bake shaders with correct settings
if (exportFlags & QSBShaders) {
auto qsbVersion = QShader::SerializedFormatVersion::Latest;
if (qsbVersionIndex == 1)
qsbVersion = QShader::SerializedFormatVersion::Qt_6_5;
else if (qsbVersionIndex == 2)
qsbVersion = QShader::SerializedFormatVersion::Qt_6_4;
QString vsFilePath = dirPath + "/" + vsFilename;
removeIfExists(vsFilePath);
m_baker.setSourceString(m_vertexShader.toUtf8(), QShader::VertexStage);
QShader vertShader = m_baker.bake();
if (vertShader.isValid())
writeToFile(vertShader.serialized(qsbVersion), vsFilePath, FileType::Binary);
exportedFilenames << vsFilename;
QString fsFilePath = dirPath + "/" + fsFilename;
removeIfExists(fsFilePath);
m_baker.setSourceString(m_fragmentShader.toUtf8(), QShader::FragmentStage);
QShader fragShader = m_baker.bake();
if (fragShader.isValid())
writeToFile(fragShader.serialized(qsbVersion), fsFilePath, FileType::Binary);
exportedFilenames << fsFilename;
}
// Copy images
if (exportFlags & Images) {
for (auto &uniform : m_uniformTable) {
if (uniform.type == UniformModel::Uniform::Type::Sampler &&
!uniform.value.toString().isEmpty() &&
uniform.exportImage) {
QString imagePath = uniform.value.toString();
QFileInfo fi(imagePath);
QString imageFilename = fi.fileName();
QString imageFilePath = dirPath + "/" + imageFilename;
imagePath = stripFileFromURL(imagePath);
if (imagePath.compare(imageFilePath, Qt::CaseInsensitive) == 0)
continue; // Exporting to same dir, so skip
removeIfExists(imageFilePath);
QFile imageFile(imagePath);
if (!imageFile.copy(imageFilePath))
qWarning("Unable to copy image file: %s", qPrintable(imageFilename));
else
exportedFilenames << imageFilename;
}
}
}
if (exportFlags & QMLComponent) {
QString qmlComponentString = getQmlEffectString();
QStringList qmlStringList = qmlComponentString.split('\n');
// Replace shaders with local versions
for (int i = 1; i < qmlStringList.size(); i++) {
QString line = qmlStringList.at(i).trimmed();
if (line.startsWith("vertexShader")) {
QString vsLine = " vertexShader: '" + vsFilename + "'";
qmlStringList[i] = vsLine;
} else if (line.startsWith("fragmentShader")) {
QString fsLine = " fragmentShader: '" + fsFilename + "'";
qmlStringList[i] = fsLine;
}
}
QString qmlString = qmlStringList.join('\n');
QString qmlFilePath = dirPath + "/" + qmlFilename;
writeToFile(qmlString.toUtf8(), qmlFilePath, FileType::Text);
exportedFilenames << qmlFilename;
// Copy blur helpers
if (m_shaderFeatures.enabled(ShaderFeatures::BlurSources)) {
QString blurHelperFilename("BlurHelper.qml");
QString blurFsFilename("bluritems.frag.qsb");
QString blurVsFilename("bluritems.vert.qsb");
QString blurHelperPath(m_settings->defaultResourcePath() + "/defaultnodes/common/");
QString blurHelperSource(blurHelperPath + blurHelperFilename);
QString blurFsSource(blurHelperPath + blurFsFilename);
QString blurVsSource(blurHelperPath + blurVsFilename);
QFile blurHelperFile(blurHelperSource);
QFile blurFsFile(blurFsSource);
QFile blurVsFile(blurVsSource);
QString blurHelperFilePath = dirPath + "/" + "BlurHelper.qml";
QString blurFsFilePath = dirPath + "/" + "bluritems.frag.qsb";
QString blurVsFilePath = dirPath + "/" + "bluritems.vert.qsb";
removeIfExists(blurHelperFilePath);
removeIfExists(blurFsFilePath);
removeIfExists(blurVsFilePath);
if (!blurHelperFile.copy(blurHelperFilePath))
qWarning("Unable to copy file: %s", qPrintable(blurHelperFilePath));
if (!blurFsFile.copy(blurFsFilePath))
qWarning("Unable to copy file: %s", qPrintable(blurFsFilePath));
if (!blurVsFile.copy(blurVsFilePath))
qWarning("Unable to copy file: %s", qPrintable(blurVsFilePath));
exportedFilenames << blurHelperFilename;
exportedFilenames << blurFsFilename;
exportedFilenames << blurVsFilename;
}
}
// Export shaders as plain-text
if (exportFlags & TextShaders) {
QString vsSourceFilePath = dirPath + "/" + vsSourceFilename;
writeToFile(m_vertexShader.toUtf8(), vsSourceFilePath, FileType::Text);
QString fsSourceFilePath = dirPath + "/" + fsSourceFilename;
writeToFile(m_fragmentShader.toUtf8(), fsSourceFilePath, FileType::Text);
}
if (exportFlags & QRCFile) {
QString qrcXmlString;
QString qrcFilePath = dirPath + "/" + qrcFilename;
QXmlStreamWriter stream(&qrcXmlString);
stream.setAutoFormatting(true);
stream.writeStartElement("RCC");
stream.writeStartElement("qresource");
stream.writeAttribute("prefix", "/");
for (const auto &filename : exportedFilenames)
stream.writeTextElement("file", filename);
stream.writeEndElement(); // qresource
stream.writeEndElement(); // RCC
writeToFile(qrcXmlString.toUtf8(), qrcFilePath, FileType::Text);
}
// Update exportFlags
if (m_exportFlags != exportFlags) {
m_exportFlags = exportFlags;
Q_EMIT exportFlagsChanged();
}
return true;
}
QFile EffectManager::resolveFileFromUrl(const QUrl &fileUrl)
{
const QQmlContext *context = qmlContext(this);
const auto resolvedUrl = context ? context->resolvedUrl(fileUrl) : fileUrl;
const auto qmlSource = QQmlFile::urlToLocalFileOrQrc(resolvedUrl);
QFileInfo fileInfo(qmlSource);
QString filePath = fileInfo.canonicalFilePath();
if (filePath.isEmpty())
filePath = fileInfo.absoluteFilePath();
return QFile(filePath);
}
void EffectManager::cleanupNodeView(bool initialize)
{
QList<int> nodes;
for (const auto &node : m_nodeView->m_nodesModel->m_nodesList) {
if (node.type == 2)
nodes << node.nodeId;
}
deleteEffectNodes(nodes);
// Clear also arrows, so source->output connection is removed
m_nodeView->m_arrowsModel->m_arrowsList.clear();
if (initialize) {
// Add first connection
addNodeConnection(0, 1);
// Add default root shaders
int nodeId = 0;
auto node = m_nodeView->m_nodesModel->getNodeWithId(nodeId);
if (node) {
node->vertexCode = getDefaultRootVertexShader().join('\n');
node->fragmentCode = getDefaultRootFragmentShader().join('\n');
node->qmlCode.clear();
m_nodeView->selectMainNode();
Q_EMIT m_nodeView->selectedNodeFragmentCodeChanged();
Q_EMIT m_nodeView->selectedNodeVertexCodeChanged();
Q_EMIT m_nodeView->selectedNodeQmlCodeChanged();
}
}
m_nodeView->updateArrowsPositions();
m_nodeView->updateActiveNodesList();
}
bool EffectManager::saveSelectedNode(const QUrl &filename)
{
QUrl fileUrl = filename;
auto saveFile = resolveFileFromUrl(fileUrl);
if (!saveFile.open(QIODevice::WriteOnly)) {
QString error = QString("Error: Couldn't save node file: '%1'").arg(fileUrl.toString());
qWarning() << qPrintable(error);
setEffectError(error);
return false;
}
auto node = m_nodeView->m_nodesModel->m_selectedNode;
if (!node) {
QString error = QStringLiteral("Error: No node selected'");
qWarning() << qPrintable(error);
setEffectError(error);
return false;
}
QFileInfo fi(saveFile);
QJsonObject nodeObject = nodeToJson(*node, true, fi.absolutePath());
QJsonDocument jsonDoc(nodeObject);
saveFile.write(jsonDoc.toJson());
return true;
}
bool EffectManager::shadersUpToDate() const
{
return m_shadersUpToDate;
}
void EffectManager::setShadersUpToDate(bool upToDate)
{
if (m_shadersUpToDate == upToDate)
return;
m_shadersUpToDate = upToDate;
Q_EMIT shadersUpToDateChanged();
}
bool EffectManager::autoPlayEffect() const
{
return m_autoPlayEffect;
}
void EffectManager::setAutoPlayEffect(bool autoPlay)
{
if (m_autoPlayEffect == autoPlay)
return;
m_autoPlayEffect = autoPlay;
Q_EMIT autoPlayEffectChanged();
}
QString EffectManager::getHelpTextString()
{
QFile helpFile(":/qqem_help.html");
if (!helpFile.open(QIODevice::ReadOnly)) {
qWarning("Couldn't open help file.");
return QString();
}
QByteArray helpData = helpFile.readAll();
return QString::fromLatin1(helpData);
}
AddNodeModel *EffectManager::addNodeModel() const
{
return m_addNodeModel;
}
// This is called when the AddNodeDialog is shown.
void EffectManager::updateAddNodeData()
{
if (m_addNodeModel && m_nodeView) {
updateQmlComponent();
// Collect already used properties
QStringList existingPropertyNames;
for (auto &uniform : m_uniformTable)
existingPropertyNames.append(uniform.name);
m_addNodeModel->updateCanBeAdded(existingPropertyNames);
}
}
void EffectManager::showHideAddNodeGroup(const QString &groupName, bool show)
{
if (m_addNodeModel)
m_addNodeModel->updateShowHide(groupName, show);
}
void EffectManager::refreshAddNodesList()
{
if (m_addNodeModel)
m_addNodeModel->updateNodesList();
}
const QRect &EffectManager::effectPadding() const
{
return m_effectPadding;
}
void EffectManager::setEffectPadding(const QRect &newEffectPadding)
{
if (m_effectPadding == newEffectPadding)
return;
m_effectPadding = newEffectPadding;
Q_EMIT effectPaddingChanged();
}
QString EffectManager::effectHeadings() const
{
return m_effectHeadings;
}
void EffectManager::setEffectHeadings(const QString &newEffectHeadings)
{
if (m_effectHeadings == newEffectHeadings)
return;
m_effectHeadings = newEffectHeadings;
Q_EMIT effectHeadingsChanged();
}
void EffectManager::autoIndentCurrentCode(int codeTab, const QString &code)
{
if (codeTab == 0) {
// Note: Indent for QML not implemented yet
} else if (codeTab == 1) {
const QString indentedCode = m_codeHelper->autoIndentGLSLCode(code);
m_nodeView->setSelectedNodeVertexCode(indentedCode);
} else {
const QString indentedCode = m_codeHelper->autoIndentGLSLCode(code);
m_nodeView->setSelectedNodeFragmentCode(indentedCode);
}
}
bool EffectManager::processKey(int codeTab, int keyCode, int modifiers, QQuickTextEdit *textEdit)
{
if (!textEdit)
return false;
bool isAccepted = false;
if (codeTab == 1 || codeTab == 2)
isAccepted = m_codeHelper->processKey(textEdit, keyCode, modifiers);
return isAccepted;
}
void EffectManager::updateImageWatchers()
{
for (const auto &uniform : std::as_const(m_uniformTable)) {
if (uniform.type == UniformModel::Uniform::Type::Sampler) {
// Watch all image properties files
QString imagePath = stripFileFromURL(uniform.value.toString());
if (imagePath.isEmpty())
continue;
m_fileWatcher.addPath(imagePath);
}
}
}
void EffectManager::clearImageWatchers()
{
const auto watchedFiles = m_fileWatcher.files();
if (!watchedFiles.isEmpty())
m_fileWatcher.removePaths(watchedFiles);
}