diff options
author | Samuel Ghinet <samuel.ghinet@qt.io> | 2022-11-23 11:49:45 +0200 |
---|---|---|
committer | Samuel Ghinet <samuel.ghinet@qt.io> | 2022-11-23 10:00:04 +0000 |
commit | 910a8864dc43f8dede7617855282f19de62ee939 (patch) | |
tree | 13a6643b5ee2412c1b4ccccba8d0ddcca32549a3 | |
parent | a575cb4f46b8f24ef0a5cc15702a15020016d343 (diff) | |
download | qt-creator-910a8864dc43f8dede7617855282f19de62ee939.tar.gz |
Use QML TreeView in Assets Library
Task-number: QDS-7344
Change-Id: Ia1ea584fc7acabb0d35b745e36fef18799f21ab5
Reviewed-by: Thomas Hartmann <thomas.hartmann@qt.io>
26 files changed, 1261 insertions, 1090 deletions
diff --git a/share/qtcreator/qmldesigner/itemLibraryQmlSources/AssetDelegate.qml b/share/qtcreator/qmldesigner/itemLibraryQmlSources/AssetDelegate.qml new file mode 100644 index 0000000000..28f671cf68 --- /dev/null +++ b/share/qtcreator/qmldesigner/itemLibraryQmlSources/AssetDelegate.qml @@ -0,0 +1,327 @@ +// Copyright (C) 2022 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0 WITH Qt-GPL-exception-1.0 + +import QtQuick +import QtQuick.Controls +import StudioTheme as StudioTheme + +TreeViewDelegate { + id: root + + required property Item assetsView + required property Item assetsRoot + + property bool hasChildWithDropHover: false + property bool isHoveringDrop: false + readonly property string suffix: model.fileName.substr(-4) + readonly property bool isFont: root.suffix === ".ttf" || root.suffix === ".otf" + readonly property bool isEffect: root.suffix === ".qep" + property bool currFileSelected: false + property int initialDepth: -1 + property bool _isDirectory: assetsModel.isDirectory(model.filePath) + property int _currentRow: model.index + property string _itemPath: model.filePath + + readonly property int _fileItemHeight: thumbnailImage.height + readonly property int _dirItemHeight: 21 + + implicitHeight: root._isDirectory ? root._dirItemHeight : root._fileItemHeight + implicitWidth: root.assetsView.width > 0 ? root.assetsView.width : 10 + + leftMargin: root._isDirectory ? 0 : thumbnailImage.width + + Component.onCompleted: { + // the depth of the root path will become available before we get to the actual + // items we display, so it's safe to set assetsView.rootPathDepth here. All other + // tree items (below the root) will have the indentation (basically, depth) adjusted. + if (model.filePath === assetsModel.rootPath()) { + root.assetsView.rootPathDepth = root.depth + root.assetsView.rootPathRow = root._currentRow + } else if (model.filePath.includes(assetsModel.rootPath())) { + root.depth -= root.assetsView.rootPathDepth + root.initialDepth = root.depth + } + } + + // workaround for a bug -- might be fixed by https://codereview.qt-project.org/c/qt/qtdeclarative/+/442721 + onYChanged: { + if (root._currentRow === root.assetsView.firstRow) { + if (root.y > root.assetsView.contentY) { + let item = root.assetsView.itemAtCell(0, root.assetsView.rootPathRow) + if (!item) + root.assetsView.contentY = root.y + } + } + } + + onImplicitWidthChanged: { + // a small hack, to fix a glitch: when resizing the width of the tree view, + // the widths of the delegate items remain the same as before, unless we re-set + // that width explicitly. + var newWidth = root.implicitWidth - (root.assetsView.verticalScrollBar.scrollBarVisible + ? root.assetsView.verticalScrollBar.width + : 0) + bg.width = newWidth + bg.implicitWidth = newWidth + } + + onDepthChanged: { + if (root.depth > root.initialDepth && root.initialDepth >= 0) + root.depth = root.initialDepth + } + + background: Rectangle { + id: bg + + color: { + if (root._isDirectory && (root.isHoveringDrop || root.hasChildWithDropHover)) + return StudioTheme.Values.themeInteraction + + if (!root._isDirectory && root.assetsView.selectedAssets[root._itemPath]) + return StudioTheme.Values.themeInteraction + + if (mouseArea.containsMouse) + return StudioTheme.Values.themeSectionHeadBackground + + return root._isDirectory + ? StudioTheme.Values.themeSectionHeadBackground + : "transparent" + } + + // this rectangle exists so as to have some visual indentation for the directories + // We prepend a default pane-colored rectangle so that the nested directory will + // look moved a bit to the right + Rectangle { + anchors.top: bg.top + anchors.bottom: bg.bottom + anchors.left: bg.left + + width: root.indentation * root.depth + implicitWidth: root.indentation * root.depth + color: StudioTheme.Values.themePanelBackground + } + } + + contentItem: Text { + id: assetLabel + text: assetLabel._computeText() + color: StudioTheme.Values.themeTextColor + font.pixelSize: 14 + anchors.verticalCenter: parent.verticalCenter + verticalAlignment: Qt.AlignVCenter + + function _computeText() + { + return root._isDirectory + ? (root.hasChildren + ? model.display.toUpperCase() + : model.display.toUpperCase() + qsTr(" (empty)")) + : model.display + } + } + + DropArea { + id: treeDropArea + + enabled: true + anchors.fill: parent + + onEntered: (drag) => { + root.assetsRoot.updateDropExtFiles(drag) + root.isHoveringDrop = drag.accepted && root.assetsRoot.dropSimpleExtFiles.length > 0 + if (root.isHoveringDrop) + root.assetsView.startDropHoverOver(root._currentRow) + } + + onDropped: (drag) => { + root.isHoveringDrop = false + root.assetsView.endDropHover(root._currentRow) + + let dirPath = root._isDirectory + ? model.filePath + : assetsModel.parentDirPath(model.filePath); + + rootView.emitExtFilesDrop(root.assetsRoot.dropSimpleExtFiles, + root.assetsRoot.dropComplexExtFiles, + dirPath) + } + + onExited: { + if (root.isHoveringDrop) { + root.isHoveringDrop = false + root.assetsView.endDropHover(root._currentRow) + } + } + } + + MouseArea { + id: mouseArea + + property bool allowTooltip: true + + anchors.fill: parent + hoverEnabled: true + acceptedButtons: Qt.LeftButton | Qt.RightButton + + onExited: tooltipBackend.hideTooltip() + onEntered: mouseArea.allowTooltip = true + + onCanceled: { + tooltipBackend.hideTooltip() + mouseArea.allowTooltip = true + } + + onPositionChanged: tooltipBackend.reposition() + + onPressed: (mouse) => { + forceActiveFocus() + mouseArea.allowTooltip = false + tooltipBackend.hideTooltip() + + if (root._isDirectory) + return + + var ctrlDown = mouse.modifiers & Qt.ControlModifier + if (mouse.button === Qt.LeftButton) { + if (!root.assetsView.isAssetSelected(root._itemPath) && !ctrlDown) + root.assetsView.clearSelectedAssets() + root.currFileSelected = ctrlDown ? !root.assetsView.isAssetSelected(root._itemPath) : true + root.assetsView.setAssetSelected(root._itemPath, root.currFileSelected) + + if (root.currFileSelected) { + let selectedPaths = root.assetsView.selectedPathsAsList() + rootView.startDragAsset(selectedPaths, mapToGlobal(mouse.x, mouse.y)) + } + } else { + if (!root.assetsView.isAssetSelected(root._itemPath) && !ctrlDown) + root.assetsView.clearSelectedAssets() + root.currFileSelected = root.assetsView.isAssetSelected(root._itemPath) || !ctrlDown + root.assetsView.setAssetSelected(root._itemPath, root.currFileSelected) + } + } + + onReleased: (mouse) => { + mouseArea.allowTooltip = true + + if (mouse.button === Qt.LeftButton) { + if (!(mouse.modifiers & Qt.ControlModifier)) + root.assetsView.selectedAssets = {} + root.assetsView.selectedAssets[root._itemPath] = root.currFileSelected + root.assetsView.selectedAssetsChanged() + } + } + + onDoubleClicked: (mouse) => { + forceActiveFocus() + allowTooltip = false + tooltipBackend.hideTooltip() + if (mouse.button === Qt.LeftButton && isEffect) + rootView.openEffectMaker(filePath) + } + + ToolTip { + visible: !root.isFont && mouseArea.containsMouse && !root.assetsView.contextMenu.visible + text: model.filePath + delay: 1000 + } + + Timer { + interval: 1000 + running: mouseArea.containsMouse && mouseArea.allowTooltip + onTriggered: { + if (suffix === ".ttf" || suffix === ".otf") { + tooltipBackend.name = model.fileName + tooltipBackend.path = model.filePath + tooltipBackend.showTooltip() + } + } + } // Timer + + onClicked: (mouse) => { + if (mouse.button === Qt.LeftButton) + root._toggleExpandCurrentRow() + else + root._openContextMenuForCurrentRow() + + + } + } // MouseArea + + function _openContextMenuForCurrentRow() + { + let modelIndex = assetsModel.indexForPath(model.filePath) + + if (root._isDirectory) { + var row = root.assetsView.rowAtIndex(modelIndex) + var expanded = root.assetsView.isExpanded(row) + + var allExpandedState = root.assetsView.computeAllExpandedState() + + function onFolderCreated(path) { + root.assetsView.addCreatedFolder(path) + } + + function onFolderRenamed() { + if (expanded) + root.assetsView.rowToExpand = row + } + + root.assetsView.contextMenu.openContextMenuForDir(modelIndex, model.filePath, + model.fileName, allExpandedState, onFolderCreated, onFolderRenamed) + } else { + let parentDirIndex = assetsModel.parentDirIndex(model.filePath) + let selectedPaths = root.assetsView.selectedPathsAsList() + root.assetsView.contextMenu.openContextMenuForFile(modelIndex, parentDirIndex, + selectedPaths) + } + } + + function _toggleExpandCurrentRow() + { + if (!root._isDirectory) + return + + let index = root.assetsView._modelIndex(root._currentRow, 0) + // if the user manually clicked on a directory, then this is definitely not a + // an automatic request to expand all. + root.assetsView.requestedExpandAll = false + + if (root.assetsView.isExpanded(root._currentRow)) { + root.assetsView.requestedExpandAll = false + root.assetsView.collapse(root._currentRow) + } else { + root.assetsView.expand(root._currentRow) + } + } + + function reloadImage() + { + if (root._isDirectory) + return + + thumbnailImage.source = "" + thumbnailImage.source = thumbnailImage._computeSource() + } + + Image { + id: thumbnailImage + visible: !root._isDirectory + x: root.depth * root.indentation + width: 48 + height: 48 + cache: false + sourceSize.width: 48 + sourceSize.height: 48 + asynchronous: true + fillMode: Image.PreserveAspectFit + source: thumbnailImage._computeSource() + + function _computeSource() + { + return root._isDirectory + ? "" + : "image://qmldesigner_assets/" + model.filePath + } + + } // Image +} // TreeViewDelegate diff --git a/share/qtcreator/qmldesigner/itemLibraryQmlSources/Assets.qml b/share/qtcreator/qmldesigner/itemLibraryQmlSources/Assets.qml index f8c0605734..d35ac63dad 100644 --- a/share/qtcreator/qmldesigner/itemLibraryQmlSources/Assets.qml +++ b/share/qtcreator/qmldesigner/itemLibraryQmlSources/Assets.qml @@ -1,31 +1,26 @@ -// Copyright (C) 2021 The Qt Company Ltd. +// Copyright (C) 2022 The Qt Company Ltd. // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0 WITH Qt-GPL-exception-1.0 -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 +import QtQuick +import HelperWidgets as HelperWidgets +import StudioControls as StudioControls +import StudioTheme 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: [] + readonly property int qtVersionAtLeast6_4: rootView.qtVersionIsAtLeast6_4() + property bool _searchBoxEmpty: true + AssetsContextMenu { id: contextMenu + assetsView: assetsView } function clearSearchFilter() @@ -63,7 +58,7 @@ Item { onDropped: { rootView.handleExtFilesDrop(root.dropSimpleExtFiles, root.dropComplexExtFiles, - assetsModel.rootDir().dirPath) + assetsModel.rootPath()) } Canvas { // marker for the drop area @@ -90,11 +85,15 @@ Item { anchors.fill: parent acceptedButtons: Qt.RightButton onClicked: { - if (!assetsModel.isEmpty) { - root.contextFilePath = "" - root.contextDir = assetsModel.rootDir() - root.isDirContextMenu = false - contextMenu.popup() + if (assetsModel.haveFiles) { + function onFolderCreated(path) { + assetsView.addCreatedFolder(path) + } + + var rootIndex = assetsModel.rootIndex() + var dirPath = assetsModel.filePath(rootIndex) + var dirName = assetsModel.fileName(rootIndex) + contextMenu.openContextMenuForRoot(rootIndex, dirPath, dirName, onFolderCreated) } } } @@ -103,13 +102,8 @@ Item { function handleViewFocusOut() { contextMenu.close() - root.selectedAssets = {} - root.selectedAssetsChanged() - } - - RegExpValidator { - id: folderNameValidator - regExp: /^(\w[^*/><?\\|:]*)$/ + assetsView.selectedAssets = {} + assetsView.selectedAssetsChanged() } Column { @@ -127,10 +121,29 @@ Item { width: parent.width - addAssetButton.width - 5 - onSearchChanged: (searchText) => rootView.handleSearchFilterChanged(searchText) + onSearchChanged: (searchText) => { + updateSearchFilterTimer.restart() + } } - IconButton { + Timer { + id: updateSearchFilterTimer + interval: 200 + repeat: false + + onTriggered: { + assetsView.resetVerticalScrollPosition() + rootView.handleSearchFilterChanged(searchBox.text) + assetsView.expandAll() + + if (root._searchBoxEmpty && searchBox.text) + root._searchBoxEmpty = false + else if (!root._searchBoxEmpty && !searchBox.text) + root._searchBoxEmpty = true + } + } + + HelperWidgets.IconButton { id: addAssetButton anchors.verticalCenter: parent.verticalCenter tooltip: qsTr("Add a new asset to the project.") @@ -146,14 +159,13 @@ Item { leftPadding: 10 color: StudioTheme.Values.themeTextColor font.pixelSize: 12 - visible: assetsModel.isEmpty && !searchBox.isEmpty() + visible: !assetsModel.haveFiles && !root._searchBoxEmpty } - Item { // placeholder when the assets library is empty width: parent.width height: parent.height - searchRow.height - visible: assetsModel.isEmpty && searchBox.isEmpty() + visible: !assetsModel.haveFiles && root._searchBoxEmpty clip: true DropArea { // handles external drop (goes into default folder based on suffix) @@ -164,7 +176,7 @@ Item { } onDropped: { - rootView.handleExtFilesDrop(root.dropSimpleExtFiles, root.dropComplexExtFiles) + rootView.emitExtFilesDrop(root.dropSimpleExtFiles, root.dropComplexExtFiles) } Column { @@ -217,8 +229,11 @@ Item { AssetsView { id: assetsView + assetsRoot: root + contextMenu: contextMenu + width: parent.width height: parent.height - y } - } + } // Column } diff --git a/share/qtcreator/qmldesigner/itemLibraryQmlSources/AssetsContextMenu.qml b/share/qtcreator/qmldesigner/itemLibraryQmlSources/AssetsContextMenu.qml index 5caa139651..84a689184c 100644 --- a/share/qtcreator/qmldesigner/itemLibraryQmlSources/AssetsContextMenu.qml +++ b/share/qtcreator/qmldesigner/itemLibraryQmlSources/AssetsContextMenu.qml @@ -1,90 +1,113 @@ -/**************************************************************************** -** -** Copyright (C) 2022 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. -** -****************************************************************************/ +// Copyright (C) 2022 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0 WITH Qt-GPL-exception-1.0 import QtQuick import QtQuick.Controls -import QtQuick.Layouts -import QtQuickDesignerTheme -import HelperWidgets as HelperWidgets import StudioControls as StudioControls import StudioTheme as StudioTheme StudioControls.Menu { - id: contextMenu + id: root + + required property Item assetsView + + property bool _isDirectory: false + property var _fileIndex: null + property string _dirPath: "" + property string _dirName: "" + property var _onFolderCreated: null + property var _onFolderRenamed: null + property var _dirIndex: null + property string _allExpandedState: "" + property var _selectedAssetPathsList: null closePolicy: Popup.CloseOnPressOutside | Popup.CloseOnEscape - onOpened: { - var numSelected = Object.values(root.selectedAssets).filter(p => p).length + function openContextMenuForRoot(rootModelIndex, dirPath, dirName, onFolderCreated) + { + root._onFolderCreated = onFolderCreated + root._fileIndex = "" + root._dirPath = dirPath + root._dirName = dirName + root._dirIndex = rootModelIndex + root._isDirectory = false + root.popup() + } + + function openContextMenuForDir(dirModelIndex, dirPath, dirName, allExpandedState, + onFolderCreated, onFolderRenamed) + { + root._onFolderCreated = onFolderCreated + root._onFolderRenamed = onFolderRenamed + root._dirPath = dirPath + root._dirName = dirName + root._fileIndex = "" + root._dirIndex = dirModelIndex + root._isDirectory = true + root._allExpandedState = allExpandedState + root.popup() + } + + function openContextMenuForFile(fileIndex, dirModelIndex, selectedAssetPathsList) + { + var numSelected = selectedAssetPathsList.filter(p => p).length deleteFileItem.text = numSelected > 1 ? qsTr("Delete Files") : qsTr("Delete File") + + root._selectedAssetPathsList = selectedAssetPathsList + root._fileIndex = fileIndex + root._dirIndex = dirModelIndex + root._dirPath = assetsModel.filePath(dirModelIndex) + root._isDirectory = false + root.popup() } StudioControls.MenuItem { text: qsTr("Expand All") - enabled: root.allExpandedState !== 1 - visible: root.isDirContextMenu + enabled: root._allExpandedState !== "all_expanded" + visible: root._isDirectory height: visible ? implicitHeight : 0 - onTriggered: assetsModel.toggleExpandAll(true) + onTriggered: root.assetsView.expandAll() } StudioControls.MenuItem { text: qsTr("Collapse All") - enabled: root.allExpandedState !== 2 - visible: root.isDirContextMenu + enabled: root._allExpandedState !== "all_collapsed" + visible: root._isDirectory height: visible ? implicitHeight : 0 - onTriggered: assetsModel.toggleExpandAll(false) + onTriggered: root.assetsView.collapseAll() } StudioControls.MenuSeparator { - visible: root.isDirContextMenu + visible: root._isDirectory height: visible ? StudioTheme.Values.border : 0 } StudioControls.MenuItem { id: deleteFileItem text: qsTr("Delete File") - visible: root.contextFilePath + visible: root._fileIndex height: deleteFileItem.visible ? deleteFileItem.implicitHeight : 0 - onTriggered: { - assetsModel.deleteFiles(Object.keys(root.selectedAssets).filter(p => root.selectedAssets[p])) - } + onTriggered: assetsModel.deleteFiles(root._selectedAssetPathsList) } StudioControls.MenuSeparator { - visible: root.contextFilePath + visible: root._fileIndex height: visible ? StudioTheme.Values.border : 0 } StudioControls.MenuItem { text: qsTr("Rename Folder") - visible: root.isDirContextMenu + visible: root._isDirectory height: visible ? implicitHeight : 0 onTriggered: renameFolderDialog.open() RenameFolderDialog { id: renameFolderDialog + parent: root.assetsView + dirPath: root._dirPath + dirName: root._dirName + + onAccepted: root._onFolderRenamed() } } @@ -93,6 +116,10 @@ StudioControls.Menu { NewFolderDialog { id: newFolderDialog + parent: root.assetsView + dirPath: root._dirPath + + onAccepted: root._onFolderCreated(newFolderDialog.createdDirPath) } onTriggered: newFolderDialog.open() @@ -100,21 +127,25 @@ StudioControls.Menu { StudioControls.MenuItem { text: qsTr("Delete Folder") - visible: root.isDirContextMenu + visible: root._isDirectory height: visible ? implicitHeight : 0 ConfirmDeleteFolderDialog { id: confirmDeleteFolderDialog + parent: root.assetsView + dirName: root._dirName + dirIndex: root._dirIndex } 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 + if (!assetsModel.hasChildren(root._dirIndex)) { + // NOTE: the folder may still not be empty -- it doesn't have files visible to the + // user, but that doesn't mean that there are no other files (e.g. files of unknown + // types) on disk in this directory. + assetsModel.deleteFolderRecursively(root._dirIndex) + } else { confirmDeleteFolderDialog.open() + } } } } diff --git a/share/qtcreator/qmldesigner/itemLibraryQmlSources/AssetsView.qml b/share/qtcreator/qmldesigner/itemLibraryQmlSources/AssetsView.qml index 77a784d92d..8b76eaf0e1 100644 --- a/share/qtcreator/qmldesigner/itemLibraryQmlSources/AssetsView.qml +++ b/share/qtcreator/qmldesigner/itemLibraryQmlSources/AssetsView.qml @@ -1,255 +1,309 @@ -/**************************************************************************** -** -** Copyright (C) 2022 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. -** -****************************************************************************/ +// Copyright (C) 2022 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0 WITH Qt-GPL-exception-1.0 import QtQuick import QtQuick.Controls -import QtQuick.Layouts -import QtQuickDesignerTheme -import HelperWidgets +import HelperWidgets as HelperWidgets import StudioControls as StudioControls -import StudioTheme as StudioTheme -ScrollView { // TODO: experiment using ListView instead of ScrollView + Column - id: assetsView +TreeView { + id: root clip: true - interactive: assetsView.verticalScrollBarVisible && !contextMenu.opened + interactive: verticalScrollBar.visible && !root.contextMenu.opened + reuseItems: false + boundsBehavior: Flickable.StopAtBounds + rowSpacing: 5 + + required property Item assetsRoot + required property StudioControls.Menu contextMenu + property alias verticalScrollBar: verticalScrollBar + + property var selectedAssets: ({}) + + // used to see if the op requested is to expand or to collapse. + property int lastRowCount: -1 + // we need this to know if we need to expand further, while we're in onRowsChanged() + property bool requestedExpandAll: true + // used to compute the visual depth of the items we show to the user. + property int rootPathDepth: 0 + property int rootPathRow: 0 + // i.e. first child of the root path + readonly property int firstRow: root.rootPathRow + 1 + property int rowToExpand: -1 + property var _createdDirectories: [] + + rowHeightProvider: (row) => { + if (row <= root.rootPathRow) + return 0 + + return -1 + } + + ScrollBar.vertical: HelperWidgets.VerticalScrollBar { + id: verticalScrollBar + scrollBarVisible: root.contentHeight > root.height + } + + model: assetsModel + + onRowsChanged: { + if (root.rows > root.rootPathRow + 1 && !assetsModel.haveFiles || + root.rows <= root.rootPathRow + 1 && assetsModel.haveFiles) { + assetsModel.syncHaveFiles() + } + + updateRows() + } - Column { - Repeater { - model: assetsModel // context property - delegate: dirSection + Timer { + id: updateRowsTimer + interval: 200 + repeat: false + + onTriggered: { + root.updateRows() } + } - 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() - } - } - } - } + Connections { + target: rootView + + function onDirectoryCreated(path) + { + root._createdDirectories.push(path) + + updateRowsTimer.restart() + } + } + + Connections { + target: assetsModel + function onDirectoryLoaded(path) + { + // updating rows for safety: the rows might have been created before the + // directory (esp. the root path) has been loaded, so we must make sure all rows are + // expanded -- otherwise, the tree may not become visible. + + updateRowsTimer.restart() + + let idx = assetsModel.indexForPath(path) + let row = root.rowAtIndex(idx) + let column = root.columnAtIndex(idx) + + if (row >= root.rootPathRow && !root.isExpanded(row)) + root.expand(row) + } + + function onRootPathChanged() + { + // when we switch from one project to another, we need to reset the state of the + // view: make sure we will do an "expand all" (otherwise, the whole tree might + // be collapsed, and with our visible root not being the actual root of the tree, + // the entire tree would be invisible) + root.lastRowCount = -1 + root.requestedExpandAll = true + } + + function onFileChanged(filePath) + { + rootView.invalidateThumbnail(filePath) + + let index = assetsModel.indexForPath(filePath) + let cell = root.cellAtIndex(index) + let fileItem = root.itemAtCell(cell) + + if (fileItem) + fileItem.reloadImage() + } + + } // Connections + + function addCreatedFolder(path) + { + root._createdDirectories.push(path) + } + + function selectedPathsAsList() + { + return Object.keys(root.selectedAssets) + .filter(itemPath => root.selectedAssets[itemPath]) + } + + // workaround for a bug -- might be fixed by https://codereview.qt-project.org/c/qt/qtdeclarative/+/442721 + function resetVerticalScrollPosition() + { + root.contentY = 0 + } + + function updateRows() + { + if (root.rows <= 0) + return + + while (root._createdDirectories.length > 0) { + let dirPath = root._createdDirectories.pop() + let index = assetsModel.indexForPath(dirPath) + let row = root.rowAtIndex(index) + + if (row > 0) + root.expand(row) + else if (row === -1 && assetsModel.indexIsValid(index)) { + // It is possible that this directory, dirPath, was created inside of a parent + // directory that was not yet expanded in the TreeView. This can happen with the + // bridge plugin. In such a situation, we don't have a "row" for it yet, so we have + // to expand its parents, from root to our `index` + let parents = assetsModel.parentIndices(index); + parents.reverse().forEach(idx => { + let row = root.rowAtIndex(idx) + if (row > 0) + root.expand(row) + }) } } - 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" - readonly property bool isEffect: suffix === ".qep" - 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() - } - } - - onDoubleClicked: (mouse) => { - forceActiveFocus() - allowTooltip = false - tooltipBackend.hideTooltip() - if (mouse.button === Qt.LeftButton && isEffect) - rootView.openEffectMaker(filePath) - } - - 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() - } - } - } - } + // we have no way to know beyond doubt here if updateRows() was called due + // to a request to expand or to collapse rows - but it should be safe to + // assume that, if we have more rows now than the last time, then it's an expand + var expanding = (root.rows >= root.lastRowCount) + + if (expanding) { + if (root.requestedExpandAll) + root._doExpandAll() + } else { + if (root.rowToExpand > 0) { + root.expand(root.rowToExpand) + root.rowToExpand = -1 } + + // on collapsing, set expandAll flag to false. + root.requestedExpandAll = false; } + + root.lastRowCount = root.rows + } + + function _doExpandAll() + { + let expandedAny = false + for (let nRow = 0; nRow < root.rows; ++nRow) { + let index = root._modelIndex(nRow, 0) + if (assetsModel.isDirectory(index) && !root.isExpanded(nRow)) { + root.expand(nRow); + expandedAny = true + } + } + + if (!expandedAny) + Qt.callLater(root.forceLayout) + } + + function expandAll() + { + // In order for _doExpandAll() to be called repeatedly (every time a new node is + // loaded, and then, expanded), we need to set requestedExpandAll to true. + root.requestedExpandAll = true + root._doExpandAll() + } + + function collapseAll() + { + root.resetVerticalScrollPosition() + + // collapse all, except for the root path - from the last item (leaves) up to the root + for (let nRow = root.rows - 1; nRow >= 0; --nRow) { + let index = root._modelIndex(nRow, 0) + // we don't want to collapse the root path, because doing so will hide the contents + // of the tree. + if (assetsModel.filePath(index) === assetsModel.rootPath()) + break + + root.collapse(nRow) + } + } + + // workaround for a bug -- might be fixed by https://codereview.qt-project.org/c/qt/qtdeclarative/+/442721 + onContentHeightChanged: { + if (root.contentHeight <= root.height) { + let first = root.itemAtCell(0, root.firstRow) + if (!first) + root.contentY = 0 + } + } + + function computeAllExpandedState() + { + var dirsWithChildren = [...Array(root.rows).keys()].filter(row => { + let index = root._modelIndex(row, 0) + return assetsModel.isDirectory(index) && assetsModel.hasChildren(index) + }) + + var countExpanded = dirsWithChildren.filter(row => root.isExpanded(row)).length + + if (countExpanded === dirsWithChildren.length) + return "all_expanded" + + if (countExpanded === 0) + return "all_collapsed" + return "" + } + + function startDropHoverOver(row) + { + let index = root._modelIndex(row, 0) + if (assetsModel.isDirectory(index)) + return + + let parentItem = root._getDelegateParentForIndex(index) + parentItem.hasChildWithDropHover = true + } + + function endDropHover(row) + { + let index = root._modelIndex(row, 0) + if (assetsModel.isDirectory(index)) + return + + let parentItem = root._getDelegateParentForIndex(index) + parentItem.hasChildWithDropHover = false + } + + function isAssetSelected(itemPath) + { + return root.selectedAssets[itemPath] ? true : false + } + + function clearSelectedAssets() + { + root.selectedAssets = {} + } + + function setAssetSelected(itemPath, selected) + { + root.selectedAssets[itemPath] = selected + root.selectedAssetsChanged() + } + + function _getDelegateParentForIndex(index) + { + let parentIndex = assetsModel.parentDirIndex(index) + let parentCell = root.cellAtIndex(parentIndex) + return root.itemAtCell(parentCell) + } + + function _modelIndex(row) + { + // The modelIndex() function exists since 6.3. In Qt 6.3, this modelIndex() function was a + // member of the TreeView, while in Qt6.4 it was moved to TableView. In Qt6.4, the order of + // the arguments was changed. + if (assetsRoot.qtVersionAtLeast6_4) + return root.modelIndex(0, row) + else + return root.modelIndex(row, 0) + } + + delegate: AssetDelegate { + assetsView: root + assetsRoot: root.assetsRoot + indentation: 5 } -} +} // TreeView diff --git a/share/qtcreator/qmldesigner/itemLibraryQmlSources/ConfirmDeleteFolderDialog.qml b/share/qtcreator/qmldesigner/itemLibraryQmlSources/ConfirmDeleteFolderDialog.qml index a4fd300975..c623af862f 100644 --- a/share/qtcreator/qmldesigner/itemLibraryQmlSources/ConfirmDeleteFolderDialog.qml +++ b/share/qtcreator/qmldesigner/itemLibraryQmlSources/ConfirmDeleteFolderDialog.qml @@ -1,38 +1,13 @@ -/**************************************************************************** -** -** Copyright (C) 2022 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. -** -****************************************************************************/ +// Copyright (C) 2022 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0 WITH Qt-GPL-exception-1.0 import QtQuick import QtQuick.Controls -import QtQuick.Layouts -import QtQuickDesignerTheme -import HelperWidgets -import StudioControls as StudioControls +import HelperWidgets as HelperWidgets import StudioTheme as StudioTheme Dialog { - id: confirmDeleteFolderDialog + id: root title: qsTr("Folder Not Empty") anchors.centerIn: parent @@ -40,6 +15,9 @@ Dialog { implicitWidth: 300 modal: true + required property string dirName + required property var dirIndex + contentItem: Column { spacing: 20 width: parent.width @@ -47,11 +25,10 @@ Dialog { Text { id: folderNotEmpty - text: qsTr("Folder \"%1\" is not empty. Delete it anyway?") - .arg(root.contextDir ? root.contextDir.dirName : "") + text: qsTr("Folder \"%1\" is not empty. Delete it anyway?").arg(root.dirName) color: StudioTheme.Values.themeTextColor wrapMode: Text.WordWrap - width: confirmDeleteFolderDialog.width + width: root.width leftPadding: 10 rightPadding: 10 @@ -63,27 +40,27 @@ Dialog { text: qsTr("If the folder has assets in use, deleting it might cause the project to not work correctly.") color: StudioTheme.Values.themeTextColor wrapMode: Text.WordWrap - width: confirmDeleteFolderDialog.width + width: root.width leftPadding: 10 rightPadding: 10 } Row { anchors.right: parent.right - Button { + HelperWidgets.Button { id: btnDelete text: qsTr("Delete") onClicked: { - assetsModel.deleteFolder(root.contextDir.dirPath) - confirmDeleteFolderDialog.accept() + assetsModel.deleteFolderRecursively(root.dirIndex) + root.accept() } } - Button { + HelperWidgets.Button { text: qsTr("Cancel") - onClicked: confirmDeleteFolderDialog.reject() + onClicked: root.reject() } } } diff --git a/share/qtcreator/qmldesigner/itemLibraryQmlSources/ErrorDialog.qml b/share/qtcreator/qmldesigner/itemLibraryQmlSources/ErrorDialog.qml new file mode 100644 index 0000000000..ce5989e2e6 --- /dev/null +++ b/share/qtcreator/qmldesigner/itemLibraryQmlSources/ErrorDialog.qml @@ -0,0 +1,40 @@ +// Copyright (C) 2022 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0+ OR GPL-3.0 WITH Qt-GPL-exception-1.0 + +import QtQuick +import QtQuick.Controls +import HelperWidgets as HelperWidgets +import StudioTheme as StudioTheme + +Dialog { + id: root + + required property string message + + anchors.centerIn: parent + closePolicy: Popup.CloseOnEscape + implicitWidth: 300 + modal: true + + contentItem: Column { + spacing: 20 + width: parent.width + + Text { + text: root.message + color: StudioTheme.Values.themeTextColor + wrapMode: Text.WordWrap + width: root.width + leftPadding: 10 + rightPadding: 10 + } + + HelperWidgets.Button { + text: qsTr("Close") + anchors.right: parent.right + onClicked: root.reject() + } + } + + onOpened: root.forceActiveFocus() +} diff --git a/share/qtcreator/qmldesigner/itemLibraryQmlSources/NewFolderDialog.qml b/share/qtcreator/qmldesigner/itemLibraryQmlSources/NewFolderDialog.qml index 130026ddce..c48bb93a5b 100644 --- a/share/qtcreator/qmldesigner/itemLibraryQmlSources/NewFolderDialog.qml +++ b/share/qtcreator/qmldesigner/itemLibraryQmlSources/NewFolderDialog.qml @@ -1,44 +1,35 @@ -/**************************************************************************** -** -** Copyright (C) 2022 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. -** -****************************************************************************/ +// Copyright (C) 2022 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0 WITH Qt-GPL-exception-1.0 import QtQuick import QtQuick.Controls -import QtQuick.Layouts -import QtQuickDesignerTheme -import HelperWidgets +import HelperWidgets as HelperWidgets import StudioControls as StudioControls import StudioTheme as StudioTheme Dialog { - id: newFolderDialog + id: root title: qsTr("Create New Folder") anchors.centerIn: parent closePolicy: Popup.CloseOnEscape modal: true + required property string dirPath + property string createdDirPath: "" + readonly property int _maxPath: 260 + + HelperWidgets.RegExpValidator { + id: folderNameValidator + regExp: /^(\w[^*/><?\\|:]*)$/ + } + + ErrorDialog { + id: creationFailedDialog + title: qsTr("Could not create folder") + message: qsTr("An error occurred while trying to create the folder.") + } + contentItem: Column { spacing: 2 @@ -58,6 +49,10 @@ Dialog { Keys.onEnterPressed: btnCreate.onClicked() Keys.onReturnPressed: btnCreate.onClicked() + + onTextChanged: { + root.createdDirPath = root.dirPath + '/' + folderName.text + } } } @@ -68,6 +63,13 @@ Dialog { visible: folderName.text === "" } + Text { + text: qsTr("Folder path is too long.") + color: "#ff0000" + anchors.right: parent.right + visible: root.createdDirPath.length > root._maxPath + } + Item { // spacer width: 1 height: 20 @@ -76,20 +78,23 @@ Dialog { Row { anchors.right: parent.right - Button { + HelperWidgets.Button { id: btnCreate text: qsTr("Create") - enabled: folderName.text !== "" + enabled: folderName.text !== "" && root.createdDirPath.length <= root._maxPath onClicked: { - assetsModel.addNewFolder(root.contextDir.dirPath + '/' + folderName.text) - newFolderDialog.accept() + root.createdDirPath = root.dirPath + '/' + folderName.text + if (assetsModel.addNewFolder(root.createdDirPath)) + root.accept() + else + creationFailedDialog.open() } } - Button { + HelperWidgets.Button { text: qsTr("Cancel") - onClicked: newFolderDialog.reject() + onClicked: root.reject() } } } @@ -99,4 +104,8 @@ Dialog { folderName.selectAll() folderName.forceActiveFocus() } + + onRejected: { + root.createdDirPath = "" + } } diff --git a/share/qtcreator/qmldesigner/itemLibraryQmlSources/RenameFolderDialog.qml b/share/qtcreator/qmldesigner/itemLibraryQmlSources/RenameFolderDialog.qml index 351c0a35fc..e94ba47f73 100644 --- a/share/qtcreator/qmldesigner/itemLibraryQmlSources/RenameFolderDialog.qml +++ b/share/qtcreator/qmldesigner/itemLibraryQmlSources/RenameFolderDialog.qml @@ -1,38 +1,14 @@ -/**************************************************************************** -** -** Copyright (C) 2022 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. -** -****************************************************************************/ +// Copyright (C) 2022 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0 WITH Qt-GPL-exception-1.0 import QtQuick import QtQuick.Controls -import QtQuick.Layouts -import QtQuickDesignerTheme -import HelperWidgets +import HelperWidgets as HelperWidgets import StudioControls as StudioControls import StudioTheme as StudioTheme Dialog { - id: renameFolderDialog + id: root title: qsTr("Rename Folder") anchors.centerIn: parent @@ -41,6 +17,13 @@ Dialog { modal: true property bool renameError: false + required property string dirPath + required property string dirName + + HelperWidgets.RegExpValidator { + id: folderNameValidator + regExp: /^(\w[^*/><?\\|:]*)$/ + } contentItem: Column { spacing: 2 @@ -50,10 +33,10 @@ Dialog { actionIndicator.visible: false translationIndicator.visible: false - width: renameFolderDialog.width - 12 + width: root.width - 12 validator: folderNameValidator - onEditChanged: renameFolderDialog.renameError = false + onEditChanged: root.renameError = false Keys.onEnterPressed: btnRename.onClicked() Keys.onReturnPressed: btnRename.onClicked() } @@ -61,15 +44,15 @@ Dialog { Text { text: qsTr("Folder name cannot be empty.") color: "#ff0000" - visible: folderRename.text === "" && !renameFolderDialog.renameError + visible: folderRename.text === "" && !root.renameError } Text { text: qsTr("Could not rename folder. Make sure no folder with the same name exists.") wrapMode: Text.WordWrap - width: renameFolderDialog.width - 12 + width: root.width - 12 color: "#ff0000" - visible: renameFolderDialog.renameError + visible: root.renameError } Item { // spacer @@ -81,7 +64,7 @@ Dialog { text: qsTr("If the folder has assets in use, renaming it might cause the project to not work correctly.") color: StudioTheme.Values.themeTextColor wrapMode: Text.WordWrap - width: renameFolderDialog.width + width: root.width leftPadding: 10 rightPadding: 10 } @@ -94,31 +77,31 @@ Dialog { Row { anchors.right: parent.right - Button { + HelperWidgets.Button { id: btnRename text: qsTr("Rename") enabled: folderRename.text !== "" onClicked: { - var success = assetsModel.renameFolder(root.contextDir.dirPath, folderRename.text) + var success = assetsModel.renameFolder(root.dirPath, folderRename.text) if (success) - renameFolderDialog.accept() + root.accept() - renameFolderDialog.renameError = !success + root.renameError = !success } } - Button { + HelperWidgets.Button { text: qsTr("Cancel") - onClicked: renameFolderDialog.reject() + onClicked: root.reject() } } } onOpened: { - folderRename.text = root.contextDir.dirName + folderRename.text = root.dirName folderRename.selectAll() folderRename.forceActiveFocus() - renameFolderDialog.renameError = false + root.renameError = false } } diff --git a/src/plugins/qmldesigner/CMakeLists.txt b/src/plugins/qmldesigner/CMakeLists.txt index c2c3c941b2..7abed7212d 100644 --- a/src/plugins/qmldesigner/CMakeLists.txt +++ b/src/plugins/qmldesigner/CMakeLists.txt @@ -740,9 +740,6 @@ extend_qtc_plugin(QmlDesigner assetslibrarywidget.cpp assetslibrarywidget.h assetslibrarymodel.cpp assetslibrarymodel.h assetslibraryiconprovider.cpp assetslibraryiconprovider.h - assetslibrarydir.cpp assetslibrarydir.h - assetslibrarydirsmodel.cpp assetslibrarydirsmodel.h - assetslibraryfilesmodel.cpp assetslibraryfilesmodel.h ) extend_qtc_plugin(QmlDesigner diff --git a/src/plugins/qmldesigner/components/assetslibrary/assetslibrarydir.cpp b/src/plugins/qmldesigner/components/assetslibrary/assetslibrarydir.cpp deleted file mode 100644 index a41ce9ce01..0000000000 --- a/src/plugins/qmldesigner/components/assetslibrary/assetslibrarydir.cpp +++ /dev/null @@ -1,75 +0,0 @@ -// Copyright (C) 2021 The Qt Company Ltd. -// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0+ OR GPL-3.0 WITH Qt-GPL-exception-1.0 - -#include "assetslibrarydir.h" -#include "assetslibrarydirsmodel.h" -#include "assetslibraryfilesmodel.h" - -namespace QmlDesigner { - -AssetsLibraryDir::AssetsLibraryDir(const QString &path, int depth, bool expanded, QObject *parent) - : QObject(parent) - , m_dirPath(path) - , m_dirDepth(depth) - , m_dirExpanded(expanded) -{ - -} - -QString AssetsLibraryDir::dirName() const { return m_dirPath.split('/').last(); } -QString AssetsLibraryDir::dirPath() const { return m_dirPath; } -int AssetsLibraryDir::dirDepth() const { return m_dirDepth; } -bool AssetsLibraryDir::dirExpanded() const { return m_dirExpanded; } -bool AssetsLibraryDir::dirVisible() const { return m_dirVisible; } - -void AssetsLibraryDir::setDirExpanded(bool expand) -{ - if (m_dirExpanded != expand) { - m_dirExpanded = expand; - emit dirExpandedChanged(); - } -} - -void AssetsLibraryDir::setDirVisible(bool visible) -{ - if (m_dirVisible != visible) { - m_dirVisible = visible; - emit dirVisibleChanged(); - } -} - -QObject *AssetsLibraryDir::filesModel() const -{ - return m_filesModel; -} - -QObject *AssetsLibraryDir::dirsModel() const -{ - return m_dirsModel; -} - -QList<AssetsLibraryDir *> AssetsLibraryDir::childAssetsDirs() const -{ - if (m_dirsModel) - return m_dirsModel->assetsDirs(); - - return {}; -} - -void AssetsLibraryDir::addDir(AssetsLibraryDir *assetsDir) -{ - if (!m_dirsModel) - m_dirsModel = new AssetsLibraryDirsModel(this); - - m_dirsModel->addDir(assetsDir); -} - -void AssetsLibraryDir::addFile(const QString &filePath) -{ - if (!m_filesModel) - m_filesModel = new AssetsLibraryFilesModel(this); - - m_filesModel->addFile(filePath); -} - -} // namespace QmlDesigner diff --git a/src/plugins/qmldesigner/components/assetslibrary/assetslibrarydir.h b/src/plugins/qmldesigner/components/assetslibrary/assetslibrarydir.h deleted file mode 100644 index eb98c70023..0000000000 --- a/src/plugins/qmldesigner/components/assetslibrary/assetslibrarydir.h +++ /dev/null @@ -1,63 +0,0 @@ -// Copyright (C) 2021 The Qt Company Ltd. -// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0+ OR GPL-3.0 WITH Qt-GPL-exception-1.0 - -#pragma once - -#include <QObject> - -namespace QmlDesigner { - -class AssetsLibraryDirsModel; -class AssetsLibraryFilesModel; - -class AssetsLibraryDir : public QObject -{ - Q_OBJECT - - Q_PROPERTY(QString dirName READ dirName NOTIFY dirNameChanged) - Q_PROPERTY(QString dirPath READ dirPath NOTIFY dirPathChanged) - Q_PROPERTY(bool dirExpanded READ dirExpanded WRITE setDirExpanded NOTIFY dirExpandedChanged) - Q_PROPERTY(bool dirVisible READ dirVisible WRITE setDirVisible NOTIFY dirVisibleChanged) - Q_PROPERTY(int dirDepth READ dirDepth NOTIFY dirDepthChanged) - Q_PROPERTY(QObject *filesModel READ filesModel NOTIFY filesModelChanged) - Q_PROPERTY(QObject *dirsModel READ dirsModel NOTIFY dirsModelChanged) - -public: - AssetsLibraryDir(const QString &path, int depth, bool expanded = true, QObject *parent = nullptr); - - QString dirName() const; - QString dirPath() const; - int dirDepth() const; - - bool dirExpanded() const; - bool dirVisible() const; - void setDirExpanded(bool expand); - void setDirVisible(bool visible); - - QObject *filesModel() const; - QObject *dirsModel() const; - - QList<AssetsLibraryDir *> childAssetsDirs() const; - - void addDir(AssetsLibraryDir *assetsDir); - void addFile(const QString &filePath); - -signals: - void dirNameChanged(); - void dirPathChanged(); - void dirDepthChanged(); - void dirExpandedChanged(); - void dirVisibleChanged(); - void filesModelChanged(); - void dirsModelChanged(); - -private: - QString m_dirPath; - int m_dirDepth = 0; - bool m_dirExpanded = true; - bool m_dirVisible = true; - AssetsLibraryDirsModel *m_dirsModel = nullptr; - AssetsLibraryFilesModel *m_filesModel = nullptr; -}; - -} // namespace QmlDesigner diff --git a/src/plugins/qmldesigner/components/assetslibrary/assetslibrarydirsmodel.cpp b/src/plugins/qmldesigner/components/assetslibrary/assetslibrarydirsmodel.cpp deleted file mode 100644 index 499380accb..0000000000 --- a/src/plugins/qmldesigner/components/assetslibrary/assetslibrarydirsmodel.cpp +++ /dev/null @@ -1,71 +0,0 @@ -// Copyright (C) 2021 The Qt Company Ltd. -// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0+ OR GPL-3.0 WITH Qt-GPL-exception-1.0 - -#include "assetslibrarydirsmodel.h" -#include "assetslibrarymodel.h" - -#include <QDebug> -#include <QMetaProperty> - -namespace QmlDesigner { - -AssetsLibraryDirsModel::AssetsLibraryDirsModel(QObject *parent) - : QAbstractListModel(parent) -{ - // add roles - const QMetaObject meta = AssetsLibraryDir::staticMetaObject; - for (int i = meta.propertyOffset(); i < meta.propertyCount(); ++i) - m_roleNames.insert(i, meta.property(i).name()); -} - -QVariant AssetsLibraryDirsModel::data(const QModelIndex &index, int role) const -{ - if (!index.isValid()) { - qWarning() << Q_FUNC_INFO << "Invalid index requested: " << QString::number(index.row()); - return {}; - } - - if (m_roleNames.contains(role)) - return m_dirs[index.row()]->property(m_roleNames[role]); - - qWarning() << Q_FUNC_INFO << "Invalid role requested: " << QString::number(role); - return {}; -} - -bool AssetsLibraryDirsModel::setData(const QModelIndex &index, const QVariant &value, int role) -{ - // currently only dirExpanded property is updatable - if (index.isValid() && m_roleNames.contains(role)) { - QVariant currValue = m_dirs.at(index.row())->property(m_roleNames.value(role)); - if (currValue != value) { - m_dirs.at(index.row())->setProperty(m_roleNames.value(role), value); - if (m_roleNames.value(role) == "dirExpanded") - AssetsLibraryModel::saveExpandedState(value.toBool(), m_dirs.at(index.row())->dirPath()); - emit dataChanged(index, index, {role}); - return true; - } - } - return false; -} - -int AssetsLibraryDirsModel::rowCount([[maybe_unused]] const QModelIndex &parent) const -{ - return m_dirs.size(); -} - -QHash<int, QByteArray> AssetsLibraryDirsModel::roleNames() const -{ - return m_roleNames; -} - -void AssetsLibraryDirsModel::addDir(AssetsLibraryDir *assetsDir) -{ - m_dirs.append(assetsDir); -} - -const QList<AssetsLibraryDir *> AssetsLibraryDirsModel::assetsDirs() const -{ - return m_dirs; -} - -} // namespace QmlDesigner diff --git a/src/plugins/qmldesigner/components/assetslibrary/assetslibrarydirsmodel.h b/src/plugins/qmldesigner/components/assetslibrary/assetslibrarydirsmodel.h deleted file mode 100644 index 7939d1ea5b..0000000000 --- a/src/plugins/qmldesigner/components/assetslibrary/assetslibrarydirsmodel.h +++ /dev/null @@ -1,32 +0,0 @@ -// Copyright (C) 2021 The Qt Company Ltd. -// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0+ OR GPL-3.0 WITH Qt-GPL-exception-1.0 - -#pragma once - -#include <QAbstractListModel> -#include "assetslibrarydir.h" - -namespace QmlDesigner { - -class AssetsLibraryDirsModel : public QAbstractListModel -{ - Q_OBJECT - -public: - AssetsLibraryDirsModel(QObject *parent = nullptr); - - QVariant data(const QModelIndex & index, int role = Qt::DisplayRole) const override; - bool setData(const QModelIndex &index, const QVariant &value, int role) override; - int rowCount(const QModelIndex & parent = QModelIndex()) const override; - QHash<int, QByteArray> roleNames() const override; - - void addDir(AssetsLibraryDir *assetsDir); - - const QList<AssetsLibraryDir *> assetsDirs() const; - -private: - QList<AssetsLibraryDir *> m_dirs; - QHash<int, QByteArray> m_roleNames; -}; - -} // namespace QmlDesigner diff --git a/src/plugins/qmldesigner/components/assetslibrary/assetslibraryfilesmodel.cpp b/src/plugins/qmldesigner/components/assetslibrary/assetslibraryfilesmodel.cpp deleted file mode 100644 index bf8824ae36..0000000000 --- a/src/plugins/qmldesigner/components/assetslibrary/assetslibraryfilesmodel.cpp +++ /dev/null @@ -1,53 +0,0 @@ -// Copyright (C) 2021 The Qt Company Ltd. -// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0+ OR GPL-3.0 WITH Qt-GPL-exception-1.0 -#include "assetslibraryfilesmodel.h" - -#include <QDebug> - -namespace QmlDesigner { - -AssetsLibraryFilesModel::AssetsLibraryFilesModel(QObject *parent) - : QAbstractListModel(parent) -{ - // add roles - m_roleNames.insert(FileNameRole, "fileName"); - m_roleNames.insert(FilePathRole, "filePath"); - m_roleNames.insert(FileDirRole, "fileDir"); -} - -QVariant AssetsLibraryFilesModel::data(const QModelIndex &index, int role) const -{ - if (!index.isValid()) { - qWarning() << Q_FUNC_INFO << "Invalid index requested: " << QString::number(index.row()); - return {}; - } - - if (role == FileNameRole) - return m_files[index.row()].split('/').last(); - - if (role == FilePathRole) - return m_files[index.row()]; - - if (role == FileDirRole) - return QVariant::fromValue(parent()); - - qWarning() << Q_FUNC_INFO << "Invalid role requested: " << QString::number(role); - return {}; -} - -int AssetsLibraryFilesModel::rowCount([[maybe_unused]] const QModelIndex &parent) const -{ - return m_files.size(); -} - -QHash<int, QByteArray> AssetsLibraryFilesModel::roleNames() const -{ - return m_roleNames; -} - -void AssetsLibraryFilesModel::addFile(const QString &filePath) -{ - m_files.append(filePath); -} - -} // namespace QmlDesigner diff --git a/src/plugins/qmldesigner/components/assetslibrary/assetslibraryfilesmodel.h b/src/plugins/qmldesigner/components/assetslibrary/assetslibraryfilesmodel.h deleted file mode 100644 index 103e57ecd2..0000000000 --- a/src/plugins/qmldesigner/components/assetslibrary/assetslibraryfilesmodel.h +++ /dev/null @@ -1,32 +0,0 @@ -// Copyright (C) 2021 The Qt Company Ltd. -// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0+ OR GPL-3.0 WITH Qt-GPL-exception-1.0 - -#pragma once - -#include <QAbstractListModel> - -namespace QmlDesigner { - -class AssetsLibraryFilesModel : public QAbstractListModel -{ - Q_OBJECT - -public: - AssetsLibraryFilesModel(QObject *parent = nullptr); - - QVariant data(const QModelIndex & index, int role = Qt::DisplayRole) const override; - int rowCount(const QModelIndex & parent = QModelIndex()) const override; - QHash<int, QByteArray> roleNames() const override; - - void addFile(const QString &filePath); - -private: - enum Roles {FileNameRole = Qt::UserRole + 1, - FilePathRole, - FileDirRole}; - - QStringList m_files; - QHash<int, QByteArray> m_roleNames; -}; - -} // QmlDesigner diff --git a/src/plugins/qmldesigner/components/assetslibrary/assetslibraryiconprovider.cpp b/src/plugins/qmldesigner/components/assetslibrary/assetslibraryiconprovider.cpp index 90f3b011ff..bc56e9b7f3 100644 --- a/src/plugins/qmldesigner/components/assetslibrary/assetslibraryiconprovider.cpp +++ b/src/plugins/qmldesigner/components/assetslibrary/assetslibraryiconprovider.cpp @@ -20,15 +20,48 @@ AssetsLibraryIconProvider::AssetsLibraryIconProvider(SynchronousImageCache &font QPixmap AssetsLibraryIconProvider::requestPixmap(const QString &id, QSize *size, const QSize &requestedSize) { QPixmap pixmap; + + if (m_thumbnails.contains(id)) { + pixmap = m_thumbnails[id]; + } else { + pixmap = fetchPixmap(id, requestedSize); + if (pixmap.isNull()) + pixmap = Utils::StyleHelper::dpiSpecificImageFile(":/AssetsLibrary/images/assets_default.png"); + + if (requestedSize.isValid()) + pixmap = pixmap.scaled(requestedSize, Qt::KeepAspectRatio); + + m_thumbnails[id] = pixmap; + } + + if (size) { + size->setWidth(pixmap.width()); + size->setHeight(pixmap.height()); + } + + return pixmap; +} + +QPixmap AssetsLibraryIconProvider::generateFontIcons(const QString &filePath, const QSize &requestedSize) const +{ + QSize reqSize = requestedSize.isValid() ? requestedSize : QSize{48, 48}; + return m_fontImageCache.icon(filePath, {}, + ImageCache::FontCollectorSizesAuxiliaryData{Utils::span{iconSizes}, + Theme::getColor(Theme::DStextColor).name(), + "Abc"}).pixmap(reqSize); +} + +QPixmap AssetsLibraryIconProvider::fetchPixmap(const QString &id, const QSize &requestedSize) const +{ const QString suffix = "*." + id.split('.').last().toLower(); if (id == "browse") { - pixmap = Utils::StyleHelper::dpiSpecificImageFile(":/AssetsLibrary/images/browse.png"); + return Utils::StyleHelper::dpiSpecificImageFile(":/AssetsLibrary/images/browse.png"); } else if (AssetsLibraryModel::supportedFontSuffixes().contains(suffix)) { - pixmap = generateFontIcons(id, requestedSize); + return generateFontIcons(id, requestedSize); } else if (AssetsLibraryModel::supportedImageSuffixes().contains(suffix)) { - pixmap = Utils::StyleHelper::dpiSpecificImageFile(id); + return Utils::StyleHelper::dpiSpecificImageFile(id); } else if (AssetsLibraryModel::supportedTexture3DSuffixes().contains(suffix)) { - pixmap = HdrImage{id}.toPixmap(); + return HdrImage{id}.toPixmap(); } else { QString type; if (AssetsLibraryModel::supportedShaderSuffixes().contains(suffix)) @@ -43,31 +76,20 @@ QPixmap AssetsLibraryIconProvider::requestPixmap(const QString &id, QSize *size, QString pathTemplate = QString(":/AssetsLibrary/images/asset_%1%2.png").arg(type); QString path = pathTemplate.arg('_' + QString::number(requestedSize.width())); - pixmap = Utils::StyleHelper::dpiSpecificImageFile(QFileInfo::exists(path) ? path - : pathTemplate.arg("")); - } - - if (size) { - size->setWidth(pixmap.width()); - size->setHeight(pixmap.height()); + return Utils::StyleHelper::dpiSpecificImageFile(QFileInfo::exists(path) + ? path + : pathTemplate.arg("")); } +} - if (pixmap.isNull()) - pixmap = Utils::StyleHelper::dpiSpecificImageFile(":/AssetsLibrary/images/assets_default.png"); - - if (requestedSize.isValid()) - return pixmap.scaled(requestedSize, Qt::KeepAspectRatio); - - return pixmap; +void AssetsLibraryIconProvider::clearCache() +{ + m_thumbnails.clear(); } -QPixmap AssetsLibraryIconProvider::generateFontIcons(const QString &filePath, const QSize &requestedSize) const +void AssetsLibraryIconProvider::invalidateThumbnail(const QString &id) { - QSize reqSize = requestedSize.isValid() ? requestedSize : QSize{48, 48}; - return m_fontImageCache.icon(filePath, {}, - ImageCache::FontCollectorSizesAuxiliaryData{Utils::span{iconSizes}, - Theme::getColor(Theme::DStextColor).name(), - "Abc"}).pixmap(reqSize); + m_thumbnails.remove(id); } } // namespace QmlDesigner diff --git a/src/plugins/qmldesigner/components/assetslibrary/assetslibraryiconprovider.h b/src/plugins/qmldesigner/components/assetslibrary/assetslibraryiconprovider.h index cf473c61e3..b18d8e7011 100644 --- a/src/plugins/qmldesigner/components/assetslibrary/assetslibraryiconprovider.h +++ b/src/plugins/qmldesigner/components/assetslibrary/assetslibraryiconprovider.h @@ -15,9 +15,12 @@ public: AssetsLibraryIconProvider(SynchronousImageCache &fontImageCache); QPixmap requestPixmap(const QString &id, QSize *size, const QSize &requestedSize) override; + void clearCache(); + void invalidateThumbnail(const QString &id); private: QPixmap generateFontIcons(const QString &filePath, const QSize &requestedSize) const; + QPixmap fetchPixmap(const QString &id, const QSize &requestedSize) const; SynchronousImageCache &m_fontImageCache; @@ -26,6 +29,7 @@ private: std::vector<QSize> iconSizes = {{128, 128}, // Drag {96, 96}, // list @2x {48, 48}}; // list + QHash<QString, QPixmap> m_thumbnails; }; } // namespace QmlDesigner diff --git a/src/plugins/qmldesigner/components/assetslibrary/assetslibrarymodel.cpp b/src/plugins/qmldesigner/components/assetslibrary/assetslibrarymodel.cpp index e18ce4352f..d1b93448b7 100644 --- a/src/plugins/qmldesigner/components/assetslibrary/assetslibrarymodel.cpp +++ b/src/plugins/qmldesigner/components/assetslibrary/assetslibrarymodel.cpp @@ -1,68 +1,44 @@ // Copyright (C) 2021 The Qt Company Ltd. // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0+ OR GPL-3.0 WITH Qt-GPL-exception-1.0 +#include <QCheckBox> +#include <QFileInfo> +#include <QFileSystemModel> +#include <QImageReader> +#include <QMessageBox> +#include <QSortFilterProxyModel> + #include "assetslibrarymodel.h" -#include "assetslibrarydirsmodel.h" -#include "assetslibraryfilesmodel.h" - -#include <designersettings.h> -#include <documentmanager.h> -#include <synchronousimagecache.h> -#include <theme.h> -#include <utils/hdrimage.h> + #include <qmldesignerplugin.h> #include <modelnodeoperations.h> #include <coreplugin/icore.h> - -#include <utils/filesystemwatcher.h> -#include <utils/stylehelper.h> - -#include <QCheckBox> -#include <QDebug> -#include <QDir> -#include <QDirIterator> -#include <QElapsedTimer> -#include <QFont> -#include <QImageReader> -#include <QLoggingCategory> -#include <QMessageBox> -#include <QMetaProperty> -#include <QPainter> -#include <QRawFont> -#include <QRegularExpression> - -static Q_LOGGING_CATEGORY(assetsLibraryBenchmark, "qtc.assetsLibrary.setRoot", QtWarningMsg) +#include <utils/qtcassert.h> namespace QmlDesigner { -AssetsLibraryModel::AssetsLibraryModel(Utils::FileSystemWatcher *fileSystemWatcher, QObject *parent) - : QAbstractListModel(parent) - , m_fileSystemWatcher(fileSystemWatcher) +AssetsLibraryModel::AssetsLibraryModel(QObject *parent) + : QSortFilterProxyModel{parent} { - // add role names - int role = 0; - const QMetaObject meta = AssetsLibraryDir::staticMetaObject; - for (int i = meta.propertyOffset(); i < meta.propertyCount(); ++i) - m_roleNames.insert(role++, meta.property(i).name()); -} + createBackendModel(); -void AssetsLibraryModel::setSearchText(const QString &searchText) -{ - if (m_searchText != searchText) { - m_searchText = searchText; - refresh(); - } + setRecursiveFilteringEnabled(true); } -void AssetsLibraryModel::saveExpandedState(bool expanded, const QString &assetPath) +void AssetsLibraryModel::createBackendModel() { - m_expandedStateHash.insert(assetPath, expanded); -} + m_sourceFsModel = new QFileSystemModel(parent()); -bool AssetsLibraryModel::loadExpandedState(const QString &assetPath) -{ - return m_expandedStateHash.value(assetPath, true); + m_sourceFsModel->setReadOnly(false); + + setSourceModel(m_sourceFsModel); + QObject::connect(m_sourceFsModel, &QFileSystemModel::directoryLoaded, this, &AssetsLibraryModel::directoryLoaded); + QObject::connect(m_sourceFsModel, &QFileSystemModel::dataChanged, this, &AssetsLibraryModel::onDataChanged); + + QObject::connect(m_sourceFsModel, &QFileSystemModel::directoryLoaded, this, [this](const QString &dir) { + syncHaveFiles(); + }); } bool AssetsLibraryModel::isEffectQmlExist(const QString &effectName) @@ -72,50 +48,57 @@ bool AssetsLibraryModel::isEffectQmlExist(const QString &effectName) return qmlPath.exists(); } -AssetsLibraryModel::DirExpandState AssetsLibraryModel::getAllExpandedState() const +void AssetsLibraryModel::onDataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight, + const QList<int> &roles) { - const auto keys = m_expandedStateHash.keys(); - bool allExpanded = true; - bool allCollapsed = true; - for (const QString &assetPath : keys) { - bool expanded = m_expandedStateHash.value(assetPath); - - if (expanded) - allCollapsed = false; - if (!expanded) - allExpanded = false; + for (int i = topLeft.row(); i <= bottomRight.row(); ++i) { + QModelIndex index = m_sourceFsModel->index(i, 0, topLeft.parent()); + QString path = m_sourceFsModel->filePath(index); - if (!allCollapsed && !allExpanded) - break; + if (!isDirectory(path)) + emit fileChanged(path); } +} + +void AssetsLibraryModel::destroyBackendModel() +{ + setSourceModel(nullptr); + m_sourceFsModel->disconnect(this); + m_sourceFsModel->deleteLater(); + m_sourceFsModel = nullptr; +} - return allExpanded ? DirExpandState::AllExpanded : allCollapsed ? DirExpandState::AllCollapsed - : DirExpandState::SomeExpanded; +void AssetsLibraryModel::setSearchText(const QString &searchText) +{ + m_searchText = searchText; + resetModel(); } -void AssetsLibraryModel::toggleExpandAll(bool expand) +bool AssetsLibraryModel::indexIsValid(const QModelIndex &index) const { - std::function<void(AssetsLibraryDir *)> expandDirRecursive; - expandDirRecursive = [&](AssetsLibraryDir *currAssetsDir) { - if (currAssetsDir->dirDepth() > 0) { - currAssetsDir->setDirExpanded(expand); - saveExpandedState(expand, currAssetsDir->dirPath()); - } + static QModelIndex invalidIndex; + return index != invalidIndex; +} - const QList<AssetsLibraryDir *> childDirs = currAssetsDir->childAssetsDirs(); - for (const auto childDir : childDirs) - expandDirRecursive(childDir); - }; +QList<QModelIndex> AssetsLibraryModel::parentIndices(const QModelIndex &index) const +{ + QModelIndex idx = index; + QModelIndex rootIdx = rootIndex(); + QList<QModelIndex> result; - beginResetModel(); - expandDirRecursive(m_assetsDir); - endResetModel(); + while (idx.isValid() && idx != rootIdx) { + result += idx; + idx = idx.parent(); + } + + return result; } void AssetsLibraryModel::deleteFiles(const QStringList &filePaths) { - bool askBeforeDelete = QmlDesignerPlugin::settings().value( - DesignerSettingsKey::ASK_BEFORE_DELETING_ASSET).toBool(); + bool askBeforeDelete = QmlDesignerPlugin::settings() + .value(DesignerSettingsKey::ASK_BEFORE_DELETING_ASSET) + .toBool(); bool assetDelete = true; if (askBeforeDelete) { @@ -123,7 +106,7 @@ void AssetsLibraryModel::deleteFiles(const QStringList &filePaths) tr("File%1 might be in use. Delete anyway?\n\n%2") .arg(filePaths.size() > 1 ? QChar('s') : QChar()) .arg(filePaths.join('\n').remove(DocumentManager::currentProjectDirPath() - .toString().append('/'))), + .toString().append('/'))), QMessageBox::No | QMessageBox::Yes); QCheckBox cb; cb.setText(tr("Do not ask this again")); @@ -162,15 +145,13 @@ bool AssetsLibraryModel::renameFolder(const QString &folderPath, const QString & dir.cdUp(); - saveExpandedState(loadExpandedState(folderPath), dir.absoluteFilePath(newName)); - return dir.rename(oldName, newName); } -void AssetsLibraryModel::addNewFolder(const QString &folderPath) +bool AssetsLibraryModel::addNewFolder(const QString &folderPath) { QString iterPath = folderPath; - QRegularExpression rgx("\\d+$"); // matches a number at the end of a string + static QRegularExpression rgx("\\d+$"); // matches a number at the end of a string QDir dir{folderPath}; while (dir.exists()) { @@ -191,8 +172,8 @@ void AssetsLibraryModel::addNewFolder(const QString &folderPath) --nPaddingZeros; iterPath = folderPath.mid(0, match.capturedStart()) - + QString('0').repeated(nPaddingZeros) - + QString::number(num); + + QString('0').repeated(nPaddingZeros) + + QString::number(num); } else { iterPath = folderPath + '1'; } @@ -200,136 +181,155 @@ void AssetsLibraryModel::addNewFolder(const QString &folderPath) dir.setPath(iterPath); } - dir.mkpath(iterPath); + return dir.mkpath(iterPath); } -void AssetsLibraryModel::deleteFolder(const QString &folderPath) +bool AssetsLibraryModel::deleteFolderRecursively(const QModelIndex &folderIndex) { - QDir{folderPath}.removeRecursively(); -} + auto idx = mapToSource(folderIndex); + bool ok = m_sourceFsModel->remove(idx); + if (!ok) + qWarning() << __FUNCTION__ << " could not remove folder recursively: " << m_sourceFsModel->filePath(idx); -QObject *AssetsLibraryModel::rootDir() const -{ - return m_assetsDir; + return ok; } -bool AssetsLibraryModel::isEmpty() const +bool AssetsLibraryModel::filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const { - return m_isEmpty; -} + QString path = m_sourceFsModel->filePath(sourceParent); -void AssetsLibraryModel::setIsEmpty(bool empty) -{ - if (m_isEmpty != empty) { - m_isEmpty = empty; - emit isEmptyChanged(); + QModelIndex sourceIdx = m_sourceFsModel->index(sourceRow, 0, sourceParent); + QString sourcePath = m_sourceFsModel->filePath(sourceIdx); + + if (!m_searchText.isEmpty() && path.startsWith(m_rootPath) && QFileInfo{path}.isDir()) { + QString sourceName = m_sourceFsModel->fileName(sourceIdx); + + return QFileInfo{sourcePath}.isFile() && sourceName.contains(m_searchText, Qt::CaseInsensitive); + } else { + return sourcePath.startsWith(m_rootPath) || m_rootPath.startsWith(sourcePath); } } -QVariant AssetsLibraryModel::data(const QModelIndex &index, int role) const +bool AssetsLibraryModel::checkHaveFiles(const QModelIndex &parentIdx) const { - if (!index.isValid()) { - qWarning() << Q_FUNC_INFO << "Invalid index requested: " << QString::number(index.row()); - return {}; - } + if (!parentIdx.isValid()) + return false; - if (m_roleNames.contains(role)) - return m_assetsDir ? m_assetsDir->property(m_roleNames.value(role)) : QVariant(""); + const int rowCount = this->rowCount(parentIdx); + for (int i = 0; i < rowCount; ++i) { + auto newIdx = this->index(i, 0, parentIdx); + if (!isDirectory(newIdx)) + return true; - qWarning() << Q_FUNC_INFO << "Invalid role requested: " << QString::number(role); - return {}; + if (checkHaveFiles(newIdx)) + return true; + } + + return false; } -int AssetsLibraryModel::rowCount([[maybe_unused]] const QModelIndex &parent) const +void AssetsLibraryModel::setHaveFiles(bool value) { - return 1; + if (m_haveFiles != value) { + m_haveFiles = value; + emit haveFilesChanged(); + } } -QHash<int, QByteArray> AssetsLibraryModel::roleNames() const +bool AssetsLibraryModel::checkHaveFiles() const { - return m_roleNames; + auto rootIdx = indexForPath(m_rootPath); + return checkHaveFiles(rootIdx); } -// called when a directory is changed to refresh the model for this directory -void AssetsLibraryModel::refresh() +void AssetsLibraryModel::syncHaveFiles() { - setRootPath(m_assetsDir->dirPath()); + setHaveFiles(checkHaveFiles()); } -void AssetsLibraryModel::setRootPath(const QString &path) +void AssetsLibraryModel::setRootPath(const QString &newPath) { - QElapsedTimer time; - if (assetsLibraryBenchmark().isInfoEnabled()) - time.start(); - - qCInfo(assetsLibraryBenchmark) << "start:" << time.elapsed(); + beginResetModel(); - static const QStringList ignoredTopLevelDirs {"imports", "asset_imports"}; + destroyBackendModel(); + createBackendModel(); - m_fileSystemWatcher->clear(); + m_rootPath = newPath; + m_sourceFsModel->setRootPath(newPath); - std::function<bool(AssetsLibraryDir *, int, bool)> parseDir; - parseDir = [this, &parseDir](AssetsLibraryDir *currAssetsDir, int currDepth, bool recursive) { - m_fileSystemWatcher->addDirectory(currAssetsDir->dirPath(), Utils::FileSystemWatcher::WatchAllChanges); + m_sourceFsModel->setNameFilters(supportedSuffixes().values()); + m_sourceFsModel->setNameFilterDisables(false); - QDir dir(currAssetsDir->dirPath()); - dir.setNameFilters(supportedSuffixes().values()); - dir.setFilter(QDir::Files); - QDirIterator itFiles(dir); - bool isEmpty = true; - while (itFiles.hasNext()) { - QString filePath = itFiles.next(); - QString fileName = filePath.split('/').last(); - if (m_searchText.isEmpty() || fileName.contains(m_searchText, Qt::CaseInsensitive)) { - currAssetsDir->addFile(filePath); - m_fileSystemWatcher->addFile(filePath, Utils::FileSystemWatcher::WatchAllChanges); - isEmpty = false; - } - } + endResetModel(); - if (recursive) { - dir.setNameFilters({}); - dir.setFilter(QDir::Dirs | QDir::NoDotAndDotDot); - QDirIterator itDirs(dir); - - while (itDirs.hasNext()) { - QDir subDir = itDirs.next(); - if (currDepth == 1 && ignoredTopLevelDirs.contains(subDir.dirName())) - continue; - - auto assetsDir = new AssetsLibraryDir(subDir.path(), currDepth, - loadExpandedState(subDir.path()), currAssetsDir); - currAssetsDir->addDir(assetsDir); - saveExpandedState(loadExpandedState(assetsDir->dirPath()), assetsDir->dirPath()); - isEmpty &= parseDir(assetsDir, currDepth + 1, true); - } - } + emit rootPathChanged(); +} - if (!m_searchText.isEmpty() && isEmpty) - currAssetsDir->setDirVisible(false); +QString AssetsLibraryModel::rootPath() const +{ + return m_rootPath; +} - return isEmpty; - }; +QString AssetsLibraryModel::filePath(const QModelIndex &index) const +{ + QModelIndex fsIdx = mapToSource(index); + return m_sourceFsModel->filePath(fsIdx); +} - qCInfo(assetsLibraryBenchmark) << "directories parsed:" << time.elapsed(); +QString AssetsLibraryModel::fileName(const QModelIndex &index) const +{ + QModelIndex fsIdx = mapToSource(index); + return m_sourceFsModel->fileName(fsIdx); +} - if (m_assetsDir) - delete m_assetsDir; +QModelIndex AssetsLibraryModel::indexForPath(const QString &path) const +{ + QModelIndex idx = m_sourceFsModel->index(path, 0); + return mapFromSource(idx); +} +void AssetsLibraryModel::resetModel() +{ beginResetModel(); - m_assetsDir = new AssetsLibraryDir(path, 0, true, this); - bool hasProject = !QmlDesignerPlugin::instance()->documentManager().currentProjectDirPath().isEmpty(); - bool isEmpty = parseDir(m_assetsDir, 1, hasProject); - setIsEmpty(isEmpty); + endResetModel(); +} - bool noAssets = m_searchText.isEmpty() && isEmpty; - // noAssets: the model has no asset files (project has no assets added) - // isEmpty: the model has no asset files (assets could exist but are filtered out) +QModelIndex AssetsLibraryModel::rootIndex() const +{ + return indexForPath(m_rootPath); +} - m_assetsDir->setDirVisible(!noAssets); // if there are no assets, hide all empty asset folders - endResetModel(); +bool AssetsLibraryModel::isDirectory(const QString &path) const +{ + QFileInfo fi{path}; + return fi.isDir(); +} - qCInfo(assetsLibraryBenchmark) << "model reset:" << time.elapsed(); +bool AssetsLibraryModel::isDirectory(const QModelIndex &index) const +{ + QString path = filePath(index); + return isDirectory(path); +} + +QModelIndex AssetsLibraryModel::parentDirIndex(const QString &path) const +{ + QModelIndex idx = indexForPath(path); + QModelIndex parentIdx = idx.parent(); + + return parentIdx; +} + +QModelIndex AssetsLibraryModel::parentDirIndex(const QModelIndex &index) const +{ + QModelIndex parentIdx = index.parent(); + return parentIdx; +} + +QString AssetsLibraryModel::parentDirPath(const QString &path) const +{ + QModelIndex idx = indexForPath(path); + QModelIndex parentIdx = idx.parent(); + return filePath(parentIdx); } const QStringList &AssetsLibraryModel::supportedImageSuffixes() @@ -408,17 +408,4 @@ const QSet<QString> &AssetsLibraryModel::supportedSuffixes() return allSuffixes; } -const QSet<QString> &AssetsLibraryModel::previewableSuffixes() const -{ - static QSet<QString> previewableSuffixes; - if (previewableSuffixes.isEmpty()) { - auto insertSuffixes = [](const QStringList &suffixes) { - for (const auto &suffix : suffixes) - previewableSuffixes.insert(suffix); - }; - insertSuffixes(supportedFontSuffixes()); - } - return previewableSuffixes; -} - } // namespace QmlDesigner diff --git a/src/plugins/qmldesigner/components/assetslibrary/assetslibrarymodel.h b/src/plugins/qmldesigner/components/assetslibrary/assetslibrarymodel.h index c7d3ee493b..6e555f07e2 100644 --- a/src/plugins/qmldesigner/components/assetslibrary/assetslibrarymodel.h +++ b/src/plugins/qmldesigner/components/assetslibrary/assetslibrarymodel.h @@ -3,39 +3,53 @@ #pragma once -#include <QAbstractListModel> -#include <QDateTime> -#include <QDir> -#include <QHash> -#include <QIcon> -#include <QPair> -#include <QSet> +#include <QFileSystemModel> +#include <QSortFilterProxyModel> +#include <QFileInfo> -namespace Utils { class FileSystemWatcher; } +#include <utils/qtcassert.h> namespace QmlDesigner { -class SynchronousImageCache; -class AssetsLibraryDir; - -class AssetsLibraryModel : public QAbstractListModel +class AssetsLibraryModel : public QSortFilterProxyModel { Q_OBJECT - Q_PROPERTY(bool isEmpty READ isEmpty WRITE setIsEmpty NOTIFY isEmptyChanged) - public: - AssetsLibraryModel(Utils::FileSystemWatcher *fileSystemWatcher, QObject *parent = nullptr); - - QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; - int rowCount(const QModelIndex &parent = QModelIndex()) const override; - QHash<int, QByteArray> roleNames() const override; + AssetsLibraryModel(QObject *parent = nullptr); - void refresh(); - void setRootPath(const QString &path); + void setRootPath(const QString &newPath); void setSearchText(const QString &searchText); - bool isEmpty() const; + Q_PROPERTY(bool haveFiles READ haveFiles NOTIFY haveFilesChanged); + + Q_INVOKABLE QString rootPath() const; + Q_INVOKABLE QString filePath(const QModelIndex &index) const; + Q_INVOKABLE QString fileName(const QModelIndex &index) const; + + Q_INVOKABLE QModelIndex indexForPath(const QString &path) const; + Q_INVOKABLE QModelIndex rootIndex() const; + Q_INVOKABLE bool isDirectory(const QString &path) const; + Q_INVOKABLE bool isDirectory(const QModelIndex &index) const; + Q_INVOKABLE QModelIndex parentDirIndex(const QString &path) const; + Q_INVOKABLE QModelIndex parentDirIndex(const QModelIndex &index) const; + Q_INVOKABLE QString parentDirPath(const QString &path) const; + Q_INVOKABLE void syncHaveFiles(); + + Q_INVOKABLE QList<QModelIndex> parentIndices(const QModelIndex &index) const; + Q_INVOKABLE bool indexIsValid(const QModelIndex &index) const; + Q_INVOKABLE void deleteFiles(const QStringList &filePaths); + Q_INVOKABLE bool renameFolder(const QString &folderPath, const QString &newName); + Q_INVOKABLE bool addNewFolder(const QString &folderPath); + Q_INVOKABLE bool deleteFolderRecursively(const QModelIndex &folderIndex); + + int columnCount(const QModelIndex &parent = QModelIndex()) const override + { + int result = QSortFilterProxyModel::columnCount(parent); + return std::min(result, 1); + } + + bool haveFiles() const { return m_haveFiles; } static const QStringList &supportedImageSuffixes(); static const QStringList &supportedFragmentShaderSuffixes(); @@ -47,44 +61,28 @@ public: static const QStringList &supportedEffectMakerSuffixes(); static const QSet<QString> &supportedSuffixes(); - const QSet<QString> &previewableSuffixes() const; - - static void saveExpandedState(bool expanded, const QString &assetPath); - static bool loadExpandedState(const QString &assetPath); - static bool isEffectQmlExist(const QString &effectName); - enum class DirExpandState { - SomeExpanded, - AllExpanded, - AllCollapsed - }; - Q_ENUM(DirExpandState) - - Q_INVOKABLE void toggleExpandAll(bool expand); - Q_INVOKABLE DirExpandState getAllExpandedState() const; - Q_INVOKABLE void deleteFiles(const QStringList &filePaths); - Q_INVOKABLE bool renameFolder(const QString &folderPath, const QString &newName); - Q_INVOKABLE void addNewFolder(const QString &folderPath); - Q_INVOKABLE void deleteFolder(const QString &folderPath); - Q_INVOKABLE QObject *rootDir() const; - signals: - void isEmptyChanged(); + void directoryLoaded(const QString &path); + void rootPathChanged(); + void haveFilesChanged(); + void fileChanged(const QString &path); private: - - void setIsEmpty(bool empty); - - QHash<QString, QPair<QDateTime, QIcon>> m_iconCache; + void setHaveFiles(bool value); + bool filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const override; + void onDataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight, const QList<int> &roles); + void resetModel(); + void createBackendModel(); + void destroyBackendModel(); + bool checkHaveFiles(const QModelIndex &parentIdx) const; + bool checkHaveFiles() const; QString m_searchText; - Utils::FileSystemWatcher *m_fileSystemWatcher = nullptr; - AssetsLibraryDir *m_assetsDir = nullptr; - bool m_isEmpty = true; - - QHash<int, QByteArray> m_roleNames; - inline static QHash<QString, bool> m_expandedStateHash; // <assetPath, isExpanded> + QString m_rootPath; + QFileSystemModel *m_sourceFsModel = nullptr; + bool m_haveFiles = false; }; } // namespace QmlDesigner diff --git a/src/plugins/qmldesigner/components/assetslibrary/assetslibrarywidget.cpp b/src/plugins/qmldesigner/components/assetslibrary/assetslibrarywidget.cpp index a335227e47..2858febe9f 100644 --- a/src/plugins/qmldesigner/components/assetslibrary/assetslibrarywidget.cpp +++ b/src/plugins/qmldesigner/components/assetslibrary/assetslibrarywidget.cpp @@ -17,7 +17,6 @@ #include <utils/algorithm.h> #include <utils/environment.h> -#include <utils/filesystemwatcher.h> #include <utils/fileutils.h> #include <utils/qtcassert.h> #include <utils/stylehelper.h> @@ -85,16 +84,12 @@ bool AssetsLibraryWidget::eventFilter(QObject *obj, QEvent *event) AssetsLibraryWidget::AssetsLibraryWidget(AsynchronousImageCache &asynchronousFontImageCache, SynchronousImageCache &synchronousFontImageCache) - : m_itemIconSize(24, 24) - , m_fontImageCache(synchronousFontImageCache) - , m_assetsIconProvider(new AssetsLibraryIconProvider(synchronousFontImageCache)) - , m_fileSystemWatcher(new Utils::FileSystemWatcher(this)) - , m_assetsModel(new AssetsLibraryModel(m_fileSystemWatcher, this)) - , m_assetsWidget(new QQuickWidget(this)) + : m_itemIconSize{24, 24} + , m_fontImageCache{synchronousFontImageCache} + , m_assetsIconProvider{new AssetsLibraryIconProvider(synchronousFontImageCache)} + , m_assetsModel{new AssetsLibraryModel(this)} + , m_assetsWidget{new QQuickWidget(this)} { - m_assetCompressionTimer.setInterval(200); - m_assetCompressionTimer.setSingleShot(true); - setWindowTitle(tr("Assets Library", "Title of assets library widget")); setMinimumWidth(250); @@ -119,21 +114,12 @@ AssetsLibraryWidget::AssetsLibraryWidget(AsynchronousImageCache &asynchronousFon m_assetsWidget->setClearColor(Theme::getColor(Theme::Color::QmlDesigner_BackgroundColorDarkAlternate)); m_assetsWidget->engine()->addImageProvider("qmldesigner_assets", m_assetsIconProvider); m_assetsWidget->rootContext()->setContextProperties(QVector<QQmlContext::PropertyPair>{ - {{"assetsModel"}, QVariant::fromValue(m_assetsModel.data())}, + {{"assetsModel"}, QVariant::fromValue(m_assetsModel)}, {{"rootView"}, QVariant::fromValue(this)}, {{"tooltipBackend"}, QVariant::fromValue(m_fontPreviewTooltipBackend.get())} }); - // If project directory contents change, or one of the asset files is modified, we must - // reconstruct the model to update the icons - connect(m_fileSystemWatcher, - &Utils::FileSystemWatcher::directoryChanged, - [this]([[maybe_unused]] const QString &changedDirPath) { - m_assetCompressionTimer.start(); - }); - - connect(m_fileSystemWatcher, &Utils::FileSystemWatcher::fileChanged, - [](const QString &changeFilePath) { + connect(m_assetsModel, &AssetsLibraryModel::fileChanged, [](const QString &changeFilePath) { QmlDesignerPlugin::instance()->emitAssetChanged(changeFilePath); }); @@ -149,23 +135,7 @@ AssetsLibraryWidget::AssetsLibraryWidget(AsynchronousImageCache &asynchronousFon m_qmlSourceUpdateShortcut = new QShortcut(QKeySequence(Qt::CTRL + Qt::Key_F6), this); connect(m_qmlSourceUpdateShortcut, &QShortcut::activated, this, &AssetsLibraryWidget::reloadQmlSource); - - connect(&m_assetCompressionTimer, &QTimer::timeout, this, [this]() { - // TODO: find a clever way to only refresh the changed directory part of the model - - // Don't bother with asset updates after model has detached, project is probably closing - if (!m_model.isNull()) { - if (QApplication::activeModalWidget()) { - // Retry later, as updating file system watchers can crash when there is an active - // modal widget - m_assetCompressionTimer.start(); - } else { - m_assetsModel->refresh(); - // reload assets qml so that an overridden file's image shows the new image - QTimer::singleShot(100, this, &AssetsLibraryWidget::reloadQmlSource); - } - } - }); + connect(this, &AssetsLibraryWidget::extFilesDrop, this, &AssetsLibraryWidget::handleExtFilesDrop, Qt::QueuedConnection); QmlDesignerPlugin::trackWidgetFocusTime(this, Constants::EVENT_ASSETSLIBRARY_TIME); @@ -173,7 +143,15 @@ AssetsLibraryWidget::AssetsLibraryWidget(AsynchronousImageCache &asynchronousFon reloadQmlSource(); } -AssetsLibraryWidget::~AssetsLibraryWidget() = default; +bool AssetsLibraryWidget::qtVersionIsAtLeast6_4() const +{ + return (QT_VERSION >= QT_VERSION_CHECK(6, 4, 0)); +} + +void AssetsLibraryWidget::invalidateThumbnail(const QString &id) +{ + m_assetsIconProvider->invalidateThumbnail(id); +} QList<QToolButton *> AssetsLibraryWidget::createToolBarWidgets() { @@ -182,8 +160,9 @@ QList<QToolButton *> AssetsLibraryWidget::createToolBarWidgets() void AssetsLibraryWidget::handleSearchFilterChanged(const QString &filterText) { - if (filterText == m_filterText || (m_assetsModel->isEmpty() && filterText.contains(m_filterText))) - return; + if (filterText == m_filterText || (!m_assetsModel->haveFiles() + && filterText.contains(m_filterText, Qt::CaseInsensitive))) + return; m_filterText = filterText; updateSearch(); @@ -194,6 +173,16 @@ void AssetsLibraryWidget::handleAddAsset() addResources({}); } +void AssetsLibraryWidget::emitExtFilesDrop(const QList<QUrl> &simpleFilePaths, + const QList<QUrl> &complexFilePaths, + const QString &targetDirPath) +{ + // workaround for but QDS-8010: we need to postpone the call to handleExtFilesDrop, otherwise + // the TreeViewDelegate might be recreated (therefore, destroyed) while we're still in a handler + // of a QML DropArea which is a child of the delegate being destroyed - this would cause a crash. + emit extFilesDrop(simpleFilePaths, complexFilePaths, targetDirPath); +} + void AssetsLibraryWidget::handleExtFilesDrop(const QList<QUrl> &simpleFilePaths, const QList<QUrl> &complexFilePaths, const QString &targetDirPath) @@ -210,7 +199,7 @@ void AssetsLibraryWidget::handleExtFilesDrop(const QList<QUrl> &simpleFilePaths, } else { AddFilesResult result = ModelNodeOperations::addFilesToProject(simpleFilePathStrings, targetDirPath); - if (result == AddFilesResult::Failed) { + if (result.status() == AddFilesResult::Failed) { Core::AsynchronousMessageBox::warning(tr("Failed to Add Files"), tr("Could not add %1 to project.") .arg(simpleFilePathStrings.join(' '))); @@ -276,6 +265,7 @@ void AssetsLibraryWidget::updateSearch() void AssetsLibraryWidget::setResourcePath(const QString &resourcePath) { m_assetsModel->setRootPath(resourcePath); + m_assetsIconProvider->clearCache(); updateSearch(); } @@ -408,10 +398,22 @@ void AssetsLibraryWidget::addResources(const QStringList &files) if (operation) { AddFilesResult result = operation(fileNames, document->fileName().parentDir().toString(), true); - if (result == AddFilesResult::Failed) { + if (result.status() == AddFilesResult::Failed) { Core::AsynchronousMessageBox::warning(tr("Failed to Add Files"), tr("Could not add %1 to project.") .arg(fileNames.join(' '))); + } else { + if (!result.directory().isEmpty()) { + emit directoryCreated(result.directory()); + } else if (result.haveDelayedResult()) { + QObject *delayedResult = result.delayedResult(); + QObject::connect(delayedResult, &QObject::destroyed, this, [this, delayedResult]() { + QVariant propValue = delayedResult->property(AddFilesResult::directoryPropName); + QString directory = propValue.toString(); + if (!directory.isEmpty()) + emit directoryCreated(directory); + }); + } } } else { Core::AsynchronousMessageBox::warning(tr("Failed to Add Files"), diff --git a/src/plugins/qmldesigner/components/assetslibrary/assetslibrarywidget.h b/src/plugins/qmldesigner/components/assetslibrary/assetslibrarywidget.h index 8ac41a44a3..1c19c65a3e 100644 --- a/src/plugins/qmldesigner/components/assetslibrary/assetslibrarywidget.h +++ b/src/plugins/qmldesigner/components/assetslibrary/assetslibrarywidget.h @@ -21,7 +21,7 @@ class QShortcut; QT_END_NAMESPACE namespace Utils { - class FileSystemWatcher; + class QtcProcess; } namespace QmlDesigner { @@ -42,7 +42,7 @@ class AssetsLibraryWidget : public QFrame public: AssetsLibraryWidget(AsynchronousImageCache &asynchronousFontImageCache, SynchronousImageCache &synchronousFontImageCache); - ~AssetsLibraryWidget(); + ~AssetsLibraryWidget() = default; QList<QToolButton *> createToolBarWidgets(); @@ -59,14 +59,26 @@ 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 handleExtFilesDrop(const QList<QUrl> &simpleFilePaths, const QList<QUrl> &complexFilePaths, - const QString &targetDirPath = {}); + const QString &targetDirPath); + + Q_INVOKABLE void emitExtFilesDrop(const QList<QUrl> &simpleFilePaths, + const QList<QUrl> &complexFilePaths, + const QString &targetDirPath = {}); + Q_INVOKABLE QSet<QString> supportedAssetSuffixes(bool complex); Q_INVOKABLE void openEffectMaker(const QString &filePath); + Q_INVOKABLE bool qtVersionIsAtLeast6_4() const; + Q_INVOKABLE void invalidateThumbnail(const QString &id); signals: void itemActivated(const QString &itemName); + void extFilesDrop(const QList<QUrl> &simpleFilePaths, + const QList<QUrl> &complexFilePaths, + const QString &targetDirPath); + void directoryCreated(const QString &path); protected: bool eventFilter(QObject *obj, QEvent *event) override; @@ -77,14 +89,12 @@ private: void addResources(const QStringList &files); void updateSearch(); - QTimer m_assetCompressionTimer; QSize m_itemIconSize; SynchronousImageCache &m_fontImageCache; AssetsLibraryIconProvider *m_assetsIconProvider = nullptr; - Utils::FileSystemWatcher *m_fileSystemWatcher = nullptr; - QPointer<AssetsLibraryModel> m_assetsModel; + AssetsLibraryModel *m_assetsModel = nullptr; QScopedPointer<QQuickWidget> m_assetsWidget; std::unique_ptr<PreviewTooltipBackend> m_fontPreviewTooltipBackend; diff --git a/src/plugins/qmldesigner/components/componentcore/designeractionmanager.cpp b/src/plugins/qmldesigner/components/componentcore/designeractionmanager.cpp index bb60e2a1c9..e99d94014f 100644 --- a/src/plugins/qmldesigner/components/componentcore/designeractionmanager.cpp +++ b/src/plugins/qmldesigner/components/componentcore/designeractionmanager.cpp @@ -277,7 +277,7 @@ QHash<QString, QStringList> DesignerActionManager::handleExternalAssetsDrop(cons AddResourceOperation operation = categoryOperation.value(category); QStringList files = categoryFiles.value(category); AddFilesResult result = operation(files, {}, true); - if (result == AddFilesResult::Succeeded) + if (result.status() == AddFilesResult::Succeeded) addedCategoryFiles.insert(category, files); } diff --git a/src/plugins/qmldesigner/components/componentcore/modelnodeoperations.cpp b/src/plugins/qmldesigner/components/componentcore/modelnodeoperations.cpp index 9edf0869e7..df7f701c4e 100644 --- a/src/plugins/qmldesigner/components/componentcore/modelnodeoperations.cpp +++ b/src/plugins/qmldesigner/components/componentcore/modelnodeoperations.cpp @@ -1042,10 +1042,10 @@ AddFilesResult addFilesToProject(const QStringList &fileNames, const QString &de { QString directory = showDialog ? AddImagesDialog::getDirectory(fileNames, defaultDir) : defaultDir; if (directory.isEmpty()) - return AddFilesResult::Cancelled; + return AddFilesResult::cancelled(directory); DesignDocument *document = QmlDesignerPlugin::instance()->currentDesignDocument(); - QTC_ASSERT(document, return AddFilesResult::Failed); + QTC_ASSERT(document, return AddFilesResult::failed(directory)); QList<QPair<QString, QString>> copyList; QStringList removeList; @@ -1073,7 +1073,7 @@ AddFilesResult addFilesToProject(const QStringList &fileNames, const QString &de for (const auto &filePair : std::as_const(copyList)) { const bool success = QFile::copy(filePair.first, filePair.second); if (!success) - return AddFilesResult::Failed; + return AddFilesResult::failed(directory); ProjectExplorer::Node *node = ProjectExplorer::ProjectTree::nodeForFile(document->fileName()); if (node) { @@ -1083,7 +1083,7 @@ AddFilesResult addFilesToProject(const QStringList &fileNames, const QString &de } } - return AddFilesResult::Succeeded; + return AddFilesResult::succeeded(directory); } static QString getAssetDefaultDirectory(const QString &assetDir, const QString &defaultDirectory) diff --git a/src/plugins/qmldesigner/components/componentcore/modelnodeoperations.h b/src/plugins/qmldesigner/components/componentcore/modelnodeoperations.h index 279e18af97..be238a0439 100644 --- a/src/plugins/qmldesigner/components/componentcore/modelnodeoperations.h +++ b/src/plugins/qmldesigner/components/componentcore/modelnodeoperations.h @@ -9,7 +9,48 @@ namespace QmlDesigner { -enum class AddFilesResult { Succeeded, Failed, Cancelled }; +class AddFilesResult +{ +public: + enum Status { Succeeded, Failed, Cancelled, Delayed }; + static constexpr char directoryPropName[] = "directory"; + + static AddFilesResult cancelled(const QString &directory = {}) + { + return AddFilesResult{Cancelled, directory}; + } + + static AddFilesResult failed(const QString &directory = {}) + { + return AddFilesResult{Failed, directory}; + } + + static AddFilesResult succeeded(const QString &directory = {}) + { + return AddFilesResult{Succeeded, directory}; + } + + static AddFilesResult delayed(QObject *delayedResult) + { + return AddFilesResult{Delayed, {}, delayedResult}; + } + + Status status() const { return m_status; } + QString directory() const { return m_directory; } + bool haveDelayedResult() const { return m_delayedResult != nullptr; } + QObject *delayedResult() const { return m_delayedResult; } + +private: + AddFilesResult(Status status, const QString &directory, QObject *delayedResult = nullptr) + : m_status{status} + , m_directory{directory} + , m_delayedResult{delayedResult} + {} + + Status m_status; + QString m_directory; + QObject *m_delayedResult = nullptr; +}; namespace ModelNodeOperations { diff --git a/src/plugins/qmldesigner/components/contentlibrary/contentlibraryview.cpp b/src/plugins/qmldesigner/components/contentlibrary/contentlibraryview.cpp index 28aa071a8e..8071363e2a 100644 --- a/src/plugins/qmldesigner/components/contentlibrary/contentlibraryview.cpp +++ b/src/plugins/qmldesigner/components/contentlibrary/contentlibraryview.cpp @@ -55,7 +55,7 @@ WidgetInfo ContentLibraryView::widgetInfo() // copy image to project AddFilesResult result = ModelNodeOperations::addImageToProject({texPath}, "images", false); - if (result == AddFilesResult::Failed) { + if (result.status() == AddFilesResult::Failed) { Core::AsynchronousMessageBox::warning(tr("Failed to Add Texture"), tr("Could not add %1 to project.").arg(texPath)); return; diff --git a/src/plugins/qmldesigner/components/itemlibrary/itemlibraryview.cpp b/src/plugins/qmldesigner/components/itemlibrary/itemlibraryview.cpp index 3e56c6a0e6..33d3b8a717 100644 --- a/src/plugins/qmldesigner/components/itemlibrary/itemlibraryview.cpp +++ b/src/plugins/qmldesigner/components/itemlibrary/itemlibraryview.cpp @@ -156,7 +156,7 @@ void ItemLibraryView::updateImport3DSupport(const QVariantMap &supportMap) Core::ICore::dialogParent()); int result = importDlg->exec(); - return result == QDialog::Accepted ? AddFilesResult::Succeeded : AddFilesResult::Cancelled; + return result == QDialog::Accepted ? AddFilesResult::succeeded() : AddFilesResult::cancelled(); }; auto add3DHandler = [&](const QString &group, const QString &ext) { |