QmlDesigner: Import bundles directly to project

Change-Id: I75b347d3fa969fe312f35e4c0c770ba03ba3cf85
Reviewed-by: Miikka Heikkinen <miikka.heikkinen@qt.io>
Reviewed-by: Ali Kianian <ali.kianian@qt.io>
This commit is contained in:
Mahmoud Badri
2024-06-26 20:56:05 +03:00
parent 87d1fa5292
commit 14b7df7cf2
11 changed files with 245 additions and 109 deletions

View File

@@ -235,7 +235,6 @@ Item {
cellWidth: root.thumbnailSize cellWidth: root.thumbnailSize
cellHeight: root.thumbnailSize + 20 cellHeight: root.thumbnailSize + 20
numColumns: root.numColumns numColumns: root.numColumns
hideHorizontalScrollBar: true
searchBox: searchBox searchBox: searchBox

View File

@@ -10,7 +10,8 @@ StudioControls.Menu {
id: root id: root
property var targetItem: null property var targetItem: null
property bool enableRemove: false // true: adds an option to remove targetItem property bool showRemoveAction: false // true: adds an option to remove targetItem
property bool showImportAction: false // true: adds an option to import a bundle from file
readonly property bool targetAvailable: targetItem && !ContentLibraryBackend.rootView.importerRunning readonly property bool targetAvailable: targetItem && !ContentLibraryBackend.rootView.importerRunning
@@ -18,12 +19,13 @@ StudioControls.Menu {
signal addToProject() signal addToProject()
signal applyToSelected(bool add) signal applyToSelected(bool add)
signal removeFromContentLib() signal removeFromContentLib()
signal importBundle()
function popupMenu(item = null) function popupMenu(item = null)
{ {
root.targetItem = item root.targetItem = item
let isMaterial = root.targetItem.bundleId === "UserMaterials" let isMaterial = item && root.targetItem.bundleId === "UserMaterials"
applyToSelectedReplace.visible = isMaterial applyToSelectedReplace.visible = isMaterial
applyToSelectedAdd.visible = isMaterial applyToSelectedAdd.visible = isMaterial
@@ -64,8 +66,15 @@ StudioControls.Menu {
StudioControls.MenuItem { StudioControls.MenuItem {
text: qsTr("Remove from Content Library") text: qsTr("Remove from Content Library")
visible: root.enableRemove && root.targetAvailable visible: root.showRemoveAction && root.targetAvailable
height: visible ? implicitHeight : 0 height: visible ? implicitHeight : 0
onTriggered: root.removeFromContentLib() onTriggered: root.removeFromContentLib()
} }
StudioControls.MenuItem {
text: qsTr("Import bundle")
visible: root.showImportAction
height: visible ? implicitHeight : 0
onTriggered: root.importBundle()
}
} }

View File

@@ -12,7 +12,7 @@ StudioControls.Menu {
property var targetTexture: null property var targetTexture: null
property bool hasSceneEnv: false property bool hasSceneEnv: false
property bool enableRemove: false // true: adds an option to remove targetTexture property bool showRemoveAction: false // true: adds an option to remove targetTexture
property bool canUse3D: targetTexture && ContentLibraryBackend.rootView.hasQuick3DImport && ContentLibraryBackend.rootView.hasMaterialLibrary property bool canUse3D: targetTexture && ContentLibraryBackend.rootView.hasQuick3DImport && ContentLibraryBackend.rootView.hasMaterialLibrary
@@ -45,7 +45,7 @@ StudioControls.Menu {
StudioControls.MenuItem { StudioControls.MenuItem {
text: qsTr("Remove from Content Library") text: qsTr("Remove from Content Library")
visible: root.targetTexture && root.enableRemove visible: root.targetTexture && root.showRemoveAction
height: visible ? implicitHeight : 0 height: visible ? implicitHeight : 0
onTriggered: ContentLibraryBackend.userModel.removeTexture(root.targetTexture) onTriggered: ContentLibraryBackend.userModel.removeTexture(root.targetTexture)
} }

View File

@@ -8,12 +8,10 @@ import StudioControls as StudioControls
import StudioTheme as StudioTheme import StudioTheme as StudioTheme
import ContentLibraryBackend import ContentLibraryBackend
HelperWidgets.ScrollView { Item {
id: root id: root
clip: true property alias adsFocus: scrollView.adsFocus
interactive: !ctxMenuItem.opened && !ctxMenuTexture.opened
&& !ContentLibraryBackend.rootView.isDragging && !HelperWidgets.Controller.contextMenuOpened
property real cellWidth: 100 property real cellWidth: 100
property real cellHeight: 120 property real cellHeight: 120
@@ -46,26 +44,49 @@ HelperWidgets.ScrollView {
} }
} }
Column {
ContentLibraryItemContextMenu { ContentLibraryItemContextMenu {
id: ctxMenuItem id: ctxMenuItem
enableRemove: true showRemoveAction: true
showImportAction: true
onApplyToSelected: (add) => ContentLibraryBackend.userModel.applyToSelected(ctxMenuItem.targetItem, add) onApplyToSelected: (add) => ContentLibraryBackend.userModel.applyToSelected(ctxMenuItem.targetItem, add)
onUnimport: root.unimport(ctxMenuItem.targetItem) onUnimport: root.unimport(ctxMenuItem.targetItem)
onAddToProject: ContentLibraryBackend.userModel.addToProject(ctxMenuItem.targetItem) onAddToProject: ContentLibraryBackend.userModel.addToProject(ctxMenuItem.targetItem)
onRemoveFromContentLib: root.removeFromContentLib(ctxMenuItem.targetItem) onRemoveFromContentLib: root.removeFromContentLib(ctxMenuItem.targetItem)
onImportBundle: ContentLibraryBackend.rootView.importBundle();
} }
ContentLibraryTextureContextMenu { ContentLibraryTextureContextMenu {
id: ctxMenuTexture id: ctxMenuTexture
enableRemove: true showRemoveAction: true
hasSceneEnv: ContentLibraryBackend.texturesModel.hasSceneEnv hasSceneEnv: ContentLibraryBackend.texturesModel.hasSceneEnv
} }
MouseArea {
id: rootMouseArea
anchors.fill: parent
acceptedButtons: Qt.RightButton
enabled: infoText.text === ""
onClicked: (mouse) => {
ctxMenuItem.popupMenu()
}
}
HelperWidgets.ScrollView {
id: scrollView
anchors.fill: parent
clip: true
interactive: !ctxMenuItem.opened && !ctxMenuTexture.opened
&& !ContentLibraryBackend.rootView.isDragging && !HelperWidgets.Controller.contextMenuOpened
hideHorizontalScrollBar: true
Column {
Repeater { Repeater {
id: categoryRepeater id: categoryRepeater
@@ -171,3 +192,4 @@ HelperWidgets.ScrollView {
} }
} }
} }
}

View File

@@ -100,6 +100,9 @@ WidgetInfo ContentLibraryView::widgetInfo()
m_widget->environmentsModel()->setHasSceneEnv(sceneEnvExists); m_widget->environmentsModel()->setHasSceneEnv(sceneEnvExists);
}); });
connect(m_widget, &ContentLibraryWidget::importBundle, this,
&ContentLibraryView::importBundleToContentLib);
connect(m_widget->materialsModel(), connect(m_widget->materialsModel(),
&ContentLibraryMaterialsModel::applyToSelectedTriggered, &ContentLibraryMaterialsModel::applyToSelectedTriggered,
this, this,
@@ -393,8 +396,8 @@ void ContentLibraryView::customNotification(const AbstractView *view,
exportLibItem(nodeList.first()); exportLibItem(nodeList.first());
} else if (identifier == "export_material_as_bundle") { } else if (identifier == "export_material_as_bundle") {
exportLibItem(nodeList.first(), data.first().value<QPixmap>()); exportLibItem(nodeList.first(), data.first().value<QPixmap>());
} else if (identifier == "import_bundle") { } else if (identifier == "import_bundle_to_project") {
importBundle(); importBundleToProject();
} }
} }
@@ -1023,7 +1026,7 @@ void ContentLibraryView::exportLibItem(const ModelNode &node, const QPixmap &ico
addIconAndCloseZip(iconPixmap); addIconAndCloseZip(iconPixmap);
} }
void ContentLibraryView::importBundle() void ContentLibraryView::importBundleToContentLib()
{ {
QString importPath = getImportPath(); QString importPath = getImportPath();
if (importPath.isEmpty()) if (importPath.isEmpty())
@@ -1049,6 +1052,7 @@ void ContentLibraryView::importBundle()
bool isMat = isMaterialBundle(bundleId); bool isMat = isMaterialBundle(bundleId);
QString bundleFolderName = isMat ? QLatin1String("materials") : QLatin1String("3d"); QString bundleFolderName = isMat ? QLatin1String("materials") : QLatin1String("3d");
auto bundlePath = Utils::FilePath::fromString(QLatin1String("%1/User/%3/") auto bundlePath = Utils::FilePath::fromString(QLatin1String("%1/User/%3/")
.arg(Paths::bundlesPathSetting(), bundleFolderName)); .arg(Paths::bundlesPathSetting(), bundleFolderName));
@@ -1096,6 +1100,7 @@ void ContentLibraryView::importBundle()
m_widget->userModel()->addItem(bundleId, name, qml, iconUrl, files); m_widget->userModel()->addItem(bundleId, name, qml, iconUrl, files);
} }
m_widget->userModel()->refreshSection(bundleId); m_widget->userModel()->refreshSection(bundleId);
zipReader.close(); zipReader.close();
@@ -1107,6 +1112,85 @@ void ContentLibraryView::importBundle()
QTC_ASSERT_EXPECTED(result,); QTC_ASSERT_EXPECTED(result,);
} }
void ContentLibraryView::importBundleToProject()
{
QString importPath = getImportPath();
if (importPath.isEmpty())
return;
auto compUtils = QmlDesignerPlugin::instance()->documentManager().generatedComponentUtils();
ZipReader zipReader(importPath);
QByteArray bundleJsonContent = zipReader.fileData(Constants::BUNDLE_JSON_FILENAME);
QTC_ASSERT(!bundleJsonContent.isEmpty(), return);
const QJsonObject importedJsonObj = QJsonDocument::fromJson(bundleJsonContent).object();
const QJsonArray importedItemsArr = importedJsonObj.value("items").toArray();
QTC_ASSERT(!importedItemsArr.isEmpty(), return);
QString bundleVersion = importedJsonObj.value("version").toString();
bool bundleVersionOk = !bundleVersion.isEmpty() && bundleVersion == BUNDLE_VERSION;
if (!bundleVersionOk) {
QMessageBox::warning(m_widget, tr("Unsupported bundle file"),
tr("The chosen bundle was created with an incompatible version of Qt Design Studio"));
return;
}
QString bundleId = importedJsonObj.value("id").toString();
QTemporaryDir tempDir;
QTC_ASSERT(tempDir.isValid(), return);
auto bundlePath = Utils::FilePath::fromString(tempDir.path());
const QStringList existingQmls = Utils::transform(compUtils.userBundlePath(bundleId)
.dirEntries(QDir::Files), [](const Utils::FilePath &path) {
return path.fileName();
});
for (const QJsonValueConstRef &itemRef : importedItemsArr) {
QJsonObject itemObj = itemRef.toObject();
QString qml = itemObj.value("qml").toString();
// confirm overwrite if an item with same name exists
if (existingQmls.contains(qml)) {
QMessageBox::StandardButton reply = QMessageBox::question(m_widget, tr("Component Exists"),
tr("A component with the same name '%1' already "
"exists in the project, are you sure "
"you want to overwrite it?")
.arg(qml), QMessageBox::Yes | QMessageBox::No);
if (reply == QMessageBox::No)
continue;
// TODO: before overwriting remove old item's dependencies (not harmful but for cleanup)
}
// add entry to model
QStringList files = itemObj.value("files").toVariant().toStringList();
QString icon = itemObj.value("icon").toString();
// copy files
QStringList allFiles = files;
allFiles << qml << icon;
for (const QString &file : std::as_const(allFiles)) {
Utils::FilePath filePath = bundlePath.pathAppended(file);
filePath.parentDir().ensureWritableDir();
QTC_ASSERT_EXPECTED(filePath.writeFileContents(zipReader.fileData(file)),);
}
QString typePrefix = compUtils.userBundleType(bundleId);
TypeName type = QLatin1String("%1.%2").arg(typePrefix, qml.chopped(4)).toLatin1();
QString err = m_widget->importer()->importComponent(bundlePath.toFSPathString(), type, qml, files);
if (err.isEmpty())
m_widget->setImporterRunning(true);
else
qWarning() << __FUNCTION__ << err;
}
zipReader.close();
}
/** /**
* @brief Generates an icon image from a qml component * @brief Generates an icon image from a qml component
* @param qmlPath path to the qml component file to be rendered * @param qmlPath path to the qml component file to be rendered

View File

@@ -66,7 +66,8 @@ private:
void exportLib3DComponent(const ModelNode &node); void exportLib3DComponent(const ModelNode &node);
void addLibItem(const ModelNode &node, const QPixmap &iconPixmap = {}); void addLibItem(const ModelNode &node, const QPixmap &iconPixmap = {});
void exportLibItem(const ModelNode &node, const QPixmap &iconPixmap = {}); void exportLibItem(const ModelNode &node, const QPixmap &iconPixmap = {});
void importBundle(); void importBundleToContentLib();
void importBundleToProject();
void getImageFromCache(const QString &qmlPath, void getImageFromCache(const QString &qmlPath,
std::function<void(const QImage &image)> successCallback); std::function<void(const QImage &image)> successCallback);
QString getExportPath(const ModelNode &node) const; QString getExportPath(const ModelNode &node) const;

View File

@@ -108,6 +108,7 @@ signals:
void isQt6ProjectChanged(); void isQt6ProjectChanged();
void importerRunningChanged(); void importerRunningChanged();
void hasModelSelectionChanged(); void hasModelSelectionChanged();
void importBundle();
protected: protected:
bool eventFilter(QObject *obj, QEvent *event) override; bool eventFilter(QObject *obj, QEvent *event) override;

View File

@@ -373,7 +373,7 @@ void Edit3DWidget::createContextMenu()
m_importBundleAction = m_contextMenu->addAction( m_importBundleAction = m_contextMenu->addAction(
contextIcon(DesignerIcons::CreateIcon), // TODO: placeholder icon contextIcon(DesignerIcons::CreateIcon), // TODO: placeholder icon
tr("Import Components"), [&] { tr("Import Components"), [&] {
view()->emitCustomNotification("import_bundle"); // To ContentLibrary view()->emitCustomNotification("import_bundle_to_project"); // To ContentLibrary
}); });
m_exportBundleAction = m_contextMenu->addAction( m_exportBundleAction = m_contextMenu->addAction(

View File

@@ -374,7 +374,7 @@ void MaterialBrowserWidget::addMaterialToContentLibrary()
void MaterialBrowserWidget::importMaterial() void MaterialBrowserWidget::importMaterial()
{ {
ModelNode mat = m_materialBrowserModel->selectedMaterial(); ModelNode mat = m_materialBrowserModel->selectedMaterial();
m_materialBrowserView->emitCustomNotification("import_bundle"); // to ContentLibrary m_materialBrowserView->emitCustomNotification("import_bundle_to_project"); // to ContentLibrary
} }
void MaterialBrowserWidget::exportMaterial() void MaterialBrowserWidget::exportMaterial()
{ {

View File

@@ -131,6 +131,25 @@ Utils::FilePath GeneratedComponentUtils::effectBundlePath() const
return basePath.resolvePath(QLatin1String(Constants::COMPONENT_BUNDLES_EFFECT_BUNDLE_TYPE)); return basePath.resolvePath(QLatin1String(Constants::COMPONENT_BUNDLES_EFFECT_BUNDLE_TYPE));
} }
Utils::FilePath GeneratedComponentUtils::userBundlePath(const QString &bundleId) const
{
Utils::FilePath basePath = componentBundlesBasePath();
if (basePath.isEmpty())
return {};
if (bundleId == userMaterialsBundleId())
return basePath.pathAppended(Constants::COMPONENT_BUNDLES_USER_MATERIAL_BUNDLE_TYPE);
if (bundleId == userEffectsBundleId())
return basePath.pathAppended(Constants::COMPONENT_BUNDLES_USER_EFFECT_BUNDLE_TYPE);
if (bundleId == user3DBundleId())
return basePath.pathAppended(Constants::COMPONENT_BUNDLES_USER_3D_BUNDLE_TYPE);
qWarning() << __FUNCTION__ << "no bundleType for bundleId:" << bundleId;
return {};
}
Utils::FilePath GeneratedComponentUtils::projectModulePath(bool generateIfNotExists) const Utils::FilePath GeneratedComponentUtils::projectModulePath(bool generateIfNotExists) const
{ {
using Utils::FilePath; using Utils::FilePath;

View File

@@ -23,6 +23,7 @@ public:
Utils::FilePath import3dBasePath() const; Utils::FilePath import3dBasePath() const;
Utils::FilePath materialBundlePath() const; Utils::FilePath materialBundlePath() const;
Utils::FilePath effectBundlePath() const; Utils::FilePath effectBundlePath() const;
Utils::FilePath userBundlePath(const QString &bundleId) const;
Utils::FilePath projectModulePath(bool generateIfNotExists = false) const; Utils::FilePath projectModulePath(bool generateIfNotExists = false) const;
bool isImport3dPath(const QString &path) const; bool isImport3dPath(const QString &path) const;