QmlDesigner: Allow adding a folder to content library

Change-Id: If44fdc0f0a7c59011854fd358f0542ce35ac1079
Reviewed-by: Miikka Heikkinen <miikka.heikkinen@qt.io>
Reviewed-by: Ali Kianian <ali.kianian@qt.io>
This commit is contained in:
Mahmoud Badri
2025-03-24 17:01:24 +02:00
parent 88b8d5ea6c
commit ee1a725da1
10 changed files with 430 additions and 189 deletions

View File

@@ -2,6 +2,7 @@
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
import QtQuick import QtQuick
import QtQuick.Layouts
import Qt.labs.qmlmodels import Qt.labs.qmlmodels
import HelperWidgets as HelperWidgets import HelperWidgets as HelperWidgets
import StudioControls as StudioControls import StudioControls as StudioControls
@@ -79,171 +80,214 @@ Item {
} }
} }
HelperWidgets.ScrollView { ColumnLayout {
id: scrollView id: col
anchors.fill: parent anchors.fill: parent
spacing: 0
clip: true Rectangle {
interactive: !ctxMenuItem.opened && !ctxMenuTexture.opened id: toolbar
&& !ContentLibraryBackend.rootView.isDragging && !HelperWidgets.Controller.contextMenuOpened
hideHorizontalScrollBar: true
Column { width: parent.width
Repeater { height: StudioTheme.Values.toolbarHeight
id: categoryRepeater 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 { HelperWidgets.ScrollView {
id: section id: scrollView
width: root.width Layout.fillWidth: true
leftPadding: StudioTheme.Values.sectionPadding Layout.fillHeight: true
rightPadding: StudioTheme.Values.sectionPadding
topPadding: StudioTheme.Values.sectionPadding
bottomPadding: StudioTheme.Values.sectionPadding
caption: categoryTitle clip: true
dropEnabled: true interactive: !ctxMenuItem.opened && !ctxMenuTexture.opened
category: "ContentLib_User" && !ContentLibraryBackend.rootView.isDragging && !HelperWidgets.Controller.contextMenuOpened
hideHorizontalScrollBar: true
function expandSection() { Column {
section.expanded = true 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("<b>Content Library</b> is not supported in Qt5 projects.")
} else if (!ContentLibraryBackend.rootView.hasQuick3DImport && categoryTitle !== "Textures") {
qsTr('To use %1, add the <b>QtQuick3D</b> module and the <b>View3D</b>
component in the <b>Components</b> view, or click
<a href=\"#add_import\"><span style=\"text-decoration:none;color:%2\">
here</span></a>.')
.arg(categoryName)
.arg(StudioTheme.Values.themeInteraction)
} else if (!ContentLibraryBackend.rootView.hasMaterialLibrary && categoryTitle !== "Textures") {
qsTr("<b>Content Library</b> is disabled inside a non-visual component.")
} else if (categoryEmpty) {
qsTr("There are no "+ categoryName + " in the <b>User Assets</b>.")
} 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 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 { onCloseButtonClicked: {
enabled: infoText.hoveredLink ContentLibraryBackend.userModel.removeBundleDir(index)
cursorShape: Qt.PointingHandCursor }
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("<b>Content Library</b> is not supported in Qt5 projects.")
} else if (!ContentLibraryBackend.rootView.hasQuick3DImport
&& categoryTitle !== "Textures" && !section.isCustomCat) {
qsTr('To use %1, add the <b>QtQuick3D</b> module and the <b>View3D</b>
component in the <b>Components</b> view, or click
<a href=\"#add_import\"><span style=\"text-decoration:none;color:%2\">
here</span></a>.')
.arg(categoryName)
.arg(StudioTheme.Values.themeInteraction)
} else if (!ContentLibraryBackend.rootView.hasMaterialLibrary
&& categoryTitle !== "Textures" && !section.isCustomCat) {
qsTr("<b>Content Library</b> 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
}
} }
} }
} }

View File

@@ -9,6 +9,7 @@
#include "contentlibrarytexture.h" #include "contentlibrarytexture.h"
#include "contentlibrarywidget.h" #include "contentlibrarywidget.h"
#include <asset.h>
#include <bundleimporter.h> #include <bundleimporter.h>
#include <designerpaths.h> #include <designerpaths.h>
#include <imageutils.h> #include <imageutils.h>
@@ -16,6 +17,7 @@
#include <qmldesignerplugin.h> #include <qmldesignerplugin.h>
#include <utils/algorithm.h> #include <utils/algorithm.h>
#include <utils/filesystemwatcher.h>
#include <utils/qtcassert.h> #include <utils/qtcassert.h>
#include <QFileInfo> #include <QFileInfo>
@@ -28,10 +30,18 @@ namespace QmlDesigner {
ContentLibraryUserModel::ContentLibraryUserModel(ContentLibraryWidget *parent) ContentLibraryUserModel::ContentLibraryUserModel(ContentLibraryWidget *parent)
: QAbstractListModel(parent) : QAbstractListModel(parent)
, m_widget(parent) , m_widget(parent)
, m_fileWatcher(Utils::makeUniqueObjectPtr<Utils::FileSystemWatcher>(parent))
{ {
createCategories(); 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 int ContentLibraryUserModel::rowCount(const QModelIndex &) const
{ {
return m_userCategories.size(); return m_userCategories.size();
@@ -44,17 +54,22 @@ QVariant ContentLibraryUserModel::data(const QModelIndex &index, int role) const
UserCategory *currCat = m_userCategories.at(index.row()); UserCategory *currCat = m_userCategories.at(index.row());
if (role == TitleRole) switch (role) {
case TitleRole:
return currCat->title(); return currCat->title();
if (role == ItemsRole) case BundlePathRole:
return currCat->bundlePath().toVariant();
case ItemsRole:
return QVariant::fromValue(currCat->items()); return QVariant::fromValue(currCat->items());
if (role == NoMatchRole) case NoMatchRole:
return currCat->noMatch(); return currCat->noMatch();
if (role == EmptyRole) case EmptyRole:
return currCat->isEmpty(); return currCat->isEmpty();
}
return {}; return {};
} }
@@ -76,6 +91,71 @@ void ContentLibraryUserModel::createCategories()
compUtils.user3DBundleId()}; compUtils.user3DBundleId()};
m_userCategories << catMaterial << catTexture << cat3D; 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<qint64> res = jsonFilePath.writeFileContents(jsonContent.toLatin1());
QTC_ASSERT_EXPECTED(res, return);
}
Utils::expected_str<QByteArray> 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, void ContentLibraryUserModel::addItem(const QString &bundleId, const QString &name,
@@ -102,22 +182,36 @@ void ContentLibraryUserModel::refreshSection(const QString &bundleId)
updateIsEmpty(); updateIsEmpty();
} }
void ContentLibraryUserModel::addTextures(const Utils::FilePaths &paths) void ContentLibraryUserModel::addTextures(const Utils::FilePaths &paths, const Utils::FilePath &bundlePath)
{ {
auto texCat = qobject_cast<UserTextureCategory *>(m_userCategories[TexturesSectionIdx]); int catIdx = bundlePathToIndex(bundlePath);
UserTextureCategory *texCat = qobject_cast<UserTextureCategory *>(m_userCategories.at(catIdx));
QTC_ASSERT(texCat, return); QTC_ASSERT(texCat, return);
texCat->addItems(paths); texCat->addItems(paths);
emit dataChanged(index(TexturesSectionIdx), index(TexturesSectionIdx), {ItemsRole, EmptyRole}); emit dataChanged(index(catIdx), index(catIdx), {ItemsRole, EmptyRole});
updateIsEmpty(); updateIsEmpty();
} }
void ContentLibraryUserModel::removeTextures(const QStringList &fileNames) void ContentLibraryUserModel::reloadTextureCategory(const Utils::FilePath &dirPath)
{
int catIdx = bundlePathToIndex(dirPath);
UserTextureCategory *texCat = qobject_cast<UserTextureCategory *>(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 // note: this method doesn't refresh the model after textures removal
auto texCat = qobject_cast<UserTextureCategory *>(m_userCategories[TexturesSectionIdx]); int catIdx = bundlePathToIndex(bundlePath);
UserTextureCategory *texCat = qobject_cast<UserTextureCategory *>(m_userCategories.at(catIdx));
QTC_ASSERT(texCat, return); QTC_ASSERT(texCat, return);
const QObjectList items = texCat->items(); const QObjectList items = texCat->items();
@@ -137,13 +231,28 @@ void ContentLibraryUserModel::removeTexture(ContentLibraryTexture *tex, bool ref
Utils::FilePath::fromString(tex->iconPath()).removeFile(); Utils::FilePath::fromString(tex->iconPath()).removeFile();
// remove from model // remove from model
m_userCategories[TexturesSectionIdx]->removeItem(tex); UserTextureCategory *itemCat = qobject_cast<UserTextureCategory *>(tex->parent());
QTC_ASSERT(itemCat, return);
itemCat->removeItem(tex);
// update model // update model
if (refresh) { if (refresh) {
emit dataChanged(index(TexturesSectionIdx), index(TexturesSectionIdx)); int catIdx = bundlePathToIndex(itemCat->bundlePath());
emit dataChanged(index(catIdx), index(catIdx));
updateIsEmpty(); 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) void ContentLibraryUserModel::removeFromContentLib(QObject *item)
@@ -154,6 +263,30 @@ void ContentLibraryUserModel::removeFromContentLib(QObject *item)
removeItem(castedItem); removeItem(castedItem);
} }
void ContentLibraryUserModel::removeBundleDir(int catIdx)
{
auto texCat = qobject_cast<UserTextureCategory *>(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) void ContentLibraryUserModel::removeItemByName(const QString &qmlFileName, const QString &bundleId)
{ {
ContentLibraryItem *itemToRemove = nullptr; ContentLibraryItem *itemToRemove = nullptr;
@@ -239,10 +372,27 @@ ContentLibraryUserModel::SectionIndex ContentLibraryUserModel::bundleIdToSection
return {}; 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<int, QByteArray> ContentLibraryUserModel::roleNames() const QHash<int, QByteArray> ContentLibraryUserModel::roleNames() const
{ {
static const QHash<int, QByteArray> roles { static const QHash<int, QByteArray> roles {
{TitleRole, "categoryTitle"}, {TitleRole, "categoryTitle"},
{BundlePathRole, "categoryBundlePath"},
{EmptyRole, "categoryEmpty"}, {EmptyRole, "categoryEmpty"},
{ItemsRole, "categoryItems"}, {ItemsRole, "categoryItems"},
{NoMatchRole, "categoryNoMatch"} {NoMatchRole, "categoryNoMatch"}

View File

@@ -6,10 +6,15 @@
#include "usercategory.h" #include "usercategory.h"
#include <utils/filepath.h> #include <utils/filepath.h>
#include <utils/uniqueobjectptr.h>
#include <QAbstractListModel> #include <QAbstractListModel>
#include <QJsonObject> #include <QJsonObject>
namespace Utils {
class FileSystemWatcher;
}
QT_FORWARD_DECLARE_CLASS(QUrl) QT_FORWARD_DECLARE_CLASS(QUrl)
namespace QmlDesigner { namespace QmlDesigner {
@@ -28,6 +33,7 @@ class ContentLibraryUserModel : public QAbstractListModel
public: public:
ContentLibraryUserModel(ContentLibraryWidget *parent = nullptr); ContentLibraryUserModel(ContentLibraryWidget *parent = nullptr);
~ContentLibraryUserModel();
int rowCount(const QModelIndex &parent = QModelIndex()) const override; int rowCount(const QModelIndex &parent = QModelIndex()) const override;
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) 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, void addItem(const QString &bundleId, const QString &name, const QString &qml,const QUrl &icon,
const QStringList &files); const QStringList &files);
void refreshSection(const QString &bundleId); void refreshSection(const QString &bundleId);
void addTextures(const Utils::FilePaths &paths); void addTextures(const Utils::FilePaths &paths, const Utils::FilePath &bundlePath);
void removeTextures(const QStringList &fileNames); void reloadTextureCategory(const Utils::FilePath &dirPath);
void removeTextures(const QStringList &fileNames, const Utils::FilePath &bundlePath);
void removeItemByName(const QString &qmlFileName, const QString &bundleId); void removeItemByName(const QString &qmlFileName, const QString &bundleId);
@@ -59,12 +66,15 @@ public:
QJsonObject &bundleObjectRef(const QString &bundleId); QJsonObject &bundleObjectRef(const QString &bundleId);
void loadBundles(bool force = false); 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 applyToSelected(QmlDesigner::ContentLibraryItem *mat, bool add = false);
Q_INVOKABLE void addToProject(ContentLibraryItem *item); Q_INVOKABLE void addToProject(ContentLibraryItem *item);
Q_INVOKABLE void removeFromProject(QObject *item); Q_INVOKABLE void removeFromProject(QObject *item);
Q_INVOKABLE void removeTexture(QmlDesigner::ContentLibraryTexture *tex, bool refresh = true); Q_INVOKABLE void removeTexture(QmlDesigner::ContentLibraryTexture *tex, bool refresh = true);
Q_INVOKABLE void removeFromContentLib(QObject *item); Q_INVOKABLE void removeFromContentLib(QObject *item);
Q_INVOKABLE void removeBundleDir(int catIdx);
signals: signals:
void hasRequiredQuick3DImportChanged(); void hasRequiredQuick3DImportChanged();
@@ -79,14 +89,20 @@ private:
EffectsSectionIdx }; EffectsSectionIdx };
void createCategories(); void createCategories();
void loadCustomCategories(const Utils::FilePath &userBundlePath);
void loadMaterialBundle(); void loadMaterialBundle();
void load3DBundle(); void load3DBundle();
void loadTextureBundle(); void loadTextureBundle();
void removeItem(ContentLibraryItem *item); void removeItem(ContentLibraryItem *item);
SectionIndex bundleIdToSectionIndex(const QString &bundleId) const; SectionIndex bundleIdToSectionIndex(const QString &bundleId) const;
int bundlePathToIndex(const QString &bundlePath) const;
int bundlePathToIndex(const Utils::FilePath &bundlePath) const;
ContentLibraryWidget *m_widget = nullptr; ContentLibraryWidget *m_widget = nullptr;
QJsonObject m_customCatsRootObj;
QJsonObject m_customCatsObj;
QString m_searchText; QString m_searchText;
Utils::UniqueObjectPtr<Utils::FileSystemWatcher> m_fileWatcher;
QList<UserCategory *> m_userCategories; QList<UserCategory *> m_userCategories;
@@ -95,7 +111,9 @@ private:
int m_quick3dMajorVersion = -1; int m_quick3dMajorVersion = -1;
int m_quick3dMinorVersion = -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 } // namespace QmlDesigner

View File

@@ -91,7 +91,7 @@ WidgetInfo ContentLibraryView::widgetInfo()
}); });
connect(m_widget, &ContentLibraryWidget::acceptTextureDrop, this, connect(m_widget, &ContentLibraryWidget::acceptTextureDrop, this,
[this](const QString &internalId) { [this](const QString &internalId, const QString &bundlePath) {
ModelNode texNode = QmlDesignerPlugin::instance()->viewManager() ModelNode texNode = QmlDesignerPlugin::instance()->viewManager()
.view()->modelNodeForInternalId(internalId.toInt()); .view()->modelNodeForInternalId(internalId.toInt());
auto [qmlString, depAssets] = m_bundleHelper->modelNodeToQmlString(texNode); auto [qmlString, depAssets] = m_bundleHelper->modelNodeToQmlString(texNode);
@@ -104,11 +104,11 @@ WidgetInfo ContentLibraryView::widgetInfo()
paths.append(path); paths.append(path);
} }
addLibAssets(paths); addLibAssets(paths, bundlePath);
}); });
connect(m_widget, &ContentLibraryWidget::acceptTexturesDrop, this, connect(m_widget, &ContentLibraryWidget::acceptTexturesDrop, this,
[this](const QList<QUrl> &urls) { [this](const QList<QUrl> &urls, const QString &bundlePath) {
QStringList paths; QStringList paths;
for (const QUrl &url : urls) { for (const QUrl &url : urls) {
@@ -117,7 +117,7 @@ WidgetInfo ContentLibraryView::widgetInfo()
if (Asset(path).isValidTextureSource()) if (Asset(path).isValidTextureSource())
paths.append(path); paths.append(path);
} }
addLibAssets(paths); addLibAssets(paths, bundlePath);
}); });
connect(m_widget, &ContentLibraryWidget::acceptMaterialDrop, this, connect(m_widget, &ContentLibraryWidget::acceptMaterialDrop, this,
@@ -569,14 +569,16 @@ void ContentLibraryView::applyBundleMaterialToDropTarget(const ModelNode &bundle
} }
#endif #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 sourcePathsToAdd;
Utils::FilePaths targetPathsToAdd; Utils::FilePaths targetPathsToAdd;
QStringList fileNamesToRemove; QStringList fileNamesToRemove;
const QStringList existingAssetsFileNames = Utils::transform(bundlePath.dirEntries(QDir::Files), const QStringList existingAssetsFileNames = Utils::transform(fullBundlePath.dirEntries(QDir::Files),
&Utils::FilePath::fileName); &Utils::FilePath::fileName);
for (const QString &path : paths) { 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 // 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 // copy resources to target bundle path
for (const Utils::FilePath &sourcePath : sourcePathsToAdd) { for (const Utils::FilePath &sourcePath : sourcePathsToAdd) {
Utils::FilePath targetPath = bundlePath.pathAppended(sourcePath.fileName()); Utils::FilePath targetPath = fullBundlePath.pathAppended(sourcePath.fileName());
Asset asset{sourcePath.toFSPathString()}; 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 // save asset
auto result = sourcePath.copyFile(targetPath); auto result = sourcePath.copyFile(targetPath);
QTC_ASSERT_EXPECTED(result,); QTC_ASSERT_EXPECTED(result,);
@@ -620,7 +614,7 @@ void ContentLibraryView::addLibAssets(const QStringList &paths)
targetPathsToAdd.append(targetPath); targetPathsToAdd.append(targetPath);
} }
m_widget->userModel()->addTextures(targetPathsToAdd); m_widget->userModel()->addTextures(targetPathsToAdd, fullBundlePath);
} }
// TODO: combine this method with BundleHelper::exportComponent() // TODO: combine this method with BundleHelper::exportComponent()

View File

@@ -63,7 +63,7 @@ private:
bool isItemBundle(const QString &bundleId) const; bool isItemBundle(const QString &bundleId) const;
void active3DSceneChanged(qint32 sceneId); void active3DSceneChanged(qint32 sceneId);
void updateBundlesQuick3DVersion(); void updateBundlesQuick3DVersion();
void addLibAssets(const QStringList &paths); void addLibAssets(const QStringList &paths, const QString &bundlePath = {});
void addLib3DComponent(const ModelNode &node); void addLib3DComponent(const ModelNode &node);
void addLibItem(const ModelNode &node, const QPixmap &iconPixmap = {}); void addLibItem(const ModelNode &node, const QPixmap &iconPixmap = {});
void importBundleToContentLib(); void importBundleToContentLib();

View File

@@ -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() void ContentLibraryWidget::createImporter()
{ {
m_importer = new BundleImporter(); m_importer = new BundleImporter();

View File

@@ -104,6 +104,7 @@ public:
Q_INVOKABLE bool has3DNode(const QByteArray &data) const; Q_INVOKABLE bool has3DNode(const QByteArray &data) const;
Q_INVOKABLE bool hasTexture(const QString &format, const QVariant &data) const; Q_INVOKABLE bool hasTexture(const QString &format, const QVariant &data) const;
Q_INVOKABLE void addQtQuick3D(); Q_INVOKABLE void addQtQuick3D();
Q_INVOKABLE void browseBundleFolder();
QSize sizeHint() const override; QSize sizeHint() const override;
@@ -127,8 +128,8 @@ signals:
void hasModelSelectionChanged(); void hasModelSelectionChanged();
void importBundle(); void importBundle();
void requestTab(int tabIndex); void requestTab(int tabIndex);
void acceptTexturesDrop(const QList<QUrl> &urls); void acceptTexturesDrop(const QList<QUrl> &urls, const QString &bundlePath = {});
void acceptTextureDrop(const QString &internalId); void acceptTextureDrop(const QString &internalId, const QString &bundlePath = {});
void acceptMaterialDrop(const QString &internalId); void acceptMaterialDrop(const QString &internalId);
void accept3DDrop(const QByteArray &internalIds); void accept3DDrop(const QByteArray &internalIds);
void importQtQuick3D(); void importQtQuick3D();

View File

@@ -5,7 +5,7 @@
#include "contentlibrarytexture.h" #include "contentlibrarytexture.h"
#include <designerpaths.h> #include <asset.h>
#include <imageutils.h> #include <imageutils.h>
namespace QmlDesigner { namespace QmlDesigner {
@@ -27,7 +27,7 @@ void UserTextureCategory::loadBundle(bool force)
m_bundlePath.ensureWritableDir(); m_bundlePath.ensureWritableDir();
m_bundlePath.pathAppended("icons").ensureWritableDir(); m_bundlePath.pathAppended("icons").ensureWritableDir();
addItems(m_bundlePath.dirEntries(QDir::Files)); addItems(m_bundlePath.dirEntries({Asset::supportedImageSuffixes(), QDir::Files}));
m_bundleLoaded = true; m_bundleLoaded = true;
} }
@@ -55,6 +55,14 @@ void UserTextureCategory::addItems(const Utils::FilePaths &paths)
QSize imgDims = info.first; QSize imgDims = info.first;
qint64 imgFileSize = info.second; 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); auto tex = new ContentLibraryTexture(this, iconFileInfo, dirPath, suffix, imgDims, imgFileSize);
m_items.append(tex); m_items.append(tex);
} }
@@ -63,4 +71,13 @@ void UserTextureCategory::addItems(const Utils::FilePaths &paths)
emit itemsChanged(); emit itemsChanged();
} }
void UserTextureCategory::clearItems()
{
qDeleteAll(m_items);
m_items.clear();
setIsEmpty(true);
emit itemsChanged();
}
} // namespace QmlDesigner } // namespace QmlDesigner

View File

@@ -16,10 +16,11 @@ class UserTextureCategory : public UserCategory
public: public:
UserTextureCategory(const QString &title, const Utils::FilePath &bundlePath); 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 filter(const QString &searchText) override;
void addItems(const Utils::FilePaths &paths); void addItems(const Utils::FilePaths &paths);
void clearItems();
}; };
} // namespace QmlDesigner } // namespace QmlDesigner

View File

@@ -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 EDIT3D_CAMERA_SPEED_CONFIG[] = "QmlDesigner.Editor3D.CameraSpeedConfig";
inline constexpr char BUNDLE_JSON_FILENAME[] = "bundle.json"; 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 BUNDLE_SUFFIX[] = "qdsbundle";
inline constexpr char COMPONENT_BUNDLES_EFFECT_BUNDLE_TYPE[] = "Effects"; inline constexpr char COMPONENT_BUNDLES_EFFECT_BUNDLE_TYPE[] = "Effects";
inline constexpr char COMPONENT_BUNDLES_ASSET_REF_FILE[] = "_asset_ref.json"; inline constexpr char COMPONENT_BUNDLES_ASSET_REF_FILE[] = "_asset_ref.json";