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

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