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
|
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0 WITH Qt-GPL-exception-1.0
|
||||||
|
|
||||||
import QtQuick 2.15
|
import QtQuick
|
||||||
import QtQuick.Controls 2.15
|
import HelperWidgets as HelperWidgets
|
||||||
import QtQuick.Layouts 1.15
|
import StudioControls as StudioControls
|
||||||
import QtQuickDesignerTheme 1.0
|
import StudioTheme as StudioTheme
|
||||||
import HelperWidgets 2.0
|
|
||||||
import StudioControls 1.0 as StudioControls
|
|
||||||
import StudioTheme 1.0 as StudioTheme
|
|
||||||
|
|
||||||
Item {
|
Item {
|
||||||
id: root
|
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
|
// Array of supported externally dropped files that are imported as-is
|
||||||
property var dropSimpleExtFiles: []
|
property var dropSimpleExtFiles: []
|
||||||
|
|
||||||
// Array of supported externally dropped files that trigger custom import process
|
// Array of supported externally dropped files that trigger custom import process
|
||||||
property var dropComplexExtFiles: []
|
property var dropComplexExtFiles: []
|
||||||
|
|
||||||
|
readonly property int qtVersionAtLeast6_4: rootView.qtVersionIsAtLeast6_4()
|
||||||
|
property bool _searchBoxEmpty: true
|
||||||
|
|
||||||
AssetsContextMenu {
|
AssetsContextMenu {
|
||||||
id: contextMenu
|
id: contextMenu
|
||||||
|
assetsView: assetsView
|
||||||
}
|
}
|
||||||
|
|
||||||
function clearSearchFilter()
|
function clearSearchFilter()
|
||||||
@@ -63,7 +58,7 @@ Item {
|
|||||||
|
|
||||||
onDropped: {
|
onDropped: {
|
||||||
rootView.handleExtFilesDrop(root.dropSimpleExtFiles, root.dropComplexExtFiles,
|
rootView.handleExtFilesDrop(root.dropSimpleExtFiles, root.dropComplexExtFiles,
|
||||||
assetsModel.rootDir().dirPath)
|
assetsModel.rootPath())
|
||||||
}
|
}
|
||||||
|
|
||||||
Canvas { // marker for the drop area
|
Canvas { // marker for the drop area
|
||||||
@@ -90,11 +85,15 @@ Item {
|
|||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
acceptedButtons: Qt.RightButton
|
acceptedButtons: Qt.RightButton
|
||||||
onClicked: {
|
onClicked: {
|
||||||
if (!assetsModel.isEmpty) {
|
if (assetsModel.haveFiles) {
|
||||||
root.contextFilePath = ""
|
function onFolderCreated(path) {
|
||||||
root.contextDir = assetsModel.rootDir()
|
assetsView.addCreatedFolder(path)
|
||||||
root.isDirContextMenu = false
|
}
|
||||||
contextMenu.popup()
|
|
||||||
|
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()
|
function handleViewFocusOut()
|
||||||
{
|
{
|
||||||
contextMenu.close()
|
contextMenu.close()
|
||||||
root.selectedAssets = {}
|
assetsView.selectedAssets = {}
|
||||||
root.selectedAssetsChanged()
|
assetsView.selectedAssetsChanged()
|
||||||
}
|
|
||||||
|
|
||||||
RegExpValidator {
|
|
||||||
id: folderNameValidator
|
|
||||||
regExp: /^(\w[^*/><?\\|:]*)$/
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Column {
|
Column {
|
||||||
@@ -127,10 +121,29 @@ Item {
|
|||||||
|
|
||||||
width: parent.width - addAssetButton.width - 5
|
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
|
id: addAssetButton
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
tooltip: qsTr("Add a new asset to the project.")
|
tooltip: qsTr("Add a new asset to the project.")
|
||||||
@@ -146,14 +159,13 @@ Item {
|
|||||||
leftPadding: 10
|
leftPadding: 10
|
||||||
color: StudioTheme.Values.themeTextColor
|
color: StudioTheme.Values.themeTextColor
|
||||||
font.pixelSize: 12
|
font.pixelSize: 12
|
||||||
visible: assetsModel.isEmpty && !searchBox.isEmpty()
|
visible: !assetsModel.haveFiles && !root._searchBoxEmpty
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
Item { // placeholder when the assets library is empty
|
Item { // placeholder when the assets library is empty
|
||||||
width: parent.width
|
width: parent.width
|
||||||
height: parent.height - searchRow.height
|
height: parent.height - searchRow.height
|
||||||
visible: assetsModel.isEmpty && searchBox.isEmpty()
|
visible: !assetsModel.haveFiles && root._searchBoxEmpty
|
||||||
clip: true
|
clip: true
|
||||||
|
|
||||||
DropArea { // handles external drop (goes into default folder based on suffix)
|
DropArea { // handles external drop (goes into default folder based on suffix)
|
||||||
@@ -164,7 +176,7 @@ Item {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onDropped: {
|
onDropped: {
|
||||||
rootView.handleExtFilesDrop(root.dropSimpleExtFiles, root.dropComplexExtFiles)
|
rootView.emitExtFilesDrop(root.dropSimpleExtFiles, root.dropComplexExtFiles)
|
||||||
}
|
}
|
||||||
|
|
||||||
Column {
|
Column {
|
||||||
@@ -217,8 +229,11 @@ Item {
|
|||||||
|
|
||||||
AssetsView {
|
AssetsView {
|
||||||
id: assetsView
|
id: assetsView
|
||||||
|
assetsRoot: root
|
||||||
|
contextMenu: contextMenu
|
||||||
|
|
||||||
width: parent.width
|
width: parent.width
|
||||||
height: parent.height - y
|
height: parent.height - y
|
||||||
}
|
}
|
||||||
}
|
} // Column
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,90 +1,113 @@
|
|||||||
/****************************************************************************
|
// Copyright (C) 2022 The Qt Company Ltd.
|
||||||
**
|
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0 WITH Qt-GPL-exception-1.0
|
||||||
** 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.
|
|
||||||
**
|
|
||||||
****************************************************************************/
|
|
||||||
|
|
||||||
import QtQuick
|
import QtQuick
|
||||||
import QtQuick.Controls
|
import QtQuick.Controls
|
||||||
import QtQuick.Layouts
|
|
||||||
import QtQuickDesignerTheme
|
|
||||||
import HelperWidgets as HelperWidgets
|
|
||||||
import StudioControls as StudioControls
|
import StudioControls as StudioControls
|
||||||
import StudioTheme as StudioTheme
|
import StudioTheme as StudioTheme
|
||||||
|
|
||||||
StudioControls.Menu {
|
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
|
closePolicy: Popup.CloseOnPressOutside | Popup.CloseOnEscape
|
||||||
|
|
||||||
onOpened: {
|
function openContextMenuForRoot(rootModelIndex, dirPath, dirName, onFolderCreated)
|
||||||
var numSelected = Object.values(root.selectedAssets).filter(p => p).length
|
{
|
||||||
|
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")
|
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 {
|
StudioControls.MenuItem {
|
||||||
text: qsTr("Expand All")
|
text: qsTr("Expand All")
|
||||||
enabled: root.allExpandedState !== 1
|
enabled: root._allExpandedState !== "all_expanded"
|
||||||
visible: root.isDirContextMenu
|
visible: root._isDirectory
|
||||||
height: visible ? implicitHeight : 0
|
height: visible ? implicitHeight : 0
|
||||||
onTriggered: assetsModel.toggleExpandAll(true)
|
onTriggered: root.assetsView.expandAll()
|
||||||
}
|
}
|
||||||
|
|
||||||
StudioControls.MenuItem {
|
StudioControls.MenuItem {
|
||||||
text: qsTr("Collapse All")
|
text: qsTr("Collapse All")
|
||||||
enabled: root.allExpandedState !== 2
|
enabled: root._allExpandedState !== "all_collapsed"
|
||||||
visible: root.isDirContextMenu
|
visible: root._isDirectory
|
||||||
height: visible ? implicitHeight : 0
|
height: visible ? implicitHeight : 0
|
||||||
onTriggered: assetsModel.toggleExpandAll(false)
|
onTriggered: root.assetsView.collapseAll()
|
||||||
}
|
}
|
||||||
|
|
||||||
StudioControls.MenuSeparator {
|
StudioControls.MenuSeparator {
|
||||||
visible: root.isDirContextMenu
|
visible: root._isDirectory
|
||||||
height: visible ? StudioTheme.Values.border : 0
|
height: visible ? StudioTheme.Values.border : 0
|
||||||
}
|
}
|
||||||
|
|
||||||
StudioControls.MenuItem {
|
StudioControls.MenuItem {
|
||||||
id: deleteFileItem
|
id: deleteFileItem
|
||||||
text: qsTr("Delete File")
|
text: qsTr("Delete File")
|
||||||
visible: root.contextFilePath
|
visible: root._fileIndex
|
||||||
height: deleteFileItem.visible ? deleteFileItem.implicitHeight : 0
|
height: deleteFileItem.visible ? deleteFileItem.implicitHeight : 0
|
||||||
onTriggered: {
|
onTriggered: assetsModel.deleteFiles(root._selectedAssetPathsList)
|
||||||
assetsModel.deleteFiles(Object.keys(root.selectedAssets).filter(p => root.selectedAssets[p]))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
StudioControls.MenuSeparator {
|
StudioControls.MenuSeparator {
|
||||||
visible: root.contextFilePath
|
visible: root._fileIndex
|
||||||
height: visible ? StudioTheme.Values.border : 0
|
height: visible ? StudioTheme.Values.border : 0
|
||||||
}
|
}
|
||||||
|
|
||||||
StudioControls.MenuItem {
|
StudioControls.MenuItem {
|
||||||
text: qsTr("Rename Folder")
|
text: qsTr("Rename Folder")
|
||||||
visible: root.isDirContextMenu
|
visible: root._isDirectory
|
||||||
height: visible ? implicitHeight : 0
|
height: visible ? implicitHeight : 0
|
||||||
onTriggered: renameFolderDialog.open()
|
onTriggered: renameFolderDialog.open()
|
||||||
|
|
||||||
RenameFolderDialog {
|
RenameFolderDialog {
|
||||||
id: renameFolderDialog
|
id: renameFolderDialog
|
||||||
|
parent: root.assetsView
|
||||||
|
dirPath: root._dirPath
|
||||||
|
dirName: root._dirName
|
||||||
|
|
||||||
|
onAccepted: root._onFolderRenamed()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -93,6 +116,10 @@ StudioControls.Menu {
|
|||||||
|
|
||||||
NewFolderDialog {
|
NewFolderDialog {
|
||||||
id: newFolderDialog
|
id: newFolderDialog
|
||||||
|
parent: root.assetsView
|
||||||
|
dirPath: root._dirPath
|
||||||
|
|
||||||
|
onAccepted: root._onFolderCreated(newFolderDialog.createdDirPath)
|
||||||
}
|
}
|
||||||
|
|
||||||
onTriggered: newFolderDialog.open()
|
onTriggered: newFolderDialog.open()
|
||||||
@@ -100,21 +127,25 @@ StudioControls.Menu {
|
|||||||
|
|
||||||
StudioControls.MenuItem {
|
StudioControls.MenuItem {
|
||||||
text: qsTr("Delete Folder")
|
text: qsTr("Delete Folder")
|
||||||
visible: root.isDirContextMenu
|
visible: root._isDirectory
|
||||||
height: visible ? implicitHeight : 0
|
height: visible ? implicitHeight : 0
|
||||||
|
|
||||||
ConfirmDeleteFolderDialog {
|
ConfirmDeleteFolderDialog {
|
||||||
id: confirmDeleteFolderDialog
|
id: confirmDeleteFolderDialog
|
||||||
|
parent: root.assetsView
|
||||||
|
dirName: root._dirName
|
||||||
|
dirIndex: root._dirIndex
|
||||||
}
|
}
|
||||||
|
|
||||||
onTriggered: {
|
onTriggered: {
|
||||||
var dirEmpty = !(root.contextDir.dirsModel && root.contextDir.dirsModel.rowCount() > 0)
|
if (!assetsModel.hasChildren(root._dirIndex)) {
|
||||||
&& !(root.contextDir.filesModel && root.contextDir.filesModel.rowCount() > 0);
|
// 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
|
||||||
if (dirEmpty)
|
// types) on disk in this directory.
|
||||||
assetsModel.deleteFolder(root.contextDir.dirPath)
|
assetsModel.deleteFolderRecursively(root._dirIndex)
|
||||||
else
|
} else {
|
||||||
confirmDeleteFolderDialog.open()
|
confirmDeleteFolderDialog.open()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,255 +1,309 @@
|
|||||||
/****************************************************************************
|
// Copyright (C) 2022 The Qt Company Ltd.
|
||||||
**
|
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0 WITH Qt-GPL-exception-1.0
|
||||||
** 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.
|
|
||||||
**
|
|
||||||
****************************************************************************/
|
|
||||||
|
|
||||||
import QtQuick
|
import QtQuick
|
||||||
import QtQuick.Controls
|
import QtQuick.Controls
|
||||||
import QtQuick.Layouts
|
import HelperWidgets as HelperWidgets
|
||||||
import QtQuickDesignerTheme
|
|
||||||
import HelperWidgets
|
|
||||||
import StudioControls as StudioControls
|
import StudioControls as StudioControls
|
||||||
import StudioTheme as StudioTheme
|
|
||||||
|
|
||||||
ScrollView { // TODO: experiment using ListView instead of ScrollView + Column
|
TreeView {
|
||||||
id: assetsView
|
id: root
|
||||||
clip: true
|
clip: true
|
||||||
interactive: assetsView.verticalScrollBarVisible && !contextMenu.opened
|
interactive: verticalScrollBar.visible && !root.contextMenu.opened
|
||||||
|
reuseItems: false
|
||||||
|
boundsBehavior: Flickable.StopAtBounds
|
||||||
|
rowSpacing: 5
|
||||||
|
|
||||||
Column {
|
required property Item assetsRoot
|
||||||
Repeater {
|
required property StudioControls.Menu contextMenu
|
||||||
model: assetsModel // context property
|
property alias verticalScrollBar: verticalScrollBar
|
||||||
delegate: dirSection
|
|
||||||
|
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
|
||||||
}
|
}
|
||||||
|
|
||||||
Component {
|
ScrollBar.vertical: HelperWidgets.VerticalScrollBar {
|
||||||
id: dirSection
|
id: verticalScrollBar
|
||||||
|
scrollBarVisible: root.contentHeight > root.height
|
||||||
Section {
|
|
||||||
id: section
|
|
||||||
|
|
||||||
width: assetsView.width -
|
|
||||||
(assetsView.verticalScrollBarVisible ? assetsView.verticalThickness : 0) - 5
|
|
||||||
caption: dirName
|
|
||||||
sectionHeight: 30
|
|
||||||
sectionFontSize: 15
|
|
||||||
leftPadding: 0
|
|
||||||
topPadding: dirDepth > 0 ? 5 : 0
|
|
||||||
bottomPadding: 0
|
|
||||||
hideHeader: dirDepth === 0
|
|
||||||
showLeftBorder: dirDepth > 0
|
|
||||||
expanded: dirExpanded
|
|
||||||
visible: dirVisible
|
|
||||||
expandOnClick: false
|
|
||||||
useDefaulContextMenu: false
|
|
||||||
dropEnabled: true
|
|
||||||
|
|
||||||
onToggleExpand: {
|
|
||||||
dirExpanded = !dirExpanded
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onDropEnter: (drag)=> {
|
model: assetsModel
|
||||||
root.updateDropExtFiles(drag)
|
|
||||||
section.highlight = drag.accepted && root.dropSimpleExtFiles.length > 0
|
onRowsChanged: {
|
||||||
|
if (root.rows > root.rootPathRow + 1 && !assetsModel.haveFiles ||
|
||||||
|
root.rows <= root.rootPathRow + 1 && assetsModel.haveFiles) {
|
||||||
|
assetsModel.syncHaveFiles()
|
||||||
}
|
}
|
||||||
|
|
||||||
onDropExit: {
|
updateRows()
|
||||||
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 {
|
Timer {
|
||||||
interval: 1000
|
id: updateRowsTimer
|
||||||
running: mouseArea.containsMouse && mouseArea.allowTooltip
|
interval: 200
|
||||||
|
repeat: false
|
||||||
|
|
||||||
onTriggered: {
|
onTriggered: {
|
||||||
if (suffix === ".ttf" || suffix === ".otf") {
|
root.updateRows()
|
||||||
tooltipBackend.name = fileName
|
|
||||||
tooltipBackend.path = filePath
|
|
||||||
tooltipBackend.showTooltip()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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.
|
||||||
**
|
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0 WITH Qt-GPL-exception-1.0
|
||||||
** 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.
|
|
||||||
**
|
|
||||||
****************************************************************************/
|
|
||||||
|
|
||||||
import QtQuick
|
import QtQuick
|
||||||
import QtQuick.Controls
|
import QtQuick.Controls
|
||||||
import QtQuick.Layouts
|
import HelperWidgets as HelperWidgets
|
||||||
import QtQuickDesignerTheme
|
|
||||||
import HelperWidgets
|
|
||||||
import StudioControls as StudioControls
|
|
||||||
import StudioTheme as StudioTheme
|
import StudioTheme as StudioTheme
|
||||||
|
|
||||||
Dialog {
|
Dialog {
|
||||||
id: confirmDeleteFolderDialog
|
id: root
|
||||||
|
|
||||||
title: qsTr("Folder Not Empty")
|
title: qsTr("Folder Not Empty")
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
@@ -40,6 +15,9 @@ Dialog {
|
|||||||
implicitWidth: 300
|
implicitWidth: 300
|
||||||
modal: true
|
modal: true
|
||||||
|
|
||||||
|
required property string dirName
|
||||||
|
required property var dirIndex
|
||||||
|
|
||||||
contentItem: Column {
|
contentItem: Column {
|
||||||
spacing: 20
|
spacing: 20
|
||||||
width: parent.width
|
width: parent.width
|
||||||
@@ -47,11 +25,10 @@ Dialog {
|
|||||||
Text {
|
Text {
|
||||||
id: folderNotEmpty
|
id: folderNotEmpty
|
||||||
|
|
||||||
text: qsTr("Folder \"%1\" is not empty. Delete it anyway?")
|
text: qsTr("Folder \"%1\" is not empty. Delete it anyway?").arg(root.dirName)
|
||||||
.arg(root.contextDir ? root.contextDir.dirName : "")
|
|
||||||
color: StudioTheme.Values.themeTextColor
|
color: StudioTheme.Values.themeTextColor
|
||||||
wrapMode: Text.WordWrap
|
wrapMode: Text.WordWrap
|
||||||
width: confirmDeleteFolderDialog.width
|
width: root.width
|
||||||
leftPadding: 10
|
leftPadding: 10
|
||||||
rightPadding: 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.")
|
text: qsTr("If the folder has assets in use, deleting it might cause the project to not work correctly.")
|
||||||
color: StudioTheme.Values.themeTextColor
|
color: StudioTheme.Values.themeTextColor
|
||||||
wrapMode: Text.WordWrap
|
wrapMode: Text.WordWrap
|
||||||
width: confirmDeleteFolderDialog.width
|
width: root.width
|
||||||
leftPadding: 10
|
leftPadding: 10
|
||||||
rightPadding: 10
|
rightPadding: 10
|
||||||
}
|
}
|
||||||
|
|
||||||
Row {
|
Row {
|
||||||
anchors.right: parent.right
|
anchors.right: parent.right
|
||||||
Button {
|
HelperWidgets.Button {
|
||||||
id: btnDelete
|
id: btnDelete
|
||||||
|
|
||||||
text: qsTr("Delete")
|
text: qsTr("Delete")
|
||||||
|
|
||||||
onClicked: {
|
onClicked: {
|
||||||
assetsModel.deleteFolder(root.contextDir.dirPath)
|
assetsModel.deleteFolderRecursively(root.dirIndex)
|
||||||
confirmDeleteFolderDialog.accept()
|
root.accept()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Button {
|
HelperWidgets.Button {
|
||||||
text: qsTr("Cancel")
|
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.
|
||||||
**
|
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0 WITH Qt-GPL-exception-1.0
|
||||||
** 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.
|
|
||||||
**
|
|
||||||
****************************************************************************/
|
|
||||||
|
|
||||||
import QtQuick
|
import QtQuick
|
||||||
import QtQuick.Controls
|
import QtQuick.Controls
|
||||||
import QtQuick.Layouts
|
import HelperWidgets as HelperWidgets
|
||||||
import QtQuickDesignerTheme
|
|
||||||
import HelperWidgets
|
|
||||||
import StudioControls as StudioControls
|
import StudioControls as StudioControls
|
||||||
import StudioTheme as StudioTheme
|
import StudioTheme as StudioTheme
|
||||||
|
|
||||||
Dialog {
|
Dialog {
|
||||||
id: newFolderDialog
|
id: root
|
||||||
|
|
||||||
title: qsTr("Create New Folder")
|
title: qsTr("Create New Folder")
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
closePolicy: Popup.CloseOnEscape
|
closePolicy: Popup.CloseOnEscape
|
||||||
modal: true
|
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 {
|
contentItem: Column {
|
||||||
spacing: 2
|
spacing: 2
|
||||||
|
|
||||||
@@ -58,6 +49,10 @@ Dialog {
|
|||||||
|
|
||||||
Keys.onEnterPressed: btnCreate.onClicked()
|
Keys.onEnterPressed: btnCreate.onClicked()
|
||||||
Keys.onReturnPressed: btnCreate.onClicked()
|
Keys.onReturnPressed: btnCreate.onClicked()
|
||||||
|
|
||||||
|
onTextChanged: {
|
||||||
|
root.createdDirPath = root.dirPath + '/' + folderName.text
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -68,6 +63,13 @@ Dialog {
|
|||||||
visible: folderName.text === ""
|
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
|
Item { // spacer
|
||||||
width: 1
|
width: 1
|
||||||
height: 20
|
height: 20
|
||||||
@@ -76,20 +78,23 @@ Dialog {
|
|||||||
Row {
|
Row {
|
||||||
anchors.right: parent.right
|
anchors.right: parent.right
|
||||||
|
|
||||||
Button {
|
HelperWidgets.Button {
|
||||||
id: btnCreate
|
id: btnCreate
|
||||||
|
|
||||||
text: qsTr("Create")
|
text: qsTr("Create")
|
||||||
enabled: folderName.text !== ""
|
enabled: folderName.text !== "" && root.createdDirPath.length <= root._maxPath
|
||||||
onClicked: {
|
onClicked: {
|
||||||
assetsModel.addNewFolder(root.contextDir.dirPath + '/' + folderName.text)
|
root.createdDirPath = root.dirPath + '/' + folderName.text
|
||||||
newFolderDialog.accept()
|
if (assetsModel.addNewFolder(root.createdDirPath))
|
||||||
|
root.accept()
|
||||||
|
else
|
||||||
|
creationFailedDialog.open()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Button {
|
HelperWidgets.Button {
|
||||||
text: qsTr("Cancel")
|
text: qsTr("Cancel")
|
||||||
onClicked: newFolderDialog.reject()
|
onClicked: root.reject()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -99,4 +104,8 @@ Dialog {
|
|||||||
folderName.selectAll()
|
folderName.selectAll()
|
||||||
folderName.forceActiveFocus()
|
folderName.forceActiveFocus()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onRejected: {
|
||||||
|
root.createdDirPath = ""
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,38 +1,14 @@
|
|||||||
/****************************************************************************
|
// Copyright (C) 2022 The Qt Company Ltd.
|
||||||
**
|
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0 WITH Qt-GPL-exception-1.0
|
||||||
** 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.
|
|
||||||
**
|
|
||||||
****************************************************************************/
|
|
||||||
|
|
||||||
import QtQuick
|
import QtQuick
|
||||||
import QtQuick.Controls
|
import QtQuick.Controls
|
||||||
import QtQuick.Layouts
|
import HelperWidgets as HelperWidgets
|
||||||
import QtQuickDesignerTheme
|
|
||||||
import HelperWidgets
|
|
||||||
import StudioControls as StudioControls
|
import StudioControls as StudioControls
|
||||||
import StudioTheme as StudioTheme
|
import StudioTheme as StudioTheme
|
||||||
|
|
||||||
Dialog {
|
Dialog {
|
||||||
id: renameFolderDialog
|
id: root
|
||||||
|
|
||||||
title: qsTr("Rename Folder")
|
title: qsTr("Rename Folder")
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
@@ -41,6 +17,13 @@ Dialog {
|
|||||||
modal: true
|
modal: true
|
||||||
|
|
||||||
property bool renameError: false
|
property bool renameError: false
|
||||||
|
required property string dirPath
|
||||||
|
required property string dirName
|
||||||
|
|
||||||
|
HelperWidgets.RegExpValidator {
|
||||||
|
id: folderNameValidator
|
||||||
|
regExp: /^(\w[^*/><?\\|:]*)$/
|
||||||
|
}
|
||||||
|
|
||||||
contentItem: Column {
|
contentItem: Column {
|
||||||
spacing: 2
|
spacing: 2
|
||||||
@@ -50,10 +33,10 @@ Dialog {
|
|||||||
|
|
||||||
actionIndicator.visible: false
|
actionIndicator.visible: false
|
||||||
translationIndicator.visible: false
|
translationIndicator.visible: false
|
||||||
width: renameFolderDialog.width - 12
|
width: root.width - 12
|
||||||
validator: folderNameValidator
|
validator: folderNameValidator
|
||||||
|
|
||||||
onEditChanged: renameFolderDialog.renameError = false
|
onEditChanged: root.renameError = false
|
||||||
Keys.onEnterPressed: btnRename.onClicked()
|
Keys.onEnterPressed: btnRename.onClicked()
|
||||||
Keys.onReturnPressed: btnRename.onClicked()
|
Keys.onReturnPressed: btnRename.onClicked()
|
||||||
}
|
}
|
||||||
@@ -61,15 +44,15 @@ Dialog {
|
|||||||
Text {
|
Text {
|
||||||
text: qsTr("Folder name cannot be empty.")
|
text: qsTr("Folder name cannot be empty.")
|
||||||
color: "#ff0000"
|
color: "#ff0000"
|
||||||
visible: folderRename.text === "" && !renameFolderDialog.renameError
|
visible: folderRename.text === "" && !root.renameError
|
||||||
}
|
}
|
||||||
|
|
||||||
Text {
|
Text {
|
||||||
text: qsTr("Could not rename folder. Make sure no folder with the same name exists.")
|
text: qsTr("Could not rename folder. Make sure no folder with the same name exists.")
|
||||||
wrapMode: Text.WordWrap
|
wrapMode: Text.WordWrap
|
||||||
width: renameFolderDialog.width - 12
|
width: root.width - 12
|
||||||
color: "#ff0000"
|
color: "#ff0000"
|
||||||
visible: renameFolderDialog.renameError
|
visible: root.renameError
|
||||||
}
|
}
|
||||||
|
|
||||||
Item { // spacer
|
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.")
|
text: qsTr("If the folder has assets in use, renaming it might cause the project to not work correctly.")
|
||||||
color: StudioTheme.Values.themeTextColor
|
color: StudioTheme.Values.themeTextColor
|
||||||
wrapMode: Text.WordWrap
|
wrapMode: Text.WordWrap
|
||||||
width: renameFolderDialog.width
|
width: root.width
|
||||||
leftPadding: 10
|
leftPadding: 10
|
||||||
rightPadding: 10
|
rightPadding: 10
|
||||||
}
|
}
|
||||||
@@ -94,31 +77,31 @@ Dialog {
|
|||||||
Row {
|
Row {
|
||||||
anchors.right: parent.right
|
anchors.right: parent.right
|
||||||
|
|
||||||
Button {
|
HelperWidgets.Button {
|
||||||
id: btnRename
|
id: btnRename
|
||||||
|
|
||||||
text: qsTr("Rename")
|
text: qsTr("Rename")
|
||||||
enabled: folderRename.text !== ""
|
enabled: folderRename.text !== ""
|
||||||
onClicked: {
|
onClicked: {
|
||||||
var success = assetsModel.renameFolder(root.contextDir.dirPath, folderRename.text)
|
var success = assetsModel.renameFolder(root.dirPath, folderRename.text)
|
||||||
if (success)
|
if (success)
|
||||||
renameFolderDialog.accept()
|
root.accept()
|
||||||
|
|
||||||
renameFolderDialog.renameError = !success
|
root.renameError = !success
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Button {
|
HelperWidgets.Button {
|
||||||
text: qsTr("Cancel")
|
text: qsTr("Cancel")
|
||||||
onClicked: renameFolderDialog.reject()
|
onClicked: root.reject()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onOpened: {
|
onOpened: {
|
||||||
folderRename.text = root.contextDir.dirName
|
folderRename.text = root.dirName
|
||||||
folderRename.selectAll()
|
folderRename.selectAll()
|
||||||
folderRename.forceActiveFocus()
|
folderRename.forceActiveFocus()
|
||||||
renameFolderDialog.renameError = false
|
root.renameError = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -740,9 +740,6 @@ extend_qtc_plugin(QmlDesigner
|
|||||||
assetslibrarywidget.cpp assetslibrarywidget.h
|
assetslibrarywidget.cpp assetslibrarywidget.h
|
||||||
assetslibrarymodel.cpp assetslibrarymodel.h
|
assetslibrarymodel.cpp assetslibrarymodel.h
|
||||||
assetslibraryiconprovider.cpp assetslibraryiconprovider.h
|
assetslibraryiconprovider.cpp assetslibraryiconprovider.h
|
||||||
assetslibrarydir.cpp assetslibrarydir.h
|
|
||||||
assetslibrarydirsmodel.cpp assetslibrarydirsmodel.h
|
|
||||||
assetslibraryfilesmodel.cpp assetslibraryfilesmodel.h
|
|
||||||
)
|
)
|
||||||
|
|
||||||
extend_qtc_plugin(QmlDesigner
|
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 AssetsLibraryIconProvider::requestPixmap(const QString &id, QSize *size, const QSize &requestedSize)
|
||||||
{
|
{
|
||||||
QPixmap pixmap;
|
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();
|
const QString suffix = "*." + id.split('.').last().toLower();
|
||||||
if (id == "browse") {
|
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)) {
|
} else if (AssetsLibraryModel::supportedFontSuffixes().contains(suffix)) {
|
||||||
pixmap = generateFontIcons(id, requestedSize);
|
return generateFontIcons(id, requestedSize);
|
||||||
} else if (AssetsLibraryModel::supportedImageSuffixes().contains(suffix)) {
|
} else if (AssetsLibraryModel::supportedImageSuffixes().contains(suffix)) {
|
||||||
pixmap = Utils::StyleHelper::dpiSpecificImageFile(id);
|
return Utils::StyleHelper::dpiSpecificImageFile(id);
|
||||||
} else if (AssetsLibraryModel::supportedTexture3DSuffixes().contains(suffix)) {
|
} else if (AssetsLibraryModel::supportedTexture3DSuffixes().contains(suffix)) {
|
||||||
pixmap = HdrImage{id}.toPixmap();
|
return HdrImage{id}.toPixmap();
|
||||||
} else {
|
} else {
|
||||||
QString type;
|
QString type;
|
||||||
if (AssetsLibraryModel::supportedShaderSuffixes().contains(suffix))
|
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 pathTemplate = QString(":/AssetsLibrary/images/asset_%1%2.png").arg(type);
|
||||||
QString path = pathTemplate.arg('_' + QString::number(requestedSize.width()));
|
QString path = pathTemplate.arg('_' + QString::number(requestedSize.width()));
|
||||||
|
|
||||||
pixmap = Utils::StyleHelper::dpiSpecificImageFile(QFileInfo::exists(path) ? path
|
return Utils::StyleHelper::dpiSpecificImageFile(QFileInfo::exists(path)
|
||||||
|
? path
|
||||||
: pathTemplate.arg(""));
|
: pathTemplate.arg(""));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (size) {
|
|
||||||
size->setWidth(pixmap.width());
|
|
||||||
size->setHeight(pixmap.height());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (pixmap.isNull())
|
void AssetsLibraryIconProvider::clearCache()
|
||||||
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
|
|
||||||
{
|
{
|
||||||
QSize reqSize = requestedSize.isValid() ? requestedSize : QSize{48, 48};
|
m_thumbnails.clear();
|
||||||
return m_fontImageCache.icon(filePath, {},
|
}
|
||||||
ImageCache::FontCollectorSizesAuxiliaryData{Utils::span{iconSizes},
|
|
||||||
Theme::getColor(Theme::DStextColor).name(),
|
void AssetsLibraryIconProvider::invalidateThumbnail(const QString &id)
|
||||||
"Abc"}).pixmap(reqSize);
|
{
|
||||||
|
m_thumbnails.remove(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace QmlDesigner
|
} // namespace QmlDesigner
|
||||||
|
|||||||
@@ -15,9 +15,12 @@ public:
|
|||||||
AssetsLibraryIconProvider(SynchronousImageCache &fontImageCache);
|
AssetsLibraryIconProvider(SynchronousImageCache &fontImageCache);
|
||||||
|
|
||||||
QPixmap requestPixmap(const QString &id, QSize *size, const QSize &requestedSize) override;
|
QPixmap requestPixmap(const QString &id, QSize *size, const QSize &requestedSize) override;
|
||||||
|
void clearCache();
|
||||||
|
void invalidateThumbnail(const QString &id);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
QPixmap generateFontIcons(const QString &filePath, const QSize &requestedSize) const;
|
QPixmap generateFontIcons(const QString &filePath, const QSize &requestedSize) const;
|
||||||
|
QPixmap fetchPixmap(const QString &id, const QSize &requestedSize) const;
|
||||||
|
|
||||||
SynchronousImageCache &m_fontImageCache;
|
SynchronousImageCache &m_fontImageCache;
|
||||||
|
|
||||||
@@ -26,6 +29,7 @@ private:
|
|||||||
std::vector<QSize> iconSizes = {{128, 128}, // Drag
|
std::vector<QSize> iconSizes = {{128, 128}, // Drag
|
||||||
{96, 96}, // list @2x
|
{96, 96}, // list @2x
|
||||||
{48, 48}}; // list
|
{48, 48}}; // list
|
||||||
|
QHash<QString, QPixmap> m_thumbnails;
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace QmlDesigner
|
} // namespace QmlDesigner
|
||||||
|
|||||||
@@ -1,68 +1,44 @@
|
|||||||
// Copyright (C) 2021 The Qt Company Ltd.
|
// 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
|
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0+ OR GPL-3.0 WITH Qt-GPL-exception-1.0
|
||||||
|
|
||||||
#include "assetslibrarymodel.h"
|
#include <QCheckBox>
|
||||||
#include "assetslibrarydirsmodel.h"
|
#include <QFileInfo>
|
||||||
#include "assetslibraryfilesmodel.h"
|
#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 <qmldesignerplugin.h>
|
||||||
#include <modelnodeoperations.h>
|
#include <modelnodeoperations.h>
|
||||||
|
|
||||||
#include <coreplugin/icore.h>
|
#include <coreplugin/icore.h>
|
||||||
|
#include <utils/qtcassert.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)
|
|
||||||
|
|
||||||
namespace QmlDesigner {
|
namespace QmlDesigner {
|
||||||
|
|
||||||
AssetsLibraryModel::AssetsLibraryModel(Utils::FileSystemWatcher *fileSystemWatcher, QObject *parent)
|
AssetsLibraryModel::AssetsLibraryModel(QObject *parent)
|
||||||
: QAbstractListModel(parent)
|
: QSortFilterProxyModel{parent}
|
||||||
, m_fileSystemWatcher(fileSystemWatcher)
|
|
||||||
{
|
{
|
||||||
// add role names
|
createBackendModel();
|
||||||
int role = 0;
|
|
||||||
const QMetaObject meta = AssetsLibraryDir::staticMetaObject;
|
setRecursiveFilteringEnabled(true);
|
||||||
for (int i = meta.propertyOffset(); i < meta.propertyCount(); ++i)
|
|
||||||
m_roleNames.insert(role++, meta.property(i).name());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void AssetsLibraryModel::setSearchText(const QString &searchText)
|
void AssetsLibraryModel::createBackendModel()
|
||||||
{
|
{
|
||||||
if (m_searchText != searchText) {
|
m_sourceFsModel = new QFileSystemModel(parent());
|
||||||
m_searchText = searchText;
|
|
||||||
refresh();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void AssetsLibraryModel::saveExpandedState(bool expanded, const QString &assetPath)
|
m_sourceFsModel->setReadOnly(false);
|
||||||
{
|
|
||||||
m_expandedStateHash.insert(assetPath, expanded);
|
|
||||||
}
|
|
||||||
|
|
||||||
bool AssetsLibraryModel::loadExpandedState(const QString &assetPath)
|
setSourceModel(m_sourceFsModel);
|
||||||
{
|
QObject::connect(m_sourceFsModel, &QFileSystemModel::directoryLoaded, this, &AssetsLibraryModel::directoryLoaded);
|
||||||
return m_expandedStateHash.value(assetPath, true);
|
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)
|
bool AssetsLibraryModel::isEffectQmlExist(const QString &effectName)
|
||||||
@@ -72,50 +48,57 @@ bool AssetsLibraryModel::isEffectQmlExist(const QString &effectName)
|
|||||||
return qmlPath.exists();
|
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();
|
for (int i = topLeft.row(); i <= bottomRight.row(); ++i) {
|
||||||
bool allExpanded = true;
|
QModelIndex index = m_sourceFsModel->index(i, 0, topLeft.parent());
|
||||||
bool allCollapsed = true;
|
QString path = m_sourceFsModel->filePath(index);
|
||||||
for (const QString &assetPath : keys) {
|
|
||||||
bool expanded = m_expandedStateHash.value(assetPath);
|
|
||||||
|
|
||||||
if (expanded)
|
if (!isDirectory(path))
|
||||||
allCollapsed = false;
|
emit fileChanged(path);
|
||||||
if (!expanded)
|
}
|
||||||
allExpanded = false;
|
|
||||||
|
|
||||||
if (!allCollapsed && !allExpanded)
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return allExpanded ? DirExpandState::AllExpanded : allCollapsed ? DirExpandState::AllCollapsed
|
void AssetsLibraryModel::destroyBackendModel()
|
||||||
: DirExpandState::SomeExpanded;
|
|
||||||
}
|
|
||||||
|
|
||||||
void AssetsLibraryModel::toggleExpandAll(bool expand)
|
|
||||||
{
|
{
|
||||||
std::function<void(AssetsLibraryDir *)> expandDirRecursive;
|
setSourceModel(nullptr);
|
||||||
expandDirRecursive = [&](AssetsLibraryDir *currAssetsDir) {
|
m_sourceFsModel->disconnect(this);
|
||||||
if (currAssetsDir->dirDepth() > 0) {
|
m_sourceFsModel->deleteLater();
|
||||||
currAssetsDir->setDirExpanded(expand);
|
m_sourceFsModel = nullptr;
|
||||||
saveExpandedState(expand, currAssetsDir->dirPath());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const QList<AssetsLibraryDir *> childDirs = currAssetsDir->childAssetsDirs();
|
void AssetsLibraryModel::setSearchText(const QString &searchText)
|
||||||
for (const auto childDir : childDirs)
|
{
|
||||||
expandDirRecursive(childDir);
|
m_searchText = searchText;
|
||||||
};
|
resetModel();
|
||||||
|
}
|
||||||
|
|
||||||
beginResetModel();
|
bool AssetsLibraryModel::indexIsValid(const QModelIndex &index) const
|
||||||
expandDirRecursive(m_assetsDir);
|
{
|
||||||
endResetModel();
|
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)
|
void AssetsLibraryModel::deleteFiles(const QStringList &filePaths)
|
||||||
{
|
{
|
||||||
bool askBeforeDelete = QmlDesignerPlugin::settings().value(
|
bool askBeforeDelete = QmlDesignerPlugin::settings()
|
||||||
DesignerSettingsKey::ASK_BEFORE_DELETING_ASSET).toBool();
|
.value(DesignerSettingsKey::ASK_BEFORE_DELETING_ASSET)
|
||||||
|
.toBool();
|
||||||
bool assetDelete = true;
|
bool assetDelete = true;
|
||||||
|
|
||||||
if (askBeforeDelete) {
|
if (askBeforeDelete) {
|
||||||
@@ -162,15 +145,13 @@ bool AssetsLibraryModel::renameFolder(const QString &folderPath, const QString &
|
|||||||
|
|
||||||
dir.cdUp();
|
dir.cdUp();
|
||||||
|
|
||||||
saveExpandedState(loadExpandedState(folderPath), dir.absoluteFilePath(newName));
|
|
||||||
|
|
||||||
return dir.rename(oldName, newName);
|
return dir.rename(oldName, newName);
|
||||||
}
|
}
|
||||||
|
|
||||||
void AssetsLibraryModel::addNewFolder(const QString &folderPath)
|
bool AssetsLibraryModel::addNewFolder(const QString &folderPath)
|
||||||
{
|
{
|
||||||
QString iterPath = 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};
|
QDir dir{folderPath};
|
||||||
|
|
||||||
while (dir.exists()) {
|
while (dir.exists()) {
|
||||||
@@ -200,136 +181,155 @@ void AssetsLibraryModel::addNewFolder(const QString &folderPath)
|
|||||||
dir.setPath(iterPath);
|
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);
|
||||||
|
|
||||||
|
QModelIndex sourceIdx = m_sourceFsModel->index(sourceRow, 0, sourceParent);
|
||||||
|
QString sourcePath = m_sourceFsModel->filePath(sourceIdx);
|
||||||
|
|
||||||
|
if (!m_searchText.isEmpty() && path.startsWith(m_rootPath) && QFileInfo{path}.isDir()) {
|
||||||
|
QString sourceName = m_sourceFsModel->fileName(sourceIdx);
|
||||||
|
|
||||||
|
return QFileInfo{sourcePath}.isFile() && sourceName.contains(m_searchText, Qt::CaseInsensitive);
|
||||||
|
} else {
|
||||||
|
return sourcePath.startsWith(m_rootPath) || m_rootPath.startsWith(sourcePath);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
bool AssetsLibraryModel::isEmpty() const
|
bool AssetsLibraryModel::checkHaveFiles(const QModelIndex &parentIdx) const
|
||||||
{
|
{
|
||||||
return m_isEmpty;
|
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
void AssetsLibraryModel::setIsEmpty(bool empty)
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void AssetsLibraryModel::setHaveFiles(bool value)
|
||||||
{
|
{
|
||||||
if (m_isEmpty != empty) {
|
if (m_haveFiles != value) {
|
||||||
m_isEmpty = empty;
|
m_haveFiles = value;
|
||||||
emit isEmptyChanged();
|
emit haveFilesChanged();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
QVariant AssetsLibraryModel::data(const QModelIndex &index, int role) const
|
bool AssetsLibraryModel::checkHaveFiles() const
|
||||||
{
|
{
|
||||||
if (!index.isValid()) {
|
auto rootIdx = indexForPath(m_rootPath);
|
||||||
qWarning() << Q_FUNC_INFO << "Invalid index requested: " << QString::number(index.row());
|
return checkHaveFiles(rootIdx);
|
||||||
return {};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (m_roleNames.contains(role))
|
void AssetsLibraryModel::syncHaveFiles()
|
||||||
return m_assetsDir ? m_assetsDir->property(m_roleNames.value(role)) : QVariant("");
|
|
||||||
|
|
||||||
qWarning() << Q_FUNC_INFO << "Invalid role requested: " << QString::number(role);
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
|
|
||||||
int AssetsLibraryModel::rowCount([[maybe_unused]] const QModelIndex &parent) const
|
|
||||||
{
|
{
|
||||||
return 1;
|
setHaveFiles(checkHaveFiles());
|
||||||
}
|
}
|
||||||
|
|
||||||
QHash<int, QByteArray> AssetsLibraryModel::roleNames() const
|
void AssetsLibraryModel::setRootPath(const QString &newPath)
|
||||||
{
|
{
|
||||||
return m_roleNames;
|
|
||||||
}
|
|
||||||
|
|
||||||
// called when a directory is changed to refresh the model for this directory
|
|
||||||
void AssetsLibraryModel::refresh()
|
|
||||||
{
|
|
||||||
setRootPath(m_assetsDir->dirPath());
|
|
||||||
}
|
|
||||||
|
|
||||||
void AssetsLibraryModel::setRootPath(const QString &path)
|
|
||||||
{
|
|
||||||
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();
|
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;
|
destroyBackendModel();
|
||||||
// noAssets: the model has no asset files (project has no assets added)
|
createBackendModel();
|
||||||
// isEmpty: the model has no asset files (assets could exist but are filtered out)
|
|
||||||
|
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();
|
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()
|
const QStringList &AssetsLibraryModel::supportedImageSuffixes()
|
||||||
@@ -408,17 +408,4 @@ const QSet<QString> &AssetsLibraryModel::supportedSuffixes()
|
|||||||
return allSuffixes;
|
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
|
} // namespace QmlDesigner
|
||||||
|
|||||||
@@ -3,39 +3,53 @@
|
|||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <QAbstractListModel>
|
#include <QFileSystemModel>
|
||||||
#include <QDateTime>
|
#include <QSortFilterProxyModel>
|
||||||
#include <QDir>
|
#include <QFileInfo>
|
||||||
#include <QHash>
|
|
||||||
#include <QIcon>
|
|
||||||
#include <QPair>
|
|
||||||
#include <QSet>
|
|
||||||
|
|
||||||
namespace Utils { class FileSystemWatcher; }
|
#include <utils/qtcassert.h>
|
||||||
|
|
||||||
namespace QmlDesigner {
|
namespace QmlDesigner {
|
||||||
|
|
||||||
class SynchronousImageCache;
|
class AssetsLibraryModel : public QSortFilterProxyModel
|
||||||
class AssetsLibraryDir;
|
|
||||||
|
|
||||||
class AssetsLibraryModel : public QAbstractListModel
|
|
||||||
{
|
{
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
|
|
||||||
Q_PROPERTY(bool isEmpty READ isEmpty WRITE setIsEmpty NOTIFY isEmptyChanged)
|
|
||||||
|
|
||||||
public:
|
public:
|
||||||
AssetsLibraryModel(Utils::FileSystemWatcher *fileSystemWatcher, QObject *parent = nullptr);
|
AssetsLibraryModel(QObject *parent = nullptr);
|
||||||
|
|
||||||
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
|
void setRootPath(const QString &newPath);
|
||||||
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
|
|
||||||
QHash<int, QByteArray> roleNames() const override;
|
|
||||||
|
|
||||||
void refresh();
|
|
||||||
void setRootPath(const QString &path);
|
|
||||||
void setSearchText(const QString &searchText);
|
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 &supportedImageSuffixes();
|
||||||
static const QStringList &supportedFragmentShaderSuffixes();
|
static const QStringList &supportedFragmentShaderSuffixes();
|
||||||
@@ -47,44 +61,28 @@ public:
|
|||||||
static const QStringList &supportedEffectMakerSuffixes();
|
static const QStringList &supportedEffectMakerSuffixes();
|
||||||
static const QSet<QString> &supportedSuffixes();
|
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);
|
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:
|
signals:
|
||||||
void isEmptyChanged();
|
void directoryLoaded(const QString &path);
|
||||||
|
void rootPathChanged();
|
||||||
|
void haveFilesChanged();
|
||||||
|
void fileChanged(const QString &path);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
void setHaveFiles(bool value);
|
||||||
void setIsEmpty(bool empty);
|
bool filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const override;
|
||||||
|
void onDataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight, const QList<int> &roles);
|
||||||
QHash<QString, QPair<QDateTime, QIcon>> m_iconCache;
|
void resetModel();
|
||||||
|
void createBackendModel();
|
||||||
|
void destroyBackendModel();
|
||||||
|
bool checkHaveFiles(const QModelIndex &parentIdx) const;
|
||||||
|
bool checkHaveFiles() const;
|
||||||
|
|
||||||
QString m_searchText;
|
QString m_searchText;
|
||||||
Utils::FileSystemWatcher *m_fileSystemWatcher = nullptr;
|
QString m_rootPath;
|
||||||
AssetsLibraryDir *m_assetsDir = nullptr;
|
QFileSystemModel *m_sourceFsModel = nullptr;
|
||||||
bool m_isEmpty = true;
|
bool m_haveFiles = false;
|
||||||
|
|
||||||
QHash<int, QByteArray> m_roleNames;
|
|
||||||
inline static QHash<QString, bool> m_expandedStateHash; // <assetPath, isExpanded>
|
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace QmlDesigner
|
} // namespace QmlDesigner
|
||||||
|
|||||||
@@ -17,7 +17,6 @@
|
|||||||
|
|
||||||
#include <utils/algorithm.h>
|
#include <utils/algorithm.h>
|
||||||
#include <utils/environment.h>
|
#include <utils/environment.h>
|
||||||
#include <utils/filesystemwatcher.h>
|
|
||||||
#include <utils/fileutils.h>
|
#include <utils/fileutils.h>
|
||||||
#include <utils/qtcassert.h>
|
#include <utils/qtcassert.h>
|
||||||
#include <utils/stylehelper.h>
|
#include <utils/stylehelper.h>
|
||||||
@@ -85,16 +84,12 @@ bool AssetsLibraryWidget::eventFilter(QObject *obj, QEvent *event)
|
|||||||
|
|
||||||
AssetsLibraryWidget::AssetsLibraryWidget(AsynchronousImageCache &asynchronousFontImageCache,
|
AssetsLibraryWidget::AssetsLibraryWidget(AsynchronousImageCache &asynchronousFontImageCache,
|
||||||
SynchronousImageCache &synchronousFontImageCache)
|
SynchronousImageCache &synchronousFontImageCache)
|
||||||
: m_itemIconSize(24, 24)
|
: m_itemIconSize{24, 24}
|
||||||
, m_fontImageCache(synchronousFontImageCache)
|
, m_fontImageCache{synchronousFontImageCache}
|
||||||
, m_assetsIconProvider(new AssetsLibraryIconProvider(synchronousFontImageCache))
|
, m_assetsIconProvider{new AssetsLibraryIconProvider(synchronousFontImageCache)}
|
||||||
, m_fileSystemWatcher(new Utils::FileSystemWatcher(this))
|
, m_assetsModel{new AssetsLibraryModel(this)}
|
||||||
, m_assetsModel(new AssetsLibraryModel(m_fileSystemWatcher, this))
|
, m_assetsWidget{new QQuickWidget(this)}
|
||||||
, m_assetsWidget(new QQuickWidget(this))
|
|
||||||
{
|
{
|
||||||
m_assetCompressionTimer.setInterval(200);
|
|
||||||
m_assetCompressionTimer.setSingleShot(true);
|
|
||||||
|
|
||||||
setWindowTitle(tr("Assets Library", "Title of assets library widget"));
|
setWindowTitle(tr("Assets Library", "Title of assets library widget"));
|
||||||
setMinimumWidth(250);
|
setMinimumWidth(250);
|
||||||
|
|
||||||
@@ -119,21 +114,12 @@ AssetsLibraryWidget::AssetsLibraryWidget(AsynchronousImageCache &asynchronousFon
|
|||||||
m_assetsWidget->setClearColor(Theme::getColor(Theme::Color::QmlDesigner_BackgroundColorDarkAlternate));
|
m_assetsWidget->setClearColor(Theme::getColor(Theme::Color::QmlDesigner_BackgroundColorDarkAlternate));
|
||||||
m_assetsWidget->engine()->addImageProvider("qmldesigner_assets", m_assetsIconProvider);
|
m_assetsWidget->engine()->addImageProvider("qmldesigner_assets", m_assetsIconProvider);
|
||||||
m_assetsWidget->rootContext()->setContextProperties(QVector<QQmlContext::PropertyPair>{
|
m_assetsWidget->rootContext()->setContextProperties(QVector<QQmlContext::PropertyPair>{
|
||||||
{{"assetsModel"}, QVariant::fromValue(m_assetsModel.data())},
|
{{"assetsModel"}, QVariant::fromValue(m_assetsModel)},
|
||||||
{{"rootView"}, QVariant::fromValue(this)},
|
{{"rootView"}, QVariant::fromValue(this)},
|
||||||
{{"tooltipBackend"}, QVariant::fromValue(m_fontPreviewTooltipBackend.get())}
|
{{"tooltipBackend"}, QVariant::fromValue(m_fontPreviewTooltipBackend.get())}
|
||||||
});
|
});
|
||||||
|
|
||||||
// If project directory contents change, or one of the asset files is modified, we must
|
connect(m_assetsModel, &AssetsLibraryModel::fileChanged, [](const QString &changeFilePath) {
|
||||||
// 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) {
|
|
||||||
QmlDesignerPlugin::instance()->emitAssetChanged(changeFilePath);
|
QmlDesignerPlugin::instance()->emitAssetChanged(changeFilePath);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -149,23 +135,7 @@ AssetsLibraryWidget::AssetsLibraryWidget(AsynchronousImageCache &asynchronousFon
|
|||||||
|
|
||||||
m_qmlSourceUpdateShortcut = new QShortcut(QKeySequence(Qt::CTRL + Qt::Key_F6), this);
|
m_qmlSourceUpdateShortcut = new QShortcut(QKeySequence(Qt::CTRL + Qt::Key_F6), this);
|
||||||
connect(m_qmlSourceUpdateShortcut, &QShortcut::activated, this, &AssetsLibraryWidget::reloadQmlSource);
|
connect(m_qmlSourceUpdateShortcut, &QShortcut::activated, this, &AssetsLibraryWidget::reloadQmlSource);
|
||||||
|
connect(this, &AssetsLibraryWidget::extFilesDrop, this, &AssetsLibraryWidget::handleExtFilesDrop, Qt::QueuedConnection);
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
QmlDesignerPlugin::trackWidgetFocusTime(this, Constants::EVENT_ASSETSLIBRARY_TIME);
|
QmlDesignerPlugin::trackWidgetFocusTime(this, Constants::EVENT_ASSETSLIBRARY_TIME);
|
||||||
|
|
||||||
@@ -173,7 +143,15 @@ AssetsLibraryWidget::AssetsLibraryWidget(AsynchronousImageCache &asynchronousFon
|
|||||||
reloadQmlSource();
|
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()
|
QList<QToolButton *> AssetsLibraryWidget::createToolBarWidgets()
|
||||||
{
|
{
|
||||||
@@ -182,7 +160,8 @@ QList<QToolButton *> AssetsLibraryWidget::createToolBarWidgets()
|
|||||||
|
|
||||||
void AssetsLibraryWidget::handleSearchFilterChanged(const QString &filterText)
|
void AssetsLibraryWidget::handleSearchFilterChanged(const QString &filterText)
|
||||||
{
|
{
|
||||||
if (filterText == m_filterText || (m_assetsModel->isEmpty() && filterText.contains(m_filterText)))
|
if (filterText == m_filterText || (!m_assetsModel->haveFiles()
|
||||||
|
&& filterText.contains(m_filterText, Qt::CaseInsensitive)))
|
||||||
return;
|
return;
|
||||||
|
|
||||||
m_filterText = filterText;
|
m_filterText = filterText;
|
||||||
@@ -194,6 +173,16 @@ void AssetsLibraryWidget::handleAddAsset()
|
|||||||
addResources({});
|
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,
|
void AssetsLibraryWidget::handleExtFilesDrop(const QList<QUrl> &simpleFilePaths,
|
||||||
const QList<QUrl> &complexFilePaths,
|
const QList<QUrl> &complexFilePaths,
|
||||||
const QString &targetDirPath)
|
const QString &targetDirPath)
|
||||||
@@ -210,7 +199,7 @@ void AssetsLibraryWidget::handleExtFilesDrop(const QList<QUrl> &simpleFilePaths,
|
|||||||
} else {
|
} else {
|
||||||
AddFilesResult result = ModelNodeOperations::addFilesToProject(simpleFilePathStrings,
|
AddFilesResult result = ModelNodeOperations::addFilesToProject(simpleFilePathStrings,
|
||||||
targetDirPath);
|
targetDirPath);
|
||||||
if (result == AddFilesResult::Failed) {
|
if (result.status() == AddFilesResult::Failed) {
|
||||||
Core::AsynchronousMessageBox::warning(tr("Failed to Add Files"),
|
Core::AsynchronousMessageBox::warning(tr("Failed to Add Files"),
|
||||||
tr("Could not add %1 to project.")
|
tr("Could not add %1 to project.")
|
||||||
.arg(simpleFilePathStrings.join(' ')));
|
.arg(simpleFilePathStrings.join(' ')));
|
||||||
@@ -276,6 +265,7 @@ void AssetsLibraryWidget::updateSearch()
|
|||||||
void AssetsLibraryWidget::setResourcePath(const QString &resourcePath)
|
void AssetsLibraryWidget::setResourcePath(const QString &resourcePath)
|
||||||
{
|
{
|
||||||
m_assetsModel->setRootPath(resourcePath);
|
m_assetsModel->setRootPath(resourcePath);
|
||||||
|
m_assetsIconProvider->clearCache();
|
||||||
updateSearch();
|
updateSearch();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -408,10 +398,22 @@ void AssetsLibraryWidget::addResources(const QStringList &files)
|
|||||||
if (operation) {
|
if (operation) {
|
||||||
AddFilesResult result = operation(fileNames,
|
AddFilesResult result = operation(fileNames,
|
||||||
document->fileName().parentDir().toString(), true);
|
document->fileName().parentDir().toString(), true);
|
||||||
if (result == AddFilesResult::Failed) {
|
if (result.status() == AddFilesResult::Failed) {
|
||||||
Core::AsynchronousMessageBox::warning(tr("Failed to Add Files"),
|
Core::AsynchronousMessageBox::warning(tr("Failed to Add Files"),
|
||||||
tr("Could not add %1 to project.")
|
tr("Could not add %1 to project.")
|
||||||
.arg(fileNames.join(' ')));
|
.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 {
|
} else {
|
||||||
Core::AsynchronousMessageBox::warning(tr("Failed to Add Files"),
|
Core::AsynchronousMessageBox::warning(tr("Failed to Add Files"),
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ class QShortcut;
|
|||||||
QT_END_NAMESPACE
|
QT_END_NAMESPACE
|
||||||
|
|
||||||
namespace Utils {
|
namespace Utils {
|
||||||
class FileSystemWatcher;
|
class QtcProcess;
|
||||||
}
|
}
|
||||||
|
|
||||||
namespace QmlDesigner {
|
namespace QmlDesigner {
|
||||||
@@ -42,7 +42,7 @@ class AssetsLibraryWidget : public QFrame
|
|||||||
public:
|
public:
|
||||||
AssetsLibraryWidget(AsynchronousImageCache &asynchronousFontImageCache,
|
AssetsLibraryWidget(AsynchronousImageCache &asynchronousFontImageCache,
|
||||||
SynchronousImageCache &synchronousFontImageCache);
|
SynchronousImageCache &synchronousFontImageCache);
|
||||||
~AssetsLibraryWidget();
|
~AssetsLibraryWidget() = default;
|
||||||
|
|
||||||
QList<QToolButton *> createToolBarWidgets();
|
QList<QToolButton *> createToolBarWidgets();
|
||||||
|
|
||||||
@@ -59,14 +59,26 @@ public:
|
|||||||
Q_INVOKABLE void startDragAsset(const QStringList &assetPaths, const QPointF &mousePos);
|
Q_INVOKABLE void startDragAsset(const QStringList &assetPaths, const QPointF &mousePos);
|
||||||
Q_INVOKABLE void handleAddAsset();
|
Q_INVOKABLE void handleAddAsset();
|
||||||
Q_INVOKABLE void handleSearchFilterChanged(const QString &filterText);
|
Q_INVOKABLE void handleSearchFilterChanged(const QString &filterText);
|
||||||
|
|
||||||
Q_INVOKABLE void handleExtFilesDrop(const QList<QUrl> &simpleFilePaths,
|
Q_INVOKABLE void handleExtFilesDrop(const QList<QUrl> &simpleFilePaths,
|
||||||
|
const QList<QUrl> &complexFilePaths,
|
||||||
|
const QString &targetDirPath);
|
||||||
|
|
||||||
|
Q_INVOKABLE void emitExtFilesDrop(const QList<QUrl> &simpleFilePaths,
|
||||||
const QList<QUrl> &complexFilePaths,
|
const QList<QUrl> &complexFilePaths,
|
||||||
const QString &targetDirPath = {});
|
const QString &targetDirPath = {});
|
||||||
|
|
||||||
Q_INVOKABLE QSet<QString> supportedAssetSuffixes(bool complex);
|
Q_INVOKABLE QSet<QString> supportedAssetSuffixes(bool complex);
|
||||||
Q_INVOKABLE void openEffectMaker(const QString &filePath);
|
Q_INVOKABLE void openEffectMaker(const QString &filePath);
|
||||||
|
Q_INVOKABLE bool qtVersionIsAtLeast6_4() const;
|
||||||
|
Q_INVOKABLE void invalidateThumbnail(const QString &id);
|
||||||
|
|
||||||
signals:
|
signals:
|
||||||
void itemActivated(const QString &itemName);
|
void itemActivated(const QString &itemName);
|
||||||
|
void extFilesDrop(const QList<QUrl> &simpleFilePaths,
|
||||||
|
const QList<QUrl> &complexFilePaths,
|
||||||
|
const QString &targetDirPath);
|
||||||
|
void directoryCreated(const QString &path);
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
bool eventFilter(QObject *obj, QEvent *event) override;
|
bool eventFilter(QObject *obj, QEvent *event) override;
|
||||||
@@ -77,14 +89,12 @@ private:
|
|||||||
void addResources(const QStringList &files);
|
void addResources(const QStringList &files);
|
||||||
void updateSearch();
|
void updateSearch();
|
||||||
|
|
||||||
QTimer m_assetCompressionTimer;
|
|
||||||
QSize m_itemIconSize;
|
QSize m_itemIconSize;
|
||||||
|
|
||||||
SynchronousImageCache &m_fontImageCache;
|
SynchronousImageCache &m_fontImageCache;
|
||||||
|
|
||||||
AssetsLibraryIconProvider *m_assetsIconProvider = nullptr;
|
AssetsLibraryIconProvider *m_assetsIconProvider = nullptr;
|
||||||
Utils::FileSystemWatcher *m_fileSystemWatcher = nullptr;
|
AssetsLibraryModel *m_assetsModel = nullptr;
|
||||||
QPointer<AssetsLibraryModel> m_assetsModel;
|
|
||||||
|
|
||||||
QScopedPointer<QQuickWidget> m_assetsWidget;
|
QScopedPointer<QQuickWidget> m_assetsWidget;
|
||||||
std::unique_ptr<PreviewTooltipBackend> m_fontPreviewTooltipBackend;
|
std::unique_ptr<PreviewTooltipBackend> m_fontPreviewTooltipBackend;
|
||||||
|
|||||||
@@ -277,7 +277,7 @@ QHash<QString, QStringList> DesignerActionManager::handleExternalAssetsDrop(cons
|
|||||||
AddResourceOperation operation = categoryOperation.value(category);
|
AddResourceOperation operation = categoryOperation.value(category);
|
||||||
QStringList files = categoryFiles.value(category);
|
QStringList files = categoryFiles.value(category);
|
||||||
AddFilesResult result = operation(files, {}, true);
|
AddFilesResult result = operation(files, {}, true);
|
||||||
if (result == AddFilesResult::Succeeded)
|
if (result.status() == AddFilesResult::Succeeded)
|
||||||
addedCategoryFiles.insert(category, files);
|
addedCategoryFiles.insert(category, files);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1042,10 +1042,10 @@ AddFilesResult addFilesToProject(const QStringList &fileNames, const QString &de
|
|||||||
{
|
{
|
||||||
QString directory = showDialog ? AddImagesDialog::getDirectory(fileNames, defaultDir) : defaultDir;
|
QString directory = showDialog ? AddImagesDialog::getDirectory(fileNames, defaultDir) : defaultDir;
|
||||||
if (directory.isEmpty())
|
if (directory.isEmpty())
|
||||||
return AddFilesResult::Cancelled;
|
return AddFilesResult::cancelled(directory);
|
||||||
|
|
||||||
DesignDocument *document = QmlDesignerPlugin::instance()->currentDesignDocument();
|
DesignDocument *document = QmlDesignerPlugin::instance()->currentDesignDocument();
|
||||||
QTC_ASSERT(document, return AddFilesResult::Failed);
|
QTC_ASSERT(document, return AddFilesResult::failed(directory));
|
||||||
|
|
||||||
QList<QPair<QString, QString>> copyList;
|
QList<QPair<QString, QString>> copyList;
|
||||||
QStringList removeList;
|
QStringList removeList;
|
||||||
@@ -1073,7 +1073,7 @@ AddFilesResult addFilesToProject(const QStringList &fileNames, const QString &de
|
|||||||
for (const auto &filePair : std::as_const(copyList)) {
|
for (const auto &filePair : std::as_const(copyList)) {
|
||||||
const bool success = QFile::copy(filePair.first, filePair.second);
|
const bool success = QFile::copy(filePair.first, filePair.second);
|
||||||
if (!success)
|
if (!success)
|
||||||
return AddFilesResult::Failed;
|
return AddFilesResult::failed(directory);
|
||||||
|
|
||||||
ProjectExplorer::Node *node = ProjectExplorer::ProjectTree::nodeForFile(document->fileName());
|
ProjectExplorer::Node *node = ProjectExplorer::ProjectTree::nodeForFile(document->fileName());
|
||||||
if (node) {
|
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)
|
static QString getAssetDefaultDirectory(const QString &assetDir, const QString &defaultDirectory)
|
||||||
|
|||||||
@@ -9,7 +9,48 @@
|
|||||||
|
|
||||||
namespace QmlDesigner {
|
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 {
|
namespace ModelNodeOperations {
|
||||||
|
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ WidgetInfo ContentLibraryView::widgetInfo()
|
|||||||
// copy image to project
|
// copy image to project
|
||||||
AddFilesResult result = ModelNodeOperations::addImageToProject({texPath}, "images", false);
|
AddFilesResult result = ModelNodeOperations::addImageToProject({texPath}, "images", false);
|
||||||
|
|
||||||
if (result == AddFilesResult::Failed) {
|
if (result.status() == AddFilesResult::Failed) {
|
||||||
Core::AsynchronousMessageBox::warning(tr("Failed to Add Texture"),
|
Core::AsynchronousMessageBox::warning(tr("Failed to Add Texture"),
|
||||||
tr("Could not add %1 to project.").arg(texPath));
|
tr("Could not add %1 to project.").arg(texPath));
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -156,7 +156,7 @@ void ItemLibraryView::updateImport3DSupport(const QVariantMap &supportMap)
|
|||||||
Core::ICore::dialogParent());
|
Core::ICore::dialogParent());
|
||||||
int result = importDlg->exec();
|
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) {
|
auto add3DHandler = [&](const QString &group, const QString &ext) {
|
||||||
|
|||||||
Reference in New Issue
Block a user