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";