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 <mats.honkamaa@qt.io>
Reviewed-by: Mahmoud Badri <mahmoud.badri@qt.io>
Reviewed-by: Ali Kianian <ali.kianian@qt.io>
This commit is contained in:
Miikka Heikkinen
2024-10-17 16:15:56 +03:00
parent 5285ff2bc9
commit 84ca365985
4 changed files with 210 additions and 42 deletions

View File

@@ -22,12 +22,17 @@ StudioControls.ComboBox {
required property Item mainRoot required property Item mainRoot
property var images: ["images/preview0.png", property var images: [Qt.url(""),
"images/preview1.png", Qt.url("images/preview0.png"),
"images/preview2.png", Qt.url("images/preview1.png"),
"images/preview3.png", Qt.url("images/preview2.png"),
"images/preview4.png"] Qt.url("images/preview3.png"),
property string selectedImage: images[0] 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) readonly property int popupHeight: Math.min(800, col.height + 2)
@@ -122,45 +127,77 @@ StudioControls.ComboBox {
border.width: 1 border.width: 1
focus: true focus: true
HelperWidgets.ScrollView { Column {
anchors.fill: parent anchors.fill: parent
anchors.margins: 1
clip: true
Column { Item {
id: col id: setCustomItem
width: parent.width
height: 40
padding: 10 HelperWidgets.Button {
spacing: 10 anchors.fill: parent
anchors.margins: 2
text: qsTr("Set Custom Image")
onClicked: {
EffectComposerBackend.effectComposerModel.chooseCustomPreviewImage()
root.popup.close()
}
}
}
Repeater {
model: root.images
Rectangle { HelperWidgets.ScrollView {
required property int index width: parent.width - 2
required property var modelData height: parent.height - setCustomItem.height
color: "transparent" clip: true
border.color: root.selectedImage === modelData ? StudioTheme.Values.themeInteraction
: "transparent"
width: 200 Column {
height: 200 id: col
Image { padding: 10
source: modelData spacing: 10
anchors.fill: parent
fillMode: Image.PreserveAspectFit
smooth: true
anchors.margins: 1
}
MouseArea { Repeater {
anchors.fill: parent model: root.images
onClicked: { Rectangle {
root.selectedImage = root.images[index] required property int index
root.popup.close() 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()
}
} }
} }
} }

View File

@@ -10,18 +10,24 @@
#include "syntaxhighlighterdata.h" #include "syntaxhighlighterdata.h"
#include "uniform.h" #include "uniform.h"
#include <asset.h>
#include <designdocument.h>
#include <modelnodeoperations.h>
#include <qmldesignerplugin.h>
#include <uniquename.h>
#include <coreplugin/icore.h>
#include <projectexplorer/projecttree.h> #include <projectexplorer/projecttree.h>
#include <projectexplorer/target.h> #include <projectexplorer/target.h>
#include <qtsupport/qtkitaspect.h> #include <qtsupport/qtkitaspect.h>
#include <uniquename.h>
#include <utils/qtcassert.h> #include <utils/qtcassert.h>
#include <utils/qtcprocess.h> #include <utils/qtcprocess.h>
#include <modelnodeoperations.h>
#include <QByteArrayView> #include <QByteArrayView>
#include <QFileDialog>
#include <QLibraryInfo> #include <QLibraryInfo>
#include <QTemporaryDir> #include <QTemporaryDir>
#include <QVector2D> #include <QVector2D>
@@ -247,6 +253,47 @@ bool EffectComposerModel::nameExists(const QString &name) const
return QFile::exists(path.arg(name)); 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 QString EffectComposerModel::fragmentShader() const
{ {
return m_fragmentShader; return m_fragmentShader;
@@ -1000,6 +1047,9 @@ void EffectComposerModel::saveComposition(const QString &name)
return; return;
} }
const Utils::FilePath compositionPath = Utils::FilePath::fromString(path);
const Utils::FilePath compositionDir = compositionPath.absolutePath();
updateExtraMargin(); updateExtraMargin();
QJsonObject json; QJsonObject json;
@@ -1007,6 +1057,17 @@ void EffectComposerModel::saveComposition(const QString &name)
json.insert("version", 1); json.insert("version", 1);
json.insert("tool", "EffectComposer"); 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 // Add nodes
QJsonArray nodesArray; QJsonArray nodesArray;
for (const CompositionNode *node : std::as_const(m_nodes)) { for (const CompositionNode *node : std::as_const(m_nodes)) {
@@ -1034,7 +1095,7 @@ void EffectComposerModel::saveComposition(const QString &name)
saveFile.close(); saveFile.close();
setCurrentComposition(name); setCurrentComposition(name);
setCompositionPath(Utils::FilePath::fromString(path)); setCompositionPath(compositionPath);
saveResources(name); saveResources(name);
setHasUnsavedChanges(false); setHasUnsavedChanges(false);
@@ -1083,10 +1144,11 @@ void EffectComposerModel::openComposition(const QString &path)
{ {
clear(true); clear(true);
const QString effectName = QFileInfo(path).baseName(); Utils::FilePath effectPath = Utils::FilePath::fromString(path);
const QString effectName = effectPath.baseName();
setCurrentComposition(effectName); setCurrentComposition(effectName);
setCompositionPath(Utils::FilePath::fromString(path)); setCompositionPath(effectPath);
QFile compFile(path); QFile compFile(path);
if (!compFile.open(QIODevice::ReadOnly)) { if (!compFile.open(QIODevice::ReadOnly)) {
@@ -1166,6 +1228,39 @@ void EffectComposerModel::openComposition(const QString &path)
else else
resetRootFragmentShader(); 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()) { if (json.contains("nodes") && json["nodes"].isArray()) {
beginResetModel(); beginResetModel();
QHash<QString, int> refCounts; QHash<QString, int> refCounts;
@@ -1194,6 +1289,8 @@ void EffectComposerModel::openComposition(const QString &path)
setHasUnsavedChanges(false); setHasUnsavedChanges(false);
emit nodesChanged(); emit nodesChanged();
emit currentPreviewImageChanged();
emit customPreviewImageChanged();
} }
void EffectComposerModel::saveResources(const QString &name) void EffectComposerModel::saveResources(const QString &name)
@@ -2151,6 +2248,28 @@ void EffectComposerModel::setCurrentComposition(const QString &newCurrentComposi
m_shadersCodeEditor.reset(); 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 Utils::FilePath EffectComposerModel::compositionPath() const
{ {
return m_compositionPath; return m_compositionPath;

View File

@@ -15,6 +15,7 @@
#include <QSet> #include <QSet>
#include <QTemporaryDir> #include <QTemporaryDir>
#include <QTimer> #include <QTimer>
#include <QUrl>
namespace ProjectExplorer { namespace ProjectExplorer {
class Target; class Target;
@@ -53,6 +54,8 @@ class EffectComposerModel : public QAbstractListModel
Q_PROPERTY(bool isEnabled READ isEnabled WRITE setIsEnabled NOTIFY isEnabledChanged) Q_PROPERTY(bool isEnabled READ isEnabled WRITE setIsEnabled NOTIFY isEnabledChanged)
Q_PROPERTY(bool hasValidTarget READ hasValidTarget WRITE setHasValidTarget NOTIFY hasValidTargetChanged) Q_PROPERTY(bool hasValidTarget READ hasValidTarget WRITE setHasValidTarget NOTIFY hasValidTargetChanged)
Q_PROPERTY(QString currentComposition READ currentComposition WRITE setCurrentComposition NOTIFY currentCompositionChanged) 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: public:
EffectComposerModel(QObject *parent = nullptr); EffectComposerModel(QObject *parent = nullptr);
@@ -77,6 +80,7 @@ public:
Q_INVOKABLE void assignToSelected(); Q_INVOKABLE void assignToSelected();
Q_INVOKABLE QString getUniqueEffectName() const; Q_INVOKABLE QString getUniqueEffectName() const;
Q_INVOKABLE bool nameExists(const QString &name) const; Q_INVOKABLE bool nameExists(const QString &name) const;
Q_INVOKABLE void chooseCustomPreviewImage();
bool shadersUpToDate() const; bool shadersUpToDate() const;
void setShadersUpToDate(bool newShadersUpToDate); void setShadersUpToDate(bool newShadersUpToDate);
@@ -116,6 +120,10 @@ public:
QString currentComposition() const; QString currentComposition() const;
void setCurrentComposition(const QString &newCurrentComposition); void setCurrentComposition(const QString &newCurrentComposition);
QUrl customPreviewImage() const;
QUrl currentPreviewImage() const;
void setCurrentPreviewImage(const QUrl &path);
Utils::FilePath compositionPath() const; Utils::FilePath compositionPath() const;
void setCompositionPath(const Utils::FilePath &newCompositionPath); void setCompositionPath(const Utils::FilePath &newCompositionPath);
@@ -140,6 +148,8 @@ signals:
void hasUnsavedChangesChanged(); void hasUnsavedChangesChanged();
void assignToSelectedTriggered(const QString &effectPath); void assignToSelectedTriggered(const QString &effectPath);
void removePropertiesFromScene(QSet<QByteArray> props, const QString &typeName); void removePropertiesFromScene(QSet<QByteArray> props, const QString &typeName);
void currentPreviewImageChanged();
void customPreviewImageChanged();
private: private:
enum Roles { enum Roles {
@@ -242,6 +252,8 @@ private:
QString m_effectTypePrefix; QString m_effectTypePrefix;
Utils::FilePath m_compositionPath; Utils::FilePath m_compositionPath;
Utils::UniqueObjectLatePtr<EffectShadersCodeEditor> m_shadersCodeEditor; Utils::UniqueObjectLatePtr<EffectShadersCodeEditor> m_shadersCodeEditor;
QUrl m_currentPreviewImage;
QUrl m_customPreviewImage;
const QRegularExpression m_spaceReg = QRegularExpression("\\s+"); const QRegularExpression m_spaceReg = QRegularExpression("\\s+");
}; };

View File

@@ -137,7 +137,7 @@ bool useLayerEffect();
bool validateEffect(const QString &effectPath); bool validateEffect(const QString &effectPath);
bool isEffectComposerActivated(); bool isEffectComposerActivated();
Utils::FilePath getImagesDefaultDirectory(); QMLDESIGNERCOMPONENTS_EXPORT Utils::FilePath getImagesDefaultDirectory();
//Item Library and Assets related drop operations //Item Library and Assets related drop operations
QMLDESIGNERCOMPONENTS_EXPORT ModelNode handleItemLibraryEffectDrop(const QString &effectPath, QMLDESIGNERCOMPONENTS_EXPORT ModelNode handleItemLibraryEffectDrop(const QString &effectPath,