/**************************************************************************** ** ** Copyright (C) 2021 The Qt Company Ltd. ** Contact: https://www.qt.io/licensing/ ** ** This file is part of Qt Creator. ** ** Commercial License Usage ** Licensees holding valid commercial Qt licenses may use this file in ** accordance with the commercial license agreement provided with the ** Software or, alternatively, in accordance with the terms contained in ** a written agreement between you and The Qt Company. For licensing terms ** and conditions see https://www.qt.io/terms-conditions. For further ** information use the contact form at https://www.qt.io/contact-us. ** ** GNU General Public License Usage ** Alternatively, this file may be used under the terms of the GNU ** General Public License version 3 as published by the Free Software ** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT ** included in the packaging of this file. Please review the following ** information to ensure the GNU General Public License requirements will ** be met: https://www.gnu.org/licenses/gpl-3.0.html. ** ****************************************************************************/ import QtQuick 2.15 import QtQuick.Controls 2.15 import QtQuick.Layouts 1.15 import QtQuickDesignerTheme 1.0 import HelperWidgets 2.0 import StudioControls 1.0 as StudioControls import StudioTheme 1.0 as StudioTheme Item { id: root property var selectedAssets: ({}) property int allExpandedState: 0 property string contextFilePath: "" property var contextDir: undefined property bool isDirContextMenu: false // Array of supported externally dropped files that are imported as-is property var dropSimpleExtFiles: [] // Array of supported externally dropped files that trigger custom import process property var dropComplexExtFiles: [] function clearSearchFilter() { searchBox.clear(); } function updateDropExtFiles(drag) { root.dropSimpleExtFiles = [] root.dropComplexExtFiles = [] var simpleSuffixes = rootView.supportedAssetSuffixes(false); var complexSuffixes = rootView.supportedAssetSuffixes(true); for (const u of drag.urls) { var url = u.toString(); var ext = '*.' + url.slice(url.lastIndexOf('.') + 1).toLowerCase() if (simpleSuffixes.includes(ext)) root.dropSimpleExtFiles.push(url) else if (complexSuffixes.includes(ext)) root.dropComplexExtFiles.push(url) } drag.accepted = root.dropSimpleExtFiles.length > 0 || root.dropComplexExtFiles.length > 0 } 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: { rootView.handleExtFilesDrop(root.dropSimpleExtFiles, root.dropComplexExtFiles, assetsModel.rootDir().dirPath) } Canvas { // marker for the drop area id: dropCanvas anchors.fill: parent visible: dropArea.containsDrag && root.dropSimpleExtFiles.length > 0 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() } } } MouseArea { // right clicking the empty area of the view anchors.fill: parent acceptedButtons: Qt.RightButton onClicked: { if (!assetsModel.isEmpty) { root.contextFilePath = "" root.contextDir = assetsModel.rootDir() root.isDirContextMenu = false contextMenu.popup() } } } // called from C++ to close context menu on focus out function handleViewFocusOut() { contextMenu.close() root.selectedAssets = {} root.selectedAssetsChanged() } StudioControls.Menu { id: contextMenu closePolicy: Popup.CloseOnPressOutside | Popup.CloseOnEscape onOpened: { 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: root.allExpandedState !== 1 visible: root.isDirContextMenu height: visible ? implicitHeight : 0 onTriggered: assetsModel.toggleExpandAll(true) } StudioControls.MenuItem { text: qsTr("Collapse All") enabled: root.allExpandedState !== 2 visible: root.isDirContextMenu height: visible ? implicitHeight : 0 onTriggered: assetsModel.toggleExpandAll(false) } StudioControls.MenuSeparator { visible: root.isDirContextMenu height: visible ? StudioTheme.Values.border : 0 } StudioControls.MenuItem { id: deleteFileItem text: qsTr("Delete File") visible: root.contextFilePath height: deleteFileItem.visible ? deleteFileItem.implicitHeight : 0 onTriggered: { assetsModel.deleteFiles(Object.keys(root.selectedAssets).filter(p => root.selectedAssets[p])) } } StudioControls.MenuSeparator { visible: root.contextFilePath height: visible ? StudioTheme.Values.border : 0 } StudioControls.MenuItem { text: qsTr("Rename Folder") visible: root.isDirContextMenu height: visible ? implicitHeight : 0 onTriggered: renameFolderDialog.open() } StudioControls.MenuItem { text: qsTr("New Folder") onTriggered: newFolderDialog.open() } StudioControls.MenuItem { text: qsTr("Delete Folder") visible: root.isDirContextMenu height: visible ? implicitHeight : 0 onTriggered: { var dirEmpty = !(root.contextDir.dirsModel && root.contextDir.dirsModel.rowCount() > 0) && !(root.contextDir.filesModel && root.contextDir.filesModel.rowCount() > 0); if (dirEmpty) assetsModel.deleteFolder(root.contextDir.dirPath) else confirmDeleteFolderDialog.open() } } } RegExpValidator { id: folderNameValidator regExp: /^(\w[^*/> { root.updateDropExtFiles(drag) } onDropped: { rootView.handleExtFilesDrop(root.dropSimpleExtFiles, root.dropComplexExtFiles) } Column { id: colNoAssets spacing: 20 x: 20 width: root.width - 2 * x anchors.verticalCenter: parent.verticalCenter Text { text: qsTr("Looks like you don't have any assets yet.") color: StudioTheme.Values.themeTextColor font.pixelSize: 18 width: colNoAssets.width horizontalAlignment: Text.AlignHCenter wrapMode: Text.WordWrap } Image { source: "image://qmldesigner_assets/browse" anchors.horizontalCenter: parent.horizontalCenter scale: maBrowse.containsMouse ? 1.2 : 1 Behavior on scale { NumberAnimation { duration: 300 easing.type: Easing.OutQuad } } MouseArea { id: maBrowse anchors.fill: parent hoverEnabled: true onClicked: rootView.handleAddAsset(); } } Text { text: qsTr("Drag-and-drop your assets here or click the '+' button to browse assets from the file system.") color: StudioTheme.Values.themeTextColor font.pixelSize: 18 width: colNoAssets.width horizontalAlignment: Text.AlignHCenter wrapMode: Text.WordWrap } } } } ScrollView { // TODO: experiment using ListView instead of ScrollView + Column id: assetsView width: parent.width height: parent.height - y clip: true interactive: assetsView.verticalScrollBarVisible && !contextMenu.opened Column { Repeater { model: assetsModel // context property delegate: dirSection } Component { id: dirSection Section { id: section width: assetsView.width - (assetsView.verticalScrollBarVisible ? assetsView.verticalThickness : 0) - 5 caption: dirName sectionHeight: 30 sectionFontSize: 15 leftPadding: 0 topPadding: dirDepth > 0 ? 5 : 0 bottomPadding: 0 hideHeader: dirDepth === 0 showLeftBorder: dirDepth > 0 expanded: dirExpanded visible: dirVisible expandOnClick: false useDefaulContextMenu: false dropEnabled: true onToggleExpand: { dirExpanded = !dirExpanded } onDropEnter: (drag)=> { root.updateDropExtFiles(drag) section.highlight = drag.accepted && root.dropSimpleExtFiles.length > 0 } onDropExit: { section.highlight = false } onDrop: { section.highlight = false rootView.handleExtFilesDrop(root.dropSimpleExtFiles, root.dropComplexExtFiles, dirPath) } onShowContextMenu: { root.contextFilePath = "" root.contextDir = model root.isDirContextMenu = true root.allExpandedState = assetsModel.getAllExpandedState() contextMenu.popup() } Column { spacing: 5 leftPadding: 5 Repeater { model: dirsModel delegate: dirSection } Repeater { model: filesModel delegate: fileSection } Text { text: qsTr("Empty folder") color: StudioTheme.Values.themeTextColorDisabled font.pixelSize: 12 visible: !(dirsModel && dirsModel.rowCount() > 0) && !(filesModel && filesModel.rowCount() > 0) MouseArea { anchors.fill: parent acceptedButtons: Qt.RightButton onClicked: { root.contextFilePath = "" root.contextDir = model root.isDirContextMenu = true contextMenu.popup() } } } } } } Component { id: fileSection Rectangle { width: assetsView.width - (assetsView.verticalScrollBarVisible ? assetsView.verticalThickness : 0) height: img.height color: root.selectedAssets[filePath] ? StudioTheme.Values.themeInteraction : (mouseArea.containsMouse ? StudioTheme.Values.themeSectionHeadBackground : "transparent") Row { spacing: 5 Image { id: img asynchronous: true fillMode: Image.PreserveAspectFit width: 48 height: 48 source: "image://qmldesigner_assets/" + filePath } Text { text: fileName color: StudioTheme.Values.themeTextColor font.pixelSize: 14 anchors.verticalCenter: parent.verticalCenter } } readonly property string suffix: fileName.substr(-4) readonly property bool isFont: suffix === ".ttf" || suffix === ".otf" property bool currFileSelected: false MouseArea { id: mouseArea property bool allowTooltip: true anchors.fill: parent hoverEnabled: true acceptedButtons: Qt.LeftButton | Qt.RightButton onExited: tooltipBackend.hideTooltip() onEntered: allowTooltip = true onCanceled: { tooltipBackend.hideTooltip() allowTooltip = true } onPositionChanged: tooltipBackend.reposition() onPressed: (mouse)=> { forceActiveFocus() allowTooltip = false tooltipBackend.hideTooltip() var ctrlDown = mouse.modifiers & Qt.ControlModifier if (mouse.button === Qt.LeftButton) { 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(root.selectedAssets).filter(p => root.selectedAssets[p]), mapToGlobal(mouse.x, mouse.y)) } } else { if (!root.selectedAssets[filePath] && !ctrlDown) root.selectedAssets = {} currFileSelected = root.selectedAssets[filePath] || !ctrlDown root.selectedAssets[filePath] = currFileSelected root.selectedAssetsChanged() root.contextFilePath = filePath root.contextDir = model.fileDir root.isDirContextMenu = false contextMenu.popup() } } onReleased: (mouse)=> { allowTooltip = true if (mouse.button === Qt.LeftButton) { if (!(mouse.modifiers & Qt.ControlModifier)) root.selectedAssets = {} root.selectedAssets[filePath] = currFileSelected root.selectedAssetsChanged() } } ToolTip { visible: !isFont && mouseArea.containsMouse && !contextMenu.visible text: filePath delay: 1000 } Timer { interval: 1000 running: mouseArea.containsMouse && mouseArea.allowTooltip onTriggered: { if (suffix === ".ttf" || suffix === ".otf") { tooltipBackend.name = fileName tooltipBackend.path = filePath tooltipBackend.showTooltip() } } } } } } } } } }