From 84ca3659858bfc76b033550205349b4d912a6ba1 Mon Sep 17 00:00:00 2001 From: Miikka Heikkinen Date: Thu, 17 Oct 2024 16:15:56 +0300 Subject: [PATCH] EffectComposer: Allow user to choose custom preview image The current preview image selection and any custom image path is stored in the .qep file on effect save. When a custom image outside of project is chosen, the image is imported into the project into the default image assets folder. Fixes: QDS-13438 Change-Id: If15049612bbca9a744a383c49716cb3648a52af3 Reviewed-by: Mats Honkamaa Reviewed-by: Mahmoud Badri Reviewed-by: Ali Kianian --- .../PreviewImagesComboBox.qml | 107 +++++++++----- .../effectcomposer/effectcomposermodel.cpp | 131 +++++++++++++++++- .../effectcomposer/effectcomposermodel.h | 12 ++ .../componentcore/modelnodeoperations.h | 2 +- 4 files changed, 210 insertions(+), 42 deletions(-) diff --git a/share/qtcreator/qmldesigner/effectComposerQmlSources/PreviewImagesComboBox.qml b/share/qtcreator/qmldesigner/effectComposerQmlSources/PreviewImagesComboBox.qml index 201fab9699d..e4280111892 100644 --- a/share/qtcreator/qmldesigner/effectComposerQmlSources/PreviewImagesComboBox.qml +++ b/share/qtcreator/qmldesigner/effectComposerQmlSources/PreviewImagesComboBox.qml @@ -22,12 +22,17 @@ StudioControls.ComboBox { required property Item mainRoot - property var images: ["images/preview0.png", - "images/preview1.png", - "images/preview2.png", - "images/preview3.png", - "images/preview4.png"] - property string selectedImage: images[0] + property var images: [Qt.url(""), + Qt.url("images/preview0.png"), + Qt.url("images/preview1.png"), + Qt.url("images/preview2.png"), + Qt.url("images/preview3.png"), + Qt.url("images/preview4.png")] + property url selectedImage: EffectComposerBackend.effectComposerModel.currentPreviewImage != Qt.url("") + ? EffectComposerBackend.effectComposerModel.currentPreviewImage + : images[1] + + Component.onCompleted: EffectComposerBackend.effectComposerModel.currentPreviewImage = images[1] readonly property int popupHeight: Math.min(800, col.height + 2) @@ -122,45 +127,77 @@ StudioControls.ComboBox { border.width: 1 focus: true - HelperWidgets.ScrollView { + Column { anchors.fill: parent - anchors.margins: 1 - clip: true - Column { - id: col + Item { + id: setCustomItem + width: parent.width + height: 40 - padding: 10 - spacing: 10 + HelperWidgets.Button { + anchors.fill: parent + anchors.margins: 2 + text: qsTr("Set Custom Image") + onClicked: { + EffectComposerBackend.effectComposerModel.chooseCustomPreviewImage() + root.popup.close() + } + } + } - Repeater { - model: root.images - Rectangle { - required property int index - required property var modelData + HelperWidgets.ScrollView { + width: parent.width - 2 + height: parent.height - setCustomItem.height - color: "transparent" - border.color: root.selectedImage === modelData ? StudioTheme.Values.themeInteraction - : "transparent" + clip: true - width: 200 - height: 200 + Column { + id: col - Image { - source: modelData - anchors.fill: parent - fillMode: Image.PreserveAspectFit - smooth: true - anchors.margins: 1 - } + padding: 10 + spacing: 10 - MouseArea { - anchors.fill: parent + Repeater { + model: root.images - onClicked: { - root.selectedImage = root.images[index] - root.popup.close() + Rectangle { + required property int index + required property var modelData + + color: "transparent" + border.color: root.selectedImage === modelData ? StudioTheme.Values.themeInteraction + : "transparent" + + width: 200 + height: 200 + visible: index > 0 + || EffectComposerBackend.effectComposerModel.customPreviewImage !== Qt.url("") + + Image { + source: index > 0 + ? parent.modelData + : EffectComposerBackend.effectComposerModel.customPreviewImage + anchors.fill: parent + fillMode: Image.PreserveAspectFit + smooth: true + anchors.margins: 1 + } + + MouseArea { + anchors.fill: parent + + onClicked: { + if (parent.index > 0) { + EffectComposerBackend.effectComposerModel.currentPreviewImage + = root.images[index] + } else { + EffectComposerBackend.effectComposerModel.currentPreviewImage + = EffectComposerBackend.effectComposerModel.customPreviewImage + } + root.popup.close() + } } } } diff --git a/src/plugins/effectcomposer/effectcomposermodel.cpp b/src/plugins/effectcomposer/effectcomposermodel.cpp index 33e72b145db..6f7a2612c98 100644 --- a/src/plugins/effectcomposer/effectcomposermodel.cpp +++ b/src/plugins/effectcomposer/effectcomposermodel.cpp @@ -10,18 +10,24 @@ #include "syntaxhighlighterdata.h" #include "uniform.h" +#include +#include +#include +#include +#include + +#include + #include #include #include -#include #include #include -#include - #include +#include #include #include #include @@ -247,6 +253,47 @@ bool EffectComposerModel::nameExists(const QString &name) const return QFile::exists(path.arg(name)); } +void EffectComposerModel::chooseCustomPreviewImage() +{ + QTimer::singleShot(0, this, [&]() { + using Utils::FilePath; + static FilePath lastDir; + const QStringList &suffixes = QmlDesigner::Asset::supportedImageSuffixes(); + QmlDesigner::DesignDocument *document = QmlDesigner::QmlDesignerPlugin::instance()->currentDesignDocument(); + const FilePath currentDir = lastDir.isEmpty() ? document->fileName().parentDir() + : lastDir; + const QStringList fileNames = QFileDialog::getOpenFileNames(Core::ICore::dialogParent(), + tr("Select custom effect background image"), + currentDir.toFSPathString(), + tr("Image Files (%1)").arg(suffixes.join(" "))); + + if (!fileNames.isEmpty()) { + FilePath imageFile = FilePath::fromString(fileNames.first()); + lastDir = imageFile.absolutePath(); + if (imageFile.exists()) { + FilePath projDir = QmlDesigner::QmlDesignerPlugin::instance()->documentManager() + .currentProjectDirPath(); + if (!imageFile.isChildOf(projDir)) { + FilePath imagesDir = QmlDesigner::ModelNodeOperations::getImagesDefaultDirectory(); + FilePath targetFile = imagesDir.pathAppended(imageFile.fileName()); + if (!targetFile.exists()) + imageFile.copyFile(targetFile); + if (targetFile.exists()) + imageFile = targetFile; + } + + m_customPreviewImage = QUrl::fromLocalFile(imageFile.toFSPathString()); + m_currentPreviewImage = m_customPreviewImage; + + setHasUnsavedChanges(true); + + emit currentPreviewImageChanged(); + emit customPreviewImageChanged(); + } + } + }); +} + QString EffectComposerModel::fragmentShader() const { return m_fragmentShader; @@ -1000,6 +1047,9 @@ void EffectComposerModel::saveComposition(const QString &name) return; } + const Utils::FilePath compositionPath = Utils::FilePath::fromString(path); + const Utils::FilePath compositionDir = compositionPath.absolutePath(); + updateExtraMargin(); QJsonObject json; @@ -1007,6 +1057,17 @@ void EffectComposerModel::saveComposition(const QString &name) json.insert("version", 1); json.insert("tool", "EffectComposer"); + Utils::FilePath customPreviewPath = Utils::FilePath::fromUrl(m_customPreviewImage); + if (m_customPreviewImage.isLocalFile()) + customPreviewPath = customPreviewPath.relativePathFrom(compositionDir); + json.insert("customPreviewImage", customPreviewPath.toUrl().toString()); + + QUrl previewUrl = m_currentPreviewImage; + if (m_currentPreviewImage == m_customPreviewImage) + previewUrl = customPreviewPath.toUrl(); + + json.insert("previewImage", previewUrl.toString()); + // Add nodes QJsonArray nodesArray; for (const CompositionNode *node : std::as_const(m_nodes)) { @@ -1034,7 +1095,7 @@ void EffectComposerModel::saveComposition(const QString &name) saveFile.close(); setCurrentComposition(name); - setCompositionPath(Utils::FilePath::fromString(path)); + setCompositionPath(compositionPath); saveResources(name); setHasUnsavedChanges(false); @@ -1083,10 +1144,11 @@ void EffectComposerModel::openComposition(const QString &path) { clear(true); - const QString effectName = QFileInfo(path).baseName(); + Utils::FilePath effectPath = Utils::FilePath::fromString(path); + const QString effectName = effectPath.baseName(); setCurrentComposition(effectName); - setCompositionPath(Utils::FilePath::fromString(path)); + setCompositionPath(effectPath); QFile compFile(path); if (!compFile.open(QIODevice::ReadOnly)) { @@ -1166,6 +1228,39 @@ void EffectComposerModel::openComposition(const QString &path) else resetRootFragmentShader(); + m_currentPreviewImage.clear(); + if (json.contains("previewImage")) { + const QString imageStr = json["previewImage"].toString(); + if (!imageStr.isEmpty()) { + const QUrl imageUrl{imageStr}; + Utils::FilePath imagePath = Utils::FilePath::fromUrl(imageUrl); + if (imageStr.startsWith("images/preview")) { // built-in preview image + m_currentPreviewImage = imageUrl; + } else if (imagePath.isAbsolutePath()) { + if (imagePath.exists()) + m_currentPreviewImage = imageUrl; + } else { + imagePath = effectPath.absolutePath().resolvePath(imagePath); + if (imagePath.exists()) + m_currentPreviewImage = imagePath.toUrl(); + } + } + } + + m_customPreviewImage.clear(); + if (json.contains("customPreviewImage")) { + QUrl imageUrl{json["customPreviewImage"].toString()}; + Utils::FilePath imagePath = Utils::FilePath::fromUrl(imageUrl); + if (imagePath.isAbsolutePath()) { + if (imagePath.exists()) + m_customPreviewImage = imageUrl; + } else { + imagePath = effectPath.absolutePath().resolvePath(imagePath); + if (imagePath.exists()) + m_customPreviewImage = imagePath.toUrl(); + } + } + if (json.contains("nodes") && json["nodes"].isArray()) { beginResetModel(); QHash refCounts; @@ -1194,6 +1289,8 @@ void EffectComposerModel::openComposition(const QString &path) setHasUnsavedChanges(false); emit nodesChanged(); + emit currentPreviewImageChanged(); + emit customPreviewImageChanged(); } void EffectComposerModel::saveResources(const QString &name) @@ -2151,6 +2248,28 @@ void EffectComposerModel::setCurrentComposition(const QString &newCurrentComposi m_shadersCodeEditor.reset(); } +QUrl EffectComposerModel::customPreviewImage() const +{ + return m_customPreviewImage; +} + +QUrl EffectComposerModel::currentPreviewImage() const +{ + return m_currentPreviewImage; +} + +void EffectComposerModel::setCurrentPreviewImage(const QUrl &path) +{ + if (m_currentPreviewImage == path) + return; + + if (!m_nodes.isEmpty()) + setHasUnsavedChanges(true); + + m_currentPreviewImage = path; + emit currentPreviewImageChanged(); +} + Utils::FilePath EffectComposerModel::compositionPath() const { return m_compositionPath; diff --git a/src/plugins/effectcomposer/effectcomposermodel.h b/src/plugins/effectcomposer/effectcomposermodel.h index 1d1389864ed..09aeb724532 100644 --- a/src/plugins/effectcomposer/effectcomposermodel.h +++ b/src/plugins/effectcomposer/effectcomposermodel.h @@ -15,6 +15,7 @@ #include #include #include +#include namespace ProjectExplorer { class Target; @@ -53,6 +54,8 @@ class EffectComposerModel : public QAbstractListModel Q_PROPERTY(bool isEnabled READ isEnabled WRITE setIsEnabled NOTIFY isEnabledChanged) Q_PROPERTY(bool hasValidTarget READ hasValidTarget WRITE setHasValidTarget NOTIFY hasValidTargetChanged) Q_PROPERTY(QString currentComposition READ currentComposition WRITE setCurrentComposition NOTIFY currentCompositionChanged) + Q_PROPERTY(QUrl currentPreviewImage READ currentPreviewImage WRITE setCurrentPreviewImage NOTIFY currentPreviewImageChanged) + Q_PROPERTY(QUrl customPreviewImage READ customPreviewImage NOTIFY customPreviewImageChanged) public: EffectComposerModel(QObject *parent = nullptr); @@ -77,6 +80,7 @@ public: Q_INVOKABLE void assignToSelected(); Q_INVOKABLE QString getUniqueEffectName() const; Q_INVOKABLE bool nameExists(const QString &name) const; + Q_INVOKABLE void chooseCustomPreviewImage(); bool shadersUpToDate() const; void setShadersUpToDate(bool newShadersUpToDate); @@ -116,6 +120,10 @@ public: QString currentComposition() const; void setCurrentComposition(const QString &newCurrentComposition); + QUrl customPreviewImage() const; + QUrl currentPreviewImage() const; + void setCurrentPreviewImage(const QUrl &path); + Utils::FilePath compositionPath() const; void setCompositionPath(const Utils::FilePath &newCompositionPath); @@ -140,6 +148,8 @@ signals: void hasUnsavedChangesChanged(); void assignToSelectedTriggered(const QString &effectPath); void removePropertiesFromScene(QSet props, const QString &typeName); + void currentPreviewImageChanged(); + void customPreviewImageChanged(); private: enum Roles { @@ -242,6 +252,8 @@ private: QString m_effectTypePrefix; Utils::FilePath m_compositionPath; Utils::UniqueObjectLatePtr m_shadersCodeEditor; + QUrl m_currentPreviewImage; + QUrl m_customPreviewImage; const QRegularExpression m_spaceReg = QRegularExpression("\\s+"); }; diff --git a/src/plugins/qmldesigner/components/componentcore/modelnodeoperations.h b/src/plugins/qmldesigner/components/componentcore/modelnodeoperations.h index 8b3d411c05f..4ae7b45c02b 100644 --- a/src/plugins/qmldesigner/components/componentcore/modelnodeoperations.h +++ b/src/plugins/qmldesigner/components/componentcore/modelnodeoperations.h @@ -137,7 +137,7 @@ bool useLayerEffect(); bool validateEffect(const QString &effectPath); bool isEffectComposerActivated(); -Utils::FilePath getImagesDefaultDirectory(); +QMLDESIGNERCOMPONENTS_EXPORT Utils::FilePath getImagesDefaultDirectory(); //Item Library and Assets related drop operations QMLDESIGNERCOMPONENTS_EXPORT ModelNode handleItemLibraryEffectDrop(const QString &effectPath,