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

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