Revision control
++ // Copyright (C) 2022 The Qt Company Ltd.
++ // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
++ // Qt-Security score:critical reason:data-parser
++
++ #include "applicationsettings.h"
++ #include "effectmanager.h"
++
++ #include <QImageReader>
++ #include <QFileInfo>
++ #include <QLibraryInfo>
++
++ const QStringList defaultSources = { "defaultnodes/images/qt_logo_green_rgb.png",
++ "defaultnodes/images/quit_logo.png",
++ "defaultnodes/images/whitecircle.png",
++ "defaultnodes/images/blackcircle.png" };
++
++ const QStringList defaultBackgrounds = { "images/background_dark.jpg",
++ "images/background_light.jpg",
++ "images/background_colorful.jpg" };
++
++ const QString KEY_CUSTOM_SOURCE_IMAGES = QStringLiteral("customSourceImages");
++ const QString KEY_RECENT_PROJECTS = QStringLiteral("recentProjects");
++ const QString KEY_PROJECT_NAME = QStringLiteral("projectName");
++ const QString KEY_PROJECT_FILE = QStringLiteral("projectFile");
++ const QString KEY_LEGACY_SHADERS = QStringLiteral("useLegacyShaders");
++ const QString KEY_CODE_FONT_FILE = QStringLiteral("codeFontFile");
++ const QString KEY_CODE_FONT_SIZE = QStringLiteral("codeFontSize");
++ const QString KEY_DEFAULT_RESOURCE_PATH = QStringLiteral("defaultResourcePath");
++ const QString KEY_CUSTOM_NODE_PATHS = QStringLiteral("customNodePaths");
++
++ const QString DEFAULT_CODE_FONT_FILE = QStringLiteral("fonts/SourceCodePro-Regular.ttf");
++ const int DEFAULT_CODE_FONT_SIZE = 14;
++
++ ImagesModel::ImagesModel(QObject *effectManager)
++ : QAbstractListModel(effectManager)
++ {
++ m_effectManager = static_cast<EffectManager *>(effectManager);
++ }
++
++ int ImagesModel::rowCount(const QModelIndex &) const
++ {
++ return m_modelList.size();
++ }
++
++ QHash<int, QByteArray> ImagesModel::roleNames() const
++ {
++ QHash<int, QByteArray> roles;
++ roles[Name] = "name";
++ roles[File] = "file";
++ roles[Width] = "width";
++ roles[Height] = "height";
++ roles[CanRemove] = "canRemove";
++ return roles;
++ }
++
++ QVariant ImagesModel::data(const QModelIndex &index, int role) const
++ {
++ if (!index.isValid())
++ return QVariant();
++
++ if (index.row() >= m_modelList.size())
++ return QVariant();
++
++ const auto &item = (m_modelList)[index.row()];
++
++ if (role == Name)
++ return QVariant::fromValue(item.name);
++ else if (role == File)
++ return QVariant::fromValue(item.file);
++ else if (role == Width)
++ return QVariant::fromValue(item.width);
++ else if (role == Height)
++ return QVariant::fromValue(item.height);
++ else if (role == CanRemove)
++ return QVariant::fromValue(item.canRemove);
++
++ return QVariant();
++ }
++
++ void ImagesModel::setImageIndex(int index) {
++ if (m_currentIndex == index)
++ return;
++ m_currentIndex = index;
++ Q_EMIT currentImageFileChanged();
++ }
++
++ QString ImagesModel::currentImageFile() const
++ {
++ if (m_modelList.size() > m_currentIndex)
++ return m_modelList.at(m_currentIndex).file;
++ return QString();
++ }
++
++ MenusModel::MenusModel(QObject *effectManager)
++ : QAbstractListModel(effectManager)
++ {
++ m_effectManager = static_cast<EffectManager *>(effectManager);
++ }
++
++ int MenusModel::rowCount(const QModelIndex &) const
++ {
++ return m_modelList.size();
++ }
++
++ QHash<int, QByteArray> MenusModel::roleNames() const
++ {
++ QHash<int, QByteArray> roles;
++ roles[Name] = "name";
++ roles[File] = "file";
++ return roles;
++ }
++
++ QVariant MenusModel::data(const QModelIndex &index, int role) const
++ {
++ if (!index.isValid())
++ return QVariant();
++
++ if (index.row() >= m_modelList.size())
++ return QVariant();
++
++ const auto &item = (m_modelList)[index.row()];
++
++ if (role == Name)
++ return QVariant::fromValue(item.name);
++ else if (role == File)
++ return QVariant::fromValue(item.file);
++
++ return QVariant();
++ }
++
++ CustomNodesModel::CustomNodesModel(QObject *effectManager)
++ : QAbstractListModel(effectManager)
++ {
++ m_effectManager = static_cast<EffectManager *>(effectManager);
++ }
++
++ int CustomNodesModel::rowCount(const QModelIndex &) const
++ {
++ return m_modelList.size();
++ }
++
++ QHash<int, QByteArray> CustomNodesModel::roleNames() const
++ {
++ QHash<int, QByteArray> roles;
++ roles[Path] = "path";
++ return roles;
++ }
++
++ QVariant CustomNodesModel::data(const QModelIndex &index, int role) const
++ {
++ if (!index.isValid())
++ return QVariant();
++
++ if (index.row() >= m_modelList.size())
++ return QVariant();
++
++ const auto &item = (m_modelList)[index.row()];
++
++ if (role == Path)
++ return QVariant::fromValue(item.path);
++
++ return QVariant();
++ }
++
++ ApplicationSettings::ApplicationSettings(QObject *parent)
++ : QObject{parent}
++ {
++ // Get canonical path into default nodes
++ // ### note: there may be more than one path
++ QString resourcesPath = QLibraryInfo::paths(QLibraryInfo::QmlImportsPath).value(0) +
++ QStringLiteral("/QtQuickEffectMaker");
++ QFileInfo fi(resourcesPath);
++ resourcesPath = fi.canonicalFilePath();
++ m_settings.setValue(KEY_DEFAULT_RESOURCE_PATH, resourcesPath);
++
++ m_effectManager = static_cast<EffectManager *>(parent);
++ m_sourceImagesModel = new ImagesModel(m_effectManager);
++ m_backgroundImagesModel = new ImagesModel(m_effectManager);
++ m_recentProjectsModel = new MenusModel(m_effectManager);
++ m_customNodesModel = new CustomNodesModel(m_effectManager);
++
++ refreshSourceImagesModel();
++ refreshCustomNodesModel();
++
++ // Add default backgrounds
++ for (const auto &source : defaultBackgrounds) {
++ ImagesModel::ImagesData d;
++ d.file = source;
++ m_backgroundImagesModel->m_modelList.append(d);
++ }
++ }
++
++ // Refresh the source images list
++ void ApplicationSettings::refreshSourceImagesModel()
++ {
++ QStringList customSources = m_settings.value(KEY_CUSTOM_SOURCE_IMAGES).value<QStringList>();
++ // Remove custom images that don't exist from settings
++ for (const auto &source : customSources) {
++ QUrl url(source);
++ QString sourceImageFile = url.toLocalFile();
++ if (!QFile::exists(sourceImageFile))
++ removeSourceImageFromSettings(source);
++ }
++
++ m_sourceImagesModel->m_modelList.clear();
++
++ // Add default Sources
++ for (const auto &source : defaultSources) {
++ QString absolutePath = m_effectManager->relativeToAbsolutePath(source, defaultResourcePath());
++ addSourceImage(absolutePath, false, false);
++ }
++
++ // Add custom sources from settings
++ customSources = m_settings.value(KEY_CUSTOM_SOURCE_IMAGES).value<QStringList>();
++ for (const auto &source : customSources)
++ addSourceImage(source, true, false);
++ }
++
++ bool ApplicationSettings::addSourceImage(const QString &sourceImage, bool canRemove, bool updateSettings)
++ {
++ if (sourceImage.isEmpty())
++ return false;
++
++ // Check for duplicates
++ for (const auto &source : m_sourceImagesModel->m_modelList) {
++ if (source.file == sourceImage) {
++ qWarning("Image already exists in the model, so not adding");
++ return false;
++ }
++ }
++
++ // Remove "file:/" from the path so it suits QImageReader
++ QUrl url(sourceImage);
++ QString sourceImageFile = url.toLocalFile();
++ QImageReader imageReader(sourceImageFile);
++ QSize imageSize(0, 0);
++ if (imageReader.canRead()) {
++ imageSize = imageReader.size();
++ } else {
++ qWarning("Can't read image: %s", qPrintable(sourceImage));
++ return false;
++ }
++
++ m_sourceImagesModel->beginResetModel();
++ ImagesModel::ImagesData d;
++ d.file = sourceImage;
++ d.width = imageSize.width();
++ d.height = imageSize.height();
++ d.canRemove = canRemove;
++ m_sourceImagesModel->m_modelList.append(d);
++ m_sourceImagesModel->endResetModel();
++
++ if (updateSettings && canRemove) {
++ // Non-default images are added also into settings
++ QStringList customSources = m_settings.value(KEY_CUSTOM_SOURCE_IMAGES).value<QStringList>();
++ if (!customSources.contains(d.file)) {
++ customSources.append(d.file);
++ m_settings.setValue(KEY_CUSTOM_SOURCE_IMAGES, customSources);
++ }
++ }
++ return true;
++ }
++
++ // Removes sourceImage from the QSettings
++ bool ApplicationSettings::removeSourceImageFromSettings(const QString &sourceImage)
++ {
++ QStringList customSources = m_settings.value(KEY_CUSTOM_SOURCE_IMAGES).value<QStringList>();
++ if (customSources.contains(sourceImage)) {
++ customSources.removeAll(sourceImage);
++ m_settings.setValue(KEY_CUSTOM_SOURCE_IMAGES, customSources);
++ return true;
++ }
++ return false;
++ }
++
++ // Removes sourceImage from the model
++ bool ApplicationSettings::removeSourceImage(const QString &sourceImage)
++ {
++ for (int i = 0; i < m_sourceImagesModel->m_modelList.size(); i++) {
++ const auto &d = m_sourceImagesModel->m_modelList.at(i);
++ if (d.file == sourceImage)
++ return removeSourceImage(i);
++ }
++ return false;
++ }
++ bool ApplicationSettings::removeSourceImage(int index)
++ {
++ if (index < 0 || index >= m_sourceImagesModel->m_modelList.size())
++ return false;
++
++ m_sourceImagesModel->beginResetModel();
++ m_sourceImagesModel->m_modelList.removeAt(index);
++ m_sourceImagesModel->endResetModel();
++
++ if (index >= defaultSources.size()) {
++ QStringList customSources = m_settings.value(KEY_CUSTOM_SOURCE_IMAGES).value<QStringList>();
++ customSources.removeAt(index - defaultSources.size());
++ m_settings.setValue(KEY_CUSTOM_SOURCE_IMAGES, customSources);
++ }
++
++ return true;
++ }
++
++ // Updates the recent projects model by adding / moving projectFile to first.
++ void ApplicationSettings::updateRecentProjectsModel(const QString &projectName, const QString &projectFile) {
++
++ int projectListIndex = -1;
++ QList<MenusModel::MenusData> recentProjectsList;
++ // Recent projects menu will contain max this amount of item
++ const int max_items = 10;
++
++ if (!projectFile.isEmpty() && !m_recentProjectsModel->m_modelList.isEmpty()
++ && m_recentProjectsModel->m_modelList.first().file == projectFile) {
++ // First element of the recent projects list is already the
++ // selected project, so nothing to update here.
++ return;
++ }
++
++ // Read from settings
++ int size = m_settings.beginReadArray(KEY_RECENT_PROJECTS);
++ for (int i = 0; i < size; ++i) {
++ if (i >= max_items)
++ break;
++ m_settings.setArrayIndex(i);
++ MenusModel::MenusData d;
++ d.name = m_settings.value(KEY_PROJECT_NAME).toString();
++ d.file = m_settings.value(KEY_PROJECT_FILE).toString();
++ if (!d.name.isEmpty() && !d.file.isEmpty()) {
++ recentProjectsList.append(d);
++ if (d.file == projectFile) {
++ // Note: Can't use 'i' here as settings index may be different than QList index.
++ projectListIndex = (recentProjectsList.size() - 1);
++ }
++ }
++ }
++ m_settings.endArray();
++
++ // Update model if entry was given
++ if (!projectName.isEmpty() && !projectFile.isEmpty()) {
++ if (projectListIndex == -1) {
++ // If file isn't in the list, add it first
++ MenusModel::MenusData d;
++ d.file = projectFile;
++ d.name = projectName;
++ recentProjectsList.prepend(d);
++ } else if (projectListIndex > 0) {
++ // Or move it on top
++ recentProjectsList.move(projectListIndex, 0);
++ }
++
++ if (recentProjectsList.size() > max_items)
++ recentProjectsList.removeLast();
++
++ // Write to settings
++ m_settings.beginWriteArray(KEY_RECENT_PROJECTS);
++ for (int i = 0; i < recentProjectsList.size(); ++i) {
++ m_settings.setArrayIndex(i);
++ const auto &d = recentProjectsList.at(i);
++ m_settings.setValue(KEY_PROJECT_NAME, d.name);
++ m_settings.setValue(KEY_PROJECT_FILE, d.file);
++ }
++ m_settings.endArray();
++ }
++
++ m_recentProjectsModel->beginResetModel();
++ m_recentProjectsModel->m_modelList = recentProjectsList;
++ m_recentProjectsModel->endResetModel();
++ }
++
++ void ApplicationSettings::clearRecentProjectsModel()
++ {
++ m_settings.beginWriteArray(KEY_RECENT_PROJECTS);
++ m_settings.endArray();
++ m_recentProjectsModel->beginResetModel();
++ m_recentProjectsModel->m_modelList.clear();
++ m_recentProjectsModel->endResetModel();
++ }
++
++ void ApplicationSettings::removeRecentProjectsModel(const QString &projectFile)
++ {
++ int size = m_settings.beginReadArray(KEY_RECENT_PROJECTS);
++ for (int i = 0; i < size; ++i) {
++ m_settings.setArrayIndex(i);
++ QString filename = m_settings.value(KEY_PROJECT_FILE).toString();
++ if (filename == projectFile) {
++ m_settings.remove(KEY_PROJECT_NAME);
++ m_settings.remove(KEY_PROJECT_FILE);
++ m_recentProjectsModel->beginResetModel();
++ m_recentProjectsModel->m_modelList.removeAt(i);
++ m_recentProjectsModel->endResetModel();
++ break;
++ }
++ }
++ m_settings.endArray();
++ }
++
++ ImagesModel *ApplicationSettings::sourceImagesModel() const
++ {
++ return m_sourceImagesModel;
++ }
++
++ ImagesModel *ApplicationSettings::backgroundImagesModel() const
++ {
++ return m_backgroundImagesModel;
++ }
++
++ MenusModel *ApplicationSettings::recentProjectsModel() const
++ {
++ return m_recentProjectsModel;
++ }
++
++ CustomNodesModel *ApplicationSettings::customNodesModel() const
++ {
++ return m_customNodesModel;
++ }
++
++ bool ApplicationSettings::useLegacyShaders() const
++ {
++ return m_settings.value(KEY_LEGACY_SHADERS, false).toBool();
++ }
++
++ void ApplicationSettings::setUseLegacyShaders(bool legacyShaders)
++ {
++ if (useLegacyShaders() == legacyShaders)
++ return;
++
++ m_settings.setValue(KEY_LEGACY_SHADERS, legacyShaders);
++ Q_EMIT useLegacyShadersChanged();
++ m_effectManager->updateBakedShaderVersions();
++ m_effectManager->doBakeShaders();
++ }
++
++ QString ApplicationSettings::codeFontFile() const
++ {
++ return m_settings.value(KEY_CODE_FONT_FILE, DEFAULT_CODE_FONT_FILE).toString();
++ }
++
++ int ApplicationSettings::codeFontSize() const
++ {
++ return m_settings.value(KEY_CODE_FONT_SIZE, DEFAULT_CODE_FONT_SIZE).toInt();
++ }
++
++ void ApplicationSettings::setCodeFontFile(const QString &font)
++ {
++ if (codeFontFile() == font)
++ return;
++
++ m_settings.setValue(KEY_CODE_FONT_FILE, font);
++ Q_EMIT codeFontFileChanged();
++ }
++
++ void ApplicationSettings::setCodeFontSize(int size)
++ {
++ if (codeFontSize() == size)
++ return;
++
++ m_settings.setValue(KEY_CODE_FONT_SIZE, size);
++ Q_EMIT codeFontSizeChanged();
++ }
++
++ void ApplicationSettings::resetCodeFont()
++ {
++ setCodeFontFile(DEFAULT_CODE_FONT_FILE);
++ setCodeFontSize(DEFAULT_CODE_FONT_SIZE);
++ }
++
++ QString ApplicationSettings::defaultResourcePath()
++ {
++ return m_settings.value(KEY_DEFAULT_RESOURCE_PATH).value<QString>();
++ }
++
++ QStringList ApplicationSettings::customNodesPaths() const
++ {
++ return m_settings.value(KEY_CUSTOM_NODE_PATHS).value<QStringList>();
++ }
++
++ // Refresh the custom nodes path list
++ void ApplicationSettings::refreshCustomNodesModel()
++ {
++ // Add custom sources from settings
++ QStringList customNodes = m_settings.value(KEY_CUSTOM_NODE_PATHS).value<QStringList>();
++ for (const auto &source : customNodes)
++ addCustomNodesPath(source, false);
++ }
++
++ bool ApplicationSettings::addCustomNodesPath(const QString &path, bool updateSettings)
++ {
++ if (path.isEmpty())
++ return false;
++
++ QString newPath = m_effectManager->stripFileFromURL(path);
++ // Check for duplicates
++ for (const auto &item : m_customNodesModel->m_modelList) {
++ if (item.path == newPath) {
++ qWarning("Path already exists in the model, so not adding");
++ return false;
++ }
++ }
++
++ m_customNodesModel->beginResetModel();
++ CustomNodesModel::NodesModelData d;
++ d.path = newPath;
++ m_customNodesModel->m_modelList.append(d);
++ m_customNodesModel->endResetModel();
++
++ if (updateSettings) {
++ // Add also into settings
++ QStringList customNodes = m_settings.value(KEY_CUSTOM_NODE_PATHS).value<QStringList>();
++ if (!customNodes.contains(d.path)) {
++ customNodes.append(d.path);
++ m_settings.setValue(KEY_CUSTOM_NODE_PATHS, customNodes);
++ }
++ }
++
++ return true;
++ }
++
++ bool ApplicationSettings::removeCustomNodesPath(int index)
++ {
++ if (index < 0 || index >= m_customNodesModel->m_modelList.size())
++ return false;
++
++ m_customNodesModel->beginResetModel();
++ m_customNodesModel->m_modelList.removeAt(index);
++ m_customNodesModel->endResetModel();
++
++ QStringList customSources = m_settings.value(KEY_CUSTOM_NODE_PATHS).value<QStringList>();
++ if (index < customSources.size()) {
++ customSources.removeAt(index);
++ m_settings.setValue(KEY_CUSTOM_NODE_PATHS, customSources);
++ }
++
++ return true;
++ }