1// Copyright (C) 2022 The Qt Company Ltd.
2// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
3
4#include "effectmanager.h"
5#include "propertyhandler.h"
6#include "syntaxhighlighterdata.h"
7#include <QDir>
8#include <QFile>
9#include <QJsonDocument>
10#include <QJsonObject>
11#include <QJsonArray>
12#include <QImageReader>
13#include <QQmlContext>
14#include <QtQml/qqmlfile.h>
15#include <QXmlStreamWriter>
16
17QQmlPropertyMap g_argData;
18
19enum class FileType
20{
21 Binary,
22 Text
23};
24
25static bool writeToFile(const QByteArray &buf, const QString &filename, FileType fileType)
26{
27 QDir().mkpath(dirPath: QFileInfo(filename).path());
28 QFile f(filename);
29 QIODevice::OpenMode flags = QIODevice::WriteOnly | QIODevice::Truncate;
30 if (fileType == FileType::Text)
31 flags |= QIODevice::Text;
32 if (!f.open(flags)) {
33 qWarning() << "Failed to open file for writing:" << filename;
34 return false;
35 }
36 f.write(data: buf);
37 return true;
38}
39
40static void removeIfExists(const QString &filePath)
41{
42 QFile file(filePath);
43 if (file.exists())
44 file.remove();
45}
46
47// Returns the boolean value of QJsonValue. It can be either boolean
48// (true, false) or string ("true", "false"). Returns the defaultValue
49// if QJsonValue is undefined, empty, or some other type.
50static bool getBoolValue(const QJsonValue &jsonValue, bool defaultValue)
51{
52 bool returnValue = defaultValue;
53 if (jsonValue.isBool()) {
54 returnValue = jsonValue.toBool();
55 } else if (jsonValue.isString()) {
56 QString s = jsonValue.toString().toLower();
57 if (s == QStringLiteral("true"))
58 returnValue = true;
59 else if (s == QStringLiteral("false"))
60 returnValue = false;
61 }
62 return returnValue;
63}
64
65EffectManager::EffectManager(QObject *parent) : QObject(parent)
66{
67 m_settings = new ApplicationSettings(this);
68 setUniformModel(new UniformModel(this));
69 m_addNodeModel = new AddNodeModel(this);
70 m_codeHelper = new CodeHelper(this);
71
72 m_vertexShaderFile.setFileTemplate(QDir::tempPath() + "/qqem_XXXXXX.vert.qsb");
73 m_fragmentShaderFile.setFileTemplate(QDir::tempPath() + "/qqem_XXXXXX.frag.qsb");
74 if (m_vertexShaderFile.open()) {
75 m_vertexShaderFilename = m_vertexShaderFile.fileName();
76 qInfo() << "Using temporary vs file:" << m_vertexShaderFilename;
77 }
78 if (m_fragmentShaderFile.open()) {
79 m_fragmentShaderFilename = m_fragmentShaderFile.fileName();
80 qInfo() << "Using temporary fs file:" << m_fragmentShaderFilename;
81 }
82
83 // Prepare baker
84 m_baker.setGeneratedShaderVariants({ QShader::StandardShader });
85 updateBakedShaderVersions();
86
87 m_shaderBakerTimer.setInterval(1000);
88 m_shaderBakerTimer.setSingleShot(true);
89 connect(sender: &m_shaderBakerTimer, signal: &QTimer::timeout, context: this, slot: &EffectManager::doBakeShaders);
90
91 m_settings->updateRecentProjectsModel();
92
93 connect(sender: m_uniformModel, signal: &UniformModel::qmlComponentChanged, context: this, slot: [this]() {
94 updateCustomUniforms();
95 updateQmlComponent();
96 // Also update the features as QML e.g. might have started using iTime
97 m_shaderFeatures.update(vs: generateVertexShader(includeUniforms: false), fs: generateFragmentShader(includeUniforms: false), qml: m_previewEffectPropertiesString);
98 });
99 connect(sender: m_uniformModel, signal: &UniformModel::uniformsChanged, context: this, slot: [this]() {
100 updateImageWatchers();
101 bakeShaders();
102 });
103 connect(sender: m_uniformModel, signal: &UniformModel::addFSCode, context: this, slot: [this](const QString &code) {
104 if (auto selectedNode = m_nodeView->m_nodesModel->m_selectedNode) {
105 selectedNode->fragmentCode += code;
106 Q_EMIT m_nodeView->selectedNodeFragmentCodeChanged();
107 }
108 });
109
110 connect(sender: &m_fileWatcher, signal: &QFileSystemWatcher::fileChanged, context: this, slot: [this]() {
111 // Update component with images not set.
112 m_loadComponentImages = false;
113 updateQmlComponent();
114 // Then enable component images with a longer delay than
115 // the component updating delay. This way Image elements
116 // will relaod the changed image files.
117 const int enableImagesDelay = effectUpdateDelay() + 100;
118 QTimer::singleShot(interval: enableImagesDelay, receiver: this, slot: [this]() {
119 m_loadComponentImages = true;
120 updateQmlComponent();
121 } );
122 });
123}
124
125UniformModel *EffectManager::uniformModel() const
126{
127 return m_uniformModel;
128}
129
130void EffectManager::setUniformModel(UniformModel *newUniformModel)
131{
132 m_uniformModel = newUniformModel;
133 if (m_uniformModel) {
134 m_uniformModel->m_effectManager = this;
135 m_uniformModel->setModelData(&m_uniformTable);
136 }
137 emit uniformModelChanged();
138}
139
140CodeHelper *EffectManager::codeHelper() const
141{
142 return m_codeHelper;
143}
144
145QString EffectManager::fragmentShader() const
146{
147 return m_fragmentShader;
148}
149
150void EffectManager::setFragmentShader(const QString &newFragmentShader)
151{
152 if (m_fragmentShader == newFragmentShader)
153 return;
154
155 m_fragmentShader = newFragmentShader;
156 emit fragmentShaderChanged();
157 setUnsavedChanges(true);
158}
159
160QString EffectManager::vertexShader() const
161{
162 return m_vertexShader;
163}
164
165void EffectManager::setVertexShader(const QString &newVertexShader)
166{
167 if (m_vertexShader == newVertexShader)
168 return;
169
170 m_vertexShader = newVertexShader;
171 emit vertexShaderChanged();
172 setUnsavedChanges(true);
173}
174
175bool EffectManager::unsavedChanges() const
176{
177 return m_unsavedChanges;
178}
179
180void EffectManager::setUnsavedChanges(bool newUnsavedChanges)
181{
182 if (m_unsavedChanges == newUnsavedChanges || m_firstBake)
183 return;
184 m_unsavedChanges = newUnsavedChanges;
185 emit unsavedChangesChanged();
186}
187
188bool EffectManager::hasProjectFilename() const
189{
190 return !m_projectFilename.isEmpty();
191}
192
193QString EffectManager::projectFilename() const
194{
195 return m_projectFilename.toString();
196}
197
198QString EffectManager::exportFilename() const
199{
200 return m_exportFilename;
201}
202
203QString EffectManager::projectName() const
204{
205 return m_projectName;
206}
207
208void EffectManager::setProjectName(const QString &name)
209{
210 if (m_projectName == name)
211 return;
212
213 m_projectName = name;
214 Q_EMIT projectNameChanged();
215}
216
217QString EffectManager::projectDirectory() const
218{
219 return m_projectDirectory;
220}
221
222QString EffectManager::exportDirectory() const
223{
224 return m_exportDirectory;
225}
226
227int EffectManager::exportFlags() const
228{
229 return m_exportFlags;
230}
231
232QStringList EffectManager::getDefaultRootVertexShader()
233{
234 if (m_defaultRootVertexShader.isEmpty()) {
235 m_defaultRootVertexShader << "void main() {";
236 m_defaultRootVertexShader << " texCoord = qt_MultiTexCoord0;";
237 m_defaultRootVertexShader << " fragCoord = qt_Vertex.xy;";
238 m_defaultRootVertexShader << " vec2 vertCoord = qt_Vertex.xy;";
239 m_defaultRootVertexShader << " @nodes";
240 m_defaultRootVertexShader << " gl_Position = qt_Matrix * vec4(vertCoord, 0.0, 1.0);";
241 m_defaultRootVertexShader << "}";
242 }
243 return m_defaultRootVertexShader;
244}
245
246QStringList EffectManager::getDefaultRootFragmentShader()
247{
248 if (m_defaultRootFragmentShader.isEmpty()) {
249 m_defaultRootFragmentShader << "void main() {";
250 m_defaultRootFragmentShader << " fragColor = texture(iSource, texCoord);";
251 m_defaultRootFragmentShader << " @nodes";
252 m_defaultRootFragmentShader << " fragColor = fragColor * qt_Opacity;";
253 m_defaultRootFragmentShader << "}";
254 }
255 return m_defaultRootFragmentShader;
256}
257
258QString EffectManager::processVertexRootLine(const QString &line)
259{
260 QString output;
261 static QRegularExpression spaceReg("\\s+");
262 QStringList lineList = line.split(sep: spaceReg, behavior: Qt::SkipEmptyParts);
263 if (lineList.length() > 1 && lineList.at(i: 0) == QStringLiteral("out")) {
264 lineList.removeFirst();
265 QString outLine = lineList.join(sep: ' ');
266 m_shaderVaryingVariables << outLine;
267 } else {
268 output = line + '\n';
269 }
270 return output;
271}
272
273QString EffectManager::processFragmentRootLine(const QString &line)
274{
275 QString output;
276 static QRegularExpression spaceReg("\\s+");
277 QStringList lineList = line.split(sep: spaceReg, behavior: Qt::SkipEmptyParts);
278 // Just skip all "in" variables. It is enough to have "out" variable in vertex.
279 if (lineList.length() > 1 && lineList.at(i: 0) == QStringLiteral("in"))
280 return QString();
281 output = line + '\n';
282 return output;
283}
284
285// Outputs the custom varying variables.
286// When outState is true, output vertex (out) version, when false output fragment (in) version.
287QString EffectManager::getCustomShaderVaryings(bool outState)
288{
289 QString output;
290 QString direction = outState ? QStringLiteral("out") : QStringLiteral("in");
291 int varLocation = m_shaderFeatures.enabled(feature: ShaderFeatures::FragCoord) ? 2 : 1;
292 for (const auto &var : m_shaderVaryingVariables) {
293 output += QString("layout(location = %1) %2 %3\n").arg(args: QString::number(varLocation), args&: direction, args: var);
294 varLocation++;
295 }
296 return output;
297}
298
299// Remove all post-processing tags ("@tag") from the code.
300// Except "@nodes" tag as that is handled later.
301QStringList EffectManager::removeTagsFromCode(const QStringList &codeLines) {
302 QStringList s;
303 for (const auto &line : codeLines) {
304 const auto trimmedLine = line.trimmed();
305 if (!trimmedLine.startsWith(c: '@') || trimmedLine.startsWith(s: "@nodes")) {
306 s << line;
307 } else {
308 // Check if the tag is known
309 bool validTag = false;
310 auto tags = SyntaxHighlighterData::reservedTagNames();
311 static QRegularExpression spaceReg("\\s+");
312 QString firstWord = trimmedLine.split(sep: spaceReg, behavior: Qt::SkipEmptyParts).first();
313 for (const auto &tag : tags) {
314 if (firstWord == QString::fromUtf8(utf8: tag)) {
315 validTag = true;
316 break;
317 }
318 }
319 if (!validTag)
320 setEffectError(errorMessage: QString("Unknown tag: %1").arg(a: trimmedLine), type: ErrorPreprocessor);
321 }
322 }
323 return s;
324}
325
326QString EffectManager::removeTagsFromCode(const QString &code) {
327 QStringList codeLines = removeTagsFromCode(codeLines: code.split(sep: '\n'));
328 return codeLines.join(sep: '\n');
329}
330
331
332QString EffectManager::generateVertexShader(bool includeUniforms) {
333 QString s;
334
335 if (includeUniforms)
336 s += getVSUniforms();
337
338 // Remove tags when not generating for features check
339 const bool removeTags = includeUniforms;
340
341 s += getDefineProperties();
342 s += getConstVariables();
343
344 // When the node is complete, add shader code in correct nodes order
345 // split to root and main parts
346 QString s_root;
347 QString s_main;
348 QStringList s_sourceCode;
349 m_shaderVaryingVariables.clear();
350 if (m_nodeView->nodeGraphComplete()) {
351 for (auto n : m_nodeView->m_activeNodesList) {
352 if (!n->vertexCode.isEmpty() && !n->disabled) {
353 if (n->type == 0) {
354 s_sourceCode = n->vertexCode.split(sep: '\n');
355 } else if (n->type == 2) {
356 QStringList vertexCode = n->vertexCode.split(sep: '\n');
357 int mainIndex = getTagIndex(code: vertexCode, QStringLiteral("main"));
358 int line = 0;
359 for (const auto &ss : vertexCode) {
360 if (mainIndex == -1 || line > mainIndex)
361 s_main += QStringLiteral(" ") + ss + '\n';
362 else if (line < mainIndex)
363 s_root += processVertexRootLine(line: ss);
364 line++;
365 }
366 }
367 }
368 }
369 }
370
371 if (s_sourceCode.isEmpty()) {
372 // If source nodes doesn't contain any code, fail to the default one
373 s_sourceCode << getDefaultRootVertexShader();
374 }
375
376 if (removeTags) {
377 s_sourceCode = removeTagsFromCode(codeLines: s_sourceCode);
378 s_root = removeTagsFromCode(code: s_root);
379 s_main = removeTagsFromCode(code: s_main);
380 }
381
382 s += getCustomShaderVaryings(outState: true);
383 s += s_root + '\n';
384
385 int nodesIndex = getTagIndex(code: s_sourceCode, QStringLiteral("nodes"));
386 int line = 0;
387 for (const auto &ss : s_sourceCode) {
388 if (line == nodesIndex)
389 s += s_main;
390 else
391 s += ss + '\n';
392 line++;
393 }
394
395 return s;
396}
397
398QString EffectManager::generateFragmentShader(bool includeUniforms) {
399 QString s;
400
401 if (includeUniforms)
402 s += getFSUniforms();
403
404 // Remove tags when not generating for features check
405 const bool removeTags = includeUniforms;
406
407 s += getDefineProperties();
408 s += getConstVariables();
409
410 // When the node is complete, add shader code in correct nodes order
411 // split to root and main parts
412 QString s_root;
413 QString s_main;
414 QStringList s_sourceCode;
415 if (m_nodeView->nodeGraphComplete()) {
416 for (auto n : m_nodeView->m_activeNodesList) {
417 if (!n->fragmentCode.isEmpty() && !n->disabled) {
418 if (n->type == 0) {
419 s_sourceCode = n->fragmentCode.split(sep: '\n');
420 } else if (n->type == 2) {
421 QStringList fragmentCode = n->fragmentCode.split(sep: '\n');
422 int mainIndex = getTagIndex(code: fragmentCode, QStringLiteral("main"));
423 int line = 0;
424 for (const auto &ss : fragmentCode) {
425 if (mainIndex == -1 || line > mainIndex)
426 s_main += QStringLiteral(" ") + ss + '\n';
427 else if (line < mainIndex)
428 s_root += processFragmentRootLine(line: ss);
429 line++;
430 }
431 }
432 }
433 }
434 }
435
436 if (s_sourceCode.isEmpty()) {
437 // If source nodes doesn't contain any code, fail to the default one
438 s_sourceCode << getDefaultRootFragmentShader();
439 }
440
441 if (removeTags) {
442 s_sourceCode = removeTagsFromCode(codeLines: s_sourceCode);
443 s_root = removeTagsFromCode(code: s_root);
444 s_main = removeTagsFromCode(code: s_main);
445 }
446
447 s += getCustomShaderVaryings(outState: false);
448 s += s_root + '\n';
449
450 int nodesIndex = getTagIndex(code: s_sourceCode, QStringLiteral("nodes"));
451 int line = 0;
452 for (const auto &ss : s_sourceCode) {
453 if (line == nodesIndex)
454 s += s_main;
455 else
456 s += ss + '\n';
457 line++;
458 }
459
460 return s;
461}
462
463int EffectManager::getTagIndex(const QStringList &code, const QString &tag)
464{
465 int index = -1;
466 int line = 0;
467 const QString tagString = QString("@%1").arg(a: tag);
468 for (const auto &s : code) {
469 auto st = s.trimmed();
470 // Check if line or first non-space content of the line matches to tag
471 static auto spaceReg = QRegularExpression("\\s");
472 auto firstSpace = st.indexOf(re: spaceReg);
473 QString firstWord = st;
474 if (firstSpace > 0)
475 firstWord = st.sliced(pos: 0, n: firstSpace);
476 if (firstWord == tagString) {
477 index = line;
478 break;
479 }
480 line++;
481 }
482 return index;
483}
484
485void EffectManager::updateBakedShaderVersions()
486{
487 QList<QShaderBaker::GeneratedShader> targets;
488 targets.append(t: { QShader::SpirvShader, QShaderVersion(100) }); // Vulkan 1.0
489 targets.append(t: { QShader::HlslShader, QShaderVersion(50) }); // Shader Model 5.0
490 targets.append(t: { QShader::MslShader, QShaderVersion(12) }); // Metal 1.2
491 targets.append(t: { QShader::GlslShader, QShaderVersion(300, QShaderVersion::GlslEs) }); // GLES 3.0+
492 targets.append(t: { QShader::GlslShader, QShaderVersion(410) }); // OpenGL 4.1+
493 targets.append(t: { QShader::GlslShader, QShaderVersion(330) }); // OpenGL 3.3
494 targets.append(t: { QShader::GlslShader, QShaderVersion(140) }); // OpenGL 3.1
495 if (m_settings->useLegacyShaders()) {
496 targets.append(t: { QShader::GlslShader, QShaderVersion(100, QShaderVersion::GlslEs) }); // GLES 2.0
497 targets.append(t: { QShader::GlslShader, QShaderVersion(120) }); // OpenGL 2.1
498 }
499 m_baker.setGeneratedShaders(targets);
500}
501
502// Bake the shaders if they have changed
503// When forced is true, will bake even when autoplay is off
504void EffectManager::bakeShaders(bool forced)
505{
506 resetEffectError(type: ErrorPreprocessor);
507 if (m_vertexShader == generateVertexShader()
508 && m_fragmentShader == generateFragmentShader()) {
509 setShadersUpToDate(true);
510 return;
511 }
512
513 setShadersUpToDate(false);
514
515 if (forced)
516 doBakeShaders();
517 else if (m_autoPlayEffect)
518 m_shaderBakerTimer.start();
519}
520
521void EffectManager::doBakeShaders()
522{
523 // First update the features based on shader content
524 // This will make sure that next calls to generate* will produce correct uniforms.
525 m_shaderFeatures.update(vs: generateVertexShader(includeUniforms: false), fs: generateFragmentShader(includeUniforms: false), qml: m_previewEffectPropertiesString);
526
527 updateCustomUniforms();
528
529 setVertexShader(generateVertexShader());
530 QString vs = m_vertexShader;
531 m_baker.setSourceString(sourceString: vs.toUtf8(), stage: QShader::VertexStage);
532
533 QShader vertShader = m_baker.bake();
534 if (!vertShader.isValid()) {
535 qWarning() << "Shader baking failed:" << qPrintable(m_baker.errorMessage());
536 setEffectError(errorMessage: m_baker.errorMessage().split(sep: '\n').first(), type: ErrorVert);
537 } else {
538 QString filename = m_vertexShaderFile.fileName();
539 writeToFile(buf: vertShader.serialized(), filename, fileType: FileType::Binary);
540 resetEffectError(type: ErrorVert);
541 }
542
543 setFragmentShader(generateFragmentShader());
544 QString fs = m_fragmentShader;
545 m_baker.setSourceString(sourceString: fs.toUtf8(), stage: QShader::FragmentStage);
546
547 QShader fragShader = m_baker.bake();
548 if (!fragShader.isValid()) {
549 qWarning() << "Shader baking failed:" << qPrintable(m_baker.errorMessage());
550 setEffectError(errorMessage: m_baker.errorMessage().split(sep: '\n').first(), type: ErrorFrag);
551 } else {
552 QString filename = m_fragmentShaderFile.fileName();
553 writeToFile(buf: fragShader.serialized(), filename, fileType: FileType::Binary);
554 resetEffectError(type: ErrorFrag);
555 }
556
557 if (vertShader.isValid() && fragShader.isValid()) {
558 Q_EMIT shadersBaked();
559 setShadersUpToDate(true);
560 }
561 m_firstBake = false;
562}
563
564const QString EffectManager::getBufUniform()
565{
566 QString s;
567 s += "layout(std140, binding = 0) uniform buf {\n";
568 s += " mat4 qt_Matrix;\n";
569 s += " float qt_Opacity;\n";
570 if (m_shaderFeatures.enabled(feature: ShaderFeatures::Time))
571 s += " float iTime;\n";
572 if (m_shaderFeatures.enabled(feature: ShaderFeatures::Frame))
573 s += " int iFrame;\n";
574 if (m_shaderFeatures.enabled(feature: ShaderFeatures::Resolution))
575 s += " vec3 iResolution;\n";
576 if (m_shaderFeatures.enabled(feature: ShaderFeatures::Mouse))
577 s += " vec4 iMouse;\n";
578 for (auto &uniform : m_uniformTable) {
579 if (!m_nodeView->m_activeNodesIds.contains(t: uniform.nodeId))
580 continue;
581 if (uniform.exportProperty &&
582 uniform.type != UniformModel::Uniform::Type::Sampler &&
583 uniform.type != UniformModel::Uniform::Type::Define) {
584 QString type = m_uniformModel->typeToUniform(type: uniform.type);
585 QString props = " " + type + " " + uniform.name + ";\n";
586 s += props;
587 }
588 }
589 s += "};\n";
590 return s;
591}
592
593const QString EffectManager::getVSUniforms()
594{
595 QString s;
596 s += "#version 440\n";
597 s += '\n';
598 s += "layout(location = 0) in vec4 qt_Vertex;\n";
599 s += "layout(location = 1) in vec2 qt_MultiTexCoord0;\n";
600 s += "layout(location = 0) out vec2 texCoord;\n";
601 if (m_shaderFeatures.enabled(feature: ShaderFeatures::FragCoord))
602 s += "layout(location = 1) out vec2 fragCoord;\n";
603 s += '\n';
604 s += getBufUniform();
605 s += '\n';
606 s += "out gl_PerVertex { vec4 gl_Position; };\n";
607 s += '\n';
608 return s;
609}
610
611const QString EffectManager::getFSUniforms()
612{
613 QString s;
614 s += "#version 440\n";
615 s += '\n';
616 s += "layout(location = 0) in vec2 texCoord;\n";
617 if (m_shaderFeatures.enabled(feature: ShaderFeatures::FragCoord))
618 s += "layout(location = 1) in vec2 fragCoord;\n";
619 s += "layout(location = 0) out vec4 fragColor;\n";
620 s += '\n';
621 s += getBufUniform();
622 s += '\n';
623
624 bool usesSource = m_shaderFeatures.enabled(feature: ShaderFeatures::Source);
625 if (usesSource)
626 s += "layout(binding = 1) uniform sampler2D iSource;\n";
627
628 // Add sampler uniforms
629 int bindingIndex = usesSource ? 2 : 1;
630 for (auto &uniform : m_uniformTable) {
631 if (!m_nodeView->m_activeNodesIds.contains(t: uniform.nodeId))
632 continue;
633 if (uniform.type == UniformModel::Uniform::Type::Sampler) {
634 // Start index from 2, 1 is source item
635 QString props = QString("layout(binding = %1) uniform sampler2D %2").arg(a: bindingIndex).arg(a: uniform.name);
636 s += props + ";\n";
637 bindingIndex++;
638 }
639 }
640 s += '\n';
641 if (m_shaderFeatures.enabled(feature: ShaderFeatures::BlurSources)) {
642 const int blurItems = 5;
643 for (int i = 1; i <= blurItems; i++) {
644 QString props = QString("layout(binding = %1) uniform sampler2D iSourceBlur%2")
645 .arg(a: bindingIndex).arg(a: QString::number(i));
646 s += props + ";\n";
647 bindingIndex++;
648 }
649 s += '\n';
650 }
651 return s;
652}
653
654const QString EffectManager::getDefineProperties()
655{
656 QString s;
657 for (auto &uniform : m_uniformTable) {
658 if (!m_nodeView->m_activeNodesIds.contains(t: uniform.nodeId))
659 continue;
660 if (uniform.type == UniformModel::Uniform::Type::Define) {
661 QString defineValue = uniform.value.toString();
662 s += QString("#define %1 %2\n").arg(args&: uniform.name, args&: defineValue);
663 }
664 }
665 if (!s.isEmpty())
666 s += '\n';
667
668 return s;
669}
670
671const QString EffectManager::getConstVariables()
672{
673 QString s;
674 for (auto &uniform : m_uniformTable) {
675 if (!m_nodeView->m_activeNodesIds.contains(t: uniform.nodeId))
676 continue;
677 if (!uniform.exportProperty) {
678 QString constValue = m_uniformModel->valueAsVariable(uniform);
679 QString type = m_uniformModel->typeToUniform(type: uniform.type);
680 s += QString("const %1 %2 = %3;\n").arg(args&: type, args&: uniform.name, args&: constValue);
681 }
682 }
683 if (!s.isEmpty())
684 s += '\n';
685
686 return s;
687}
688
689// Returns name for image mipmap property.
690// e.g. "myImage" -> "myImageMipmap".
691QString EffectManager::mipmapPropertyName(const QString &name) const
692{
693 QString simplifiedName = name.simplified();
694 simplifiedName = simplifiedName.remove(c: ' ');
695 simplifiedName += "Mipmap";
696 return simplifiedName;
697}
698
699QString EffectManager::getQmlImagesString(bool localFiles)
700{
701 QString imagesString;
702 for (auto &uniform : m_uniformTable) {
703 if (!m_nodeView->m_activeNodesIds.contains(t: uniform.nodeId))
704 continue;
705 if (uniform.type == UniformModel::Uniform::Type::Sampler) {
706 if (localFiles && !uniform.exportImage)
707 continue;
708 QString imagePath = uniform.value.toString();
709 if (imagePath.isEmpty())
710 continue;
711 imagesString += " Image {\n";
712 QString simplifiedName = UniformModel::getImageElementName(uniform);
713 imagesString += QString(" id: %1\n").arg(a: simplifiedName);
714 imagesString += " anchors.fill: parent\n";
715 // File paths are absolute, return as local when requested
716 if (localFiles) {
717 QFileInfo fi(imagePath);
718 imagePath = fi.fileName();
719 }
720 if (m_loadComponentImages)
721 imagesString += QString(" source: \"%1\"\n").arg(a: imagePath);
722 if (!localFiles) {
723 QString mipmapProperty = mipmapPropertyName(name: uniform.name);
724 imagesString += QString(" mipmap: g_propertyData.%1\n").arg(a: mipmapProperty);
725 } else if (uniform.enableMipmap) {
726 imagesString += " mipmap: true\n";
727 }
728 imagesString += " visible: false\n";
729 imagesString += " }\n";
730 }
731 }
732 return imagesString;
733}
734
735// Generates string of the custom properties (uniforms) into ShaderEffect component
736// Also generates QML images elements for samplers.
737void EffectManager::updateCustomUniforms()
738{
739 QString exportedRootPropertiesString;
740 QString previewEffectPropertiesString;
741 QString exportedEffectPropertiesString;
742 for (auto &uniform : m_uniformTable) {
743 if (!m_nodeView->m_activeNodesIds.contains(t: uniform.nodeId))
744 continue;
745 if (!uniform.exportProperty)
746 continue;
747 const bool isDefine = uniform.type == UniformModel::Uniform::Type::Define;
748 const bool isImage = uniform.type == UniformModel::Uniform::Type::Sampler;
749 QString type = m_uniformModel->typeToProperty(type: uniform.type);
750 QString value = m_uniformModel->valueAsString(uniform);
751 QString bindedValue = m_uniformModel->valueAsBinding(uniform);
752 // When user has set custom uniform value, use it as-is
753 if (uniform.useCustomValue) {
754 value = uniform.customValue;
755 bindedValue = value;
756 }
757 // Note: Define type properties appear also as QML properties (in preview) in case QML side
758 // needs to use them. This is used at least by BlurHelper BLUR_HELPER_MAX_LEVEL.
759 QString propertyName = isDefine ? uniform.name.toLower() : uniform.name;
760 if (!uniform.useCustomValue && !isDefine && !uniform.description.isEmpty()) {
761 // When exporting, add API documentation for properties
762 QStringList descriptionLines = uniform.description.split(sep: '\n');
763 for (const auto &line: std::as_const(t&: descriptionLines)) {
764 if (line.trimmed().isEmpty())
765 exportedRootPropertiesString += QStringLiteral(" //\n");
766 else
767 exportedRootPropertiesString += QStringLiteral(" // ") + line + '\n';
768 }
769 }
770 QString valueString = value.isEmpty() ? QString() : QString(": %1").arg(a: value);
771 QString bindedValueString = bindedValue.isEmpty() ? QString() : QString(": %1").arg(a: bindedValue);
772 // Custom values are not readonly, others inside the effect can be
773 QString readOnly = uniform.useCustomValue ? QString() : QStringLiteral("readonly ");
774 previewEffectPropertiesString += " " + readOnly + "property " + type + " " + propertyName + bindedValueString + '\n';
775 // Define type properties are not added into exports
776 if (!isDefine) {
777 if (uniform.useCustomValue) {
778 // Custom values are only inside the effect, with description comments
779 if (!uniform.description.isEmpty()) {
780 QStringList descriptionLines = uniform.description.split(sep: '\n');
781 for (const auto &line: descriptionLines)
782 exportedEffectPropertiesString += QStringLiteral(" // ") + line + '\n';
783 }
784 exportedEffectPropertiesString += QStringLiteral(" ") + readOnly + "property " + type + " " + propertyName + bindedValueString + '\n';
785 } else {
786 // Custom values are not added into root
787 if (isImage && !uniform.exportImage) {
788 // When exporting image is disabled, remove value from root property
789 valueString.clear();
790 }
791 exportedRootPropertiesString += " property " + type + " " + propertyName + valueString + '\n';
792 exportedEffectPropertiesString += QStringLiteral(" ") + readOnly + "property alias " + propertyName + ": rootItem." + uniform.name + '\n';
793 }
794 }
795 }
796
797 // See if any of the properties changed
798 if (m_exportedRootPropertiesString != exportedRootPropertiesString
799 || m_previewEffectPropertiesString != previewEffectPropertiesString
800 || m_exportedEffectPropertiesString != exportedEffectPropertiesString) {
801 setUnsavedChanges(true);
802 m_exportedRootPropertiesString = exportedRootPropertiesString;
803 m_previewEffectPropertiesString = previewEffectPropertiesString;
804 m_exportedEffectPropertiesString = exportedEffectPropertiesString;
805 }
806}
807
808QString EffectManager::getQmlEffectString()
809{
810 QString s;
811 if (!m_effectHeadings.isEmpty()) {
812 s += m_effectHeadings;
813 s += '\n';
814 }
815 s += QString("// Created with Qt Quick Effect Maker (version %1), %2\n\n")
816 .arg(qApp->applicationVersion(), args: QDateTime::currentDateTime().toString());
817 s += "import QtQuick\n";
818 s += '\n';
819 s += "Item {\n";
820 s += " id: rootItem\n";
821 s += '\n';
822 if (m_shaderFeatures.enabled(feature: ShaderFeatures::Source)) {
823 s += " // This is the main source for the effect\n";
824 s += " property Item source: null\n";
825 }
826 if (m_shaderFeatures.enabled(feature: ShaderFeatures::Time) ||
827 m_shaderFeatures.enabled(feature: ShaderFeatures::Frame)) {
828 s += " // Enable this to animate iTime property\n";
829 s += " property bool timeRunning: false\n";
830 }
831 if (m_shaderFeatures.enabled(feature: ShaderFeatures::Time)) {
832 s += " // When timeRunning is false, this can be used to control iTime manually\n";
833 s += " property real animatedTime: frameAnimation.elapsedTime\n";
834 }
835 if (m_shaderFeatures.enabled(feature: ShaderFeatures::Frame)) {
836 s += " // When timeRunning is false, this can be used to control iFrame manually\n";
837 s += " property int animatedFrame: frameAnimation.currentFrame\n";
838 }
839 s += '\n';
840 // Custom properties
841 if (!m_exportedRootPropertiesString.isEmpty()) {
842 s += m_exportedRootPropertiesString;
843 s += '\n';
844 }
845 if (m_shaderFeatures.enabled(feature: ShaderFeatures::Time) ||
846 m_shaderFeatures.enabled(feature: ShaderFeatures::Frame)) {
847 s += " FrameAnimation {\n";
848 s += " id: frameAnimation\n";
849 s += " running: rootItem.timeRunning\n";
850 s += " }\n";
851 s += '\n';
852 }
853 if (m_shaderFeatures.enabled(feature: ShaderFeatures::Mouse)) {
854 s += " // Mouse handling for iMouse variable\n";
855 s += " property real _effectMouseX: 0\n";
856 s += " property real _effectMouseY: 0\n";
857 s += " property real _effectMouseZ: 0\n";
858 s += " property real _effectMouseW: 0\n";
859 s += " MouseArea {\n";
860 s += " anchors.fill: parent\n";
861 s += " onPressed: (mouse)=> {\n";
862 s += " _effectMouseX = mouse.x\n";
863 s += " _effectMouseY = mouse.y\n";
864 s += " _effectMouseZ = mouse.x\n";
865 s += " _effectMouseW = mouse.y\n";
866 s += " clickTimer.restart();\n";
867 s += " }\n";
868 s += " onPositionChanged: (mouse)=> {\n";
869 s += " _effectMouseX = mouse.x\n";
870 s += " _effectMouseY = mouse.y\n";
871 s += " }\n";
872 s += " onReleased: (mouse)=> {\n";
873 s += " _effectMouseZ = -(_effectMouseZ)\n";
874 s += " }\n";
875 s += " Timer {\n";
876 s += " id: clickTimer\n";
877 s += " interval: 20\n";
878 s += " onTriggered: {\n";
879 s += " _effectMouseW = -(_effectMouseW)\n";
880 s += " }\n";
881 s += " }\n";
882 s += " }\n";
883 s += '\n';
884 }
885 if (m_shaderFeatures.enabled(feature: ShaderFeatures::BlurSources)) {
886 s += " BlurHelper {\n";
887 s += " id: blurHelper\n";
888 s += " anchors.fill: parent\n";
889 int blurMax = 32;
890 if (g_propertyData.contains(key: "BLUR_HELPER_MAX_LEVEL"))
891 blurMax = g_propertyData["BLUR_HELPER_MAX_LEVEL"].toInt();
892 s += QString(" property int blurMax: %1\n").arg(a: blurMax);
893 s += " property real blurMultiplier: rootItem.blurMultiplier\n";
894 s += " }\n";
895 }
896 s += getQmlComponentString(localFiles: true);
897 s += "}\n";
898 return s;
899}
900
901QString EffectManager::getQmlComponentString(bool localFiles)
902{
903 auto addProperty = [localFiles](const QString &name, const QString &var, const QString &type, bool blurHelper = false)
904 {
905 if (localFiles) {
906 const QString parent = blurHelper ? "blurHelper." : "rootItem.";
907 return QString("readonly property alias %1: %2%3\n").arg(args: name, args: parent, args: var);
908 } else {
909 const QString parent = blurHelper ? "blurHelper." : QString();
910 return QString("readonly property %1 %2: %3%4\n").arg(args: type, args: name, args: parent, args: var);
911 }
912 };
913
914 QString customImagesString = getQmlImagesString(localFiles);
915 QString vertexShaderFilename = "file:///" + m_vertexShaderFilename;
916 QString fragmentShaderFilename = "file:///" + m_fragmentShaderFilename;
917 QString s;
918 QString l1 = localFiles ? QStringLiteral(" ") : QStringLiteral("");
919 QString l2 = localFiles ? QStringLiteral(" ") : QStringLiteral(" ");
920 QString l3 = localFiles ? QStringLiteral(" ") : QStringLiteral(" ");
921
922 if (!localFiles)
923 s += "import QtQuick\n";
924 s += l1 + "ShaderEffect {\n";
925 if (m_shaderFeatures.enabled(feature: ShaderFeatures::Source))
926 s += l2 + addProperty("iSource", "source", "Item");
927 if (m_shaderFeatures.enabled(feature: ShaderFeatures::Time))
928 s += l2 + addProperty("iTime", "animatedTime", "real");
929 if (m_shaderFeatures.enabled(feature: ShaderFeatures::Frame))
930 s += l2 + addProperty("iFrame", "animatedFrame", "int");
931 if (m_shaderFeatures.enabled(feature: ShaderFeatures::Resolution)) {
932 // Note: Pixel ratio is currently always 1.0
933 s += l2 + "readonly property vector3d iResolution: Qt.vector3d(width, height, 1.0)\n";
934 }
935 if (m_shaderFeatures.enabled(feature: ShaderFeatures::Mouse)) {
936 s += l2 + "readonly property vector4d iMouse: Qt.vector4d(rootItem._effectMouseX, rootItem._effectMouseY,\n";
937 s += l2 + " rootItem._effectMouseZ, rootItem._effectMouseW)\n";
938 }
939 if (m_shaderFeatures.enabled(feature: ShaderFeatures::BlurSources)) {
940 s += l2 + addProperty("iSourceBlur1", "blurSrc1", "Item", true);
941 s += l2 + addProperty("iSourceBlur2", "blurSrc2", "Item", true);
942 s += l2 + addProperty("iSourceBlur3", "blurSrc3", "Item", true);
943 s += l2 + addProperty("iSourceBlur4", "blurSrc4", "Item", true);
944 s += l2 + addProperty("iSourceBlur5", "blurSrc5", "Item", true);
945 }
946 // When used in editor preview component, we need property with value
947 // and when in exported component, property with binding to root value.
948 if (localFiles)
949 s += m_exportedEffectPropertiesString;
950 else
951 s += m_previewEffectPropertiesString;
952
953 if (!customImagesString.isEmpty())
954 s += '\n' + customImagesString;
955
956 // Add here all the custom QML code from nodes
957 if (m_nodeView) {
958 QString qmlCode;
959 for (auto n : m_nodeView->m_activeNodesList) {
960 if (!n->disabled && !n->qmlCode.isEmpty()) {
961 QString spacing = localFiles ? QStringLiteral(" ") : QStringLiteral(" ");
962 qmlCode += QStringLiteral("\n");
963 qmlCode += spacing + QString("//%1\n").arg(a: n->name);
964 QStringList qmlLines = n->qmlCode.split(sep: "\n");
965 for (const auto &line: qmlLines)
966 qmlCode += spacing + line + "\n";
967 }
968 }
969 s += qmlCode;
970 if (qmlCode != m_qmlCode) {
971 m_qmlCode = qmlCode;
972 setUnsavedChanges(true);
973 }
974 }
975
976 s += '\n';
977 s += l2 + "vertexShader: '" + vertexShaderFilename + "'\n";
978 s += l2 + "fragmentShader: '" + fragmentShaderFilename + "'\n";
979 s += l2 + "anchors.fill: parent\n";
980 if (m_shaderFeatures.enabled(feature: ShaderFeatures::GridMesh)) {
981 QString gridSize = QString("%1, %2").arg(a: m_shaderFeatures.m_gridMeshWidth).arg(a: m_shaderFeatures.m_gridMeshHeight);
982 s += l2 + "mesh: GridMesh {\n";
983 s += l3 + QString("resolution: Qt.size(%1)\n").arg(a: gridSize);
984 s += l2 + "}\n";
985 }
986 s += l1 + "}\n";
987 return s;
988}
989
990void EffectManager::updateQmlComponent() {
991 // Clear possible QML runtime errors
992 resetEffectError(type: ErrorQMLRuntime);
993 QString s = getQmlComponentString(localFiles: false);
994 setQmlComponentString(s);
995}
996
997NodeView *EffectManager::nodeView() const
998{
999 return m_nodeView;
1000}
1001
1002void EffectManager::setNodeView(NodeView *newNodeView)
1003{
1004 if (m_nodeView == newNodeView)
1005 return;
1006 m_nodeView = newNodeView;
1007 emit nodeViewChanged();
1008
1009 if (m_nodeView) {
1010 m_nodeView->m_effectManager = this;
1011 connect(sender: m_nodeView, signal: &NodeView::activeNodesListChanged, context: this, slot: [this]() {
1012 updateQmlComponent();
1013 bakeShaders(forced: true);
1014 });
1015 connect(sender: m_nodeView, signal: &NodeView::selectedNodeIdChanged, context: this, slot: [this]() {
1016 // Update visibility of properties based on the selected node
1017 m_uniformModel->beginResetModel();
1018 QList<UniformModel::Uniform>::iterator it = m_uniformModel->m_uniformTable->begin();
1019 while (it != m_uniformModel->m_uniformTable->end()) {
1020 if (m_nodeView->m_selectedNodeId == 0)
1021 (*it).visible = true;
1022 else if ((*it).nodeId == m_nodeView->m_selectedNodeId)
1023 (*it).visible = true;
1024 else
1025 (*it).visible = false;
1026 it++;
1027 }
1028 m_uniformModel->endResetModel();
1029 });
1030 }
1031}
1032
1033// This will be called once when nodesview etc. components exist
1034void EffectManager::initialize()
1035{
1036 bool projectOpened = false;
1037 QString overrideExportPath;
1038 if (g_argData.contains(key: "export_path")) {
1039 // Set export path first, so it gets saved if new project is created
1040 overrideExportPath = g_argData.value(key: "export_path").toString();
1041 m_exportDirectory = overrideExportPath;
1042 }
1043
1044 if (g_argData.contains(key: "effects_project_path")) {
1045 // Open or create project given as commandline parameter
1046 QString projectFile = g_argData.value(key: "effects_project_path").toString();
1047 QString currentPath = QDir::currentPath();
1048 QString fullFilePath = relativeToAbsolutePath(path: projectFile, toPath: currentPath);
1049 bool createProject = g_argData.contains(key: "create_project");
1050 if (createProject) {
1051 QFileInfo fi(fullFilePath);
1052 projectOpened = newProject(filepath: fi.path(), filename: fi.baseName(), clearNodeView: true, createProjectDir: false);
1053 } else {
1054 projectOpened = loadProject(filename: fullFilePath);
1055 }
1056 }
1057
1058 if (!overrideExportPath.isEmpty()) {
1059 // Re-apply export path as loading the project may have changed it
1060 m_exportDirectory = overrideExportPath;
1061 Q_EMIT exportDirectoryChanged();
1062 }
1063
1064 if (!projectOpened) {
1065 // If project not open, reset the node view
1066 cleanupNodeView();
1067 }
1068
1069 QQmlContext *rootContext = QQmlEngine::contextForObject(this);
1070 if (rootContext) {
1071 auto *engine = rootContext->engine();
1072 // Set up QML runtime error handling
1073 connect(sender: engine, signal: &QQmlEngine::warnings, context: this, slot: [this](const QList<QQmlError> &warnings) {
1074 if (warnings.isEmpty())
1075 return;
1076 QQmlError error = warnings.first();
1077 QString errorMessage = error.toString();
1078 errorMessage.replace(QStringLiteral("<Unknown File>"), QStringLiteral("ERROR: "));
1079 qInfo() << "QML:" << error.line() << ":" << errorMessage;
1080 setEffectError(errorMessage, type: ErrorQMLRuntime, lineNumber: error.line());
1081 }
1082 );
1083 }
1084
1085 // Select the main node
1086 m_nodeView->selectSingleNode(nodeId: 0);
1087 m_nodeView->updateCodeSelectorModel();
1088
1089 m_nodeView->m_initialized = true;
1090}
1091
1092// Detects common GLSL error messages and returns potential
1093// additional error information related to them.
1094QString EffectManager::detectErrorMessage(const QString &errorMessage)
1095{
1096 QHash<QString, QString> nodeErrors {
1097 { "'BLUR_HELPER_MAX_LEVEL' : undeclared identifier", "BlurHelper"},
1098 { "'iSourceBlur1' : undeclared identifier", "BlurHelper"},
1099 { "'hash23' : no matching overloaded function found", "NoiseHelper" },
1100 { "'HASH_BOX_SIZE' : undeclared identifier", "NoiseHelper" },
1101 { "'pseudo3dNoise' : no matching overloaded function found", "NoiseHelper" }
1102 };
1103
1104 QString missingNodeError = QStringLiteral("Are you missing a %1 node?\n");
1105 QHash<QString, QString>::const_iterator i = nodeErrors.constBegin();
1106 while (i != nodeErrors.constEnd()) {
1107 if (errorMessage.contains(s: i.key()))
1108 return missingNodeError.arg(a: i.value());
1109 ++i;
1110 }
1111 return QString();
1112}
1113
1114// Return first error message (if any)
1115EffectError EffectManager::effectError() const
1116{
1117 for (const auto &e : m_effectErrors) {
1118 if (!e.m_message.isEmpty())
1119 return e;
1120 }
1121 return EffectError();
1122}
1123
1124// Set the effect error message with optional type and lineNumber.
1125// Type comes from ErrorTypes, defaulting to common errors (-1).
1126// Note that type must match with UI editor tab index.
1127void EffectManager::setEffectError(const QString &errorMessage, int type, int lineNumber)
1128{
1129 EffectError error;
1130 error.m_type = type;
1131 if (type == 1 || type == 2) {
1132 // For shaders, get the line number from baker output.
1133 // Which is something like "ERROR: :15: message"
1134 int glslErrorLineNumber = -1;
1135 static QRegularExpression spaceReg("\\s+");
1136 QStringList errorStringList = errorMessage.split(sep: spaceReg, behavior: Qt::SkipEmptyParts);
1137 if (errorStringList.size() >= 2) {
1138 QString lineString = errorStringList.at(i: 1).trimmed();
1139 if (lineString.size() >= 3) {
1140 // String is ":[linenumber]:", get only the number.
1141 glslErrorLineNumber = lineString.sliced(pos: 1, n: lineString.size() - 2).toInt();
1142 }
1143 }
1144 error.m_line = glslErrorLineNumber;
1145 } else {
1146 // For QML (and others) use given linenumber
1147 error.m_line = lineNumber;
1148 }
1149
1150 QString additionalErrorInfo = detectErrorMessage(errorMessage);
1151 error.m_message = additionalErrorInfo + errorMessage;
1152 m_effectErrors.insert(key: type, value: error);
1153 Q_EMIT effectErrorChanged();
1154}
1155
1156void EffectManager::resetEffectError(int type)
1157{
1158 if (m_effectErrors.contains(key: type)) {
1159 m_effectErrors.remove(key: type);
1160 Q_EMIT effectErrorChanged();
1161 }
1162}
1163
1164const QString &EffectManager::fragmentShaderFilename() const
1165{
1166 return m_fragmentShaderFilename;
1167}
1168
1169void EffectManager::setFragmentShaderFilename(const QString &newFragmentShaderFilename)
1170{
1171 if (m_fragmentShaderFilename == newFragmentShaderFilename)
1172 return;
1173 m_fragmentShaderFilename = newFragmentShaderFilename;
1174 emit fragmentShaderFilenameChanged();
1175}
1176
1177const QString &EffectManager::vertexShaderFilename() const
1178{
1179 return m_vertexShaderFilename;
1180}
1181
1182void EffectManager::setVertexShaderFilename(const QString &newVertexShaderFilename)
1183{
1184 if (m_vertexShaderFilename == newVertexShaderFilename)
1185 return;
1186 m_vertexShaderFilename = newVertexShaderFilename;
1187 emit vertexShaderFilenameChanged();
1188}
1189
1190const QString &EffectManager::qmlComponentString() const
1191{
1192 return m_qmlComponentString;
1193}
1194
1195void EffectManager::setQmlComponentString(const QString &string)
1196{
1197 if (m_qmlComponentString == string)
1198 return;
1199
1200 m_qmlComponentString = string;
1201 emit qmlComponentStringChanged();
1202}
1203
1204NodesModel::Node EffectManager::loadEffectNode(const QString &filename)
1205{
1206 QFile loadFile(filename);
1207
1208 NodesModel::Node node;
1209
1210 if (!loadFile.open(flags: QIODevice::ReadOnly)) {
1211 qWarning(msg: "Couldn't open node file.");
1212 return node;
1213 }
1214
1215 if (m_nodeView)
1216 m_nodeView->initializeNode(node);
1217
1218 QByteArray loadData = loadFile.readAll();
1219 QJsonParseError parseError;
1220 QJsonDocument jsonDoc(QJsonDocument::fromJson(json: loadData, error: &parseError));
1221 if (parseError.error != QJsonParseError::NoError) {
1222 QString error = QString("Error parsing the effect node: %1:").arg(a: filename);
1223 QString errorDetails = QString("%1: %2").arg(a: parseError.offset).arg(a: parseError.errorString());
1224 qWarning() << qPrintable(error);
1225 qWarning() << qPrintable(errorDetails);
1226 return node;
1227 }
1228
1229 QJsonObject json = jsonDoc.object();
1230 QFileInfo fi(loadFile);
1231 createNodeFromJson(rootJson: json, node, fullNode: true, nodePath: fi.absolutePath());
1232
1233 return node;
1234}
1235
1236bool EffectManager::addEffectNode(const QString &filename, int startNodeId, int endNodeId)
1237{
1238 auto node = loadEffectNode(filename);
1239 addNodeIntoView(node, startNodeId, endNodeId);
1240
1241 return true;
1242}
1243
1244bool EffectManager::addNodeIntoView(NodesModel::Node &node, int startNodeId, int endNodeId)
1245{
1246 m_nodeView->m_nodesModel->beginResetModel();
1247 m_nodeView->m_arrowsModel->beginResetModel();
1248
1249 if (startNodeId > -1 && endNodeId > -1) {
1250 auto n1 = m_nodeView->m_nodesModel->getNodeWithId(id: startNodeId);
1251 auto n2 = m_nodeView->m_nodesModel->getNodeWithId(id: endNodeId);
1252 if (n1 && n2) {
1253 // Remove already existing arrow
1254 for (auto &arrow : m_nodeView->m_arrowsModel->m_arrowsList) {
1255 if (arrow.endNodeId == endNodeId)
1256 m_nodeView->m_arrowsModel->m_arrowsList.removeAll(t: arrow);
1257 }
1258 // Update next nodes
1259 n1->nextNodeId = node.nodeId;
1260 node.nextNodeId = n2->nodeId;
1261 // Add new arrow
1262 ArrowsModel::Arrow a1{.startX: 0, .startY: 0, .endX: 0, .endY: 0, .startNodeId: n1->nodeId, .endNodeId: node.nodeId};
1263 m_nodeView->m_arrowsModel->m_arrowsList << a1;
1264 ArrowsModel::Arrow a2{.startX: 0, .startY: 0, .endX: 0, .endY: 0, .startNodeId: node.nodeId, .endNodeId: n2->nodeId};
1265 m_nodeView->m_arrowsModel->m_arrowsList << a2;
1266 // Position the new node
1267 float centerX = (n1->x + n1->width / 2.0f + n2->x + n2->width / 2.0f) / 2.0f;
1268 float centerY = (n1->y + n1->height / 2.0f + n2->y + n2->height / 2.0f) / 2.0f;
1269 node.x = centerX - node.width / 2.0f;
1270 node.y = centerY - node.height / 2.0f;
1271 }
1272 }
1273
1274 // Add Node uniforms into uniform model
1275 for (const auto &u : node.jsonUniforms)
1276 m_uniformModel->appendUniform(uniform: u);
1277
1278 m_nodeView->m_nodesModel->m_nodesList << node;
1279 m_nodeView->m_nodesModel->endResetModel();
1280 m_nodeView->m_arrowsModel->endResetModel();
1281
1282 m_nodeView->updateArrowsPositions();
1283 m_nodeView->updateActiveNodesList();
1284 // Select the newly created node
1285 m_nodeView->selectSingleNode(nodeId: node.nodeId);
1286
1287 setUnsavedChanges(true);
1288
1289 return true;
1290}
1291
1292bool EffectManager::addNodeConnection(int startNodeId, int endNodeId)
1293{
1294 auto n1 = m_nodeView->m_nodesModel->getNodeWithId(id: startNodeId);
1295 auto n2 = m_nodeView->m_nodesModel->getNodeWithId(id: endNodeId);
1296 if (!n1 || !n2) {
1297 qWarning(msg: "Can't connect unknown nodes");
1298 return false;
1299 }
1300 if (n1->nodeId == n2->nodeId) {
1301 qWarning(msg: "Can't connect node with itself");
1302 return false;
1303 }
1304 // Remove already existing arrow
1305 for (auto &arrow : m_nodeView->m_arrowsModel->m_arrowsList) {
1306 if (arrow.endNodeId == endNodeId)
1307 m_nodeView->m_arrowsModel->m_arrowsList.removeAll(t: arrow);
1308 }
1309 // Update next node
1310 n1->nextNodeId = n2->nodeId;
1311 // Add new arrow
1312 ArrowsModel::Arrow a1{.startX: 0, .startY: 0, .endX: 0, .endY: 0, .startNodeId: n1->nodeId, .endNodeId: n2->nodeId};
1313 m_nodeView->m_arrowsModel->m_arrowsList << a1;
1314
1315 return true;
1316}
1317
1318QString EffectManager::codeFromJsonArray(const QJsonArray &codeArray)
1319{
1320 QString codeString;
1321 for (const auto& element : codeArray) {
1322 codeString += element.toString();
1323 codeString += '\n';
1324 }
1325 codeString.chop(n: 1); // Remove last '\n'
1326 return codeString;
1327}
1328
1329// Parameter fullNode means nodes from files with QEN etc.
1330// Parameter nodePath is the directory where node is loaded from.
1331bool EffectManager::createNodeFromJson(const QJsonObject &rootJson, NodesModel::Node &node, bool fullNode, const QString &nodePath)
1332{
1333 QJsonObject json;
1334 if (fullNode) {
1335 if (!rootJson.contains(key: "QEN")) {
1336 qWarning(msg: "Invalid Node file");
1337 return false;
1338 }
1339
1340 json = rootJson["QEN"].toObject();
1341
1342 int version = -1;
1343 if (json.contains(key: "version"))
1344 version = json["version"].toInt(defaultValue: -1);
1345 if (version != 1) {
1346 QString error = QString("Error: Unknown effect node version (%1)").arg(a: version);
1347 qWarning() << qPrintable(error);
1348 setEffectError(errorMessage: error);
1349 return false;
1350 }
1351 } else {
1352 json = rootJson;
1353 }
1354
1355 if (json.contains(key: "name")) {
1356 node.name = json["name"].toString();
1357 } else {
1358 QString error = QString("Error: Node missing a name");
1359 qWarning() << qPrintable(error);
1360 setEffectError(errorMessage: error);
1361 return false;
1362 }
1363
1364 // When loading nodes they contain extra data
1365 if (json.contains(key: "nodeId"))
1366 node.nodeId = json["nodeId"].toInt();
1367 if (json.contains(key: "x"))
1368 node.x = json["x"].toDouble();
1369 if (json.contains(key: "y"))
1370 node.y = json["y"].toDouble();
1371 if (json.contains(key: "disabled"))
1372 node.disabled = getBoolValue(jsonValue: json["disabled"], defaultValue: false);
1373
1374 if (m_nodeView) {
1375 // Update the node size based on its type
1376 m_nodeView->initializeNodeSize(node);
1377 node.name = m_nodeView->getUniqueNodeName(origName: node.name);
1378 }
1379
1380 node.description = json["description"].toString();
1381
1382 if (json.contains(key: "fragmentCode") && json["fragmentCode"].isArray())
1383 node.fragmentCode = codeFromJsonArray(codeArray: json["fragmentCode"].toArray());
1384 if (json.contains(key: "vertexCode") && json["vertexCode"].isArray())
1385 node.vertexCode = codeFromJsonArray(codeArray: json["vertexCode"].toArray());
1386 if (json.contains(key: "qmlCode") && json["qmlCode"].isArray())
1387 node.qmlCode = codeFromJsonArray(codeArray: json["qmlCode"].toArray());
1388
1389 if (json.contains(key: "properties") && json["properties"].isArray()) {
1390 QJsonArray propertiesArray = json["properties"].toArray();
1391 for (const auto& element : propertiesArray) {
1392 auto propertyObject = element.toObject();
1393 UniformModel::Uniform u = {};
1394 u.nodeId = node.nodeId;
1395 u.name = propertyObject["name"].toString().toUtf8();
1396 u.description = propertyObject["description"].toString();
1397 u.type = m_uniformModel->typeFromString(typeString: propertyObject["type"].toString());
1398 u.exportProperty = getBoolValue(jsonValue: propertyObject["exported"], defaultValue: true);
1399 QString value, defaultValue, minValue, maxValue;
1400 defaultValue = propertyObject["defaultValue"].toString();
1401 if (u.type == UniformModel::Uniform::Type::Sampler) {
1402 if (!defaultValue.isEmpty())
1403 defaultValue = relativeToAbsolutePath(path: defaultValue, toPath: nodePath);
1404 if (propertyObject.contains(key: "enableMipmap"))
1405 u.enableMipmap = getBoolValue(jsonValue: propertyObject["enableMipmap"], defaultValue: false);
1406 if (propertyObject.contains(key: "exportImage"))
1407 u.exportImage = getBoolValue(jsonValue: propertyObject["exportImage"], defaultValue: true);
1408 // Update the mipmap property
1409 QString mipmapProperty = mipmapPropertyName(name: u.name);
1410 g_propertyData[mipmapProperty] = u.enableMipmap;
1411 }
1412 if (propertyObject.contains(key: "value")) {
1413 value = propertyObject["value"].toString();
1414 if (u.type == UniformModel::Uniform::Type::Sampler && !value.isEmpty())
1415 value = relativeToAbsolutePath(path: value, toPath: nodePath);
1416 } else {
1417 // QEN files don't store the current value, so with those use default value
1418 value = defaultValue;
1419 }
1420 u.customValue = propertyObject["customValue"].toString();
1421 u.useCustomValue = getBoolValue(jsonValue: propertyObject["useCustomValue"], defaultValue: false);
1422 minValue = propertyObject["minValue"].toString();
1423 maxValue = propertyObject["maxValue"].toString();
1424 m_uniformModel->setUniformValueData(uniform: &u, value, defaultValue, minValue, maxValue);
1425 node.jsonUniforms << u;
1426 }
1427 }
1428
1429 return true;
1430}
1431
1432bool EffectManager::deleteEffectNodes(QList<int> nodeIds)
1433{
1434 if (nodeIds.isEmpty())
1435 return false;
1436
1437 m_nodeView->m_nodesModel->beginResetModel();
1438 m_nodeView->m_arrowsModel->beginResetModel();
1439 m_uniformModel->beginResetModel();
1440
1441 for (auto nodeId : nodeIds) {
1442 auto node = m_nodeView->m_nodesModel->getNodeWithId(id: nodeId);
1443 if (!node)
1444 continue;
1445
1446 if (node->type != NodesModel::NodeType::CustomNode)
1447 continue;
1448
1449 // Remove possibly existing arrows
1450 {
1451 QList<ArrowsModel::Arrow>::iterator it = m_nodeView->m_arrowsModel->m_arrowsList.begin();
1452 while (it != m_nodeView->m_arrowsModel->m_arrowsList.end()) {
1453 if ((*it).startNodeId == node->nodeId) {
1454 // Update nextNode
1455 node->nextNodeId = -1;
1456 it = m_nodeView->m_arrowsModel->m_arrowsList.erase(pos: it);
1457 } else if ((*it).endNodeId == node->nodeId) {
1458 // Update nextNode
1459 if (auto n = m_nodeView->m_nodesModel->getNodeWithId(id: (*it).startNodeId))
1460 n->nextNodeId = -1;
1461 it = m_nodeView->m_arrowsModel->m_arrowsList.erase(pos: it);
1462 } else {
1463 it++;
1464 }
1465 }
1466 }
1467
1468 // Remove properties
1469 {
1470 QList<UniformModel::Uniform>::iterator it = m_uniformModel->m_uniformTable->begin();
1471 while (it != m_uniformModel->m_uniformTable->end()) {
1472 if ((*it).nodeId == nodeId)
1473 it = m_uniformModel->m_uniformTable->erase(pos: it);
1474 else
1475 it++;
1476 }
1477 }
1478
1479 // Remove node
1480 m_nodeView->m_nodesModel->m_nodesList.removeAll(t: *node);
1481 }
1482
1483 m_nodeView->m_nodesModel->endResetModel();
1484 m_nodeView->m_arrowsModel->endResetModel();
1485 m_uniformModel->endResetModel();
1486
1487 m_nodeView->updateActiveNodesList();
1488
1489 // Select the Main node
1490 m_nodeView->selectMainNode();
1491
1492 return true;
1493}
1494
1495QString EffectManager::getSupportedImageFormatsFilter() const
1496{
1497 auto formats = QImageReader::supportedImageFormats();
1498 QString imageFilter = QStringLiteral("Image files (");
1499 for (const auto &format : std::as_const(t&: formats))
1500 imageFilter += QStringLiteral("*.") + format + QStringLiteral(" ");
1501 imageFilter += QStringLiteral(")");
1502 return imageFilter;
1503}
1504
1505void EffectManager::cleanupProject()
1506{
1507 m_exportFilename.clear();
1508 Q_EMIT exportFilenameChanged();
1509 m_exportDirectory.clear();
1510 Q_EMIT exportDirectoryChanged();
1511 m_exportFlags = QMLComponent | QSBShaders | Images;
1512 Q_EMIT exportFlagsChanged();
1513 // Reset also settings
1514 setEffectPadding(QRect(0, 0, 0, 0));
1515 setEffectHeadings(QString());
1516 clearImageWatchers();
1517}
1518
1519QString EffectManager::replaceOldTagsWithNew(const QString &code) {
1520 QString s = code;
1521 s = s.replace(before: "//main", after: "@main");
1522 s = s.replace(before: "//nodes", after: "@nodes");
1523 s = s.replace(before: "//mesh", after: "@mesh");
1524 s = s.replace(before: "//blursources", after: "@blursources");
1525 return s;
1526}
1527
1528bool EffectManager::loadProject(const QUrl &filename)
1529{
1530 auto loadFile = resolveFileFromUrl(fileUrl: filename);
1531 resetEffectError();
1532
1533 if (!loadFile.open(flags: QIODevice::ReadOnly)) {
1534 QString error = QString("Couldn't open project file: '%1'").arg(a: filename.toString());
1535 qWarning() << qPrintable(error);
1536 setEffectError(errorMessage: error);
1537 m_settings->removeRecentProjectsModel(projectFile: filename.toString());
1538 return false;
1539 }
1540
1541 QByteArray data = loadFile.readAll();
1542 QJsonParseError parseError;
1543 QJsonDocument jsonDoc(QJsonDocument::fromJson(json: data, error: &parseError));
1544 if (parseError.error != QJsonParseError::NoError) {
1545 QString error = QString("Error parsing the project file: %1: %2").arg(a: parseError.offset).arg(a: parseError.errorString());
1546 qWarning() << qPrintable(error);
1547 setEffectError(errorMessage: error);
1548 m_settings->removeRecentProjectsModel(projectFile: filename.toString());
1549 return false;
1550 }
1551 QJsonObject rootJson = jsonDoc.object();
1552 if (!rootJson.contains(key: "QEP")) {
1553 QString error = QStringLiteral("Error: Invalid project file");
1554 qWarning() << qPrintable(error);
1555 setEffectError(errorMessage: error);
1556 m_settings->removeRecentProjectsModel(projectFile: filename.toString());
1557 return false;
1558 }
1559
1560 QJsonObject json = rootJson["QEP"].toObject();
1561
1562 int version = -1;
1563 if (json.contains(key: "version"))
1564 version = json["version"].toInt(defaultValue: -1);
1565
1566 if (version != 1) {
1567 QString error = QString("Error: Unknown project version (%1)").arg(a: version);
1568 qWarning() << qPrintable(error);
1569 setEffectError(errorMessage: error);
1570 m_settings->removeRecentProjectsModel(projectFile: filename.toString());
1571 return false;
1572 }
1573
1574 // Get the QQEM version this project was saved with.
1575 // As a number, so we can use it for comparisons.
1576 // Start with 0.4 as QQM 0.41 was the first version which started
1577 // saving these version numbers.
1578 double qqemVersion = 0.4;
1579 if (json.contains(key: "QQEM")) {
1580 QString versionString = json["QQEM"].toString();
1581 bool ok;
1582 double versionNumber = versionString.toDouble(ok: &ok);
1583 if (ok) {
1584 qqemVersion = versionNumber;
1585 } else {
1586 QString error = QString("Warning: Invalid QQEM version (%1)").arg(a: versionString);
1587 qWarning() << qPrintable(error);
1588 setEffectError(errorMessage: error);
1589 }
1590 }
1591
1592 // At this point we consider project to be OK, so start cleanup & load
1593 cleanupProject();
1594 cleanupNodeView(initialize: false);
1595 m_uniformTable.clear();
1596 updateCustomUniforms();
1597
1598 // Update project directory & name
1599 m_projectFilename = filename;
1600 Q_EMIT projectFilenameChanged();
1601 Q_EMIT hasProjectFilenameChanged();
1602
1603 QFileInfo fi(loadFile);
1604 m_projectDirectory = fi.path();
1605 Q_EMIT projectDirectoryChanged();
1606
1607 setProjectName(fi.baseName());
1608
1609 m_settings->updateRecentProjectsModel(projectName: m_projectName, projectFile: m_projectFilename.toString());
1610
1611 // Get export directory & name
1612 if (json.contains(key: "exportName")) {
1613 m_exportFilename = json["exportName"].toString();
1614 Q_EMIT exportFilenameChanged();
1615 }
1616 if (json.contains(key: "exportDirectory")) {
1617 QString exportDirectory = json["exportDirectory"].toString();
1618 m_exportDirectory = relativeToAbsolutePath(path: exportDirectory, toPath: m_projectDirectory);
1619 Q_EMIT exportDirectoryChanged();
1620 }
1621 if (json.contains(key: "exportFlags")) {
1622 m_exportFlags = json["exportFlags"].toInt();
1623 Q_EMIT exportFlagsChanged();
1624 }
1625
1626 if (json.contains(key: "settings") && json["settings"].isObject()) {
1627 QJsonObject settingsObject = json["settings"].toObject();
1628 // Effect item padding
1629 QRect padding(0, 0, 0, 0);
1630 padding.setX(settingsObject["paddingLeft"].toInt());
1631 padding.setY(settingsObject["paddingTop"].toInt());
1632 padding.setWidth(settingsObject["paddingRight"].toInt());
1633 padding.setHeight(settingsObject["paddingBottom"].toInt());
1634 setEffectPadding(padding);
1635 // Effect headings
1636 if (settingsObject.contains(key: "headings") && settingsObject["headings"].isArray())
1637 setEffectHeadings(codeFromJsonArray(codeArray: settingsObject["headings"].toArray()));
1638 }
1639
1640 if (json.contains(key: "nodes") && json["nodes"].isArray()) {
1641 QJsonArray nodesArray = json["nodes"].toArray();
1642 for (const auto& nodeElement : nodesArray) {
1643 QJsonObject nodeObject = nodeElement.toObject();
1644
1645 int type = NodesModel::CustomNode;
1646 if (nodeObject.contains(key: "type"))
1647 type = nodeObject["type"].toInt();
1648 if (type == NodesModel::CustomNode) {
1649 NodesModel::Node node;
1650 m_nodeView->initializeNode(node);
1651 createNodeFromJson(rootJson: nodeObject, node, fullNode: false, nodePath: m_projectDirectory);
1652 addNodeIntoView(node);
1653 } else {
1654 // Source / Output node
1655 int nodeId = nodeObject["nodeId"].toInt();
1656 auto node = m_nodeView->m_nodesModel->getNodeWithId(id: nodeId);
1657 if (node) {
1658 node->x = nodeObject["x"].toDouble();
1659 node->y = nodeObject["y"].toDouble();
1660 if (node->type == NodesModel::SourceNode) {
1661 // Source can contain also shaders
1662 if (nodeObject.contains(key: "vertexCode") && nodeObject["vertexCode"].isArray())
1663 node->vertexCode = codeFromJsonArray(codeArray: nodeObject["vertexCode"].toArray());
1664 else
1665 node->vertexCode = getDefaultRootVertexShader().join(sep: '\n');
1666 if (nodeObject.contains(key: "fragmentCode") && nodeObject["fragmentCode"].isArray())
1667 node->fragmentCode = codeFromJsonArray(codeArray: nodeObject["fragmentCode"].toArray());
1668 else
1669 node->fragmentCode = getDefaultRootFragmentShader().join(sep: '\n');
1670 // And QML
1671 if (nodeObject.contains(key: "qmlCode") && nodeObject["qmlCode"].isArray())
1672 node->qmlCode = codeFromJsonArray(codeArray: nodeObject["qmlCode"].toArray());
1673 Q_EMIT m_nodeView->selectedNodeFragmentCodeChanged();
1674 Q_EMIT m_nodeView->selectedNodeVertexCodeChanged();
1675 Q_EMIT m_nodeView->selectedNodeQmlCodeChanged();
1676 }
1677 }
1678 }
1679 }
1680 }
1681
1682 if (json.contains(key: "connections") && json["connections"].isArray()) {
1683 QJsonArray connectionsArray = json["connections"].toArray();
1684 for (const auto& connectionElement : connectionsArray) {
1685 QJsonObject connectionObject = connectionElement.toObject();
1686 int fromId = connectionObject["fromId"].toInt();
1687 int toId = connectionObject["toId"].toInt();
1688 addNodeConnection(startNodeId: fromId, endNodeId: toId);
1689 }
1690 }
1691
1692 // Replace old tags ("//nodes") with new format ("@nodes")
1693 // Projects saved with version <= 0.40 use the old tags format
1694 if (qqemVersion <= 0.4) {
1695 m_nodeView->m_nodesModel->beginResetModel();
1696 for (auto &node : m_nodeView->m_nodesModel->m_nodesList) {
1697 node.vertexCode = replaceOldTagsWithNew(code: node.vertexCode);
1698 node.fragmentCode = replaceOldTagsWithNew(code: node.fragmentCode);
1699 if (node.selected) {
1700 Q_EMIT m_nodeView->selectedNodeVertexCodeChanged();
1701 Q_EMIT m_nodeView->selectedNodeFragmentCodeChanged();
1702 }
1703 }
1704 m_nodeView->m_nodesModel->endResetModel();
1705 }
1706
1707 m_nodeView->updateActiveNodesList();
1708 // Layout nodes automatically to suit current view size
1709 // But wait that we are definitely in the design mode
1710 QTimer::singleShot(interval: 1, receiver: m_nodeView, slot: [this]() {
1711 m_nodeView->layoutNodes(distribute: false);
1712 } );
1713
1714 setUnsavedChanges(false);
1715
1716 return true;
1717}
1718
1719bool EffectManager::saveProject(const QUrl &filename)
1720{
1721 QUrl fileUrl = filename;
1722 // When this is called without a filename, use previous one
1723 if (filename.isEmpty())
1724 fileUrl = m_projectFilename;
1725
1726 auto saveFile = resolveFileFromUrl(fileUrl);
1727
1728 if (!saveFile.open(flags: QIODevice::WriteOnly)) {
1729 QString error = QString("Error: Couldn't save project file: '%1'").arg(a: fileUrl.toString());
1730 qWarning() << qPrintable(error);
1731 setEffectError(errorMessage: error);
1732 return false;
1733 }
1734
1735 m_projectFilename = fileUrl;
1736 Q_EMIT projectFilenameChanged();
1737 Q_EMIT hasProjectFilenameChanged();
1738
1739 QFileInfo fi(saveFile);
1740 setProjectName(fi.baseName());
1741
1742 QJsonObject json;
1743 // File format version
1744 json.insert(key: "version", value: 1);
1745 // QQEM version
1746 json.insert(key: "QQEM", qApp->applicationVersion());
1747
1748 // Add project settings
1749 QJsonObject settingsObject;
1750 if (m_effectPadding.x() != 0)
1751 settingsObject.insert(key: "paddingLeft", value: m_effectPadding.x());
1752 if (m_effectPadding.y() != 0)
1753 settingsObject.insert(key: "paddingTop", value: m_effectPadding.y());
1754 if (m_effectPadding.width() != 0)
1755 settingsObject.insert(key: "paddingRight", value: m_effectPadding.width());
1756 if (m_effectPadding.height() != 0)
1757 settingsObject.insert(key: "paddingBottom", value: m_effectPadding.height());
1758 if (!m_effectHeadings.isEmpty()) {
1759 QJsonArray headingsArray;
1760 QStringList hLines = m_effectHeadings.split(sep: '\n');
1761 for (const auto &line: std::as_const(t&: hLines))
1762 headingsArray.append(value: line);
1763
1764 if (!headingsArray.isEmpty())
1765 settingsObject.insert(key: "headings", value: headingsArray);
1766 }
1767 if (!settingsObject.isEmpty())
1768 json.insert(key: "settings", value: settingsObject);
1769
1770 // Add export directory & name
1771 if (!m_exportFilename.isEmpty())
1772 json.insert(key: "exportName", value: m_exportFilename);
1773 if (!m_exportDirectory.isEmpty()) {
1774 // Export directory is stored as a relative path.
1775 QString relativeExportPath = absoluteToRelativePath(path: m_exportDirectory, toPath: m_projectDirectory);
1776 json.insert(key: "exportDirectory", value: relativeExportPath);
1777 }
1778 json.insert(key: "exportFlags", value: m_exportFlags);
1779
1780 // Add nodes
1781 QJsonArray nodesArray;
1782 for (const auto &node : m_nodeView->m_nodesModel->m_nodesList) {
1783 QJsonObject nodeObject = nodeToJson(node, simplified: false, nodePath: fi.absolutePath());
1784 nodesArray.append(value: nodeObject);
1785 }
1786 if (!nodesArray.isEmpty())
1787 json.insert(key: "nodes", value: nodesArray);
1788
1789 // Add connections
1790 QJsonArray connectionsArray;
1791 for (const auto &arrow : m_nodeView->m_arrowsModel->m_arrowsList) {
1792 QJsonObject arrowObject;
1793 arrowObject.insert(key: "fromId", value: arrow.startNodeId);
1794 arrowObject.insert(key: "toId", value: arrow.endNodeId);
1795 // Add connection into array
1796 connectionsArray.append(value: arrowObject);
1797 }
1798 if (!connectionsArray.isEmpty())
1799 json.insert(key: "connections", value: connectionsArray);
1800
1801 QJsonObject rootJson;
1802 rootJson.insert(key: "QEP", value: json);
1803 QJsonDocument jsonDoc(rootJson);
1804 saveFile.write(data: jsonDoc.toJson());
1805
1806 setUnsavedChanges(false);
1807
1808 if (!filename.isEmpty()) {
1809 // When called with filename (so initial save or save as),
1810 // add into recent projects list.
1811 m_settings->updateRecentProjectsModel(projectName: m_projectName, projectFile: m_projectFilename.toString());
1812 }
1813
1814 return true;
1815}
1816
1817// Takes in absolute path (e.g. "file:///C:/myimages/steel1.jpg") and
1818// path to convert to (e.g. "C:/qqem/defaultnodes".
1819// Retuns relative path (e.g. "../myimages/steel1.jpg")
1820QString EffectManager::absoluteToRelativePath(const QString &path, const QString &toPath) {
1821 if (path.isEmpty())
1822 return QString();
1823 QUrl url(path);
1824 QString filePath = (url.scheme() == QStringLiteral("file")) ? url.toLocalFile() : url.toString();
1825 QDir dir(toPath);
1826 QString localPath = dir.relativeFilePath(fileName: filePath);
1827 return localPath;
1828}
1829
1830// Takes in path relative to project path (e.g. "../myimages/steel1.jpg") and
1831// path to convert to (e.g. "C:/qqem/defaultnodes".
1832// Returns absolute path (e.g. "file:///C:/qqem/myimages/steel1.jpg")
1833QString EffectManager::relativeToAbsolutePath(const QString &path, const QString &toPath) {
1834 QString filePath = path;
1835 QDir dir(toPath);
1836 QString absPath = dir.absoluteFilePath(fileName: filePath);
1837 absPath = QDir::cleanPath(path: absPath);
1838 absPath = QUrl::fromLocalFile(localfile: absPath).toString();
1839 return absPath;
1840}
1841
1842// Removes "file:" from the URL path.
1843// So e.g. "file:///C:/myimages/steel1.jpg" -> "C:/myimages/steel1.jpg"
1844QString EffectManager::stripFileFromURL(const QString &urlString) const
1845{
1846 QUrl url(urlString);
1847 QString filePath = (url.scheme() == QStringLiteral("file")) ? url.toLocalFile() : url.toString();
1848 return filePath;
1849}
1850
1851// Adds "file:" scheme to the URL path.
1852// So e.g. "C:/myimages/steel1.jpg" -> "file:///C:/myimages/steel1.jpg"
1853QString EffectManager::addFileToURL(const QString &urlString) const
1854{
1855 if (!urlString.startsWith(s: "file:"))
1856 return QStringLiteral("file:///") + urlString;
1857 return urlString;
1858}
1859
1860// Returns absolute directory of the path.
1861// When useFileScheme is true, "file:" scheme is added into result.
1862// e.g. "file:///C:/temp/temp.txt" -> "file:///C:/temp"
1863QString EffectManager::getDirectory(const QString &path, bool useFileScheme) const
1864{
1865 QString filePath = stripFileFromURL(urlString: path);
1866 QFileInfo fi(filePath);
1867 QString dir = fi.canonicalPath();
1868 if (useFileScheme)
1869 dir = addFileToURL(urlString: dir);
1870 return dir;
1871}
1872
1873QString EffectManager::getDefaultImagesDirectory(bool useFileScheme) const
1874{
1875 QString dir = m_settings->defaultResourcePath() + QStringLiteral("/defaultnodes/images");
1876 if (useFileScheme)
1877 dir = addFileToURL(urlString: dir);
1878 return dir;
1879}
1880
1881// Creates JSON presentation of the \a node.
1882// When simplified is true, temporary UI data is ignored (position, nodeId etx.)
1883// For projects these are saved, for node components not.
1884QJsonObject EffectManager::nodeToJson(const NodesModel::Node &node, bool simplified, const QString &nodePath)
1885{
1886 QJsonObject nodeObject;
1887 nodeObject.insert(key: "name", value: node.name);
1888 if (!node.description.isEmpty())
1889 nodeObject.insert(key: "description", value: node.description);
1890 if (!simplified) {
1891 nodeObject.insert(key: "type", value: node.type);
1892 nodeObject.insert(key: "nodeId", value: node.nodeId);
1893 nodeObject.insert(key: "x", value: node.x);
1894 nodeObject.insert(key: "y", value: node.y);
1895 if (node.disabled)
1896 nodeObject.insert(key: "disabled", value: true);
1897 } else {
1898 nodeObject.insert(key: "version", value: 1);
1899 }
1900 // Add properties
1901 QJsonArray propertiesArray;
1902 for (auto &uniform : m_uniformTable) {
1903 if (uniform.nodeId != node.nodeId)
1904 continue;
1905 QJsonObject uniformObject;
1906 uniformObject.insert(key: "name", value: QString(uniform.name));
1907 QString type = m_uniformModel->stringFromType(type: uniform.type);
1908 uniformObject.insert(key: "type", value: type);
1909 if (!simplified) {
1910 QString value = m_uniformModel->variantAsDataString(type: uniform.type, variant: uniform.value);
1911 if (uniform.type == UniformModel::Uniform::Type::Sampler)
1912 value = absoluteToRelativePath(path: value, toPath: nodePath);
1913 uniformObject.insert(key: "value", value);
1914 }
1915 QString defaultValue = m_uniformModel->variantAsDataString(type: uniform.type, variant: uniform.defaultValue);
1916 if (uniform.type == UniformModel::Uniform::Type::Sampler) {
1917 defaultValue = absoluteToRelativePath(path: defaultValue, toPath: nodePath);
1918 if (uniform.enableMipmap)
1919 uniformObject.insert(key: "enableMipmap", value: uniform.enableMipmap);
1920 if (!uniform.exportImage)
1921 uniformObject.insert(key: "exportImage", value: false);
1922 }
1923 uniformObject.insert(key: "defaultValue", value: defaultValue);
1924 if (!uniform.description.isEmpty())
1925 uniformObject.insert(key: "description", value: uniform.description);
1926 if (uniform.type == UniformModel::Uniform::Type::Float
1927 || uniform.type == UniformModel::Uniform::Type::Int
1928 || uniform.type == UniformModel::Uniform::Type::Vec2
1929 || uniform.type == UniformModel::Uniform::Type::Vec3
1930 || uniform.type == UniformModel::Uniform::Type::Vec4) {
1931 uniformObject.insert(key: "minValue", value: m_uniformModel->variantAsDataString(type: uniform.type, variant: uniform.minValue));
1932 uniformObject.insert(key: "maxValue", value: m_uniformModel->variantAsDataString(type: uniform.type, variant: uniform.maxValue));
1933 }
1934 if (!uniform.customValue.isEmpty())
1935 uniformObject.insert(key: "customValue", value: uniform.customValue);
1936 if (uniform.useCustomValue)
1937 uniformObject.insert(key: "useCustomValue", value: true);
1938
1939 if (!uniform.exportProperty)
1940 uniformObject.insert(key: "exported", value: false);
1941
1942 propertiesArray.append(value: uniformObject);
1943 }
1944 if (!propertiesArray.isEmpty())
1945 nodeObject.insert(key: "properties", value: propertiesArray);
1946
1947 // Add shaders
1948 if (!node.fragmentCode.trimmed().isEmpty()) {
1949 QJsonArray fragmentCodeArray;
1950 QStringList fsLines = node.fragmentCode.split(sep: '\n');
1951 for (const auto &line: fsLines)
1952 fragmentCodeArray.append(value: line);
1953
1954 if (!fragmentCodeArray.isEmpty())
1955 nodeObject.insert(key: "fragmentCode", value: fragmentCodeArray);
1956 }
1957 if (!node.vertexCode.trimmed().isEmpty()) {
1958 QJsonArray vertexCodeArray;
1959 QStringList vsLines = node.vertexCode.split(sep: '\n');
1960 for (const auto &line: vsLines)
1961 vertexCodeArray.append(value: line);
1962
1963 if (!vertexCodeArray.isEmpty())
1964 nodeObject.insert(key: "vertexCode", value: vertexCodeArray);
1965 }
1966 // Add QML code
1967 if (!node.qmlCode.trimmed().isEmpty()) {
1968 QJsonArray qmlCodeArray;
1969 QStringList qmlLines = node.qmlCode.split(sep: '\n');
1970 for (const auto &line: qmlLines)
1971 qmlCodeArray.append(value: line);
1972
1973 if (!qmlCodeArray.isEmpty())
1974 nodeObject.insert(key: "qmlCode", value: qmlCodeArray);
1975 }
1976
1977 if (simplified) {
1978 QJsonObject rootJson;
1979 rootJson.insert(key: "QEN", value: nodeObject);
1980 return rootJson;
1981 }
1982 return nodeObject;
1983}
1984
1985bool EffectManager::newProject(const QString &filepath, const QString &filename, bool clearNodeView, bool createProjectDir)
1986{
1987 if (filepath.isEmpty()) {
1988 qWarning(msg: "No path");
1989 return false;
1990 }
1991
1992 if (filename.isEmpty()) {
1993 qWarning(msg: "No filename");
1994 return false;
1995 }
1996
1997 if (clearNodeView) {
1998 resetEffectError();
1999 cleanupProject();
2000 cleanupNodeView(initialize: true);
2001 }
2002
2003 QString dirPath = filepath;
2004
2005 if (createProjectDir)
2006 dirPath += "/" + filename;
2007
2008 // Make sure that dir exists
2009 QDir dir(dirPath);
2010 if (!dir.exists())
2011 dir.mkpath(dirPath: ".");
2012
2013 m_projectDirectory = dirPath;
2014 Q_EMIT projectDirectoryChanged();
2015
2016 // Create project file
2017 QString projectFilename;
2018 if (!dirPath.startsWith(s: "file:"))
2019 projectFilename += "file:///";
2020 projectFilename += dirPath + "/" + filename + ".qep";
2021 m_projectFilename = projectFilename;
2022 Q_EMIT projectFilenameChanged();
2023 Q_EMIT hasProjectFilenameChanged();
2024
2025 m_nodeView->updateActiveNodesList();
2026 // Layout nodes automatically to suit current view size
2027 // But wait that we are definitely in the design mode
2028 QTimer::singleShot(interval: 1, receiver: m_nodeView, slot: [this]() {
2029 m_nodeView->layoutNodes(distribute: false);
2030 } );
2031
2032 // Save the new project, with whatever nodes user had at this point
2033 saveProject(filename: m_projectFilename);
2034
2035 return true;
2036}
2037
2038void EffectManager::closeProject()
2039{
2040 cleanupProject();
2041 cleanupNodeView(initialize: true);
2042
2043 // Update project directory & name
2044 m_projectFilename.clear();
2045 Q_EMIT projectFilenameChanged();
2046 Q_EMIT hasProjectFilenameChanged();
2047
2048 m_projectDirectory.clear();
2049 Q_EMIT projectDirectoryChanged();
2050
2051 setProjectName(QString());
2052}
2053
2054bool EffectManager::exportEffect(const QString &dirPath, const QString &filename, int exportFlags, int qsbVersionIndex)
2055{
2056 if (dirPath.isEmpty() || filename.isEmpty()) {
2057 QString error = QString("Error: Couldn't export the effect: '%1/%2'").arg(args: dirPath, args: filename);
2058 qWarning() << qPrintable(error);
2059 setEffectError(errorMessage: error);
2060 return false;
2061 }
2062
2063 // Update export filename & path
2064 m_exportFilename = filename;
2065 Q_EMIT exportFilenameChanged();
2066 m_exportDirectory = dirPath;
2067 Q_EMIT exportDirectoryChanged();
2068
2069 // Make sure that uniforms are up-to-date
2070 updateCustomUniforms();
2071
2072 // Make sure that dir exists
2073 QDir dir(dirPath);
2074 if (!dir.exists())
2075 dir.mkpath(dirPath: ".");
2076
2077 QString qmlFilename = filename + ".qml";
2078 QString vsFilename = filename + ".vert.qsb";
2079 QString fsFilename = filename + ".frag.qsb";
2080 QString vsSourceFilename = filename + ".vert";
2081 QString fsSourceFilename = filename + ".frag";
2082 QString qrcFilename = filename + ".qrc";
2083 QStringList exportedFilenames;
2084
2085 // Make sure that QML component starts with capital letter
2086 qmlFilename[0] = qmlFilename[0].toUpper();
2087 // Shaders & qrc on the other hand will be all lowercase
2088 vsFilename = vsFilename.toLower();
2089 fsFilename = fsFilename.toLower();
2090 vsSourceFilename = vsSourceFilename.toLower();
2091 fsSourceFilename = fsSourceFilename.toLower();
2092 qrcFilename = qrcFilename.toLower();
2093
2094 // Bake shaders with correct settings
2095 if (exportFlags & QSBShaders) {
2096 auto qsbVersion = QShader::SerializedFormatVersion::Latest;
2097 if (qsbVersionIndex == 1)
2098 qsbVersion = QShader::SerializedFormatVersion::Qt_6_5;
2099 else if (qsbVersionIndex == 2)
2100 qsbVersion = QShader::SerializedFormatVersion::Qt_6_4;
2101
2102 QString vsFilePath = dirPath + "/" + vsFilename;
2103 removeIfExists(filePath: vsFilePath);
2104 m_baker.setSourceString(sourceString: m_vertexShader.toUtf8(), stage: QShader::VertexStage);
2105 QShader vertShader = m_baker.bake();
2106 if (vertShader.isValid())
2107 writeToFile(buf: vertShader.serialized(version: qsbVersion), filename: vsFilePath, fileType: FileType::Binary);
2108
2109 exportedFilenames << vsFilename;
2110 QString fsFilePath = dirPath + "/" + fsFilename;
2111 removeIfExists(filePath: fsFilePath);
2112 m_baker.setSourceString(sourceString: m_fragmentShader.toUtf8(), stage: QShader::FragmentStage);
2113 QShader fragShader = m_baker.bake();
2114 if (fragShader.isValid())
2115 writeToFile(buf: fragShader.serialized(version: qsbVersion), filename: fsFilePath, fileType: FileType::Binary);
2116
2117 exportedFilenames << fsFilename;
2118 }
2119
2120 // Copy images
2121 if (exportFlags & Images) {
2122 for (auto &uniform : m_uniformTable) {
2123 if (uniform.type == UniformModel::Uniform::Type::Sampler &&
2124 !uniform.value.toString().isEmpty() &&
2125 uniform.exportImage) {
2126 QString imagePath = uniform.value.toString();
2127 QFileInfo fi(imagePath);
2128 QString imageFilename = fi.fileName();
2129 QString imageFilePath = dirPath + "/" + imageFilename;
2130 imagePath = stripFileFromURL(urlString: imagePath);
2131 if (imagePath.compare(s: imageFilePath, cs: Qt::CaseInsensitive) == 0)
2132 continue; // Exporting to same dir, so skip
2133 removeIfExists(filePath: imageFilePath);
2134 QFile imageFile(imagePath);
2135 if (!imageFile.copy(newName: imageFilePath))
2136 qWarning(msg: "Unable to copy image file: %s", qPrintable(imageFilename));
2137 else
2138 exportedFilenames << imageFilename;
2139 }
2140 }
2141 }
2142
2143 if (exportFlags & QMLComponent) {
2144 QString qmlComponentString = getQmlEffectString();
2145 QStringList qmlStringList = qmlComponentString.split(sep: '\n');
2146
2147 // Replace shaders with local versions
2148 for (int i = 1; i < qmlStringList.size(); i++) {
2149 QString line = qmlStringList.at(i).trimmed();
2150 if (line.startsWith(s: "vertexShader")) {
2151 QString vsLine = " vertexShader: '" + vsFilename + "'";
2152 qmlStringList[i] = vsLine;
2153 } else if (line.startsWith(s: "fragmentShader")) {
2154 QString fsLine = " fragmentShader: '" + fsFilename + "'";
2155 qmlStringList[i] = fsLine;
2156 }
2157 }
2158
2159 QString qmlString = qmlStringList.join(sep: '\n');
2160 QString qmlFilePath = dirPath + "/" + qmlFilename;
2161 writeToFile(buf: qmlString.toUtf8(), filename: qmlFilePath, fileType: FileType::Text);
2162 exportedFilenames << qmlFilename;
2163
2164 // Copy blur helpers
2165 if (m_shaderFeatures.enabled(feature: ShaderFeatures::BlurSources)) {
2166 QString blurHelperFilename("BlurHelper.qml");
2167 QString blurFsFilename("bluritems.frag.qsb");
2168 QString blurVsFilename("bluritems.vert.qsb");
2169 QString blurHelperPath(m_settings->defaultResourcePath() + "/defaultnodes/common/");
2170 QString blurHelperSource(blurHelperPath + blurHelperFilename);
2171 QString blurFsSource(blurHelperPath + blurFsFilename);
2172 QString blurVsSource(blurHelperPath + blurVsFilename);
2173 QFile blurHelperFile(blurHelperSource);
2174 QFile blurFsFile(blurFsSource);
2175 QFile blurVsFile(blurVsSource);
2176 QString blurHelperFilePath = dirPath + "/" + "BlurHelper.qml";
2177 QString blurFsFilePath = dirPath + "/" + "bluritems.frag.qsb";
2178 QString blurVsFilePath = dirPath + "/" + "bluritems.vert.qsb";
2179 removeIfExists(filePath: blurHelperFilePath);
2180 removeIfExists(filePath: blurFsFilePath);
2181 removeIfExists(filePath: blurVsFilePath);
2182 if (!blurHelperFile.copy(newName: blurHelperFilePath))
2183 qWarning(msg: "Unable to copy file: %s", qPrintable(blurHelperFilePath));
2184 if (!blurFsFile.copy(newName: blurFsFilePath))
2185 qWarning(msg: "Unable to copy file: %s", qPrintable(blurFsFilePath));
2186 if (!blurVsFile.copy(newName: blurVsFilePath))
2187 qWarning(msg: "Unable to copy file: %s", qPrintable(blurVsFilePath));
2188 exportedFilenames << blurHelperFilename;
2189 exportedFilenames << blurFsFilename;
2190 exportedFilenames << blurVsFilename;
2191 }
2192 }
2193
2194 // Export shaders as plain-text
2195 if (exportFlags & TextShaders) {
2196 QString vsSourceFilePath = dirPath + "/" + vsSourceFilename;
2197 writeToFile(buf: m_vertexShader.toUtf8(), filename: vsSourceFilePath, fileType: FileType::Text);
2198 QString fsSourceFilePath = dirPath + "/" + fsSourceFilename;
2199 writeToFile(buf: m_fragmentShader.toUtf8(), filename: fsSourceFilePath, fileType: FileType::Text);
2200 }
2201
2202 if (exportFlags & QRCFile) {
2203 QString qrcXmlString;
2204 QString qrcFilePath = dirPath + "/" + qrcFilename;
2205 QXmlStreamWriter stream(&qrcXmlString);
2206 stream.setAutoFormatting(true);
2207 stream.writeStartElement(qualifiedName: "RCC");
2208 stream.writeStartElement(qualifiedName: "qresource");
2209 stream.writeAttribute(qualifiedName: "prefix", value: "/");
2210 for (const auto &filename : exportedFilenames)
2211 stream.writeTextElement(qualifiedName: "file", text: filename);
2212 stream.writeEndElement(); // qresource
2213 stream.writeEndElement(); // RCC
2214 writeToFile(buf: qrcXmlString.toUtf8(), filename: qrcFilePath, fileType: FileType::Text);
2215 }
2216
2217 // Update exportFlags
2218 if (m_exportFlags != exportFlags) {
2219 m_exportFlags = exportFlags;
2220 Q_EMIT exportFlagsChanged();
2221 }
2222 return true;
2223}
2224
2225QFile EffectManager::resolveFileFromUrl(const QUrl &fileUrl)
2226{
2227 const QQmlContext *context = qmlContext(this);
2228 const auto resolvedUrl = context ? context->resolvedUrl(fileUrl) : fileUrl;
2229 const auto qmlSource = QQmlFile::urlToLocalFileOrQrc(resolvedUrl);
2230
2231 QFileInfo fileInfo(qmlSource);
2232 QString filePath = fileInfo.canonicalFilePath();
2233 if (filePath.isEmpty())
2234 filePath = fileInfo.absoluteFilePath();
2235 return QFile(filePath);
2236}
2237
2238void EffectManager::cleanupNodeView(bool initialize)
2239{
2240 QList<int> nodes;
2241 for (const auto &node : m_nodeView->m_nodesModel->m_nodesList) {
2242 if (node.type == 2)
2243 nodes << node.nodeId;
2244 }
2245 deleteEffectNodes(nodeIds: nodes);
2246
2247 // Clear also arrows, so source->output connection is removed
2248 m_nodeView->m_arrowsModel->m_arrowsList.clear();
2249
2250 if (initialize) {
2251 // Add first connection
2252 addNodeConnection(startNodeId: 0, endNodeId: 1);
2253 // Add default root shaders
2254 int nodeId = 0;
2255 auto node = m_nodeView->m_nodesModel->getNodeWithId(id: nodeId);
2256 if (node) {
2257 node->vertexCode = getDefaultRootVertexShader().join(sep: '\n');
2258 node->fragmentCode = getDefaultRootFragmentShader().join(sep: '\n');
2259 node->qmlCode.clear();
2260 m_nodeView->selectMainNode();
2261 Q_EMIT m_nodeView->selectedNodeFragmentCodeChanged();
2262 Q_EMIT m_nodeView->selectedNodeVertexCodeChanged();
2263 Q_EMIT m_nodeView->selectedNodeQmlCodeChanged();
2264 }
2265 }
2266 m_nodeView->updateArrowsPositions();
2267 m_nodeView->updateActiveNodesList();
2268}
2269
2270bool EffectManager::saveSelectedNode(const QUrl &filename)
2271{
2272 QUrl fileUrl = filename;
2273
2274 auto saveFile = resolveFileFromUrl(fileUrl);
2275
2276 if (!saveFile.open(flags: QIODevice::WriteOnly)) {
2277 QString error = QString("Error: Couldn't save node file: '%1'").arg(a: fileUrl.toString());
2278 qWarning() << qPrintable(error);
2279 setEffectError(errorMessage: error);
2280 return false;
2281 }
2282
2283 auto node = m_nodeView->m_nodesModel->m_selectedNode;
2284 if (!node) {
2285 QString error = QStringLiteral("Error: No node selected'");
2286 qWarning() << qPrintable(error);
2287 setEffectError(errorMessage: error);
2288 return false;
2289 }
2290
2291 QFileInfo fi(saveFile);
2292 QJsonObject nodeObject = nodeToJson(node: *node, simplified: true, nodePath: fi.absolutePath());
2293 QJsonDocument jsonDoc(nodeObject);
2294 saveFile.write(data: jsonDoc.toJson());
2295
2296 return true;
2297}
2298
2299bool EffectManager::shadersUpToDate() const
2300{
2301 return m_shadersUpToDate;
2302}
2303
2304void EffectManager::setShadersUpToDate(bool upToDate)
2305{
2306 if (m_shadersUpToDate == upToDate)
2307 return;
2308 m_shadersUpToDate = upToDate;
2309 Q_EMIT shadersUpToDateChanged();
2310}
2311
2312bool EffectManager::autoPlayEffect() const
2313{
2314 return m_autoPlayEffect;
2315}
2316
2317void EffectManager::setAutoPlayEffect(bool autoPlay)
2318{
2319 if (m_autoPlayEffect == autoPlay)
2320 return;
2321 m_autoPlayEffect = autoPlay;
2322 Q_EMIT autoPlayEffectChanged();
2323}
2324
2325QString EffectManager::getHelpTextString()
2326{
2327 QFile helpFile(":/qqem_help.html");
2328
2329 if (!helpFile.open(flags: QIODevice::ReadOnly)) {
2330 qWarning(msg: "Couldn't open help file.");
2331 return QString();
2332 }
2333
2334 QByteArray helpData = helpFile.readAll();
2335 return QString::fromLatin1(ba: helpData);
2336}
2337
2338AddNodeModel *EffectManager::addNodeModel() const
2339{
2340 return m_addNodeModel;
2341}
2342
2343// This is called when the AddNodeDialog is shown.
2344void EffectManager::updateAddNodeData()
2345{
2346 if (m_addNodeModel && m_nodeView) {
2347 updateQmlComponent();
2348 // Collect already used properties
2349 QStringList existingPropertyNames;
2350 for (auto &uniform : m_uniformTable)
2351 existingPropertyNames.append(t: uniform.name);
2352 m_addNodeModel->updateCanBeAdded(propertyNames: existingPropertyNames);
2353 }
2354}
2355
2356void EffectManager::showHideAddNodeGroup(const QString &groupName, bool show)
2357{
2358 if (m_addNodeModel)
2359 m_addNodeModel->updateShowHide(groupName, show);
2360}
2361
2362void EffectManager::refreshAddNodesList()
2363{
2364 if (m_addNodeModel)
2365 m_addNodeModel->updateNodesList();
2366}
2367
2368const QRect &EffectManager::effectPadding() const
2369{
2370 return m_effectPadding;
2371}
2372
2373void EffectManager::setEffectPadding(const QRect &newEffectPadding)
2374{
2375 if (m_effectPadding == newEffectPadding)
2376 return;
2377 m_effectPadding = newEffectPadding;
2378 Q_EMIT effectPaddingChanged();
2379}
2380
2381QString EffectManager::effectHeadings() const
2382{
2383 return m_effectHeadings;
2384}
2385
2386void EffectManager::setEffectHeadings(const QString &newEffectHeadings)
2387{
2388 if (m_effectHeadings == newEffectHeadings)
2389 return;
2390 m_effectHeadings = newEffectHeadings;
2391 Q_EMIT effectHeadingsChanged();
2392}
2393
2394void EffectManager::autoIndentCurrentCode(int codeTab, const QString &code)
2395{
2396 if (codeTab == 0) {
2397 // Note: Indent for QML not implemented yet
2398 } else if (codeTab == 1) {
2399 const QString indentedCode = m_codeHelper->autoIndentGLSLCode(code);
2400 m_nodeView->setSelectedNodeVertexCode(indentedCode);
2401 } else {
2402 const QString indentedCode = m_codeHelper->autoIndentGLSLCode(code);
2403 m_nodeView->setSelectedNodeFragmentCode(indentedCode);
2404 }
2405}
2406
2407bool EffectManager::processKey(int codeTab, int keyCode, int modifiers, QQuickTextEdit *textEdit)
2408{
2409 if (!textEdit)
2410 return false;
2411
2412 bool isAccepted = false;
2413 if (codeTab == 1 || codeTab == 2)
2414 isAccepted = m_codeHelper->processKey(textEdit, keyCode, modifiers);
2415
2416 return isAccepted;
2417}
2418
2419void EffectManager::updateImageWatchers()
2420{
2421 for (const auto &uniform : std::as_const(t&: m_uniformTable)) {
2422 if (uniform.type == UniformModel::Uniform::Type::Sampler) {
2423 // Watch all image properties files
2424 QString imagePath = stripFileFromURL(urlString: uniform.value.toString());
2425 if (imagePath.isEmpty())
2426 continue;
2427 m_fileWatcher.addPath(file: imagePath);
2428 }
2429 }
2430}
2431
2432void EffectManager::clearImageWatchers()
2433{
2434 const auto watchedFiles = m_fileWatcher.files();
2435 if (!watchedFiles.isEmpty())
2436 m_fileWatcher.removePaths(files: watchedFiles);
2437}
2438

source code of qtquickeffectmaker/tools/qqem/effectmanager.cpp