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 "applicationsettings.h"
6#include "effectmanager.h"
7
8#include <QImageReader>
9#include <QFileInfo>
10#include <QLibraryInfo>
11
12const QStringList defaultSources = { "defaultnodes/images/qt_logo_green_rgb.png",
13 "defaultnodes/images/quit_logo.png",
14 "defaultnodes/images/whitecircle.png",
15 "defaultnodes/images/blackcircle.png" };
16
17const QStringList defaultBackgrounds = { "images/background_dark.jpg",
18 "images/background_light.jpg",
19 "images/background_colorful.jpg" };
20
21const QString KEY_CUSTOM_SOURCE_IMAGES = QStringLiteral("customSourceImages");
22const QString KEY_RECENT_PROJECTS = QStringLiteral("recentProjects");
23const QString KEY_PROJECT_NAME = QStringLiteral("projectName");
24const QString KEY_PROJECT_FILE = QStringLiteral("projectFile");
25const QString KEY_LEGACY_SHADERS = QStringLiteral("useLegacyShaders");
26const QString KEY_CODE_FONT_FILE = QStringLiteral("codeFontFile");
27const QString KEY_CODE_FONT_SIZE = QStringLiteral("codeFontSize");
28const QString KEY_DEFAULT_RESOURCE_PATH = QStringLiteral("defaultResourcePath");
29const QString KEY_CUSTOM_NODE_PATHS = QStringLiteral("customNodePaths");
30
31const QString DEFAULT_CODE_FONT_FILE = QStringLiteral("fonts/SourceCodePro-Regular.ttf");
32const int DEFAULT_CODE_FONT_SIZE = 14;
33
34ImagesModel::ImagesModel(QObject *effectManager)
35 : QAbstractListModel(effectManager)
36{
37 m_effectManager = static_cast<EffectManager *>(effectManager);
38}
39
40int ImagesModel::rowCount(const QModelIndex &) const
41{
42 return m_modelList.size();
43}
44
45QHash<int, QByteArray> ImagesModel::roleNames() const
46{
47 QHash<int, QByteArray> roles;
48 roles[Name] = "name";
49 roles[File] = "file";
50 roles[Width] = "width";
51 roles[Height] = "height";
52 roles[CanRemove] = "canRemove";
53 return roles;
54}
55
56QVariant ImagesModel::data(const QModelIndex &index, int role) const
57{
58 if (!index.isValid())
59 return QVariant();
60
61 if (index.row() >= m_modelList.size())
62 return QVariant();
63
64 const auto &item = (m_modelList)[index.row()];
65
66 if (role == Name)
67 return QVariant::fromValue(value: item.name);
68 else if (role == File)
69 return QVariant::fromValue(value: item.file);
70 else if (role == Width)
71 return QVariant::fromValue(value: item.width);
72 else if (role == Height)
73 return QVariant::fromValue(value: item.height);
74 else if (role == CanRemove)
75 return QVariant::fromValue(value: item.canRemove);
76
77 return QVariant();
78}
79
80void ImagesModel::setImageIndex(int index) {
81 if (m_currentIndex == index)
82 return;
83 m_currentIndex = index;
84 Q_EMIT currentImageFileChanged();
85}
86
87QString ImagesModel::currentImageFile() const
88{
89 if (m_modelList.size() > m_currentIndex)
90 return m_modelList.at(i: m_currentIndex).file;
91 return QString();
92}
93
94MenusModel::MenusModel(QObject *effectManager)
95 : QAbstractListModel(effectManager)
96{
97 m_effectManager = static_cast<EffectManager *>(effectManager);
98}
99
100int MenusModel::rowCount(const QModelIndex &) const
101{
102 return m_modelList.size();
103}
104
105QHash<int, QByteArray> MenusModel::roleNames() const
106{
107 QHash<int, QByteArray> roles;
108 roles[Name] = "name";
109 roles[File] = "file";
110 return roles;
111}
112
113QVariant MenusModel::data(const QModelIndex &index, int role) const
114{
115 if (!index.isValid())
116 return QVariant();
117
118 if (index.row() >= m_modelList.size())
119 return QVariant();
120
121 const auto &item = (m_modelList)[index.row()];
122
123 if (role == Name)
124 return QVariant::fromValue(value: item.name);
125 else if (role == File)
126 return QVariant::fromValue(value: item.file);
127
128 return QVariant();
129}
130
131CustomNodesModel::CustomNodesModel(QObject *effectManager)
132 : QAbstractListModel(effectManager)
133{
134 m_effectManager = static_cast<EffectManager *>(effectManager);
135}
136
137int CustomNodesModel::rowCount(const QModelIndex &) const
138{
139 return m_modelList.size();
140}
141
142QHash<int, QByteArray> CustomNodesModel::roleNames() const
143{
144 QHash<int, QByteArray> roles;
145 roles[Path] = "path";
146 return roles;
147}
148
149QVariant CustomNodesModel::data(const QModelIndex &index, int role) const
150{
151 if (!index.isValid())
152 return QVariant();
153
154 if (index.row() >= m_modelList.size())
155 return QVariant();
156
157 const auto &item = (m_modelList)[index.row()];
158
159 if (role == Path)
160 return QVariant::fromValue(value: item.path);
161
162 return QVariant();
163}
164
165ApplicationSettings::ApplicationSettings(QObject *parent)
166 : QObject{parent}
167{
168 // Get canonical path into default nodes
169 QString resourcesPath = QLibraryInfo::path(p: QLibraryInfo::QmlImportsPath) +
170 QStringLiteral("/QtQuickEffectMaker");
171 QFileInfo fi(resourcesPath);
172 resourcesPath = fi.canonicalFilePath();
173 m_settings.setValue(key: KEY_DEFAULT_RESOURCE_PATH, value: resourcesPath);
174
175 m_effectManager = static_cast<EffectManager *>(parent);
176 m_sourceImagesModel = new ImagesModel(m_effectManager);
177 m_backgroundImagesModel = new ImagesModel(m_effectManager);
178 m_recentProjectsModel = new MenusModel(m_effectManager);
179 m_customNodesModel = new CustomNodesModel(m_effectManager);
180
181 refreshSourceImagesModel();
182 refreshCustomNodesModel();
183
184 // Add default backgrounds
185 for (const auto &source : defaultBackgrounds) {
186 ImagesModel::ImagesData d;
187 d.file = source;
188 m_backgroundImagesModel->m_modelList.append(t: d);
189 }
190}
191
192// Refresh the source images list
193void ApplicationSettings::refreshSourceImagesModel()
194{
195 QStringList customSources = m_settings.value(key: KEY_CUSTOM_SOURCE_IMAGES).value<QStringList>();
196 // Remove custom images that don't exist from settings
197 for (const auto &source : customSources) {
198 QUrl url(source);
199 QString sourceImageFile = url.toLocalFile();
200 if (!QFile::exists(fileName: sourceImageFile))
201 removeSourceImageFromSettings(sourceImage: source);
202 }
203
204 m_sourceImagesModel->m_modelList.clear();
205
206 // Add default Sources
207 for (const auto &source : defaultSources) {
208 QString absolutePath = m_effectManager->relativeToAbsolutePath(path: source, toPath: defaultResourcePath());
209 addSourceImage(sourceImage: absolutePath, canRemove: false, updateSettings: false);
210 }
211
212 // Add custom sources from settings
213 customSources = m_settings.value(key: KEY_CUSTOM_SOURCE_IMAGES).value<QStringList>();
214 for (const auto &source : customSources)
215 addSourceImage(sourceImage: source, canRemove: true, updateSettings: false);
216}
217
218bool ApplicationSettings::addSourceImage(const QString &sourceImage, bool canRemove, bool updateSettings)
219{
220 if (sourceImage.isEmpty())
221 return false;
222
223 // Check for duplicates
224 for (const auto &source : m_sourceImagesModel->m_modelList) {
225 if (source.file == sourceImage) {
226 qWarning(msg: "Image already exists in the model, so not adding");
227 return false;
228 }
229 }
230
231 // Remove "file:/" from the path so it suits QImageReader
232 QUrl url(sourceImage);
233 QString sourceImageFile = url.toLocalFile();
234 QImageReader imageReader(sourceImageFile);
235 QSize imageSize(0, 0);
236 if (imageReader.canRead()) {
237 imageSize = imageReader.size();
238 } else {
239 qWarning(msg: "Can't read image: %s", qPrintable(sourceImage));
240 return false;
241 }
242
243 m_sourceImagesModel->beginResetModel();
244 ImagesModel::ImagesData d;
245 d.file = sourceImage;
246 d.width = imageSize.width();
247 d.height = imageSize.height();
248 d.canRemove = canRemove;
249 m_sourceImagesModel->m_modelList.append(t: d);
250 m_sourceImagesModel->endResetModel();
251
252 if (updateSettings && canRemove) {
253 // Non-default images are added also into settings
254 QStringList customSources = m_settings.value(key: KEY_CUSTOM_SOURCE_IMAGES).value<QStringList>();
255 if (!customSources.contains(str: d.file)) {
256 customSources.append(t: d.file);
257 m_settings.setValue(key: KEY_CUSTOM_SOURCE_IMAGES, value: customSources);
258 }
259 }
260 return true;
261}
262
263// Removes sourceImage from the QSettings
264bool ApplicationSettings::removeSourceImageFromSettings(const QString &sourceImage)
265{
266 QStringList customSources = m_settings.value(key: KEY_CUSTOM_SOURCE_IMAGES).value<QStringList>();
267 if (customSources.contains(str: sourceImage)) {
268 customSources.removeAll(t: sourceImage);
269 m_settings.setValue(key: KEY_CUSTOM_SOURCE_IMAGES, value: customSources);
270 return true;
271 }
272 return false;
273}
274
275// Removes sourceImage from the model
276bool ApplicationSettings::removeSourceImage(const QString &sourceImage)
277{
278 for (int i = 0; i < m_sourceImagesModel->m_modelList.size(); i++) {
279 const auto &d = m_sourceImagesModel->m_modelList.at(i);
280 if (d.file == sourceImage)
281 return removeSourceImage(index: i);
282 }
283 return false;
284}
285bool ApplicationSettings::removeSourceImage(int index)
286{
287 if (index < 0 || index >= m_sourceImagesModel->m_modelList.size())
288 return false;
289
290 m_sourceImagesModel->beginResetModel();
291 m_sourceImagesModel->m_modelList.removeAt(i: index);
292 m_sourceImagesModel->endResetModel();
293
294 if (index >= defaultSources.size()) {
295 QStringList customSources = m_settings.value(key: KEY_CUSTOM_SOURCE_IMAGES).value<QStringList>();
296 customSources.removeAt(i: index - defaultSources.size());
297 m_settings.setValue(key: KEY_CUSTOM_SOURCE_IMAGES, value: customSources);
298 }
299
300 return true;
301}
302
303// Updates the recent projects model by adding / moving projectFile to first.
304void ApplicationSettings::updateRecentProjectsModel(const QString &projectName, const QString &projectFile) {
305
306 int projectListIndex = -1;
307 QList<MenusModel::MenusData> recentProjectsList;
308 // Recent projects menu will contain max this amount of item
309 const int max_items = 10;
310
311 if (!projectFile.isEmpty() && !m_recentProjectsModel->m_modelList.isEmpty()
312 && m_recentProjectsModel->m_modelList.first().file == projectFile) {
313 // First element of the recent projects list is already the
314 // selected project, so nothing to update here.
315 return;
316 }
317
318 // Read from settings
319 int size = m_settings.beginReadArray(prefix: KEY_RECENT_PROJECTS);
320 for (int i = 0; i < size; ++i) {
321 if (i >= max_items)
322 break;
323 m_settings.setArrayIndex(i);
324 MenusModel::MenusData d;
325 d.name = m_settings.value(key: KEY_PROJECT_NAME).toString();
326 d.file = m_settings.value(key: KEY_PROJECT_FILE).toString();
327 if (!d.name.isEmpty() && !d.file.isEmpty()) {
328 recentProjectsList.append(t: d);
329 if (d.file == projectFile) {
330 // Note: Can't use 'i' here as settings index may be different than QList index.
331 projectListIndex = (recentProjectsList.size() - 1);
332 }
333 }
334 }
335 m_settings.endArray();
336
337 // Update model if entry was given
338 if (!projectName.isEmpty() && !projectFile.isEmpty()) {
339 if (projectListIndex == -1) {
340 // If file isn't in the list, add it first
341 MenusModel::MenusData d;
342 d.file = projectFile;
343 d.name = projectName;
344 recentProjectsList.prepend(t: d);
345 } else if (projectListIndex > 0) {
346 // Or move it on top
347 recentProjectsList.move(from: projectListIndex, to: 0);
348 }
349
350 if (recentProjectsList.size() > max_items)
351 recentProjectsList.removeLast();
352
353 // Write to settings
354 m_settings.beginWriteArray(prefix: KEY_RECENT_PROJECTS);
355 for (int i = 0; i < recentProjectsList.size(); ++i) {
356 m_settings.setArrayIndex(i);
357 const auto &d = recentProjectsList.at(i);
358 m_settings.setValue(key: KEY_PROJECT_NAME, value: d.name);
359 m_settings.setValue(key: KEY_PROJECT_FILE, value: d.file);
360 }
361 m_settings.endArray();
362 }
363
364 m_recentProjectsModel->beginResetModel();
365 m_recentProjectsModel->m_modelList = recentProjectsList;
366 m_recentProjectsModel->endResetModel();
367}
368
369void ApplicationSettings::clearRecentProjectsModel()
370{
371 m_settings.beginWriteArray(prefix: KEY_RECENT_PROJECTS);
372 m_settings.endArray();
373 m_recentProjectsModel->beginResetModel();
374 m_recentProjectsModel->m_modelList.clear();
375 m_recentProjectsModel->endResetModel();
376}
377
378void ApplicationSettings::removeRecentProjectsModel(const QString &projectFile)
379{
380 int size = m_settings.beginReadArray(prefix: KEY_RECENT_PROJECTS);
381 for (int i = 0; i < size; ++i) {
382 m_settings.setArrayIndex(i);
383 QString filename = m_settings.value(key: KEY_PROJECT_FILE).toString();
384 if (filename == projectFile) {
385 m_settings.remove(key: KEY_PROJECT_NAME);
386 m_settings.remove(key: KEY_PROJECT_FILE);
387 m_recentProjectsModel->beginResetModel();
388 m_recentProjectsModel->m_modelList.removeAt(i);
389 m_recentProjectsModel->endResetModel();
390 break;
391 }
392 }
393 m_settings.endArray();
394}
395
396ImagesModel *ApplicationSettings::sourceImagesModel() const
397{
398 return m_sourceImagesModel;
399}
400
401ImagesModel *ApplicationSettings::backgroundImagesModel() const
402{
403 return m_backgroundImagesModel;
404}
405
406MenusModel *ApplicationSettings::recentProjectsModel() const
407{
408 return m_recentProjectsModel;
409}
410
411CustomNodesModel *ApplicationSettings::customNodesModel() const
412{
413 return m_customNodesModel;
414}
415
416bool ApplicationSettings::useLegacyShaders() const
417{
418 return m_settings.value(key: KEY_LEGACY_SHADERS, defaultValue: false).toBool();
419}
420
421void ApplicationSettings::setUseLegacyShaders(bool legacyShaders)
422{
423 if (useLegacyShaders() == legacyShaders)
424 return;
425
426 m_settings.setValue(key: KEY_LEGACY_SHADERS, value: legacyShaders);
427 Q_EMIT useLegacyShadersChanged();
428 m_effectManager->updateBakedShaderVersions();
429 m_effectManager->doBakeShaders();
430}
431
432QString ApplicationSettings::codeFontFile() const
433{
434 return m_settings.value(key: KEY_CODE_FONT_FILE, defaultValue: DEFAULT_CODE_FONT_FILE).toString();
435}
436
437int ApplicationSettings::codeFontSize() const
438{
439 return m_settings.value(key: KEY_CODE_FONT_SIZE, defaultValue: DEFAULT_CODE_FONT_SIZE).toInt();
440}
441
442void ApplicationSettings::setCodeFontFile(const QString &font)
443{
444 if (codeFontFile() == font)
445 return;
446
447 m_settings.setValue(key: KEY_CODE_FONT_FILE, value: font);
448 Q_EMIT codeFontFileChanged();
449}
450
451void ApplicationSettings::setCodeFontSize(int size)
452{
453 if (codeFontSize() == size)
454 return;
455
456 m_settings.setValue(key: KEY_CODE_FONT_SIZE, value: size);
457 Q_EMIT codeFontSizeChanged();
458}
459
460void ApplicationSettings::resetCodeFont()
461{
462 setCodeFontFile(DEFAULT_CODE_FONT_FILE);
463 setCodeFontSize(DEFAULT_CODE_FONT_SIZE);
464}
465
466QString ApplicationSettings::defaultResourcePath()
467{
468 return m_settings.value(key: KEY_DEFAULT_RESOURCE_PATH).value<QString>();
469}
470
471QStringList ApplicationSettings::customNodesPaths() const
472{
473 return m_settings.value(key: KEY_CUSTOM_NODE_PATHS).value<QStringList>();
474}
475
476// Refresh the custom nodes path list
477void ApplicationSettings::refreshCustomNodesModel()
478{
479 // Add custom sources from settings
480 QStringList customNodes = m_settings.value(key: KEY_CUSTOM_NODE_PATHS).value<QStringList>();
481 for (const auto &source : customNodes)
482 addCustomNodesPath(path: source, updateSettings: false);
483}
484
485bool ApplicationSettings::addCustomNodesPath(const QString &path, bool updateSettings)
486{
487 if (path.isEmpty())
488 return false;
489
490 QString newPath = m_effectManager->stripFileFromURL(urlString: path);
491 // Check for duplicates
492 for (const auto &item : m_customNodesModel->m_modelList) {
493 if (item.path == newPath) {
494 qWarning(msg: "Path already exists in the model, so not adding");
495 return false;
496 }
497 }
498
499 m_customNodesModel->beginResetModel();
500 CustomNodesModel::NodesModelData d;
501 d.path = newPath;
502 m_customNodesModel->m_modelList.append(t: d);
503 m_customNodesModel->endResetModel();
504
505 if (updateSettings) {
506 // Add also into settings
507 QStringList customNodes = m_settings.value(key: KEY_CUSTOM_NODE_PATHS).value<QStringList>();
508 if (!customNodes.contains(str: d.path)) {
509 customNodes.append(t: d.path);
510 m_settings.setValue(key: KEY_CUSTOM_NODE_PATHS, value: customNodes);
511 }
512 }
513
514 return true;
515}
516
517bool ApplicationSettings::removeCustomNodesPath(int index)
518{
519 if (index < 0 || index >= m_customNodesModel->m_modelList.size())
520 return false;
521
522 m_customNodesModel->beginResetModel();
523 m_customNodesModel->m_modelList.removeAt(i: index);
524 m_customNodesModel->endResetModel();
525
526 QStringList customSources = m_settings.value(key: KEY_CUSTOM_NODE_PATHS).value<QStringList>();
527 if (index < customSources.size()) {
528 customSources.removeAt(i: index);
529 m_settings.setValue(key: KEY_CUSTOM_NODE_PATHS, value: customSources);
530 }
531
532 return true;
533}
534

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