From ee1a725da132144eaa49912f294fb75cd19dcc5b Mon Sep 17 00:00:00 2001 From: Mahmoud Badri Date: Mon, 24 Mar 2025 17:01:24 +0200 Subject: [PATCH] QmlDesigner: Allow adding a folder to content library Change-Id: If44fdc0f0a7c59011854fd358f0542ce35ac1079 Reviewed-by: Miikka Heikkinen Reviewed-by: Ali Kianian --- .../ContentLibraryUserView.qml | 346 ++++++++++-------- .../contentlibraryusermodel.cpp | 172 ++++++++- .../contentlibrary/contentlibraryusermodel.h | 24 +- .../contentlibrary/contentlibraryview.cpp | 30 +- .../contentlibrary/contentlibraryview.h | 2 +- .../contentlibrary/contentlibrarywidget.cpp | 15 + .../contentlibrary/contentlibrarywidget.h | 5 +- .../contentlibrary/usertexturecategory.cpp | 21 +- .../contentlibrary/usertexturecategory.h | 3 +- .../qmldesigner/qmldesignerconstants.h | 1 + 10 files changed, 430 insertions(+), 189 deletions(-) diff --git a/share/qtcreator/qmldesigner/contentLibraryQmlSource/ContentLibraryUserView.qml b/share/qtcreator/qmldesigner/contentLibraryQmlSource/ContentLibraryUserView.qml index 4ee3af47f83..91d131bb9aa 100644 --- a/share/qtcreator/qmldesigner/contentLibraryQmlSource/ContentLibraryUserView.qml +++ b/share/qtcreator/qmldesigner/contentLibraryQmlSource/ContentLibraryUserView.qml @@ -2,6 +2,7 @@ // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 import QtQuick +import QtQuick.Layouts import Qt.labs.qmlmodels import HelperWidgets as HelperWidgets import StudioControls as StudioControls @@ -79,171 +80,214 @@ Item { } } - HelperWidgets.ScrollView { - id: scrollView + ColumnLayout { + id: col + anchors.fill: parent + spacing: 0 - clip: true - interactive: !ctxMenuItem.opened && !ctxMenuTexture.opened - && !ContentLibraryBackend.rootView.isDragging && !HelperWidgets.Controller.contextMenuOpened - hideHorizontalScrollBar: true + Rectangle { + id: toolbar - Column { - Repeater { - id: categoryRepeater + width: parent.width + height: StudioTheme.Values.toolbarHeight + color: StudioTheme.Values.themeToolbarBackground - model: ContentLibraryBackend.userModel + HelperWidgets.AbstractButton { + style: StudioTheme.Values.viewBarButtonStyle + buttonIcon: StudioTheme.Constants.add_medium + enabled: hasMaterial && hasModelSelection && hasQuick3DImport && hasMaterialLibrary + tooltip: qsTr("Add a custom bundle folder.") + onClicked: ContentLibraryBackend.rootView.browseBundleFolder() + x: 5 // left margin + } + } - delegate: HelperWidgets.Section { - id: section + HelperWidgets.ScrollView { + id: scrollView - width: root.width - leftPadding: StudioTheme.Values.sectionPadding - rightPadding: StudioTheme.Values.sectionPadding - topPadding: StudioTheme.Values.sectionPadding - bottomPadding: StudioTheme.Values.sectionPadding + Layout.fillWidth: true + Layout.fillHeight: true - caption: categoryTitle - dropEnabled: true - category: "ContentLib_User" + clip: true + interactive: !ctxMenuItem.opened && !ctxMenuTexture.opened + && !ContentLibraryBackend.rootView.isDragging && !HelperWidgets.Controller.contextMenuOpened + hideHorizontalScrollBar: true - function expandSection() { - section.expanded = true - } + Column { + Repeater { + id: categoryRepeater - property alias count: repeater.count + model: ContentLibraryBackend.userModel - onCountChanged: root.assignMaxCount() + delegate: HelperWidgets.Section { + id: section - onDropEnter: (drag) => { - let has3DNode = ContentLibraryBackend.rootView - .has3DNode(drag.getDataAsArrayBuffer(drag.formats[0])) - - let hasTexture = ContentLibraryBackend.rootView - .hasTexture(drag.formats[0], drag.urls) - - drag.accepted = (categoryTitle === "Textures" && hasTexture) - || (categoryTitle === "Materials" && drag.formats[0] === "application/vnd.qtdesignstudio.material") - || (categoryTitle === "3D" && has3DNode) - - section.highlight = drag.accepted - } - - onDropExit: { - section.highlight = false - } - - onDrop: (drag) => { - section.highlight = false - drag.accept() - section.expandSection() - - if (categoryTitle === "Textures") { - if (drag.formats[0] === "application/vnd.qtdesignstudio.assets") - ContentLibraryBackend.rootView.acceptTexturesDrop(drag.urls) - else if (drag.formats[0] === "application/vnd.qtdesignstudio.texture") - ContentLibraryBackend.rootView.acceptTextureDrop(drag.getDataAsString(drag.formats[0])) - } else if (categoryTitle === "Materials") { - ContentLibraryBackend.rootView.acceptMaterialDrop(drag.getDataAsString(drag.formats[0])) - } else if (categoryTitle === "3D") { - ContentLibraryBackend.rootView.accept3DDrop(drag.getDataAsArrayBuffer(drag.formats[0])) - } - } - - Grid { - width: section.width - section.leftPadding - section.rightPadding - spacing: StudioTheme.Values.sectionGridSpacing - columns: root.numColumns - - Repeater { - id: repeater - model: categoryItems - - delegate: DelegateChooser { - role: "bundleId" - - DelegateChoice { - roleValue: "UserMaterials" - ContentLibraryItem { - width: root.cellWidth - height: root.cellHeight - visible: modelData.bundleItemVisible && !infoText.visible - - onShowContextMenu: ctxMenuItem.popupMenu(modelData) - onAddToProject: ContentLibraryBackend.userModel.addToProject(modelData) - } - } - DelegateChoice { - roleValue: "UserTextures" - delegate: ContentLibraryTexture { - width: root.cellWidth - height: root.cellWidth // for textures use a square size since there is no name row - - onShowContextMenu: ctxMenuTexture.popupMenu(modelData) - } - } - DelegateChoice { - roleValue: "User3D" - delegate: ContentLibraryItem { - width: root.cellWidth - height: root.cellHeight - visible: modelData.bundleItemVisible && !infoText.visible - - onShowContextMenu: ctxMenuItem.popupMenu(modelData) - onAddToProject: ContentLibraryBackend.userModel.addToProject(modelData) - } - } - } - - onCountChanged: root.assignMaxCount() - } - } - - Text { - text: qsTr("No match found."); - color: StudioTheme.Values.themeTextColor - font.pixelSize: StudioTheme.Values.baseFontSize - leftPadding: 10 - visible: infoText.text === "" && !searchBox.isEmpty() && categoryNoMatch - } - - Text { - id: infoText - - text: { - let categoryName = (categoryTitle === "3D") ? categoryTitle + " assets" - : categoryTitle.toLowerCase() - if (!ContentLibraryBackend.rootView.isQt6Project) { - qsTr("Content Library is not supported in Qt5 projects.") - } else if (!ContentLibraryBackend.rootView.hasQuick3DImport && categoryTitle !== "Textures") { - qsTr('To use %1, add the QtQuick3D module and the View3D - component in the Components view, or click - - here.') - .arg(categoryName) - .arg(StudioTheme.Values.themeInteraction) - } else if (!ContentLibraryBackend.rootView.hasMaterialLibrary && categoryTitle !== "Textures") { - qsTr("Content Library is disabled inside a non-visual component.") - } else if (categoryEmpty) { - qsTr("There are no "+ categoryName + " in the User Assets.") - } else { - "" - } - } - textFormat: Text.RichText - color: StudioTheme.Values.themeTextColor - font.pixelSize: StudioTheme.Values.mediumFontSize - padding: 10 - visible: infoText.text !== "" - horizontalAlignment: Text.AlignHCenter - wrapMode: Text.WordWrap width: root.width + leftPadding: StudioTheme.Values.sectionPadding + rightPadding: StudioTheme.Values.sectionPadding + topPadding: StudioTheme.Values.sectionPadding + bottomPadding: StudioTheme.Values.sectionPadding - onLinkActivated: ContentLibraryBackend.rootView.addQtQuick3D() + caption: categoryTitle + dropEnabled: true + category: "ContentLib_User" + showCloseButton: section.isCustomCat + closeButtonToolTip: qsTr("Remove folder") + closeButtonIcon: StudioTheme.Constants.deletepermanently_small - HoverHandler { - enabled: infoText.hoveredLink - cursorShape: Qt.PointingHandCursor + onCloseButtonClicked: { + ContentLibraryBackend.userModel.removeBundleDir(index) + } + + function expandSection() { + section.expanded = true + } + + property alias count: repeater.count + property bool isCustomCat: !["Textures", "Materials", "3D"].includes(section.caption); + + onCountChanged: root.assignMaxCount() + + onDropEnter: (drag) => { + let has3DNode = ContentLibraryBackend.rootView + .has3DNode(drag.getDataAsArrayBuffer(drag.formats[0])) + + let hasTexture = ContentLibraryBackend.rootView + .hasTexture(drag.formats[0], drag.urls) + + drag.accepted = (categoryTitle === "Textures" && hasTexture) + || (categoryTitle === "Materials" && drag.formats[0] === "application/vnd.qtdesignstudio.material") + || (categoryTitle === "3D" && has3DNode) + || (section.isCustomCat && hasTexture) + + section.highlight = drag.accepted + } + + onDropExit: { + section.highlight = false + } + + onDrop: (drag) => { + section.highlight = false + drag.accept() + section.expandSection() + + if (categoryTitle === "Textures") { + if (drag.formats[0] === "application/vnd.qtdesignstudio.assets") + ContentLibraryBackend.rootView.acceptTexturesDrop(drag.urls) + else if (drag.formats[0] === "application/vnd.qtdesignstudio.texture") + ContentLibraryBackend.rootView.acceptTextureDrop(drag.getDataAsString(drag.formats[0])) + } else if (categoryTitle === "Materials") { + ContentLibraryBackend.rootView.acceptMaterialDrop(drag.getDataAsString(drag.formats[0])) + } else if (categoryTitle === "3D") { + ContentLibraryBackend.rootView.accept3DDrop(drag.getDataAsArrayBuffer(drag.formats[0])) + } else { // custom bundle folder + if (drag.formats[0] === "application/vnd.qtdesignstudio.assets") + ContentLibraryBackend.rootView.acceptTexturesDrop(drag.urls, categoryBundlePath) + else if (drag.formats[0] === "application/vnd.qtdesignstudio.texture") + ContentLibraryBackend.rootView.acceptTextureDrop(drag.getDataAsString(drag.formats[0]), categoryBundlePath) + } + } + + Grid { + width: section.width - section.leftPadding - section.rightPadding + spacing: StudioTheme.Values.sectionGridSpacing + columns: root.numColumns + + Repeater { + id: repeater + + model: categoryItems + + delegate: DelegateChooser { + role: "bundleId" + + DelegateChoice { + roleValue: "UserMaterials" + ContentLibraryItem { + width: root.cellWidth + height: root.cellHeight + visible: modelData.bundleItemVisible && !infoText.visible + + onShowContextMenu: ctxMenuItem.popupMenu(modelData) + onAddToProject: ContentLibraryBackend.userModel.addToProject(modelData) + } + } + DelegateChoice { + roleValue: "UserTextures" + delegate: ContentLibraryTexture { + width: root.cellWidth + height: root.cellWidth // for textures use a square size since there is no name row + + onShowContextMenu: ctxMenuTexture.popupMenu(modelData) + } + } + DelegateChoice { + roleValue: "User3D" + delegate: ContentLibraryItem { + width: root.cellWidth + height: root.cellHeight + visible: modelData.bundleItemVisible && !infoText.visible + + onShowContextMenu: ctxMenuItem.popupMenu(modelData) + onAddToProject: ContentLibraryBackend.userModel.addToProject(modelData) + } + } + } + + onCountChanged: root.assignMaxCount() + } + } + + Text { + text: qsTr("No match found."); + color: StudioTheme.Values.themeTextColor + font.pixelSize: StudioTheme.Values.baseFontSize + leftPadding: 10 + visible: infoText.text === "" && !searchBox.isEmpty() && categoryNoMatch + } + + Text { + id: infoText + + text: { + let categoryName = (categoryTitle === "3D") ? categoryTitle + " assets" + : categoryTitle.toLowerCase() + if (!ContentLibraryBackend.rootView.isQt6Project) { + qsTr("Content Library is not supported in Qt5 projects.") + } else if (!ContentLibraryBackend.rootView.hasQuick3DImport + && categoryTitle !== "Textures" && !section.isCustomCat) { + qsTr('To use %1, add the QtQuick3D module and the View3D + component in the Components view, or click + + here.') + .arg(categoryName) + .arg(StudioTheme.Values.themeInteraction) + } else if (!ContentLibraryBackend.rootView.hasMaterialLibrary + && categoryTitle !== "Textures" && !section.isCustomCat) { + qsTr("Content Library is disabled inside a non-visual component.") + } else if (categoryEmpty) { + qsTr("There are no items in this category.") + } else { + "" + } + } + textFormat: Text.RichText + color: StudioTheme.Values.themeTextColor + font.pixelSize: StudioTheme.Values.mediumFontSize + padding: 10 + visible: infoText.text !== "" + horizontalAlignment: Text.AlignHCenter + wrapMode: Text.WordWrap + width: root.width + + onLinkActivated: ContentLibraryBackend.rootView.addQtQuick3D() + + HoverHandler { + enabled: infoText.hoveredLink + cursorShape: Qt.PointingHandCursor + } } } } diff --git a/src/plugins/qmldesigner/components/contentlibrary/contentlibraryusermodel.cpp b/src/plugins/qmldesigner/components/contentlibrary/contentlibraryusermodel.cpp index 47b06781123..042d25cf709 100644 --- a/src/plugins/qmldesigner/components/contentlibrary/contentlibraryusermodel.cpp +++ b/src/plugins/qmldesigner/components/contentlibrary/contentlibraryusermodel.cpp @@ -9,6 +9,7 @@ #include "contentlibrarytexture.h" #include "contentlibrarywidget.h" +#include #include #include #include @@ -16,6 +17,7 @@ #include #include +#include #include #include @@ -28,10 +30,18 @@ namespace QmlDesigner { ContentLibraryUserModel::ContentLibraryUserModel(ContentLibraryWidget *parent) : QAbstractListModel(parent) , m_widget(parent) + , m_fileWatcher(Utils::makeUniqueObjectPtr(parent)) { createCategories(); + + connect(m_fileWatcher.get(), &Utils::FileSystemWatcher::directoryChanged, this, + [this] (const QString &dirPath) { + reloadTextureCategory(Utils::FilePath::fromString(dirPath)); + }); } +ContentLibraryUserModel::~ContentLibraryUserModel() = default; + int ContentLibraryUserModel::rowCount(const QModelIndex &) const { return m_userCategories.size(); @@ -44,17 +54,22 @@ QVariant ContentLibraryUserModel::data(const QModelIndex &index, int role) const UserCategory *currCat = m_userCategories.at(index.row()); - if (role == TitleRole) + switch (role) { + case TitleRole: return currCat->title(); - if (role == ItemsRole) + case BundlePathRole: + return currCat->bundlePath().toVariant(); + + case ItemsRole: return QVariant::fromValue(currCat->items()); - if (role == NoMatchRole) + case NoMatchRole: return currCat->noMatch(); - if (role == EmptyRole) + case EmptyRole: return currCat->isEmpty(); + } return {}; } @@ -76,6 +91,71 @@ void ContentLibraryUserModel::createCategories() compUtils.user3DBundleId()}; m_userCategories << catMaterial << catTexture << cat3D; + + loadCustomCategories(userBundlePath); +} + +void ContentLibraryUserModel::loadCustomCategories(const Utils::FilePath &userBundlePath) +{ + auto jsonFilePath = userBundlePath.pathAppended(Constants::CUSTOM_BUNDLES_JSON_FILENAME); + if (!jsonFilePath.exists()) { + const QString jsonContent = QStringLiteral(R"({ "version": "%1", "items": {}})") + .arg(CUSTOM_BUNDLES_JSON_FILE_VERSION); + Utils::expected_str res = jsonFilePath.writeFileContents(jsonContent.toLatin1()); + QTC_ASSERT_EXPECTED(res, return); + } + + Utils::expected_str jsonContents = jsonFilePath.fileContents(); + QTC_ASSERT_EXPECTED(jsonContents, return); + + QJsonDocument jsonDoc = QJsonDocument::fromJson(jsonContents.value()); + QTC_ASSERT(!jsonDoc.isNull(), return); + + m_customCatsRootObj = jsonDoc.object(); + m_customCatsObj = m_customCatsRootObj.value("items").toObject(); + + for (auto it = m_customCatsObj.constBegin(); it != m_customCatsObj.constEnd(); ++it) { + auto dirPath = Utils::FilePath::fromString(it.key()); + if (!dirPath.exists()) + continue; + + addBundleDir(dirPath); + } +} + +bool ContentLibraryUserModel::bundleDirExists(const QString &dirPath) const +{ + return m_customCatsObj.contains(dirPath); +} + +void ContentLibraryUserModel::addBundleDir(const Utils::FilePath &dirPath) +{ + QTC_ASSERT(!dirPath.isEmpty(), return); + + // TODO: detect if a bundle exists in the dir, determine its type, and create a matching category. + // For now we consider a custom folder as a texture bundle + + auto newCat = new UserTextureCategory{dirPath.fileName(), dirPath}; + newCat->loadBundle(); + + beginInsertRows({}, m_userCategories.size(), m_userCategories.size()); + m_userCategories << newCat; + endInsertRows(); + + m_fileWatcher->addDirectory(dirPath, Utils::FileSystemWatcher::WatchAllChanges); + + // add the folder to custom bundles json file if it is missing + const QString dirPathStr = dirPath.toFSPathString(); + if (!m_customCatsObj.contains(dirPathStr)) { + m_customCatsObj.insert(dirPathStr, QJsonObject{}); + + m_customCatsRootObj["items"] = m_customCatsObj; + + auto userBundlePath = Utils::FilePath::fromString(Paths::bundlesPathSetting() + "/User"); + auto jsonFilePath = userBundlePath.pathAppended(Constants::CUSTOM_BUNDLES_JSON_FILENAME); + auto result = jsonFilePath.writeFileContents(QJsonDocument(m_customCatsRootObj).toJson()); + QTC_ASSERT_EXPECTED(result,); + } } void ContentLibraryUserModel::addItem(const QString &bundleId, const QString &name, @@ -102,22 +182,36 @@ void ContentLibraryUserModel::refreshSection(const QString &bundleId) updateIsEmpty(); } -void ContentLibraryUserModel::addTextures(const Utils::FilePaths &paths) +void ContentLibraryUserModel::addTextures(const Utils::FilePaths &paths, const Utils::FilePath &bundlePath) { - auto texCat = qobject_cast(m_userCategories[TexturesSectionIdx]); + int catIdx = bundlePathToIndex(bundlePath); + UserTextureCategory *texCat = qobject_cast(m_userCategories.at(catIdx)); QTC_ASSERT(texCat, return); texCat->addItems(paths); - emit dataChanged(index(TexturesSectionIdx), index(TexturesSectionIdx), {ItemsRole, EmptyRole}); + emit dataChanged(index(catIdx), index(catIdx), {ItemsRole, EmptyRole}); updateIsEmpty(); } -void ContentLibraryUserModel::removeTextures(const QStringList &fileNames) +void ContentLibraryUserModel::reloadTextureCategory(const Utils::FilePath &dirPath) +{ + int catIdx = bundlePathToIndex(dirPath); + UserTextureCategory *texCat = qobject_cast(m_userCategories.at(catIdx)); + QTC_ASSERT(texCat, return); + + const Utils::FilePaths &paths = dirPath.dirEntries({Asset::supportedImageSuffixes(), QDir::Files}); + + texCat->clearItems(); + addTextures(paths, dirPath); +} + +void ContentLibraryUserModel::removeTextures(const QStringList &fileNames, const Utils::FilePath &bundlePath) { // note: this method doesn't refresh the model after textures removal - auto texCat = qobject_cast(m_userCategories[TexturesSectionIdx]); + int catIdx = bundlePathToIndex(bundlePath); + UserTextureCategory *texCat = qobject_cast(m_userCategories.at(catIdx)); QTC_ASSERT(texCat, return); const QObjectList items = texCat->items(); @@ -137,13 +231,28 @@ void ContentLibraryUserModel::removeTexture(ContentLibraryTexture *tex, bool ref Utils::FilePath::fromString(tex->iconPath()).removeFile(); // remove from model - m_userCategories[TexturesSectionIdx]->removeItem(tex); + UserTextureCategory *itemCat = qobject_cast(tex->parent()); + QTC_ASSERT(itemCat, return); + itemCat->removeItem(tex); // update model if (refresh) { - emit dataChanged(index(TexturesSectionIdx), index(TexturesSectionIdx)); + int catIdx = bundlePathToIndex(itemCat->bundlePath()); + emit dataChanged(index(catIdx), index(catIdx)); updateIsEmpty(); } + + const QString bundlePathStr = itemCat->bundlePath().toFSPathString(); + if (m_customCatsObj.contains(bundlePathStr)) { + m_customCatsObj.remove(bundlePathStr); + + m_customCatsRootObj["items"] = m_customCatsObj; + + auto userBundlePath = Utils::FilePath::fromString(Paths::bundlesPathSetting() + "/User"); + auto jsonFilePath = userBundlePath.pathAppended(Constants::CUSTOM_BUNDLES_JSON_FILENAME); + auto result = jsonFilePath.writeFileContents(QJsonDocument(m_customCatsRootObj).toJson()); + QTC_ASSERT_EXPECTED(result,); + } } void ContentLibraryUserModel::removeFromContentLib(QObject *item) @@ -154,6 +263,30 @@ void ContentLibraryUserModel::removeFromContentLib(QObject *item) removeItem(castedItem); } +void ContentLibraryUserModel::removeBundleDir(int catIdx) +{ + auto texCat = qobject_cast(m_userCategories.at(catIdx)); + QTC_ASSERT(texCat, return); + + QString dirPath = texCat->bundlePath().toFSPathString(); + + // remove from json + QTC_ASSERT(m_customCatsObj.contains(dirPath), return); + m_customCatsObj.remove(dirPath); + m_customCatsRootObj["items"] = m_customCatsObj; + + auto userBundlePath = Utils::FilePath::fromString(Paths::bundlesPathSetting() + "/User"); + auto jsonFilePath = userBundlePath.pathAppended(Constants::CUSTOM_BUNDLES_JSON_FILENAME); + auto result = jsonFilePath.writeFileContents(QJsonDocument(m_customCatsRootObj).toJson()); + QTC_ASSERT_EXPECTED(result, return); + + // remove from model + beginRemoveRows({}, catIdx, catIdx); + delete texCat; + m_userCategories.removeAt(catIdx); + endRemoveRows(); +} + void ContentLibraryUserModel::removeItemByName(const QString &qmlFileName, const QString &bundleId) { ContentLibraryItem *itemToRemove = nullptr; @@ -239,10 +372,27 @@ ContentLibraryUserModel::SectionIndex ContentLibraryUserModel::bundleIdToSection return {}; } +int ContentLibraryUserModel::bundlePathToIndex(const QString &bundlePath) const +{ + return bundlePathToIndex(Utils::FilePath::fromString(bundlePath)); +} + +int ContentLibraryUserModel::bundlePathToIndex(const Utils::FilePath &bundlePath) const +{ + for (int i = 0; i < m_userCategories.size(); ++i) { + if (m_userCategories.at(i)->bundlePath() == bundlePath) + return i; + } + + qWarning() << __FUNCTION__ << "Invalid bundlePath:" << bundlePath; + return -1; +} + QHash ContentLibraryUserModel::roleNames() const { static const QHash roles { {TitleRole, "categoryTitle"}, + {BundlePathRole, "categoryBundlePath"}, {EmptyRole, "categoryEmpty"}, {ItemsRole, "categoryItems"}, {NoMatchRole, "categoryNoMatch"} diff --git a/src/plugins/qmldesigner/components/contentlibrary/contentlibraryusermodel.h b/src/plugins/qmldesigner/components/contentlibrary/contentlibraryusermodel.h index 172bad093a3..72b849aeba3 100644 --- a/src/plugins/qmldesigner/components/contentlibrary/contentlibraryusermodel.h +++ b/src/plugins/qmldesigner/components/contentlibrary/contentlibraryusermodel.h @@ -6,10 +6,15 @@ #include "usercategory.h" #include +#include #include #include +namespace Utils { +class FileSystemWatcher; +} + QT_FORWARD_DECLARE_CLASS(QUrl) namespace QmlDesigner { @@ -28,6 +33,7 @@ class ContentLibraryUserModel : public QAbstractListModel public: ContentLibraryUserModel(ContentLibraryWidget *parent = nullptr); + ~ContentLibraryUserModel(); int rowCount(const QModelIndex &parent = QModelIndex()) const override; QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; @@ -50,8 +56,9 @@ public: void addItem(const QString &bundleId, const QString &name, const QString &qml,const QUrl &icon, const QStringList &files); void refreshSection(const QString &bundleId); - void addTextures(const Utils::FilePaths &paths); - void removeTextures(const QStringList &fileNames); + void addTextures(const Utils::FilePaths &paths, const Utils::FilePath &bundlePath); + void reloadTextureCategory(const Utils::FilePath &dirPath); + void removeTextures(const QStringList &fileNames, const Utils::FilePath &bundlePath); void removeItemByName(const QString &qmlFileName, const QString &bundleId); @@ -59,12 +66,15 @@ public: QJsonObject &bundleObjectRef(const QString &bundleId); void loadBundles(bool force = false); + void addBundleDir(const Utils::FilePath &dirPath); + bool bundleDirExists(const QString &dirPath) const; Q_INVOKABLE void applyToSelected(QmlDesigner::ContentLibraryItem *mat, bool add = false); Q_INVOKABLE void addToProject(ContentLibraryItem *item); Q_INVOKABLE void removeFromProject(QObject *item); Q_INVOKABLE void removeTexture(QmlDesigner::ContentLibraryTexture *tex, bool refresh = true); Q_INVOKABLE void removeFromContentLib(QObject *item); + Q_INVOKABLE void removeBundleDir(int catIdx); signals: void hasRequiredQuick3DImportChanged(); @@ -79,14 +89,20 @@ private: EffectsSectionIdx }; void createCategories(); + void loadCustomCategories(const Utils::FilePath &userBundlePath); void loadMaterialBundle(); void load3DBundle(); void loadTextureBundle(); void removeItem(ContentLibraryItem *item); SectionIndex bundleIdToSectionIndex(const QString &bundleId) const; + int bundlePathToIndex(const QString &bundlePath) const; + int bundlePathToIndex(const Utils::FilePath &bundlePath) const; ContentLibraryWidget *m_widget = nullptr; + QJsonObject m_customCatsRootObj; + QJsonObject m_customCatsObj; QString m_searchText; + Utils::UniqueObjectPtr m_fileWatcher; QList m_userCategories; @@ -95,7 +111,9 @@ private: int m_quick3dMajorVersion = -1; int m_quick3dMinorVersion = -1; - enum Roles { TitleRole = Qt::UserRole + 1, ItemsRole, EmptyRole, NoMatchRole }; + enum Roles { TitleRole = Qt::UserRole + 1, BundlePathRole, ItemsRole, EmptyRole, NoMatchRole }; + + static constexpr char CUSTOM_BUNDLES_JSON_FILE_VERSION[] = "1.0"; }; } // namespace QmlDesigner diff --git a/src/plugins/qmldesigner/components/contentlibrary/contentlibraryview.cpp b/src/plugins/qmldesigner/components/contentlibrary/contentlibraryview.cpp index d907e616524..2a181dfbc54 100644 --- a/src/plugins/qmldesigner/components/contentlibrary/contentlibraryview.cpp +++ b/src/plugins/qmldesigner/components/contentlibrary/contentlibraryview.cpp @@ -91,7 +91,7 @@ WidgetInfo ContentLibraryView::widgetInfo() }); connect(m_widget, &ContentLibraryWidget::acceptTextureDrop, this, - [this](const QString &internalId) { + [this](const QString &internalId, const QString &bundlePath) { ModelNode texNode = QmlDesignerPlugin::instance()->viewManager() .view()->modelNodeForInternalId(internalId.toInt()); auto [qmlString, depAssets] = m_bundleHelper->modelNodeToQmlString(texNode); @@ -104,11 +104,11 @@ WidgetInfo ContentLibraryView::widgetInfo() paths.append(path); } - addLibAssets(paths); + addLibAssets(paths, bundlePath); }); connect(m_widget, &ContentLibraryWidget::acceptTexturesDrop, this, - [this](const QList &urls) { + [this](const QList &urls, const QString &bundlePath) { QStringList paths; for (const QUrl &url : urls) { @@ -117,7 +117,7 @@ WidgetInfo ContentLibraryView::widgetInfo() if (Asset(path).isValidTextureSource()) paths.append(path); } - addLibAssets(paths); + addLibAssets(paths, bundlePath); }); connect(m_widget, &ContentLibraryWidget::acceptMaterialDrop, this, @@ -569,14 +569,16 @@ void ContentLibraryView::applyBundleMaterialToDropTarget(const ModelNode &bundle } #endif -void ContentLibraryView::addLibAssets(const QStringList &paths) +void ContentLibraryView::addLibAssets(const QStringList &paths, const QString &bundlePath) { - auto bundlePath = Utils::FilePath::fromString(Paths::bundlesPathSetting() + "/User/textures"); + auto fullBundlePath = Utils::FilePath::fromString(bundlePath.isEmpty() + ? Paths::bundlesPathSetting() + "/User/textures" + : bundlePath); Utils::FilePaths sourcePathsToAdd; Utils::FilePaths targetPathsToAdd; QStringList fileNamesToRemove; - const QStringList existingAssetsFileNames = Utils::transform(bundlePath.dirEntries(QDir::Files), + const QStringList existingAssetsFileNames = Utils::transform(fullBundlePath.dirEntries(QDir::Files), &Utils::FilePath::fileName); for (const QString &path : paths) { @@ -598,21 +600,13 @@ void ContentLibraryView::addLibAssets(const QStringList &paths) } // remove the to-be-overwritten resources from target bundle path - m_widget->userModel()->removeTextures(fileNamesToRemove); + m_widget->userModel()->removeTextures(fileNamesToRemove, fullBundlePath); // copy resources to target bundle path for (const Utils::FilePath &sourcePath : sourcePathsToAdd) { - Utils::FilePath targetPath = bundlePath.pathAppended(sourcePath.fileName()); + Utils::FilePath targetPath = fullBundlePath.pathAppended(sourcePath.fileName()); Asset asset{sourcePath.toFSPathString()}; - // save icon - QString iconSavePath = bundlePath.pathAppended("icons/" + sourcePath.baseName() + ".png") - .toFSPathString(); - QPixmap icon = asset.pixmap({120, 120}); - bool iconSaved = icon.save(iconSavePath); - if (!iconSaved) - qWarning() << __FUNCTION__ << "icon save failed"; - // save asset auto result = sourcePath.copyFile(targetPath); QTC_ASSERT_EXPECTED(result,); @@ -620,7 +614,7 @@ void ContentLibraryView::addLibAssets(const QStringList &paths) targetPathsToAdd.append(targetPath); } - m_widget->userModel()->addTextures(targetPathsToAdd); + m_widget->userModel()->addTextures(targetPathsToAdd, fullBundlePath); } // TODO: combine this method with BundleHelper::exportComponent() diff --git a/src/plugins/qmldesigner/components/contentlibrary/contentlibraryview.h b/src/plugins/qmldesigner/components/contentlibrary/contentlibraryview.h index e8cc641c32c..0872d51a1bd 100644 --- a/src/plugins/qmldesigner/components/contentlibrary/contentlibraryview.h +++ b/src/plugins/qmldesigner/components/contentlibrary/contentlibraryview.h @@ -63,7 +63,7 @@ private: bool isItemBundle(const QString &bundleId) const; void active3DSceneChanged(qint32 sceneId); void updateBundlesQuick3DVersion(); - void addLibAssets(const QStringList &paths); + void addLibAssets(const QStringList &paths, const QString &bundlePath = {}); void addLib3DComponent(const ModelNode &node); void addLibItem(const ModelNode &node, const QPixmap &iconPixmap = {}); void importBundleToContentLib(); diff --git a/src/plugins/qmldesigner/components/contentlibrary/contentlibrarywidget.cpp b/src/plugins/qmldesigner/components/contentlibrary/contentlibrarywidget.cpp index 588d1851707..9b3860cebf1 100644 --- a/src/plugins/qmldesigner/components/contentlibrary/contentlibrarywidget.cpp +++ b/src/plugins/qmldesigner/components/contentlibrary/contentlibrarywidget.cpp @@ -190,6 +190,21 @@ ContentLibraryWidget::~ContentLibraryWidget() { } +void ContentLibraryWidget::browseBundleFolder() +{ + DesignDocument *document = QmlDesignerPlugin::instance()->currentDesignDocument(); + QTC_ASSERT(document, return); + const QString currentDir = document->fileName().parentDir().toUrlishString(); + + QString dir = QFileDialog::getExistingDirectory(Core::ICore::dialogParent(), + tr("Choose Directory"), + currentDir, + QFileDialog::ShowDirsOnly | QFileDialog::DontResolveSymlinks); + + if (!dir.isEmpty() && !m_userModel->bundleDirExists(dir)) + m_userModel->addBundleDir(Utils::FilePath::fromString(dir)); +} + void ContentLibraryWidget::createImporter() { m_importer = new BundleImporter(); diff --git a/src/plugins/qmldesigner/components/contentlibrary/contentlibrarywidget.h b/src/plugins/qmldesigner/components/contentlibrary/contentlibrarywidget.h index 76a4c24b8b8..ee7cb3509d2 100644 --- a/src/plugins/qmldesigner/components/contentlibrary/contentlibrarywidget.h +++ b/src/plugins/qmldesigner/components/contentlibrary/contentlibrarywidget.h @@ -104,6 +104,7 @@ public: Q_INVOKABLE bool has3DNode(const QByteArray &data) const; Q_INVOKABLE bool hasTexture(const QString &format, const QVariant &data) const; Q_INVOKABLE void addQtQuick3D(); + Q_INVOKABLE void browseBundleFolder(); QSize sizeHint() const override; @@ -127,8 +128,8 @@ signals: void hasModelSelectionChanged(); void importBundle(); void requestTab(int tabIndex); - void acceptTexturesDrop(const QList &urls); - void acceptTextureDrop(const QString &internalId); + void acceptTexturesDrop(const QList &urls, const QString &bundlePath = {}); + void acceptTextureDrop(const QString &internalId, const QString &bundlePath = {}); void acceptMaterialDrop(const QString &internalId); void accept3DDrop(const QByteArray &internalIds); void importQtQuick3D(); diff --git a/src/plugins/qmldesigner/components/contentlibrary/usertexturecategory.cpp b/src/plugins/qmldesigner/components/contentlibrary/usertexturecategory.cpp index 22b9cf20aa3..ec603068505 100644 --- a/src/plugins/qmldesigner/components/contentlibrary/usertexturecategory.cpp +++ b/src/plugins/qmldesigner/components/contentlibrary/usertexturecategory.cpp @@ -5,7 +5,7 @@ #include "contentlibrarytexture.h" -#include +#include #include namespace QmlDesigner { @@ -27,7 +27,7 @@ void UserTextureCategory::loadBundle(bool force) m_bundlePath.ensureWritableDir(); m_bundlePath.pathAppended("icons").ensureWritableDir(); - addItems(m_bundlePath.dirEntries(QDir::Files)); + addItems(m_bundlePath.dirEntries({Asset::supportedImageSuffixes(), QDir::Files})); m_bundleLoaded = true; } @@ -55,6 +55,14 @@ void UserTextureCategory::addItems(const Utils::FilePaths &paths) QSize imgDims = info.first; qint64 imgFileSize = info.second; + if (!iconFileInfo.exists()) { // generate an icon if one doesn't exist + Asset asset{filePath.toFSPathString()}; + QPixmap icon = asset.pixmap({120, 120}); + bool iconSaved = icon.save(iconFileInfo.filePath()); + if (!iconSaved) + qWarning() << __FUNCTION__ << "icon save failed"; + } + auto tex = new ContentLibraryTexture(this, iconFileInfo, dirPath, suffix, imgDims, imgFileSize); m_items.append(tex); } @@ -63,4 +71,13 @@ void UserTextureCategory::addItems(const Utils::FilePaths &paths) emit itemsChanged(); } +void UserTextureCategory::clearItems() +{ + qDeleteAll(m_items); + m_items.clear(); + + setIsEmpty(true); + emit itemsChanged(); +} + } // namespace QmlDesigner diff --git a/src/plugins/qmldesigner/components/contentlibrary/usertexturecategory.h b/src/plugins/qmldesigner/components/contentlibrary/usertexturecategory.h index 511e2ea3f83..8687f322fd3 100644 --- a/src/plugins/qmldesigner/components/contentlibrary/usertexturecategory.h +++ b/src/plugins/qmldesigner/components/contentlibrary/usertexturecategory.h @@ -16,10 +16,11 @@ class UserTextureCategory : public UserCategory public: UserTextureCategory(const QString &title, const Utils::FilePath &bundlePath); - void loadBundle(bool force) override; + void loadBundle(bool force = false) override; void filter(const QString &searchText) override; void addItems(const Utils::FilePaths &paths); + void clearItems(); }; } // namespace QmlDesigner diff --git a/src/plugins/qmldesigner/qmldesignerconstants.h b/src/plugins/qmldesigner/qmldesignerconstants.h index 58fc32e9932..7bde8178caa 100644 --- a/src/plugins/qmldesigner/qmldesignerconstants.h +++ b/src/plugins/qmldesigner/qmldesignerconstants.h @@ -59,6 +59,7 @@ inline constexpr char EDIT3D_SNAP_CONFIG[] = "QmlDesigner.Editor3D.SnapConfig"; inline constexpr char EDIT3D_CAMERA_SPEED_CONFIG[] = "QmlDesigner.Editor3D.CameraSpeedConfig"; inline constexpr char BUNDLE_JSON_FILENAME[] = "bundle.json"; +inline constexpr char CUSTOM_BUNDLES_JSON_FILENAME[] = "custom_bundles.json"; inline constexpr char BUNDLE_SUFFIX[] = "qdsbundle"; inline constexpr char COMPONENT_BUNDLES_EFFECT_BUNDLE_TYPE[] = "Effects"; inline constexpr char COMPONENT_BUNDLES_ASSET_REF_FILE[] = "_asset_ref.json";