QmlDesigner: Enable dropping external assets to a specific folder

Fixes: QDS-6311
Change-Id: I25bccfa6788e3b5d47fc598decbf7e03a00399b4
Reviewed-by: Henning Gründl <henning.gruendl@qt.io>
Reviewed-by: Miikka Heikkinen <miikka.heikkinen@qt.io>
This commit is contained in:
Mahmoud Badri
2022-03-03 12:40:40 +02:00
parent 8da5a772fe
commit c250d31ca0
8 changed files with 154 additions and 81 deletions

View File

@@ -32,45 +32,67 @@ import StudioControls 1.0 as StudioControls
import StudioTheme 1.0 as StudioTheme import StudioTheme 1.0 as StudioTheme
Item { Item {
id: rootItem id: root
property var selectedAssets: ({}) property var selectedAssets: ({})
property int allExpandedState: 0 property int allExpandedState: 0
property string contextFilePath: "" property string contextFilePath: ""
property var contextDir: undefined property var contextDir: undefined
property bool isDirContextMenu: false property bool isDirContextMenu: false
property var dropExtFiles: [] // array of supported externally dropped files
function clearSearchFilter() function clearSearchFilter()
{ {
searchBox.text = ""; searchBox.text = "";
} }
DropArea { function updateDropExtFiles(drag)
id: dropArea {
root.dropExtFiles = []
property var files // list of supported dropped files for (const u of drag.urls) {
var url = u.toString();
enabled: true
anchors.fill: parent
onEntered: (drag)=> {
files = []
for (var i = 0; i < drag.urls.length; ++i) {
var url = drag.urls[i].toString();
if (url.startsWith("file:///")) // remove file scheme (happens on Windows) if (url.startsWith("file:///")) // remove file scheme (happens on Windows)
url = url.substr(8) url = url.substr(8)
var ext = url.slice(url.lastIndexOf('.') + 1).toLowerCase() var ext = url.slice(url.lastIndexOf('.') + 1).toLowerCase()
if (rootView.supportedDropSuffixes().includes('*.' + ext)) if (rootView.supportedDropSuffixes().includes('*.' + ext))
files.push(url) root.dropExtFiles.push(url)
} }
if (files.length === 0) drag.accepted = root.dropExtFiles.length > 0
drag.accepted = false; }
DropArea { // handles external drop on empty area of the view (goes to root folder)
id: dropArea
y: assetsView.y + assetsView.contentHeight + 5
width: parent.width
height: parent.height - y
onEntered: (drag)=> {
root.updateDropExtFiles(drag)
} }
onDropped: { onDropped: {
if (files.length > 0) rootView.handleExtFilesDrop(root.dropExtFiles, assetsModel.rootDir().dirPath)
rootView.handleFilesDrop(files) }
Canvas { // marker for the drop area
id: dropCanvas
anchors.fill: parent
visible: dropArea.containsDrag
onWidthChanged: dropCanvas.requestPaint()
onHeightChanged: dropCanvas.requestPaint()
onPaint: {
var ctx = getContext("2d")
ctx.reset()
ctx.strokeStyle = StudioTheme.Values.themeInteraction
ctx.lineWidth = 2
ctx.setLineDash([4, 4])
ctx.rect(5, 5, dropCanvas.width - 10, dropCanvas.height - 10)
ctx.stroke()
}
} }
} }
@@ -79,9 +101,9 @@ Item {
acceptedButtons: Qt.RightButton acceptedButtons: Qt.RightButton
onClicked: { onClicked: {
if (!assetsModel.isEmpty) { if (!assetsModel.isEmpty) {
contextFilePath = "" root.contextFilePath = ""
contextDir = assetsModel.rootDir() root.contextDir = assetsModel.rootDir()
isDirContextMenu = false root.isDirContextMenu = false
contextMenu.popup() contextMenu.popup()
} }
} }
@@ -91,8 +113,8 @@ Item {
function handleViewFocusOut() function handleViewFocusOut()
{ {
contextMenu.close() contextMenu.close()
selectedAssets = {} root.selectedAssets = {}
selectedAssetsChanged() root.selectedAssetsChanged()
} }
StudioControls.Menu { StudioControls.Menu {
@@ -101,49 +123,49 @@ Item {
closePolicy: Popup.CloseOnPressOutside | Popup.CloseOnEscape closePolicy: Popup.CloseOnPressOutside | Popup.CloseOnEscape
onOpened: { onOpened: {
var numSelected = Object.values(selectedAssets).filter(p => p).length var numSelected = Object.values(root.selectedAssets).filter(p => p).length
deleteFileItem.text = numSelected > 1 ? qsTr("Delete Files") : qsTr("Delete File") deleteFileItem.text = numSelected > 1 ? qsTr("Delete Files") : qsTr("Delete File")
} }
StudioControls.MenuItem { StudioControls.MenuItem {
text: qsTr("Expand All") text: qsTr("Expand All")
enabled: allExpandedState !== 1 enabled: root.allExpandedState !== 1
visible: isDirContextMenu visible: root.isDirContextMenu
height: visible ? implicitHeight : 0 height: visible ? implicitHeight : 0
onTriggered: assetsModel.toggleExpandAll(true) onTriggered: assetsModel.toggleExpandAll(true)
} }
StudioControls.MenuItem { StudioControls.MenuItem {
text: qsTr("Collapse All") text: qsTr("Collapse All")
enabled: allExpandedState !== 2 enabled: root.allExpandedState !== 2
visible: isDirContextMenu visible: root.isDirContextMenu
height: visible ? implicitHeight : 0 height: visible ? implicitHeight : 0
onTriggered: assetsModel.toggleExpandAll(false) onTriggered: assetsModel.toggleExpandAll(false)
} }
StudioControls.MenuSeparator { StudioControls.MenuSeparator {
visible: isDirContextMenu visible: root.isDirContextMenu
height: visible ? StudioTheme.Values.border : 0 height: visible ? StudioTheme.Values.border : 0
} }
StudioControls.MenuItem { StudioControls.MenuItem {
id: deleteFileItem id: deleteFileItem
text: qsTr("Delete File") text: qsTr("Delete File")
visible: contextFilePath visible: root.contextFilePath
height: visible ? implicitHeight : 0 height: deleteFileItem.visible ? deleteFileItem.implicitHeight : 0
onTriggered: { onTriggered: {
assetsModel.deleteFiles(Object.keys(selectedAssets).filter(p => selectedAssets[p])) assetsModel.deleteFiles(Object.keys(root.selectedAssets).filter(p => root.selectedAssets[p]))
} }
} }
StudioControls.MenuSeparator { StudioControls.MenuSeparator {
visible: contextFilePath visible: root.contextFilePath
height: visible ? StudioTheme.Values.border : 0 height: visible ? StudioTheme.Values.border : 0
} }
StudioControls.MenuItem { StudioControls.MenuItem {
text: qsTr("Rename Folder") text: qsTr("Rename Folder")
visible: isDirContextMenu visible: root.isDirContextMenu
height: visible ? implicitHeight : 0 height: visible ? implicitHeight : 0
onTriggered: renameFolderDialog.open() onTriggered: renameFolderDialog.open()
} }
@@ -155,14 +177,14 @@ Item {
StudioControls.MenuItem { StudioControls.MenuItem {
text: qsTr("Delete Folder") text: qsTr("Delete Folder")
visible: isDirContextMenu visible: root.isDirContextMenu
height: visible ? implicitHeight : 0 height: visible ? implicitHeight : 0
onTriggered: { onTriggered: {
var dirEmpty = !(contextDir.dirsModel && contextDir.dirsModel.rowCount() > 0) var dirEmpty = !(root.contextDir.dirsModel && root.contextDir.dirsModel.rowCount() > 0)
&& !(contextDir.filesModel && contextDir.filesModel.rowCount() > 0); && !(root.contextDir.filesModel && root.contextDir.filesModel.rowCount() > 0);
if (dirEmpty) if (dirEmpty)
assetsModel.deleteFolder(contextDir.dirPath) assetsModel.deleteFolder(root.contextDir.dirPath)
else else
confirmDeleteFolderDialog.open() confirmDeleteFolderDialog.open()
} }
@@ -243,7 +265,7 @@ Item {
text: qsTr("Rename") text: qsTr("Rename")
enabled: folderRename.text !== "" enabled: folderRename.text !== ""
onClicked: { onClicked: {
var success = assetsModel.renameFolder(contextDir.dirPath, folderRename.text) var success = assetsModel.renameFolder(root.contextDir.dirPath, folderRename.text)
if (success) if (success)
renameFolderDialog.accept() renameFolderDialog.accept()
@@ -259,7 +281,7 @@ Item {
} }
onOpened: { onOpened: {
folderRename.text = contextDir.dirName folderRename.text = root.contextDir.dirName
folderRename.selectAll() folderRename.selectAll()
folderRename.forceActiveFocus() folderRename.forceActiveFocus()
renameFolderDialog.renameError = false renameFolderDialog.renameError = false
@@ -317,7 +339,7 @@ Item {
text: qsTr("Create") text: qsTr("Create")
enabled: folderName.text !== "" enabled: folderName.text !== ""
onClicked: { onClicked: {
assetsModel.addNewFolder(contextDir.dirPath + '/' + folderName.text) assetsModel.addNewFolder(root.contextDir.dirPath + '/' + folderName.text)
newFolderDialog.accept() newFolderDialog.accept()
} }
} }
@@ -353,7 +375,7 @@ Item {
id: folderNotEmpty id: folderNotEmpty
text: qsTr("Folder \"%1\" is not empty. Delete it anyway?") text: qsTr("Folder \"%1\" is not empty. Delete it anyway?")
.arg(contextDir ? contextDir.dirName : "") .arg(root.contextDir ? root.contextDir.dirName : "")
color: StudioTheme.Values.themeTextColor color: StudioTheme.Values.themeTextColor
wrapMode: Text.WordWrap wrapMode: Text.WordWrap
width: confirmDeleteFolderDialog.width width: confirmDeleteFolderDialog.width
@@ -381,7 +403,7 @@ Item {
text: qsTr("Delete") text: qsTr("Delete")
onClicked: { onClicked: {
assetsModel.deleteFolder(contextDir.dirPath) assetsModel.deleteFolder(root.contextDir.dirPath)
confirmDeleteFolderDialog.accept() confirmDeleteFolderDialog.accept()
} }
} }
@@ -441,7 +463,7 @@ Item {
spacing: 20 spacing: 20
x: 20 x: 20
width: rootItem.width - 2 * x width: root.width - 2 * x
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
Text { Text {
@@ -500,6 +522,8 @@ Item {
id: dirSection id: dirSection
Section { Section {
id: section
width: assetsView.width - width: assetsView.width -
(assetsView.verticalScrollBarVisible ? assetsView.verticalThickness : 0) - 5 (assetsView.verticalScrollBarVisible ? assetsView.verticalThickness : 0) - 5
caption: dirName caption: dirName
@@ -514,16 +538,31 @@ Item {
visible: dirVisible visible: dirVisible
expandOnClick: false expandOnClick: false
useDefaulContextMenu: false useDefaulContextMenu: false
dropEnabled: true
onToggleExpand: { onToggleExpand: {
dirExpanded = !dirExpanded dirExpanded = !dirExpanded
} }
onDropEnter: (drag)=> {
root.updateDropExtFiles(drag)
section.highlight = drag.accepted
}
onDropExit: {
section.highlight = false
}
onDrop: {
section.highlight = false
rootView.handleExtFilesDrop(root.dropExtFiles, dirPath)
}
onShowContextMenu: { onShowContextMenu: {
contextFilePath = "" root.contextFilePath = ""
contextDir = model root.contextDir = model
isDirContextMenu = true root.isDirContextMenu = true
allExpandedState = assetsModel.getAllExpandedState() root.allExpandedState = assetsModel.getAllExpandedState()
contextMenu.popup() contextMenu.popup()
} }
@@ -552,9 +591,9 @@ Item {
anchors.fill: parent anchors.fill: parent
acceptedButtons: Qt.RightButton acceptedButtons: Qt.RightButton
onClicked: { onClicked: {
contextFilePath = "" root.contextFilePath = ""
contextDir = model root.contextDir = model
isDirContextMenu = true root.isDirContextMenu = true
contextMenu.popup() contextMenu.popup()
} }
} }
@@ -570,7 +609,8 @@ Item {
width: assetsView.width - width: assetsView.width -
(assetsView.verticalScrollBarVisible ? assetsView.verticalThickness : 0) (assetsView.verticalScrollBarVisible ? assetsView.verticalThickness : 0)
height: img.height height: img.height
color: selectedAssets[filePath] ? StudioTheme.Values.themeInteraction color: root.selectedAssets[filePath]
? StudioTheme.Values.themeInteraction
: (mouseArea.containsMouse ? StudioTheme.Values.themeSectionHeadBackground : (mouseArea.containsMouse ? StudioTheme.Values.themeSectionHeadBackground
: "transparent") : "transparent")
@@ -611,29 +651,29 @@ Item {
forceActiveFocus() forceActiveFocus()
var ctrlDown = mouse.modifiers & Qt.ControlModifier var ctrlDown = mouse.modifiers & Qt.ControlModifier
if (mouse.button === Qt.LeftButton) { if (mouse.button === Qt.LeftButton) {
if (!selectedAssets[filePath] && !ctrlDown) if (!root.selectedAssets[filePath] && !ctrlDown)
selectedAssets = {} root.selectedAssets = {}
currFileSelected = ctrlDown ? !selectedAssets[filePath] : true currFileSelected = ctrlDown ? !root.selectedAssets[filePath] : true
selectedAssets[filePath] = currFileSelected root.selectedAssets[filePath] = currFileSelected
selectedAssetsChanged() root.selectedAssetsChanged()
if (currFileSelected) { if (currFileSelected) {
rootView.startDragAsset( rootView.startDragAsset(
Object.keys(selectedAssets).filter(p => selectedAssets[p]), Object.keys(root.selectedAssets).filter(p => root.selectedAssets[p]),
mapToGlobal(mouse.x, mouse.y)) mapToGlobal(mouse.x, mouse.y))
} }
} else { } else {
if (!selectedAssets[filePath] && !ctrlDown) if (!root.selectedAssets[filePath] && !ctrlDown)
selectedAssets = {} root.selectedAssets = {}
currFileSelected = selectedAssets[filePath] || !ctrlDown currFileSelected = root.selectedAssets[filePath] || !ctrlDown
selectedAssets[filePath] = currFileSelected root.selectedAssets[filePath] = currFileSelected
selectedAssetsChanged() root.selectedAssetsChanged()
contextFilePath = filePath root.contextFilePath = filePath
contextDir = model.fileDir root.contextDir = model.fileDir
root.isDirContextMenu = false
tooltipBackend.hideTooltip() tooltipBackend.hideTooltip()
isDirContextMenu = false
contextMenu.popup() contextMenu.popup()
} }
} }
@@ -641,9 +681,9 @@ Item {
onReleased: (mouse)=> { onReleased: (mouse)=> {
if (mouse.button === Qt.LeftButton) { if (mouse.button === Qt.LeftButton) {
if (!(mouse.modifiers & Qt.ControlModifier)) if (!(mouse.modifiers & Qt.ControlModifier))
selectedAssets = {} root.selectedAssets = {}
selectedAssets[filePath] = currFileSelected root.selectedAssets[filePath] = currFileSelected
selectedAssetsChanged() root.selectedAssetsChanged()
} }
} }

View File

@@ -54,6 +54,8 @@ Item {
property bool expandOnClick: true // if false, toggleExpand signal will be emitted instead property bool expandOnClick: true // if false, toggleExpand signal will be emitted instead
property bool addTopPadding: true property bool addTopPadding: true
property bool addBottomPadding: true property bool addBottomPadding: true
property bool dropEnabled: false
property bool highlight: false
property bool useDefaulContextMenu: true property bool useDefaulContextMenu: true
@@ -72,9 +74,22 @@ Item {
function onExpandAll() { section.expanded = true } function onExpandAll() { section.expanded = true }
} }
signal drop(var drag)
signal dropEnter(var drag)
signal dropExit()
signal showContextMenu() signal showContextMenu()
signal toggleExpand() signal toggleExpand()
DropArea {
id: dropArea
enabled: section.dropEnabled
anchors.fill: parent
onEntered: (drag)=> section.dropEnter(drag)
onDropped: (drag)=> section.drop(drag)
onExited: section.dropExit()
}
Rectangle { Rectangle {
id: header id: header
@@ -82,7 +97,9 @@ Item {
visible: !section.hideHeader visible: !section.hideHeader
anchors.left: parent.left anchors.left: parent.left
anchors.right: parent.right anchors.right: parent.right
color: Qt.lighter(StudioTheme.Values.themeSectionHeadBackground, 1.0 + (0.2 * section.level)) color: section.highlight ? StudioTheme.Values.themeInteraction
: Qt.lighter(StudioTheme.Values.themeSectionHeadBackground, 1.0
+ (0.2 * section.level))
Item { Item {
StudioControls.Menu { StudioControls.Menu {

View File

@@ -373,7 +373,7 @@ void AssetsLibraryModel::setSearchText(const QString &searchText)
} }
} }
const QSet<QString> &AssetsLibraryModel::supportedSuffixes() const const QSet<QString> &AssetsLibraryModel::supportedSuffixes()
{ {
static QSet<QString> allSuffixes; static QSet<QString> allSuffixes;
if (allSuffixes.isEmpty()) { if (allSuffixes.isEmpty()) {

View File

@@ -68,6 +68,7 @@ public:
static const QStringList &supportedAudioSuffixes(); static const QStringList &supportedAudioSuffixes();
static const QStringList &supportedVideoSuffixes(); static const QStringList &supportedVideoSuffixes();
static const QStringList &supportedTexture3DSuffixes(); static const QStringList &supportedTexture3DSuffixes();
static const QSet<QString> &supportedSuffixes();
const QSet<QString> &previewableSuffixes() const; const QSet<QString> &previewableSuffixes() const;
@@ -93,7 +94,6 @@ signals:
void isEmptyChanged(); void isEmptyChanged();
private: private:
const QSet<QString> &supportedSuffixes() const;
void setIsEmpty(bool empty); void setIsEmpty(bool empty);

View File

@@ -213,9 +213,24 @@ void AssetsLibraryWidget::handleAddAsset()
addResources({}); addResources({});
} }
void AssetsLibraryWidget::handleFilesDrop(const QStringList &filesPaths) void AssetsLibraryWidget::handleExtFilesDrop(const QStringList &filesPaths, const QString &targetDirPath)
{ {
addResources(filesPaths); QStringList assetPaths;
QStringList otherPaths; // as of now 3D models, and 3D Studio presentations
std::tie(assetPaths, otherPaths) = Utils::partition(filesPaths, [](const QString &path) {
QString suffix = "*." + path.split('.').last().toLower();
return AssetsLibraryModel::supportedSuffixes().contains(suffix);
});
AddFilesResult result = ModelNodeOperations::addFilesToProject(assetPaths, targetDirPath);
if (result == AddFilesResult::Failed) {
Core::AsynchronousMessageBox::warning(tr("Failed to Add Files"),
tr("Could not add %1 to project.")
.arg(filesPaths.join(' ')));
}
if (!otherPaths.empty())
addResources(otherPaths);
} }
QSet<QString> AssetsLibraryWidget::supportedDropSuffixes() QSet<QString> AssetsLibraryWidget::supportedDropSuffixes()

View File

@@ -80,7 +80,7 @@ public:
Q_INVOKABLE void startDragAsset(const QStringList &assetPaths, const QPointF &mousePos); Q_INVOKABLE void startDragAsset(const QStringList &assetPaths, const QPointF &mousePos);
Q_INVOKABLE void handleAddAsset(); Q_INVOKABLE void handleAddAsset();
Q_INVOKABLE void handleSearchfilterChanged(const QString &filterText); Q_INVOKABLE void handleSearchfilterChanged(const QString &filterText);
Q_INVOKABLE void handleFilesDrop(const QStringList &filesPaths); Q_INVOKABLE void handleExtFilesDrop(const QStringList &filesPaths, const QString &targetDirPath);
Q_INVOKABLE QSet<QString> supportedDropSuffixes(); Q_INVOKABLE QSet<QString> supportedDropSuffixes();
signals: signals:

View File

@@ -989,7 +989,7 @@ void addTabBarToStackedContainer(const SelectionContext &selectionContext)
} }
static AddFilesResult addFilesToProject(const QStringList &fileNames, const QString &defaultDirectory) AddFilesResult addFilesToProject(const QStringList &fileNames, const QString &defaultDirectory)
{ {
QString directory = AddImagesDialog::getDirectory(fileNames, defaultDirectory); QString directory = AddImagesDialog::getDirectory(fileNames, defaultDirectory);
if (directory.isEmpty()) if (directory.isEmpty())

View File

@@ -76,6 +76,7 @@ void addItemToStackedContainer(const SelectionContext &selectionContext);
void increaseIndexOfStackedContainer(const SelectionContext &selectionContext); void increaseIndexOfStackedContainer(const SelectionContext &selectionContext);
void decreaseIndexOfStackedContainer(const SelectionContext &selectionContext); void decreaseIndexOfStackedContainer(const SelectionContext &selectionContext);
void addTabBarToStackedContainer(const SelectionContext &selectionContext); void addTabBarToStackedContainer(const SelectionContext &selectionContext);
AddFilesResult addFilesToProject(const QStringList &fileNames, const QString &defaultDirectory);
AddFilesResult addImageToProject(const QStringList &fileNames, const QString &directory); AddFilesResult addImageToProject(const QStringList &fileNames, const QString &directory);
AddFilesResult addFontToProject(const QStringList &fileNames, const QString &directory); AddFilesResult addFontToProject(const QStringList &fileNames, const QString &directory);
AddFilesResult addSoundToProject(const QStringList &fileNames, const QString &directory); AddFilesResult addSoundToProject(const QStringList &fileNames, const QString &directory);