summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorSamuel Ghinet <samuel.ghinet@qt.io>2022-11-23 11:49:45 +0200
committerSamuel Ghinet <samuel.ghinet@qt.io>2022-11-23 10:00:04 +0000
commit910a8864dc43f8dede7617855282f19de62ee939 (patch)
tree13a6643b5ee2412c1b4ccccba8d0ddcca32549a3
parenta575cb4f46b8f24ef0a5cc15702a15020016d343 (diff)
downloadqt-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>
-rw-r--r--share/qtcreator/qmldesigner/itemLibraryQmlSources/AssetDelegate.qml327
-rw-r--r--share/qtcreator/qmldesigner/itemLibraryQmlSources/Assets.qml83
-rw-r--r--share/qtcreator/qmldesigner/itemLibraryQmlSources/AssetsContextMenu.qml131
-rw-r--r--share/qtcreator/qmldesigner/itemLibraryQmlSources/AssetsView.qml534
-rw-r--r--share/qtcreator/qmldesigner/itemLibraryQmlSources/ConfirmDeleteFolderDialog.qml53
-rw-r--r--share/qtcreator/qmldesigner/itemLibraryQmlSources/ErrorDialog.qml40
-rw-r--r--share/qtcreator/qmldesigner/itemLibraryQmlSources/NewFolderDialog.qml77
-rw-r--r--share/qtcreator/qmldesigner/itemLibraryQmlSources/RenameFolderDialog.qml67
-rw-r--r--src/plugins/qmldesigner/CMakeLists.txt3
-rw-r--r--src/plugins/qmldesigner/components/assetslibrary/assetslibrarydir.cpp75
-rw-r--r--src/plugins/qmldesigner/components/assetslibrary/assetslibrarydir.h63
-rw-r--r--src/plugins/qmldesigner/components/assetslibrary/assetslibrarydirsmodel.cpp71
-rw-r--r--src/plugins/qmldesigner/components/assetslibrary/assetslibrarydirsmodel.h32
-rw-r--r--src/plugins/qmldesigner/components/assetslibrary/assetslibraryfilesmodel.cpp53
-rw-r--r--src/plugins/qmldesigner/components/assetslibrary/assetslibraryfilesmodel.h32
-rw-r--r--src/plugins/qmldesigner/components/assetslibrary/assetslibraryiconprovider.cpp70
-rw-r--r--src/plugins/qmldesigner/components/assetslibrary/assetslibraryiconprovider.h4
-rw-r--r--src/plugins/qmldesigner/components/assetslibrary/assetslibrarymodel.cpp365
-rw-r--r--src/plugins/qmldesigner/components/assetslibrary/assetslibrarymodel.h104
-rw-r--r--src/plugins/qmldesigner/components/assetslibrary/assetslibrarywidget.cpp88
-rw-r--r--src/plugins/qmldesigner/components/assetslibrary/assetslibrarywidget.h22
-rw-r--r--src/plugins/qmldesigner/components/componentcore/designeractionmanager.cpp2
-rw-r--r--src/plugins/qmldesigner/components/componentcore/modelnodeoperations.cpp8
-rw-r--r--src/plugins/qmldesigner/components/componentcore/modelnodeoperations.h43
-rw-r--r--src/plugins/qmldesigner/components/contentlibrary/contentlibraryview.cpp2
-rw-r--r--src/plugins/qmldesigner/components/itemlibrary/itemlibraryview.cpp2
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) {