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

View File

@@ -54,6 +54,8 @@ Item {
property bool expandOnClick: true // if false, toggleExpand signal will be emitted instead
property bool addTopPadding: true
property bool addBottomPadding: true
property bool dropEnabled: false
property bool highlight: false
property bool useDefaulContextMenu: true
@@ -72,9 +74,22 @@ Item {
function onExpandAll() { section.expanded = true }
}
signal drop(var drag)
signal dropEnter(var drag)
signal dropExit()
signal showContextMenu()
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 {
id: header
@@ -82,7 +97,9 @@ Item {
visible: !section.hideHeader
anchors.left: parent.left
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 {
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;
if (allSuffixes.isEmpty()) {

View File

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

View File

@@ -213,9 +213,24 @@ void AssetsLibraryWidget::handleAddAsset()
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()

View File

@@ -80,7 +80,7 @@ public:
Q_INVOKABLE void startDragAsset(const QStringList &assetPaths, const QPointF &mousePos);
Q_INVOKABLE void handleAddAsset();
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();
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);
if (directory.isEmpty())

View File

@@ -76,6 +76,7 @@ void addItemToStackedContainer(const SelectionContext &selectionContext);
void increaseIndexOfStackedContainer(const SelectionContext &selectionContext);
void decreaseIndexOfStackedContainer(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 addFontToProject(const QStringList &fileNames, const QString &directory);
AddFilesResult addSoundToProject(const QStringList &fileNames, const QString &directory);