forked from qt-creator/qt-creator
Use QML TreeView in Assets Library
Task-number: QDS-7344 Change-Id: Ia1ea584fc7acabb0d35b745e36fef18799f21ab5 Reviewed-by: Thomas Hartmann <thomas.hartmann@qt.io>
This commit is contained in:
@@ -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
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
Column {
|
||||
Repeater {
|
||||
model: assetsModel // context property
|
||||
delegate: dirSection
|
||||
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()
|
||||
}
|
||||
|
||||
Component {
|
||||
id: dirSection
|
||||
updateRows()
|
||||
}
|
||||
|
||||
Section {
|
||||
id: section
|
||||
Timer {
|
||||
id: updateRowsTimer
|
||||
interval: 200
|
||||
repeat: false
|
||||
|
||||
width: assetsView.width -
|
||||
(assetsView.verticalScrollBarVisible ? assetsView.verticalThickness : 0) - 5
|
||||
caption: dirName
|
||||
sectionHeight: 30
|
||||
sectionFontSize: 15
|
||||
leftPadding: 0
|
||||
topPadding: dirDepth > 0 ? 5 : 0
|
||||
bottomPadding: 0
|
||||
hideHeader: dirDepth === 0
|
||||
showLeftBorder: dirDepth > 0
|
||||
expanded: dirExpanded
|
||||
visible: dirVisible
|
||||
expandOnClick: false
|
||||
useDefaulContextMenu: false
|
||||
dropEnabled: true
|
||||
|
||||
onToggleExpand: {
|
||||
dirExpanded = !dirExpanded
|
||||
}
|
||||
|
||||
onDropEnter: (drag)=> {
|
||||
root.updateDropExtFiles(drag)
|
||||
section.highlight = drag.accepted && root.dropSimpleExtFiles.length > 0
|
||||
}
|
||||
|
||||
onDropExit: {
|
||||
section.highlight = false
|
||||
}
|
||||
|
||||
onDrop: {
|
||||
section.highlight = false
|
||||
rootView.handleExtFilesDrop(root.dropSimpleExtFiles,
|
||||
root.dropComplexExtFiles,
|
||||
dirPath)
|
||||
}
|
||||
|
||||
onShowContextMenu: {
|
||||
root.contextFilePath = ""
|
||||
root.contextDir = model
|
||||
root.isDirContextMenu = true
|
||||
root.allExpandedState = assetsModel.getAllExpandedState()
|
||||
contextMenu.popup()
|
||||
}
|
||||
|
||||
Column {
|
||||
spacing: 5
|
||||
leftPadding: 5
|
||||
|
||||
Repeater {
|
||||
model: dirsModel
|
||||
delegate: dirSection
|
||||
}
|
||||
|
||||
Repeater {
|
||||
model: filesModel
|
||||
delegate: fileSection
|
||||
}
|
||||
|
||||
Text {
|
||||
text: qsTr("Empty folder")
|
||||
color: StudioTheme.Values.themeTextColorDisabled
|
||||
font.pixelSize: 12
|
||||
visible: !(dirsModel && dirsModel.rowCount() > 0)
|
||||
&& !(filesModel && filesModel.rowCount() > 0)
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
acceptedButtons: Qt.RightButton
|
||||
onClicked: {
|
||||
root.contextFilePath = ""
|
||||
root.contextDir = model
|
||||
root.isDirContextMenu = true
|
||||
contextMenu.popup()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: fileSection
|
||||
|
||||
Rectangle {
|
||||
width: assetsView.width -
|
||||
(assetsView.verticalScrollBarVisible ? assetsView.verticalThickness : 0)
|
||||
height: img.height
|
||||
color: root.selectedAssets[filePath]
|
||||
? StudioTheme.Values.themeInteraction
|
||||
: (mouseArea.containsMouse ? StudioTheme.Values.themeSectionHeadBackground
|
||||
: "transparent")
|
||||
|
||||
Row {
|
||||
spacing: 5
|
||||
|
||||
Image {
|
||||
id: img
|
||||
asynchronous: true
|
||||
fillMode: Image.PreserveAspectFit
|
||||
width: 48
|
||||
height: 48
|
||||
source: "image://qmldesigner_assets/" + filePath
|
||||
}
|
||||
|
||||
Text {
|
||||
text: fileName
|
||||
color: StudioTheme.Values.themeTextColor
|
||||
font.pixelSize: 14
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
|
||||
readonly property string suffix: fileName.substr(-4)
|
||||
readonly property bool isFont: suffix === ".ttf" || suffix === ".otf"
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
onTriggered: {
|
||||
root.updateRows()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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 = ""
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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(""));
|
||||
return Utils::StyleHelper::dpiSpecificImageFile(QFileInfo::exists(path)
|
||||
? path
|
||||
: pathTemplate.arg(""));
|
||||
}
|
||||
|
||||
if (size) {
|
||||
size->setWidth(pixmap.width());
|
||||
size->setHeight(pixmap.height());
|
||||
}
|
||||
|
||||
if (pixmap.isNull())
|
||||
pixmap = Utils::StyleHelper::dpiSpecificImageFile(":/AssetsLibrary/images/assets_default.png");
|
||||
|
||||
if (requestedSize.isValid())
|
||||
return pixmap.scaled(requestedSize, Qt::KeepAspectRatio);
|
||||
|
||||
return pixmap;
|
||||
}
|
||||
|
||||
QPixmap AssetsLibraryIconProvider::generateFontIcons(const QString &filePath, const QSize &requestedSize) const
|
||||
void AssetsLibraryIconProvider::clearCache()
|
||||
{
|
||||
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.clear();
|
||||
}
|
||||
|
||||
void AssetsLibraryIconProvider::invalidateThumbnail(const QString &id)
|
||||
{
|
||||
m_thumbnails.remove(id);
|
||||
}
|
||||
|
||||
} // namespace QmlDesigner
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 "assetslibrarymodel.h"
|
||||
#include "assetslibrarydirsmodel.h"
|
||||
#include "assetslibraryfilesmodel.h"
|
||||
#include <QCheckBox>
|
||||
#include <QFileInfo>
|
||||
#include <QFileSystemModel>
|
||||
#include <QImageReader>
|
||||
#include <QMessageBox>
|
||||
#include <QSortFilterProxyModel>
|
||||
|
||||
#include "assetslibrarymodel.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();
|
||||
|
||||
setRecursiveFilteringEnabled(true);
|
||||
}
|
||||
|
||||
void AssetsLibraryModel::setSearchText(const QString &searchText)
|
||||
void AssetsLibraryModel::createBackendModel()
|
||||
{
|
||||
if (m_searchText != searchText) {
|
||||
m_searchText = searchText;
|
||||
refresh();
|
||||
}
|
||||
}
|
||||
m_sourceFsModel = new QFileSystemModel(parent());
|
||||
|
||||
void AssetsLibraryModel::saveExpandedState(bool expanded, const QString &assetPath)
|
||||
{
|
||||
m_expandedStateHash.insert(assetPath, expanded);
|
||||
}
|
||||
m_sourceFsModel->setReadOnly(false);
|
||||
|
||||
bool AssetsLibraryModel::loadExpandedState(const QString &assetPath)
|
||||
{
|
||||
return m_expandedStateHash.value(assetPath, true);
|
||||
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);
|
||||
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 (expanded)
|
||||
allCollapsed = false;
|
||||
if (!expanded)
|
||||
allExpanded = false;
|
||||
|
||||
if (!allCollapsed && !allExpanded)
|
||||
break;
|
||||
if (!isDirectory(path))
|
||||
emit fileChanged(path);
|
||||
}
|
||||
|
||||
return allExpanded ? DirExpandState::AllExpanded : allCollapsed ? DirExpandState::AllCollapsed
|
||||
: DirExpandState::SomeExpanded;
|
||||
}
|
||||
|
||||
void AssetsLibraryModel::toggleExpandAll(bool expand)
|
||||
void AssetsLibraryModel::destroyBackendModel()
|
||||
{
|
||||
std::function<void(AssetsLibraryDir *)> expandDirRecursive;
|
||||
expandDirRecursive = [&](AssetsLibraryDir *currAssetsDir) {
|
||||
if (currAssetsDir->dirDepth() > 0) {
|
||||
currAssetsDir->setDirExpanded(expand);
|
||||
saveExpandedState(expand, currAssetsDir->dirPath());
|
||||
}
|
||||
setSourceModel(nullptr);
|
||||
m_sourceFsModel->disconnect(this);
|
||||
m_sourceFsModel->deleteLater();
|
||||
m_sourceFsModel = nullptr;
|
||||
}
|
||||
|
||||
const QList<AssetsLibraryDir *> childDirs = currAssetsDir->childAssetsDirs();
|
||||
for (const auto childDir : childDirs)
|
||||
expandDirRecursive(childDir);
|
||||
};
|
||||
void AssetsLibraryModel::setSearchText(const QString &searchText)
|
||||
{
|
||||
m_searchText = searchText;
|
||||
resetModel();
|
||||
}
|
||||
|
||||
beginResetModel();
|
||||
expandDirRecursive(m_assetsDir);
|
||||
endResetModel();
|
||||
bool AssetsLibraryModel::indexIsValid(const QModelIndex &index) const
|
||||
{
|
||||
static QModelIndex invalidIndex;
|
||||
return index != invalidIndex;
|
||||
}
|
||||
|
||||
QList<QModelIndex> AssetsLibraryModel::parentIndices(const QModelIndex &index) const
|
||||
{
|
||||
QModelIndex idx = index;
|
||||
QModelIndex rootIdx = rootIndex();
|
||||
QList<QModelIndex> result;
|
||||
|
||||
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);
|
||||
|
||||
return ok;
|
||||
}
|
||||
|
||||
QObject *AssetsLibraryModel::rootDir() const
|
||||
bool AssetsLibraryModel::filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const
|
||||
{
|
||||
return m_assetsDir;
|
||||
}
|
||||
QString path = m_sourceFsModel->filePath(sourceParent);
|
||||
|
||||
bool AssetsLibraryModel::isEmpty() const
|
||||
{
|
||||
return m_isEmpty;
|
||||
}
|
||||
QModelIndex sourceIdx = m_sourceFsModel->index(sourceRow, 0, sourceParent);
|
||||
QString sourcePath = m_sourceFsModel->filePath(sourceIdx);
|
||||
|
||||
void AssetsLibraryModel::setIsEmpty(bool empty)
|
||||
{
|
||||
if (m_isEmpty != empty) {
|
||||
m_isEmpty = empty;
|
||||
emit isEmptyChanged();
|
||||
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;
|
||||
|
||||
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;
|
||||
|
||||
if (checkHaveFiles(newIdx))
|
||||
return true;
|
||||
}
|
||||
|
||||
if (m_roleNames.contains(role))
|
||||
return m_assetsDir ? m_assetsDir->property(m_roleNames.value(role)) : QVariant("");
|
||||
|
||||
qWarning() << Q_FUNC_INFO << "Invalid role requested: " << QString::number(role);
|
||||
return {};
|
||||
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();
|
||||
|
||||
static const QStringList ignoredTopLevelDirs {"imports", "asset_imports"};
|
||||
|
||||
m_fileSystemWatcher->clear();
|
||||
|
||||
std::function<bool(AssetsLibraryDir *, int, bool)> parseDir;
|
||||
parseDir = [this, &parseDir](AssetsLibraryDir *currAssetsDir, int currDepth, bool recursive) {
|
||||
m_fileSystemWatcher->addDirectory(currAssetsDir->dirPath(), Utils::FileSystemWatcher::WatchAllChanges);
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
if (!m_searchText.isEmpty() && isEmpty)
|
||||
currAssetsDir->setDirVisible(false);
|
||||
|
||||
return isEmpty;
|
||||
};
|
||||
|
||||
qCInfo(assetsLibraryBenchmark) << "directories parsed:" << time.elapsed();
|
||||
|
||||
if (m_assetsDir)
|
||||
delete m_assetsDir;
|
||||
|
||||
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);
|
||||
|
||||
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)
|
||||
destroyBackendModel();
|
||||
createBackendModel();
|
||||
|
||||
m_rootPath = newPath;
|
||||
m_sourceFsModel->setRootPath(newPath);
|
||||
|
||||
m_sourceFsModel->setNameFilters(supportedSuffixes().values());
|
||||
m_sourceFsModel->setNameFilterDisables(false);
|
||||
|
||||
m_assetsDir->setDirVisible(!noAssets); // if there are no assets, hide all empty asset folders
|
||||
endResetModel();
|
||||
|
||||
qCInfo(assetsLibraryBenchmark) << "model reset:" << time.elapsed();
|
||||
emit rootPathChanged();
|
||||
}
|
||||
|
||||
QString AssetsLibraryModel::rootPath() const
|
||||
{
|
||||
return m_rootPath;
|
||||
}
|
||||
|
||||
QString AssetsLibraryModel::filePath(const QModelIndex &index) const
|
||||
{
|
||||
QModelIndex fsIdx = mapToSource(index);
|
||||
return m_sourceFsModel->filePath(fsIdx);
|
||||
}
|
||||
|
||||
QString AssetsLibraryModel::fileName(const QModelIndex &index) const
|
||||
{
|
||||
QModelIndex fsIdx = mapToSource(index);
|
||||
return m_sourceFsModel->fileName(fsIdx);
|
||||
}
|
||||
|
||||
QModelIndex AssetsLibraryModel::indexForPath(const QString &path) const
|
||||
{
|
||||
QModelIndex idx = m_sourceFsModel->index(path, 0);
|
||||
return mapFromSource(idx);
|
||||
}
|
||||
|
||||
void AssetsLibraryModel::resetModel()
|
||||
{
|
||||
beginResetModel();
|
||||
endResetModel();
|
||||
}
|
||||
|
||||
QModelIndex AssetsLibraryModel::rootIndex() const
|
||||
{
|
||||
return indexForPath(m_rootPath);
|
||||
}
|
||||
|
||||
bool AssetsLibraryModel::isDirectory(const QString &path) const
|
||||
{
|
||||
QFileInfo fi{path};
|
||||
return fi.isDir();
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
@@ -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);
|
||||
AssetsLibraryModel(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 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
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 {
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user