Files
qt-creator/share/qtcreator/qmldesigner/assetsLibraryQmlSources/AssetsView.qml
Samuel Ghinet 339ebeab8e QmlDesigner: Implement keyboard commands in AssetsLibrary
Up and Down now change selection--they work only for files, they skip
directories.

The Delete key will (ask confirmation to) delete the selected files,
just as it happens when clicking Delete from the Context menu.

Also, did a small fix in the AssetDelegate, where, on mouse release,
you could have a directory added to the selection list.

Task-number: QDS-8710
Change-Id: I05e232ed0ce94709df41a5150a49300ad65c0a86
Reviewed-by: Mahmoud Badri <mahmoud.badri@qt.io>
2023-03-09 15:57:57 +00:00

482 lines
14 KiB
QML

// 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 HelperWidgets as HelperWidgets
import StudioControls as StudioControls
import AssetsLibraryBackend
TreeView {
id: root
clip: true
interactive: verticalScrollBar.visible && !root.contextMenu.opened && !rootView.isDragging
reuseItems: false
boundsBehavior: Flickable.StopAtBounds
rowSpacing: 5
property var assetsModel: AssetsLibraryBackend.assetsModel
property var rootView: AssetsLibraryBackend.rootView
property var tooltipBackend: AssetsLibraryBackend.tooltipBackend
required property Item assetsRoot
required property StudioControls.Menu contextMenu
property alias verticalScrollBar: verticalScrollBar
property var selectedAssets: ({})
// the latest file that was clicked, or changed to via Up or Down keys
property string currentFilePath: ""
// 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
readonly property int lastRow: root.rows - 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()
}
root.updateRows()
}
Timer {
id: updateRowsTimer
interval: 200
repeat: false
onTriggered: {
root.updateRows()
}
}
Connections {
target: rootView
function onDirectoryCreated(path)
{
root.__createdDirectories.push(path)
updateRowsTimer.restart()
}
function onDeleteSelectedAssetsRequested()
{
let selectedPaths = root.selectedPathsAsList()
if (!selectedPaths.length)
return
let deleted = assetsModel.requestDeleteFiles(selectedPaths)
if (!deleted) {
confirmDeleteFiles.files = selectedPaths
confirmDeleteFiles.open()
}
}
}
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)
})
}
}
// 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 {
// 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)
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)
// 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)
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)
if (assetsModel.isDirectory(index)) {
let item = root.__getDelegateItemForIndex(index)
if (item)
item.isHighlighted = true
return
}
let parentItem = root.__getDelegateParentForIndex(index)
if (parentItem)
parentItem.hasChildWithDropHover = true
}
function endDropHover(row)
{
let index = root.__modelIndex(row)
if (assetsModel.isDirectory(index)) {
let item = root.__getDelegateItemForIndex(index)
if (item)
item.isHighlighted = false
return
}
let parentItem = root.__getDelegateParentForIndex(index)
if (parentItem)
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 __getDelegateItemForIndex(index)
{
let cell = root.cellAtIndex(index)
return root.itemAtCell(cell)
}
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 Qt 6.4, the order of
// the arguments was changed, and in Qt 6.5 the order was changed again. Due to this mess,
// the whole function was deprecated in Qt 6.4.3 and replaced with index() function.
if (assetsRoot.qtVersion >= 0x060403)
return root.index(row, 0)
else if (assetsRoot.qtVersion >= 0x060400)
return root.modelIndex(0, row)
else
return root.modelIndex(row, 0)
}
function __selectRow(row: int)
{
let index = root.__modelIndex(row)
if (assetsModel.isDirectory(index))
return
let filePath = assetsModel.filePath(index)
root.clearSelectedAssets()
root.setAssetSelected(filePath, true)
root.currentFilePath = filePath
}
Keys.enabled: true
Keys.onUpPressed: {
if (!root.currentFilePath)
return
let index = assetsModel.indexForPath(root.currentFilePath)
let row = root.rowAtIndex(index)
let nextRow = row
let nextIndex = index
do {
if (nextRow <= root.firstRow)
return // don't select hidden rows
nextRow--
nextIndex = root.__modelIndex(nextRow)
} while (assetsModel.isDirectory(nextIndex))
root.__selectRow(nextRow)
root.positionViewAtRow(nextRow, TableView.Contain)
}
Keys.onDownPressed: {
if (!root.currentFilePath)
return
let index = assetsModel.indexForPath(root.currentFilePath)
let row = root.rowAtIndex(index)
let nextRow = row
let nextIndex = index
do {
if (nextRow >= root.lastRow)
return // don't select hidden rows
nextRow++
nextIndex = root.__modelIndex(nextRow)
} while (assetsModel.isDirectory(nextIndex))
root.__selectRow(nextRow)
root.positionViewAtRow(nextRow, TableView.Contain)
}
ConfirmDeleteFilesDialog {
id: confirmDeleteFiles
parent: root
files: []
onAccepted: root.clearSelectedAssets()
onClosed: confirmDeleteFiles.files = []
}
DropArea {
id: dropArea
enabled: true
anchors.fill: parent
property bool __isHoveringDrop: false
property int __rowHoveringOver: -1
function __rowAndItem(drag)
{
let pos = dropArea.mapToItem(root, drag.x, drag.y)
let cell = root.cellAtPos(pos.x, pos.y, true)
let item = root.itemAtCell(cell)
return [cell.y, item]
}
onEntered: (drag) => {
root.assetsRoot.updateDropExtFiles(drag)
let [row, item] = dropArea.__rowAndItem(drag)
dropArea.__isHoveringDrop = drag.accepted && root.assetsRoot.dropSimpleExtFiles.length > 0
if (item && dropArea.__isHoveringDrop)
root.startDropHoverOver(row)
dropArea.__rowHoveringOver = row
}
onDropped: (drag) => {
let [row, item] = dropArea.__rowAndItem(drag)
if (item) {
drag.accept()
root.endDropHover(row)
let dirPath = item.getDirPath()
rootView.emitExtFilesDrop(root.assetsRoot.dropSimpleExtFiles,
root.assetsRoot.dropComplexExtFiles,
dirPath)
}
dropArea.__isHoveringDrop = false
dropArea.__rowHoveringOver = -1
}
onPositionChanged: (drag) => {
let [row, item] = dropArea.__rowAndItem(drag)
if (dropArea.__rowHoveringOver !== row && dropArea.__rowHoveringOver > -1) {
root.endDropHover(dropArea.__rowHoveringOver)
if (item)
root.startDropHoverOver(row)
}
dropArea.__rowHoveringOver = row
}
onExited: {
if (!dropArea.__isHoveringDrop || dropArea.__rowHoveringOver === -1)
return
root.endDropHover(dropArea.__rowHoveringOver)
dropArea.__isHoveringDrop = false
dropArea.__rowHoveringOver = -1
}
}
delegate: AssetDelegate {
assetsView: root
assetsRoot: root.assetsRoot
indentation: 5
}
} // TreeView