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:
Samuel Ghinet
2022-11-23 11:49:45 +02:00
parent a575cb4f46
commit 910a8864dc
26 changed files with 1274 additions and 1103 deletions

View File

@@ -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

View File

@@ -1,31 +1,26 @@
// Copyright (C) 2021 The Qt Company Ltd.
// Copyright (C) 2022 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0 WITH Qt-GPL-exception-1.0
import QtQuick 2.15
import QtQuick.Controls 2.15
import QtQuick.Layouts 1.15
import QtQuickDesignerTheme 1.0
import HelperWidgets 2.0
import StudioControls 1.0 as StudioControls
import StudioTheme 1.0 as StudioTheme
import QtQuick
import HelperWidgets as HelperWidgets
import StudioControls as StudioControls
import StudioTheme as StudioTheme
Item {
id: root
property var selectedAssets: ({})
property int allExpandedState: 0
property string contextFilePath: ""
property var contextDir: undefined
property bool isDirContextMenu: false
// Array of supported externally dropped files that are imported as-is
property var dropSimpleExtFiles: []
// Array of supported externally dropped files that trigger custom import process
property var dropComplexExtFiles: []
readonly property int qtVersionAtLeast6_4: rootView.qtVersionIsAtLeast6_4()
property bool _searchBoxEmpty: true
AssetsContextMenu {
id: contextMenu
assetsView: assetsView
}
function clearSearchFilter()
@@ -63,7 +58,7 @@ Item {
onDropped: {
rootView.handleExtFilesDrop(root.dropSimpleExtFiles, root.dropComplexExtFiles,
assetsModel.rootDir().dirPath)
assetsModel.rootPath())
}
Canvas { // marker for the drop area
@@ -90,11 +85,15 @@ Item {
anchors.fill: parent
acceptedButtons: Qt.RightButton
onClicked: {
if (!assetsModel.isEmpty) {
root.contextFilePath = ""
root.contextDir = assetsModel.rootDir()
root.isDirContextMenu = false
contextMenu.popup()
if (assetsModel.haveFiles) {
function onFolderCreated(path) {
assetsView.addCreatedFolder(path)
}
var rootIndex = assetsModel.rootIndex()
var dirPath = assetsModel.filePath(rootIndex)
var dirName = assetsModel.fileName(rootIndex)
contextMenu.openContextMenuForRoot(rootIndex, dirPath, dirName, onFolderCreated)
}
}
}
@@ -103,13 +102,8 @@ Item {
function handleViewFocusOut()
{
contextMenu.close()
root.selectedAssets = {}
root.selectedAssetsChanged()
}
RegExpValidator {
id: folderNameValidator
regExp: /^(\w[^*/><?\\|:]*)$/
assetsView.selectedAssets = {}
assetsView.selectedAssetsChanged()
}
Column {
@@ -127,10 +121,29 @@ Item {
width: parent.width - addAssetButton.width - 5
onSearchChanged: (searchText) => rootView.handleSearchFilterChanged(searchText)
onSearchChanged: (searchText) => {
updateSearchFilterTimer.restart()
}
}
IconButton {
Timer {
id: updateSearchFilterTimer
interval: 200
repeat: false
onTriggered: {
assetsView.resetVerticalScrollPosition()
rootView.handleSearchFilterChanged(searchBox.text)
assetsView.expandAll()
if (root._searchBoxEmpty && searchBox.text)
root._searchBoxEmpty = false
else if (!root._searchBoxEmpty && !searchBox.text)
root._searchBoxEmpty = true
}
}
HelperWidgets.IconButton {
id: addAssetButton
anchors.verticalCenter: parent.verticalCenter
tooltip: qsTr("Add a new asset to the project.")
@@ -146,14 +159,13 @@ Item {
leftPadding: 10
color: StudioTheme.Values.themeTextColor
font.pixelSize: 12
visible: assetsModel.isEmpty && !searchBox.isEmpty()
visible: !assetsModel.haveFiles && !root._searchBoxEmpty
}
Item { // placeholder when the assets library is empty
width: parent.width
height: parent.height - searchRow.height
visible: assetsModel.isEmpty && searchBox.isEmpty()
visible: !assetsModel.haveFiles && root._searchBoxEmpty
clip: true
DropArea { // handles external drop (goes into default folder based on suffix)
@@ -164,7 +176,7 @@ Item {
}
onDropped: {
rootView.handleExtFilesDrop(root.dropSimpleExtFiles, root.dropComplexExtFiles)
rootView.emitExtFilesDrop(root.dropSimpleExtFiles, root.dropComplexExtFiles)
}
Column {
@@ -217,8 +229,11 @@ Item {
AssetsView {
id: assetsView
assetsRoot: root
contextMenu: contextMenu
width: parent.width
height: parent.height - y
}
}
} // Column
}

View File

@@ -1,90 +1,113 @@
/****************************************************************************
**
** Copyright (C) 2022 The Qt Company Ltd.
** Contact: https://www.qt.io/licensing/
**
** This file is part of Qt Creator.
**
** Commercial License Usage
** Licensees holding valid commercial Qt licenses may use this file in
** accordance with the commercial license agreement provided with the
** Software or, alternatively, in accordance with the terms contained in
** a written agreement between you and The Qt Company. For licensing terms
** and conditions see https://www.qt.io/terms-conditions. For further
** information use the contact form at https://www.qt.io/contact-us.
**
** GNU General Public License Usage
** Alternatively, this file may be used under the terms of the GNU
** General Public License version 3 as published by the Free Software
** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT
** included in the packaging of this file. Please review the following
** information to ensure the GNU General Public License requirements will
** be met: https://www.gnu.org/licenses/gpl-3.0.html.
**
****************************************************************************/
// Copyright (C) 2022 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0 WITH Qt-GPL-exception-1.0
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import QtQuickDesignerTheme
import HelperWidgets as HelperWidgets
import StudioControls as StudioControls
import StudioTheme as StudioTheme
StudioControls.Menu {
id: contextMenu
id: root
required property Item assetsView
property bool _isDirectory: false
property var _fileIndex: null
property string _dirPath: ""
property string _dirName: ""
property var _onFolderCreated: null
property var _onFolderRenamed: null
property var _dirIndex: null
property string _allExpandedState: ""
property var _selectedAssetPathsList: null
closePolicy: Popup.CloseOnPressOutside | Popup.CloseOnEscape
onOpened: {
var numSelected = Object.values(root.selectedAssets).filter(p => p).length
function openContextMenuForRoot(rootModelIndex, dirPath, dirName, onFolderCreated)
{
root._onFolderCreated = onFolderCreated
root._fileIndex = ""
root._dirPath = dirPath
root._dirName = dirName
root._dirIndex = rootModelIndex
root._isDirectory = false
root.popup()
}
function openContextMenuForDir(dirModelIndex, dirPath, dirName, allExpandedState,
onFolderCreated, onFolderRenamed)
{
root._onFolderCreated = onFolderCreated
root._onFolderRenamed = onFolderRenamed
root._dirPath = dirPath
root._dirName = dirName
root._fileIndex = ""
root._dirIndex = dirModelIndex
root._isDirectory = true
root._allExpandedState = allExpandedState
root.popup()
}
function openContextMenuForFile(fileIndex, dirModelIndex, selectedAssetPathsList)
{
var numSelected = selectedAssetPathsList.filter(p => p).length
deleteFileItem.text = numSelected > 1 ? qsTr("Delete Files") : qsTr("Delete File")
root._selectedAssetPathsList = selectedAssetPathsList
root._fileIndex = fileIndex
root._dirIndex = dirModelIndex
root._dirPath = assetsModel.filePath(dirModelIndex)
root._isDirectory = false
root.popup()
}
StudioControls.MenuItem {
text: qsTr("Expand All")
enabled: root.allExpandedState !== 1
visible: root.isDirContextMenu
enabled: root._allExpandedState !== "all_expanded"
visible: root._isDirectory
height: visible ? implicitHeight : 0
onTriggered: assetsModel.toggleExpandAll(true)
onTriggered: root.assetsView.expandAll()
}
StudioControls.MenuItem {
text: qsTr("Collapse All")
enabled: root.allExpandedState !== 2
visible: root.isDirContextMenu
enabled: root._allExpandedState !== "all_collapsed"
visible: root._isDirectory
height: visible ? implicitHeight : 0
onTriggered: assetsModel.toggleExpandAll(false)
onTriggered: root.assetsView.collapseAll()
}
StudioControls.MenuSeparator {
visible: root.isDirContextMenu
visible: root._isDirectory
height: visible ? StudioTheme.Values.border : 0
}
StudioControls.MenuItem {
id: deleteFileItem
text: qsTr("Delete File")
visible: root.contextFilePath
visible: root._fileIndex
height: deleteFileItem.visible ? deleteFileItem.implicitHeight : 0
onTriggered: {
assetsModel.deleteFiles(Object.keys(root.selectedAssets).filter(p => root.selectedAssets[p]))
}
onTriggered: assetsModel.deleteFiles(root._selectedAssetPathsList)
}
StudioControls.MenuSeparator {
visible: root.contextFilePath
visible: root._fileIndex
height: visible ? StudioTheme.Values.border : 0
}
StudioControls.MenuItem {
text: qsTr("Rename Folder")
visible: root.isDirContextMenu
visible: root._isDirectory
height: visible ? implicitHeight : 0
onTriggered: renameFolderDialog.open()
RenameFolderDialog {
id: renameFolderDialog
parent: root.assetsView
dirPath: root._dirPath
dirName: root._dirName
onAccepted: root._onFolderRenamed()
}
}
@@ -93,6 +116,10 @@ StudioControls.Menu {
NewFolderDialog {
id: newFolderDialog
parent: root.assetsView
dirPath: root._dirPath
onAccepted: root._onFolderCreated(newFolderDialog.createdDirPath)
}
onTriggered: newFolderDialog.open()
@@ -100,21 +127,25 @@ StudioControls.Menu {
StudioControls.MenuItem {
text: qsTr("Delete Folder")
visible: root.isDirContextMenu
visible: root._isDirectory
height: visible ? implicitHeight : 0
ConfirmDeleteFolderDialog {
id: confirmDeleteFolderDialog
parent: root.assetsView
dirName: root._dirName
dirIndex: root._dirIndex
}
onTriggered: {
var dirEmpty = !(root.contextDir.dirsModel && root.contextDir.dirsModel.rowCount() > 0)
&& !(root.contextDir.filesModel && root.contextDir.filesModel.rowCount() > 0);
if (dirEmpty)
assetsModel.deleteFolder(root.contextDir.dirPath)
else
if (!assetsModel.hasChildren(root._dirIndex)) {
// NOTE: the folder may still not be empty -- it doesn't have files visible to the
// user, but that doesn't mean that there are no other files (e.g. files of unknown
// types) on disk in this directory.
assetsModel.deleteFolderRecursively(root._dirIndex)
} else {
confirmDeleteFolderDialog.open()
}
}
}
}

View File

@@ -1,255 +1,309 @@
/****************************************************************************
**
** Copyright (C) 2022 The Qt Company Ltd.
** Contact: https://www.qt.io/licensing/
**
** This file is part of Qt Creator.
**
** Commercial License Usage
** Licensees holding valid commercial Qt licenses may use this file in
** accordance with the commercial license agreement provided with the
** Software or, alternatively, in accordance with the terms contained in
** a written agreement between you and The Qt Company. For licensing terms
** and conditions see https://www.qt.io/terms-conditions. For further
** information use the contact form at https://www.qt.io/contact-us.
**
** GNU General Public License Usage
** Alternatively, this file may be used under the terms of the GNU
** General Public License version 3 as published by the Free Software
** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT
** included in the packaging of this file. Please review the following
** information to ensure the GNU General Public License requirements will
** be met: https://www.gnu.org/licenses/gpl-3.0.html.
**
****************************************************************************/
// Copyright (C) 2022 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0 WITH Qt-GPL-exception-1.0
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import QtQuickDesignerTheme
import HelperWidgets
import HelperWidgets as HelperWidgets
import StudioControls as StudioControls
import StudioTheme as StudioTheme
ScrollView { // TODO: experiment using ListView instead of ScrollView + Column
id: assetsView
TreeView {
id: root
clip: true
interactive: assetsView.verticalScrollBarVisible && !contextMenu.opened
interactive: verticalScrollBar.visible && !root.contextMenu.opened
reuseItems: false
boundsBehavior: Flickable.StopAtBounds
rowSpacing: 5
Column {
Repeater {
model: assetsModel // context property
delegate: dirSection
required property Item assetsRoot
required property StudioControls.Menu contextMenu
property alias verticalScrollBar: verticalScrollBar
property var selectedAssets: ({})
// used to see if the op requested is to expand or to collapse.
property int lastRowCount: -1
// we need this to know if we need to expand further, while we're in onRowsChanged()
property bool requestedExpandAll: true
// used to compute the visual depth of the items we show to the user.
property int rootPathDepth: 0
property int rootPathRow: 0
// i.e. first child of the root path
readonly property int firstRow: root.rootPathRow + 1
property int rowToExpand: -1
property var _createdDirectories: []
rowHeightProvider: (row) => {
if (row <= root.rootPathRow)
return 0
return -1
}
ScrollBar.vertical: HelperWidgets.VerticalScrollBar {
id: verticalScrollBar
scrollBarVisible: root.contentHeight > root.height
}
model: assetsModel
onRowsChanged: {
if (root.rows > root.rootPathRow + 1 && !assetsModel.haveFiles ||
root.rows <= root.rootPathRow + 1 && assetsModel.haveFiles) {
assetsModel.syncHaveFiles()
}
Component {
id: dirSection
updateRows()
}
Section {
id: section
Timer {
id: updateRowsTimer
interval: 200
repeat: false
width: assetsView.width -
(assetsView.verticalScrollBarVisible ? assetsView.verticalThickness : 0) - 5
caption: dirName
sectionHeight: 30
sectionFontSize: 15
leftPadding: 0
topPadding: dirDepth > 0 ? 5 : 0
bottomPadding: 0
hideHeader: dirDepth === 0
showLeftBorder: dirDepth > 0
expanded: dirExpanded
visible: dirVisible
expandOnClick: false
useDefaulContextMenu: false
dropEnabled: true
onToggleExpand: {
dirExpanded = !dirExpanded
}
onDropEnter: (drag)=> {
root.updateDropExtFiles(drag)
section.highlight = drag.accepted && root.dropSimpleExtFiles.length > 0
}
onDropExit: {
section.highlight = false
}
onDrop: {
section.highlight = false
rootView.handleExtFilesDrop(root.dropSimpleExtFiles,
root.dropComplexExtFiles,
dirPath)
}
onShowContextMenu: {
root.contextFilePath = ""
root.contextDir = model
root.isDirContextMenu = true
root.allExpandedState = assetsModel.getAllExpandedState()
contextMenu.popup()
}
Column {
spacing: 5
leftPadding: 5
Repeater {
model: dirsModel
delegate: dirSection
}
Repeater {
model: filesModel
delegate: fileSection
}
Text {
text: qsTr("Empty folder")
color: StudioTheme.Values.themeTextColorDisabled
font.pixelSize: 12
visible: !(dirsModel && dirsModel.rowCount() > 0)
&& !(filesModel && filesModel.rowCount() > 0)
MouseArea {
anchors.fill: parent
acceptedButtons: Qt.RightButton
onClicked: {
root.contextFilePath = ""
root.contextDir = model
root.isDirContextMenu = true
contextMenu.popup()
}
}
}
}
}
}
Component {
id: fileSection
Rectangle {
width: assetsView.width -
(assetsView.verticalScrollBarVisible ? assetsView.verticalThickness : 0)
height: img.height
color: root.selectedAssets[filePath]
? StudioTheme.Values.themeInteraction
: (mouseArea.containsMouse ? StudioTheme.Values.themeSectionHeadBackground
: "transparent")
Row {
spacing: 5
Image {
id: img
asynchronous: true
fillMode: Image.PreserveAspectFit
width: 48
height: 48
source: "image://qmldesigner_assets/" + filePath
}
Text {
text: fileName
color: StudioTheme.Values.themeTextColor
font.pixelSize: 14
anchors.verticalCenter: parent.verticalCenter
}
}
readonly property string suffix: fileName.substr(-4)
readonly property bool isFont: suffix === ".ttf" || suffix === ".otf"
readonly property bool isEffect: suffix === ".qep"
property bool currFileSelected: false
MouseArea {
id: mouseArea
property bool allowTooltip: true
anchors.fill: parent
hoverEnabled: true
acceptedButtons: Qt.LeftButton | Qt.RightButton
onExited: tooltipBackend.hideTooltip()
onEntered: allowTooltip = true
onCanceled: {
tooltipBackend.hideTooltip()
allowTooltip = true
}
onPositionChanged: tooltipBackend.reposition()
onPressed: (mouse) => {
forceActiveFocus()
allowTooltip = false
tooltipBackend.hideTooltip()
var ctrlDown = mouse.modifiers & Qt.ControlModifier
if (mouse.button === Qt.LeftButton) {
if (!root.selectedAssets[filePath] && !ctrlDown)
root.selectedAssets = {}
currFileSelected = ctrlDown ? !root.selectedAssets[filePath] : true
root.selectedAssets[filePath] = currFileSelected
root.selectedAssetsChanged()
if (currFileSelected) {
rootView.startDragAsset(
Object.keys(root.selectedAssets).filter(p => root.selectedAssets[p]),
mapToGlobal(mouse.x, mouse.y))
}
} else {
if (!root.selectedAssets[filePath] && !ctrlDown)
root.selectedAssets = {}
currFileSelected = root.selectedAssets[filePath] || !ctrlDown
root.selectedAssets[filePath] = currFileSelected
root.selectedAssetsChanged()
root.contextFilePath = filePath
root.contextDir = model.fileDir
root.isDirContextMenu = false
contextMenu.popup()
}
}
onReleased: (mouse) => {
allowTooltip = true
if (mouse.button === Qt.LeftButton) {
if (!(mouse.modifiers & Qt.ControlModifier))
root.selectedAssets = {}
root.selectedAssets[filePath] = currFileSelected
root.selectedAssetsChanged()
}
}
onDoubleClicked: (mouse) => {
forceActiveFocus()
allowTooltip = false
tooltipBackend.hideTooltip()
if (mouse.button === Qt.LeftButton && isEffect)
rootView.openEffectMaker(filePath)
}
ToolTip {
visible: !isFont && mouseArea.containsMouse && !contextMenu.visible
text: filePath
delay: 1000
}
Timer {
interval: 1000
running: mouseArea.containsMouse && mouseArea.allowTooltip
onTriggered: {
if (suffix === ".ttf" || suffix === ".otf") {
tooltipBackend.name = fileName
tooltipBackend.path = filePath
tooltipBackend.showTooltip()
}
}
}
}
}
onTriggered: {
root.updateRows()
}
}
}
Connections {
target: rootView
function onDirectoryCreated(path)
{
root._createdDirectories.push(path)
updateRowsTimer.restart()
}
}
Connections {
target: assetsModel
function onDirectoryLoaded(path)
{
// updating rows for safety: the rows might have been created before the
// directory (esp. the root path) has been loaded, so we must make sure all rows are
// expanded -- otherwise, the tree may not become visible.
updateRowsTimer.restart()
let idx = assetsModel.indexForPath(path)
let row = root.rowAtIndex(idx)
let column = root.columnAtIndex(idx)
if (row >= root.rootPathRow && !root.isExpanded(row))
root.expand(row)
}
function onRootPathChanged()
{
// when we switch from one project to another, we need to reset the state of the
// view: make sure we will do an "expand all" (otherwise, the whole tree might
// be collapsed, and with our visible root not being the actual root of the tree,
// the entire tree would be invisible)
root.lastRowCount = -1
root.requestedExpandAll = true
}
function onFileChanged(filePath)
{
rootView.invalidateThumbnail(filePath)
let index = assetsModel.indexForPath(filePath)
let cell = root.cellAtIndex(index)
let fileItem = root.itemAtCell(cell)
if (fileItem)
fileItem.reloadImage()
}
} // Connections
function addCreatedFolder(path)
{
root._createdDirectories.push(path)
}
function selectedPathsAsList()
{
return Object.keys(root.selectedAssets)
.filter(itemPath => root.selectedAssets[itemPath])
}
// workaround for a bug -- might be fixed by https://codereview.qt-project.org/c/qt/qtdeclarative/+/442721
function resetVerticalScrollPosition()
{
root.contentY = 0
}
function updateRows()
{
if (root.rows <= 0)
return
while (root._createdDirectories.length > 0) {
let dirPath = root._createdDirectories.pop()
let index = assetsModel.indexForPath(dirPath)
let row = root.rowAtIndex(index)
if (row > 0)
root.expand(row)
else if (row === -1 && assetsModel.indexIsValid(index)) {
// It is possible that this directory, dirPath, was created inside of a parent
// directory that was not yet expanded in the TreeView. This can happen with the
// bridge plugin. In such a situation, we don't have a "row" for it yet, so we have
// to expand its parents, from root to our `index`
let parents = assetsModel.parentIndices(index);
parents.reverse().forEach(idx => {
let row = root.rowAtIndex(idx)
if (row > 0)
root.expand(row)
})
}
}
// we have no way to know beyond doubt here if updateRows() was called due
// to a request to expand or to collapse rows - but it should be safe to
// assume that, if we have more rows now than the last time, then it's an expand
var expanding = (root.rows >= root.lastRowCount)
if (expanding) {
if (root.requestedExpandAll)
root._doExpandAll()
} else {
if (root.rowToExpand > 0) {
root.expand(root.rowToExpand)
root.rowToExpand = -1
}
// on collapsing, set expandAll flag to false.
root.requestedExpandAll = false;
}
root.lastRowCount = root.rows
}
function _doExpandAll()
{
let expandedAny = false
for (let nRow = 0; nRow < root.rows; ++nRow) {
let index = root._modelIndex(nRow, 0)
if (assetsModel.isDirectory(index) && !root.isExpanded(nRow)) {
root.expand(nRow);
expandedAny = true
}
}
if (!expandedAny)
Qt.callLater(root.forceLayout)
}
function expandAll()
{
// In order for _doExpandAll() to be called repeatedly (every time a new node is
// loaded, and then, expanded), we need to set requestedExpandAll to true.
root.requestedExpandAll = true
root._doExpandAll()
}
function collapseAll()
{
root.resetVerticalScrollPosition()
// collapse all, except for the root path - from the last item (leaves) up to the root
for (let nRow = root.rows - 1; nRow >= 0; --nRow) {
let index = root._modelIndex(nRow, 0)
// we don't want to collapse the root path, because doing so will hide the contents
// of the tree.
if (assetsModel.filePath(index) === assetsModel.rootPath())
break
root.collapse(nRow)
}
}
// workaround for a bug -- might be fixed by https://codereview.qt-project.org/c/qt/qtdeclarative/+/442721
onContentHeightChanged: {
if (root.contentHeight <= root.height) {
let first = root.itemAtCell(0, root.firstRow)
if (!first)
root.contentY = 0
}
}
function computeAllExpandedState()
{
var dirsWithChildren = [...Array(root.rows).keys()].filter(row => {
let index = root._modelIndex(row, 0)
return assetsModel.isDirectory(index) && assetsModel.hasChildren(index)
})
var countExpanded = dirsWithChildren.filter(row => root.isExpanded(row)).length
if (countExpanded === dirsWithChildren.length)
return "all_expanded"
if (countExpanded === 0)
return "all_collapsed"
return ""
}
function startDropHoverOver(row)
{
let index = root._modelIndex(row, 0)
if (assetsModel.isDirectory(index))
return
let parentItem = root._getDelegateParentForIndex(index)
parentItem.hasChildWithDropHover = true
}
function endDropHover(row)
{
let index = root._modelIndex(row, 0)
if (assetsModel.isDirectory(index))
return
let parentItem = root._getDelegateParentForIndex(index)
parentItem.hasChildWithDropHover = false
}
function isAssetSelected(itemPath)
{
return root.selectedAssets[itemPath] ? true : false
}
function clearSelectedAssets()
{
root.selectedAssets = {}
}
function setAssetSelected(itemPath, selected)
{
root.selectedAssets[itemPath] = selected
root.selectedAssetsChanged()
}
function _getDelegateParentForIndex(index)
{
let parentIndex = assetsModel.parentDirIndex(index)
let parentCell = root.cellAtIndex(parentIndex)
return root.itemAtCell(parentCell)
}
function _modelIndex(row)
{
// The modelIndex() function exists since 6.3. In Qt 6.3, this modelIndex() function was a
// member of the TreeView, while in Qt6.4 it was moved to TableView. In Qt6.4, the order of
// the arguments was changed.
if (assetsRoot.qtVersionAtLeast6_4)
return root.modelIndex(0, row)
else
return root.modelIndex(row, 0)
}
delegate: AssetDelegate {
assetsView: root
assetsRoot: root.assetsRoot
indentation: 5
}
} // TreeView

View File

@@ -1,38 +1,13 @@
/****************************************************************************
**
** Copyright (C) 2022 The Qt Company Ltd.
** Contact: https://www.qt.io/licensing/
**
** This file is part of Qt Creator.
**
** Commercial License Usage
** Licensees holding valid commercial Qt licenses may use this file in
** accordance with the commercial license agreement provided with the
** Software or, alternatively, in accordance with the terms contained in
** a written agreement between you and The Qt Company. For licensing terms
** and conditions see https://www.qt.io/terms-conditions. For further
** information use the contact form at https://www.qt.io/contact-us.
**
** GNU General Public License Usage
** Alternatively, this file may be used under the terms of the GNU
** General Public License version 3 as published by the Free Software
** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT
** included in the packaging of this file. Please review the following
** information to ensure the GNU General Public License requirements will
** be met: https://www.gnu.org/licenses/gpl-3.0.html.
**
****************************************************************************/
// Copyright (C) 2022 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0 WITH Qt-GPL-exception-1.0
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import QtQuickDesignerTheme
import HelperWidgets
import StudioControls as StudioControls
import HelperWidgets as HelperWidgets
import StudioTheme as StudioTheme
Dialog {
id: confirmDeleteFolderDialog
id: root
title: qsTr("Folder Not Empty")
anchors.centerIn: parent
@@ -40,6 +15,9 @@ Dialog {
implicitWidth: 300
modal: true
required property string dirName
required property var dirIndex
contentItem: Column {
spacing: 20
width: parent.width
@@ -47,11 +25,10 @@ Dialog {
Text {
id: folderNotEmpty
text: qsTr("Folder \"%1\" is not empty. Delete it anyway?")
.arg(root.contextDir ? root.contextDir.dirName : "")
text: qsTr("Folder \"%1\" is not empty. Delete it anyway?").arg(root.dirName)
color: StudioTheme.Values.themeTextColor
wrapMode: Text.WordWrap
width: confirmDeleteFolderDialog.width
width: root.width
leftPadding: 10
rightPadding: 10
@@ -63,27 +40,27 @@ Dialog {
text: qsTr("If the folder has assets in use, deleting it might cause the project to not work correctly.")
color: StudioTheme.Values.themeTextColor
wrapMode: Text.WordWrap
width: confirmDeleteFolderDialog.width
width: root.width
leftPadding: 10
rightPadding: 10
}
Row {
anchors.right: parent.right
Button {
HelperWidgets.Button {
id: btnDelete
text: qsTr("Delete")
onClicked: {
assetsModel.deleteFolder(root.contextDir.dirPath)
confirmDeleteFolderDialog.accept()
assetsModel.deleteFolderRecursively(root.dirIndex)
root.accept()
}
}
Button {
HelperWidgets.Button {
text: qsTr("Cancel")
onClicked: confirmDeleteFolderDialog.reject()
onClicked: root.reject()
}
}
}

View File

@@ -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()
}

View File

@@ -1,44 +1,35 @@
/****************************************************************************
**
** Copyright (C) 2022 The Qt Company Ltd.
** Contact: https://www.qt.io/licensing/
**
** This file is part of Qt Creator.
**
** Commercial License Usage
** Licensees holding valid commercial Qt licenses may use this file in
** accordance with the commercial license agreement provided with the
** Software or, alternatively, in accordance with the terms contained in
** a written agreement between you and The Qt Company. For licensing terms
** and conditions see https://www.qt.io/terms-conditions. For further
** information use the contact form at https://www.qt.io/contact-us.
**
** GNU General Public License Usage
** Alternatively, this file may be used under the terms of the GNU
** General Public License version 3 as published by the Free Software
** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT
** included in the packaging of this file. Please review the following
** information to ensure the GNU General Public License requirements will
** be met: https://www.gnu.org/licenses/gpl-3.0.html.
**
****************************************************************************/
// Copyright (C) 2022 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0 WITH Qt-GPL-exception-1.0
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import QtQuickDesignerTheme
import HelperWidgets
import HelperWidgets as HelperWidgets
import StudioControls as StudioControls
import StudioTheme as StudioTheme
Dialog {
id: newFolderDialog
id: root
title: qsTr("Create New Folder")
anchors.centerIn: parent
closePolicy: Popup.CloseOnEscape
modal: true
required property string dirPath
property string createdDirPath: ""
readonly property int _maxPath: 260
HelperWidgets.RegExpValidator {
id: folderNameValidator
regExp: /^(\w[^*/><?\\|:]*)$/
}
ErrorDialog {
id: creationFailedDialog
title: qsTr("Could not create folder")
message: qsTr("An error occurred while trying to create the folder.")
}
contentItem: Column {
spacing: 2
@@ -58,6 +49,10 @@ Dialog {
Keys.onEnterPressed: btnCreate.onClicked()
Keys.onReturnPressed: btnCreate.onClicked()
onTextChanged: {
root.createdDirPath = root.dirPath + '/' + folderName.text
}
}
}
@@ -68,6 +63,13 @@ Dialog {
visible: folderName.text === ""
}
Text {
text: qsTr("Folder path is too long.")
color: "#ff0000"
anchors.right: parent.right
visible: root.createdDirPath.length > root._maxPath
}
Item { // spacer
width: 1
height: 20
@@ -76,20 +78,23 @@ Dialog {
Row {
anchors.right: parent.right
Button {
HelperWidgets.Button {
id: btnCreate
text: qsTr("Create")
enabled: folderName.text !== ""
enabled: folderName.text !== "" && root.createdDirPath.length <= root._maxPath
onClicked: {
assetsModel.addNewFolder(root.contextDir.dirPath + '/' + folderName.text)
newFolderDialog.accept()
root.createdDirPath = root.dirPath + '/' + folderName.text
if (assetsModel.addNewFolder(root.createdDirPath))
root.accept()
else
creationFailedDialog.open()
}
}
Button {
HelperWidgets.Button {
text: qsTr("Cancel")
onClicked: newFolderDialog.reject()
onClicked: root.reject()
}
}
}
@@ -99,4 +104,8 @@ Dialog {
folderName.selectAll()
folderName.forceActiveFocus()
}
onRejected: {
root.createdDirPath = ""
}
}

View File

@@ -1,38 +1,14 @@
/****************************************************************************
**
** Copyright (C) 2022 The Qt Company Ltd.
** Contact: https://www.qt.io/licensing/
**
** This file is part of Qt Creator.
**
** Commercial License Usage
** Licensees holding valid commercial Qt licenses may use this file in
** accordance with the commercial license agreement provided with the
** Software or, alternatively, in accordance with the terms contained in
** a written agreement between you and The Qt Company. For licensing terms
** and conditions see https://www.qt.io/terms-conditions. For further
** information use the contact form at https://www.qt.io/contact-us.
**
** GNU General Public License Usage
** Alternatively, this file may be used under the terms of the GNU
** General Public License version 3 as published by the Free Software
** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT
** included in the packaging of this file. Please review the following
** information to ensure the GNU General Public License requirements will
** be met: https://www.gnu.org/licenses/gpl-3.0.html.
**
****************************************************************************/
// Copyright (C) 2022 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0 WITH Qt-GPL-exception-1.0
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import QtQuickDesignerTheme
import HelperWidgets
import HelperWidgets as HelperWidgets
import StudioControls as StudioControls
import StudioTheme as StudioTheme
Dialog {
id: renameFolderDialog
id: root
title: qsTr("Rename Folder")
anchors.centerIn: parent
@@ -41,6 +17,13 @@ Dialog {
modal: true
property bool renameError: false
required property string dirPath
required property string dirName
HelperWidgets.RegExpValidator {
id: folderNameValidator
regExp: /^(\w[^*/><?\\|:]*)$/
}
contentItem: Column {
spacing: 2
@@ -50,10 +33,10 @@ Dialog {
actionIndicator.visible: false
translationIndicator.visible: false
width: renameFolderDialog.width - 12
width: root.width - 12
validator: folderNameValidator
onEditChanged: renameFolderDialog.renameError = false
onEditChanged: root.renameError = false
Keys.onEnterPressed: btnRename.onClicked()
Keys.onReturnPressed: btnRename.onClicked()
}
@@ -61,15 +44,15 @@ Dialog {
Text {
text: qsTr("Folder name cannot be empty.")
color: "#ff0000"
visible: folderRename.text === "" && !renameFolderDialog.renameError
visible: folderRename.text === "" && !root.renameError
}
Text {
text: qsTr("Could not rename folder. Make sure no folder with the same name exists.")
wrapMode: Text.WordWrap
width: renameFolderDialog.width - 12
width: root.width - 12
color: "#ff0000"
visible: renameFolderDialog.renameError
visible: root.renameError
}
Item { // spacer
@@ -81,7 +64,7 @@ Dialog {
text: qsTr("If the folder has assets in use, renaming it might cause the project to not work correctly.")
color: StudioTheme.Values.themeTextColor
wrapMode: Text.WordWrap
width: renameFolderDialog.width
width: root.width
leftPadding: 10
rightPadding: 10
}
@@ -94,31 +77,31 @@ Dialog {
Row {
anchors.right: parent.right
Button {
HelperWidgets.Button {
id: btnRename
text: qsTr("Rename")
enabled: folderRename.text !== ""
onClicked: {
var success = assetsModel.renameFolder(root.contextDir.dirPath, folderRename.text)
var success = assetsModel.renameFolder(root.dirPath, folderRename.text)
if (success)
renameFolderDialog.accept()
root.accept()
renameFolderDialog.renameError = !success
root.renameError = !success
}
}
Button {
HelperWidgets.Button {
text: qsTr("Cancel")
onClicked: renameFolderDialog.reject()
onClicked: root.reject()
}
}
}
onOpened: {
folderRename.text = root.contextDir.dirName
folderRename.text = root.dirName
folderRename.selectAll()
folderRename.forceActiveFocus()
renameFolderDialog.renameError = false
root.renameError = false
}
}

View File

@@ -740,9 +740,6 @@ extend_qtc_plugin(QmlDesigner
assetslibrarywidget.cpp assetslibrarywidget.h
assetslibrarymodel.cpp assetslibrarymodel.h
assetslibraryiconprovider.cpp assetslibraryiconprovider.h
assetslibrarydir.cpp assetslibrarydir.h
assetslibrarydirsmodel.cpp assetslibrarydirsmodel.h
assetslibraryfilesmodel.cpp assetslibraryfilesmodel.h
)
extend_qtc_plugin(QmlDesigner

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -20,15 +20,48 @@ AssetsLibraryIconProvider::AssetsLibraryIconProvider(SynchronousImageCache &font
QPixmap AssetsLibraryIconProvider::requestPixmap(const QString &id, QSize *size, const QSize &requestedSize)
{
QPixmap pixmap;
if (m_thumbnails.contains(id)) {
pixmap = m_thumbnails[id];
} else {
pixmap = fetchPixmap(id, requestedSize);
if (pixmap.isNull())
pixmap = Utils::StyleHelper::dpiSpecificImageFile(":/AssetsLibrary/images/assets_default.png");
if (requestedSize.isValid())
pixmap = pixmap.scaled(requestedSize, Qt::KeepAspectRatio);
m_thumbnails[id] = pixmap;
}
if (size) {
size->setWidth(pixmap.width());
size->setHeight(pixmap.height());
}
return pixmap;
}
QPixmap AssetsLibraryIconProvider::generateFontIcons(const QString &filePath, const QSize &requestedSize) const
{
QSize reqSize = requestedSize.isValid() ? requestedSize : QSize{48, 48};
return m_fontImageCache.icon(filePath, {},
ImageCache::FontCollectorSizesAuxiliaryData{Utils::span{iconSizes},
Theme::getColor(Theme::DStextColor).name(),
"Abc"}).pixmap(reqSize);
}
QPixmap AssetsLibraryIconProvider::fetchPixmap(const QString &id, const QSize &requestedSize) const
{
const QString suffix = "*." + id.split('.').last().toLower();
if (id == "browse") {
pixmap = Utils::StyleHelper::dpiSpecificImageFile(":/AssetsLibrary/images/browse.png");
return Utils::StyleHelper::dpiSpecificImageFile(":/AssetsLibrary/images/browse.png");
} else if (AssetsLibraryModel::supportedFontSuffixes().contains(suffix)) {
pixmap = generateFontIcons(id, requestedSize);
return generateFontIcons(id, requestedSize);
} else if (AssetsLibraryModel::supportedImageSuffixes().contains(suffix)) {
pixmap = Utils::StyleHelper::dpiSpecificImageFile(id);
return Utils::StyleHelper::dpiSpecificImageFile(id);
} else if (AssetsLibraryModel::supportedTexture3DSuffixes().contains(suffix)) {
pixmap = HdrImage{id}.toPixmap();
return HdrImage{id}.toPixmap();
} else {
QString type;
if (AssetsLibraryModel::supportedShaderSuffixes().contains(suffix))
@@ -43,31 +76,20 @@ QPixmap AssetsLibraryIconProvider::requestPixmap(const QString &id, QSize *size,
QString pathTemplate = QString(":/AssetsLibrary/images/asset_%1%2.png").arg(type);
QString path = pathTemplate.arg('_' + QString::number(requestedSize.width()));
pixmap = Utils::StyleHelper::dpiSpecificImageFile(QFileInfo::exists(path) ? path
: pathTemplate.arg(""));
return Utils::StyleHelper::dpiSpecificImageFile(QFileInfo::exists(path)
? path
: pathTemplate.arg(""));
}
if (size) {
size->setWidth(pixmap.width());
size->setHeight(pixmap.height());
}
if (pixmap.isNull())
pixmap = Utils::StyleHelper::dpiSpecificImageFile(":/AssetsLibrary/images/assets_default.png");
if (requestedSize.isValid())
return pixmap.scaled(requestedSize, Qt::KeepAspectRatio);
return pixmap;
}
QPixmap AssetsLibraryIconProvider::generateFontIcons(const QString &filePath, const QSize &requestedSize) const
void AssetsLibraryIconProvider::clearCache()
{
QSize reqSize = requestedSize.isValid() ? requestedSize : QSize{48, 48};
return m_fontImageCache.icon(filePath, {},
ImageCache::FontCollectorSizesAuxiliaryData{Utils::span{iconSizes},
Theme::getColor(Theme::DStextColor).name(),
"Abc"}).pixmap(reqSize);
m_thumbnails.clear();
}
void AssetsLibraryIconProvider::invalidateThumbnail(const QString &id)
{
m_thumbnails.remove(id);
}
} // namespace QmlDesigner

View File

@@ -15,9 +15,12 @@ public:
AssetsLibraryIconProvider(SynchronousImageCache &fontImageCache);
QPixmap requestPixmap(const QString &id, QSize *size, const QSize &requestedSize) override;
void clearCache();
void invalidateThumbnail(const QString &id);
private:
QPixmap generateFontIcons(const QString &filePath, const QSize &requestedSize) const;
QPixmap fetchPixmap(const QString &id, const QSize &requestedSize) const;
SynchronousImageCache &m_fontImageCache;
@@ -26,6 +29,7 @@ private:
std::vector<QSize> iconSizes = {{128, 128}, // Drag
{96, 96}, // list @2x
{48, 48}}; // list
QHash<QString, QPixmap> m_thumbnails;
};
} // namespace QmlDesigner

View File

@@ -1,68 +1,44 @@
// Copyright (C) 2021 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0+ OR GPL-3.0 WITH Qt-GPL-exception-1.0
#include "assetslibrarymodel.h"
#include "assetslibrarydirsmodel.h"
#include "assetslibraryfilesmodel.h"
#include <QCheckBox>
#include <QFileInfo>
#include <QFileSystemModel>
#include <QImageReader>
#include <QMessageBox>
#include <QSortFilterProxyModel>
#include "assetslibrarymodel.h"
#include <designersettings.h>
#include <documentmanager.h>
#include <synchronousimagecache.h>
#include <theme.h>
#include <utils/hdrimage.h>
#include <qmldesignerplugin.h>
#include <modelnodeoperations.h>
#include <coreplugin/icore.h>
#include <utils/filesystemwatcher.h>
#include <utils/stylehelper.h>
#include <QCheckBox>
#include <QDebug>
#include <QDir>
#include <QDirIterator>
#include <QElapsedTimer>
#include <QFont>
#include <QImageReader>
#include <QLoggingCategory>
#include <QMessageBox>
#include <QMetaProperty>
#include <QPainter>
#include <QRawFont>
#include <QRegularExpression>
static Q_LOGGING_CATEGORY(assetsLibraryBenchmark, "qtc.assetsLibrary.setRoot", QtWarningMsg)
#include <utils/qtcassert.h>
namespace QmlDesigner {
AssetsLibraryModel::AssetsLibraryModel(Utils::FileSystemWatcher *fileSystemWatcher, QObject *parent)
: QAbstractListModel(parent)
, m_fileSystemWatcher(fileSystemWatcher)
AssetsLibraryModel::AssetsLibraryModel(QObject *parent)
: QSortFilterProxyModel{parent}
{
// add role names
int role = 0;
const QMetaObject meta = AssetsLibraryDir::staticMetaObject;
for (int i = meta.propertyOffset(); i < meta.propertyCount(); ++i)
m_roleNames.insert(role++, meta.property(i).name());
createBackendModel();
setRecursiveFilteringEnabled(true);
}
void AssetsLibraryModel::setSearchText(const QString &searchText)
void AssetsLibraryModel::createBackendModel()
{
if (m_searchText != searchText) {
m_searchText = searchText;
refresh();
}
}
m_sourceFsModel = new QFileSystemModel(parent());
void AssetsLibraryModel::saveExpandedState(bool expanded, const QString &assetPath)
{
m_expandedStateHash.insert(assetPath, expanded);
}
m_sourceFsModel->setReadOnly(false);
bool AssetsLibraryModel::loadExpandedState(const QString &assetPath)
{
return m_expandedStateHash.value(assetPath, true);
setSourceModel(m_sourceFsModel);
QObject::connect(m_sourceFsModel, &QFileSystemModel::directoryLoaded, this, &AssetsLibraryModel::directoryLoaded);
QObject::connect(m_sourceFsModel, &QFileSystemModel::dataChanged, this, &AssetsLibraryModel::onDataChanged);
QObject::connect(m_sourceFsModel, &QFileSystemModel::directoryLoaded, this, [this](const QString &dir) {
syncHaveFiles();
});
}
bool AssetsLibraryModel::isEffectQmlExist(const QString &effectName)
@@ -72,50 +48,57 @@ bool AssetsLibraryModel::isEffectQmlExist(const QString &effectName)
return qmlPath.exists();
}
AssetsLibraryModel::DirExpandState AssetsLibraryModel::getAllExpandedState() const
void AssetsLibraryModel::onDataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight,
const QList<int> &roles)
{
const auto keys = m_expandedStateHash.keys();
bool allExpanded = true;
bool allCollapsed = true;
for (const QString &assetPath : keys) {
bool expanded = m_expandedStateHash.value(assetPath);
for (int i = topLeft.row(); i <= bottomRight.row(); ++i) {
QModelIndex index = m_sourceFsModel->index(i, 0, topLeft.parent());
QString path = m_sourceFsModel->filePath(index);
if (expanded)
allCollapsed = false;
if (!expanded)
allExpanded = false;
if (!allCollapsed && !allExpanded)
break;
if (!isDirectory(path))
emit fileChanged(path);
}
return allExpanded ? DirExpandState::AllExpanded : allCollapsed ? DirExpandState::AllCollapsed
: DirExpandState::SomeExpanded;
}
void AssetsLibraryModel::toggleExpandAll(bool expand)
void AssetsLibraryModel::destroyBackendModel()
{
std::function<void(AssetsLibraryDir *)> expandDirRecursive;
expandDirRecursive = [&](AssetsLibraryDir *currAssetsDir) {
if (currAssetsDir->dirDepth() > 0) {
currAssetsDir->setDirExpanded(expand);
saveExpandedState(expand, currAssetsDir->dirPath());
}
setSourceModel(nullptr);
m_sourceFsModel->disconnect(this);
m_sourceFsModel->deleteLater();
m_sourceFsModel = nullptr;
}
const QList<AssetsLibraryDir *> childDirs = currAssetsDir->childAssetsDirs();
for (const auto childDir : childDirs)
expandDirRecursive(childDir);
};
void AssetsLibraryModel::setSearchText(const QString &searchText)
{
m_searchText = searchText;
resetModel();
}
beginResetModel();
expandDirRecursive(m_assetsDir);
endResetModel();
bool AssetsLibraryModel::indexIsValid(const QModelIndex &index) const
{
static QModelIndex invalidIndex;
return index != invalidIndex;
}
QList<QModelIndex> AssetsLibraryModel::parentIndices(const QModelIndex &index) const
{
QModelIndex idx = index;
QModelIndex rootIdx = rootIndex();
QList<QModelIndex> result;
while (idx.isValid() && idx != rootIdx) {
result += idx;
idx = idx.parent();
}
return result;
}
void AssetsLibraryModel::deleteFiles(const QStringList &filePaths)
{
bool askBeforeDelete = QmlDesignerPlugin::settings().value(
DesignerSettingsKey::ASK_BEFORE_DELETING_ASSET).toBool();
bool askBeforeDelete = QmlDesignerPlugin::settings()
.value(DesignerSettingsKey::ASK_BEFORE_DELETING_ASSET)
.toBool();
bool assetDelete = true;
if (askBeforeDelete) {
@@ -123,7 +106,7 @@ void AssetsLibraryModel::deleteFiles(const QStringList &filePaths)
tr("File%1 might be in use. Delete anyway?\n\n%2")
.arg(filePaths.size() > 1 ? QChar('s') : QChar())
.arg(filePaths.join('\n').remove(DocumentManager::currentProjectDirPath()
.toString().append('/'))),
.toString().append('/'))),
QMessageBox::No | QMessageBox::Yes);
QCheckBox cb;
cb.setText(tr("Do not ask this again"));
@@ -162,15 +145,13 @@ bool AssetsLibraryModel::renameFolder(const QString &folderPath, const QString &
dir.cdUp();
saveExpandedState(loadExpandedState(folderPath), dir.absoluteFilePath(newName));
return dir.rename(oldName, newName);
}
void AssetsLibraryModel::addNewFolder(const QString &folderPath)
bool AssetsLibraryModel::addNewFolder(const QString &folderPath)
{
QString iterPath = folderPath;
QRegularExpression rgx("\\d+$"); // matches a number at the end of a string
static QRegularExpression rgx("\\d+$"); // matches a number at the end of a string
QDir dir{folderPath};
while (dir.exists()) {
@@ -191,8 +172,8 @@ void AssetsLibraryModel::addNewFolder(const QString &folderPath)
--nPaddingZeros;
iterPath = folderPath.mid(0, match.capturedStart())
+ QString('0').repeated(nPaddingZeros)
+ QString::number(num);
+ QString('0').repeated(nPaddingZeros)
+ QString::number(num);
} else {
iterPath = folderPath + '1';
}
@@ -200,136 +181,155 @@ void AssetsLibraryModel::addNewFolder(const QString &folderPath)
dir.setPath(iterPath);
}
dir.mkpath(iterPath);
return dir.mkpath(iterPath);
}
void AssetsLibraryModel::deleteFolder(const QString &folderPath)
bool AssetsLibraryModel::deleteFolderRecursively(const QModelIndex &folderIndex)
{
QDir{folderPath}.removeRecursively();
auto idx = mapToSource(folderIndex);
bool ok = m_sourceFsModel->remove(idx);
if (!ok)
qWarning() << __FUNCTION__ << " could not remove folder recursively: " << m_sourceFsModel->filePath(idx);
return ok;
}
QObject *AssetsLibraryModel::rootDir() const
bool AssetsLibraryModel::filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const
{
return m_assetsDir;
}
QString path = m_sourceFsModel->filePath(sourceParent);
bool AssetsLibraryModel::isEmpty() const
{
return m_isEmpty;
}
QModelIndex sourceIdx = m_sourceFsModel->index(sourceRow, 0, sourceParent);
QString sourcePath = m_sourceFsModel->filePath(sourceIdx);
void AssetsLibraryModel::setIsEmpty(bool empty)
{
if (m_isEmpty != empty) {
m_isEmpty = empty;
emit isEmptyChanged();
if (!m_searchText.isEmpty() && path.startsWith(m_rootPath) && QFileInfo{path}.isDir()) {
QString sourceName = m_sourceFsModel->fileName(sourceIdx);
return QFileInfo{sourcePath}.isFile() && sourceName.contains(m_searchText, Qt::CaseInsensitive);
} else {
return sourcePath.startsWith(m_rootPath) || m_rootPath.startsWith(sourcePath);
}
}
QVariant AssetsLibraryModel::data(const QModelIndex &index, int role) const
bool AssetsLibraryModel::checkHaveFiles(const QModelIndex &parentIdx) const
{
if (!index.isValid()) {
qWarning() << Q_FUNC_INFO << "Invalid index requested: " << QString::number(index.row());
return {};
if (!parentIdx.isValid())
return false;
const int rowCount = this->rowCount(parentIdx);
for (int i = 0; i < rowCount; ++i) {
auto newIdx = this->index(i, 0, parentIdx);
if (!isDirectory(newIdx))
return true;
if (checkHaveFiles(newIdx))
return true;
}
if (m_roleNames.contains(role))
return m_assetsDir ? m_assetsDir->property(m_roleNames.value(role)) : QVariant("");
qWarning() << Q_FUNC_INFO << "Invalid role requested: " << QString::number(role);
return {};
return false;
}
int AssetsLibraryModel::rowCount([[maybe_unused]] const QModelIndex &parent) const
void AssetsLibraryModel::setHaveFiles(bool value)
{
return 1;
if (m_haveFiles != value) {
m_haveFiles = value;
emit haveFilesChanged();
}
}
QHash<int, QByteArray> AssetsLibraryModel::roleNames() const
bool AssetsLibraryModel::checkHaveFiles() const
{
return m_roleNames;
auto rootIdx = indexForPath(m_rootPath);
return checkHaveFiles(rootIdx);
}
// called when a directory is changed to refresh the model for this directory
void AssetsLibraryModel::refresh()
void AssetsLibraryModel::syncHaveFiles()
{
setRootPath(m_assetsDir->dirPath());
setHaveFiles(checkHaveFiles());
}
void AssetsLibraryModel::setRootPath(const QString &path)
void AssetsLibraryModel::setRootPath(const QString &newPath)
{
QElapsedTimer time;
if (assetsLibraryBenchmark().isInfoEnabled())
time.start();
qCInfo(assetsLibraryBenchmark) << "start:" << time.elapsed();
static const QStringList ignoredTopLevelDirs {"imports", "asset_imports"};
m_fileSystemWatcher->clear();
std::function<bool(AssetsLibraryDir *, int, bool)> parseDir;
parseDir = [this, &parseDir](AssetsLibraryDir *currAssetsDir, int currDepth, bool recursive) {
m_fileSystemWatcher->addDirectory(currAssetsDir->dirPath(), Utils::FileSystemWatcher::WatchAllChanges);
QDir dir(currAssetsDir->dirPath());
dir.setNameFilters(supportedSuffixes().values());
dir.setFilter(QDir::Files);
QDirIterator itFiles(dir);
bool isEmpty = true;
while (itFiles.hasNext()) {
QString filePath = itFiles.next();
QString fileName = filePath.split('/').last();
if (m_searchText.isEmpty() || fileName.contains(m_searchText, Qt::CaseInsensitive)) {
currAssetsDir->addFile(filePath);
m_fileSystemWatcher->addFile(filePath, Utils::FileSystemWatcher::WatchAllChanges);
isEmpty = false;
}
}
if (recursive) {
dir.setNameFilters({});
dir.setFilter(QDir::Dirs | QDir::NoDotAndDotDot);
QDirIterator itDirs(dir);
while (itDirs.hasNext()) {
QDir subDir = itDirs.next();
if (currDepth == 1 && ignoredTopLevelDirs.contains(subDir.dirName()))
continue;
auto assetsDir = new AssetsLibraryDir(subDir.path(), currDepth,
loadExpandedState(subDir.path()), currAssetsDir);
currAssetsDir->addDir(assetsDir);
saveExpandedState(loadExpandedState(assetsDir->dirPath()), assetsDir->dirPath());
isEmpty &= parseDir(assetsDir, currDepth + 1, true);
}
}
if (!m_searchText.isEmpty() && isEmpty)
currAssetsDir->setDirVisible(false);
return isEmpty;
};
qCInfo(assetsLibraryBenchmark) << "directories parsed:" << time.elapsed();
if (m_assetsDir)
delete m_assetsDir;
beginResetModel();
m_assetsDir = new AssetsLibraryDir(path, 0, true, this);
bool hasProject = !QmlDesignerPlugin::instance()->documentManager().currentProjectDirPath().isEmpty();
bool isEmpty = parseDir(m_assetsDir, 1, hasProject);
setIsEmpty(isEmpty);
bool noAssets = m_searchText.isEmpty() && isEmpty;
// noAssets: the model has no asset files (project has no assets added)
// isEmpty: the model has no asset files (assets could exist but are filtered out)
destroyBackendModel();
createBackendModel();
m_rootPath = newPath;
m_sourceFsModel->setRootPath(newPath);
m_sourceFsModel->setNameFilters(supportedSuffixes().values());
m_sourceFsModel->setNameFilterDisables(false);
m_assetsDir->setDirVisible(!noAssets); // if there are no assets, hide all empty asset folders
endResetModel();
qCInfo(assetsLibraryBenchmark) << "model reset:" << time.elapsed();
emit rootPathChanged();
}
QString AssetsLibraryModel::rootPath() const
{
return m_rootPath;
}
QString AssetsLibraryModel::filePath(const QModelIndex &index) const
{
QModelIndex fsIdx = mapToSource(index);
return m_sourceFsModel->filePath(fsIdx);
}
QString AssetsLibraryModel::fileName(const QModelIndex &index) const
{
QModelIndex fsIdx = mapToSource(index);
return m_sourceFsModel->fileName(fsIdx);
}
QModelIndex AssetsLibraryModel::indexForPath(const QString &path) const
{
QModelIndex idx = m_sourceFsModel->index(path, 0);
return mapFromSource(idx);
}
void AssetsLibraryModel::resetModel()
{
beginResetModel();
endResetModel();
}
QModelIndex AssetsLibraryModel::rootIndex() const
{
return indexForPath(m_rootPath);
}
bool AssetsLibraryModel::isDirectory(const QString &path) const
{
QFileInfo fi{path};
return fi.isDir();
}
bool AssetsLibraryModel::isDirectory(const QModelIndex &index) const
{
QString path = filePath(index);
return isDirectory(path);
}
QModelIndex AssetsLibraryModel::parentDirIndex(const QString &path) const
{
QModelIndex idx = indexForPath(path);
QModelIndex parentIdx = idx.parent();
return parentIdx;
}
QModelIndex AssetsLibraryModel::parentDirIndex(const QModelIndex &index) const
{
QModelIndex parentIdx = index.parent();
return parentIdx;
}
QString AssetsLibraryModel::parentDirPath(const QString &path) const
{
QModelIndex idx = indexForPath(path);
QModelIndex parentIdx = idx.parent();
return filePath(parentIdx);
}
const QStringList &AssetsLibraryModel::supportedImageSuffixes()
@@ -408,17 +408,4 @@ const QSet<QString> &AssetsLibraryModel::supportedSuffixes()
return allSuffixes;
}
const QSet<QString> &AssetsLibraryModel::previewableSuffixes() const
{
static QSet<QString> previewableSuffixes;
if (previewableSuffixes.isEmpty()) {
auto insertSuffixes = [](const QStringList &suffixes) {
for (const auto &suffix : suffixes)
previewableSuffixes.insert(suffix);
};
insertSuffixes(supportedFontSuffixes());
}
return previewableSuffixes;
}
} // namespace QmlDesigner

View File

@@ -3,39 +3,53 @@
#pragma once
#include <QAbstractListModel>
#include <QDateTime>
#include <QDir>
#include <QHash>
#include <QIcon>
#include <QPair>
#include <QSet>
#include <QFileSystemModel>
#include <QSortFilterProxyModel>
#include <QFileInfo>
namespace Utils { class FileSystemWatcher; }
#include <utils/qtcassert.h>
namespace QmlDesigner {
class SynchronousImageCache;
class AssetsLibraryDir;
class AssetsLibraryModel : public QAbstractListModel
class AssetsLibraryModel : public QSortFilterProxyModel
{
Q_OBJECT
Q_PROPERTY(bool isEmpty READ isEmpty WRITE setIsEmpty NOTIFY isEmptyChanged)
public:
AssetsLibraryModel(Utils::FileSystemWatcher *fileSystemWatcher, QObject *parent = nullptr);
AssetsLibraryModel(QObject *parent = nullptr);
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
QHash<int, QByteArray> roleNames() const override;
void refresh();
void setRootPath(const QString &path);
void setRootPath(const QString &newPath);
void setSearchText(const QString &searchText);
bool isEmpty() const;
Q_PROPERTY(bool haveFiles READ haveFiles NOTIFY haveFilesChanged);
Q_INVOKABLE QString rootPath() const;
Q_INVOKABLE QString filePath(const QModelIndex &index) const;
Q_INVOKABLE QString fileName(const QModelIndex &index) const;
Q_INVOKABLE QModelIndex indexForPath(const QString &path) const;
Q_INVOKABLE QModelIndex rootIndex() const;
Q_INVOKABLE bool isDirectory(const QString &path) const;
Q_INVOKABLE bool isDirectory(const QModelIndex &index) const;
Q_INVOKABLE QModelIndex parentDirIndex(const QString &path) const;
Q_INVOKABLE QModelIndex parentDirIndex(const QModelIndex &index) const;
Q_INVOKABLE QString parentDirPath(const QString &path) const;
Q_INVOKABLE void syncHaveFiles();
Q_INVOKABLE QList<QModelIndex> parentIndices(const QModelIndex &index) const;
Q_INVOKABLE bool indexIsValid(const QModelIndex &index) const;
Q_INVOKABLE void deleteFiles(const QStringList &filePaths);
Q_INVOKABLE bool renameFolder(const QString &folderPath, const QString &newName);
Q_INVOKABLE bool addNewFolder(const QString &folderPath);
Q_INVOKABLE bool deleteFolderRecursively(const QModelIndex &folderIndex);
int columnCount(const QModelIndex &parent = QModelIndex()) const override
{
int result = QSortFilterProxyModel::columnCount(parent);
return std::min(result, 1);
}
bool haveFiles() const { return m_haveFiles; }
static const QStringList &supportedImageSuffixes();
static const QStringList &supportedFragmentShaderSuffixes();
@@ -47,44 +61,28 @@ public:
static const QStringList &supportedEffectMakerSuffixes();
static const QSet<QString> &supportedSuffixes();
const QSet<QString> &previewableSuffixes() const;
static void saveExpandedState(bool expanded, const QString &assetPath);
static bool loadExpandedState(const QString &assetPath);
static bool isEffectQmlExist(const QString &effectName);
enum class DirExpandState {
SomeExpanded,
AllExpanded,
AllCollapsed
};
Q_ENUM(DirExpandState)
Q_INVOKABLE void toggleExpandAll(bool expand);
Q_INVOKABLE DirExpandState getAllExpandedState() const;
Q_INVOKABLE void deleteFiles(const QStringList &filePaths);
Q_INVOKABLE bool renameFolder(const QString &folderPath, const QString &newName);
Q_INVOKABLE void addNewFolder(const QString &folderPath);
Q_INVOKABLE void deleteFolder(const QString &folderPath);
Q_INVOKABLE QObject *rootDir() const;
signals:
void isEmptyChanged();
void directoryLoaded(const QString &path);
void rootPathChanged();
void haveFilesChanged();
void fileChanged(const QString &path);
private:
void setIsEmpty(bool empty);
QHash<QString, QPair<QDateTime, QIcon>> m_iconCache;
void setHaveFiles(bool value);
bool filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const override;
void onDataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight, const QList<int> &roles);
void resetModel();
void createBackendModel();
void destroyBackendModel();
bool checkHaveFiles(const QModelIndex &parentIdx) const;
bool checkHaveFiles() const;
QString m_searchText;
Utils::FileSystemWatcher *m_fileSystemWatcher = nullptr;
AssetsLibraryDir *m_assetsDir = nullptr;
bool m_isEmpty = true;
QHash<int, QByteArray> m_roleNames;
inline static QHash<QString, bool> m_expandedStateHash; // <assetPath, isExpanded>
QString m_rootPath;
QFileSystemModel *m_sourceFsModel = nullptr;
bool m_haveFiles = false;
};
} // namespace QmlDesigner

View File

@@ -17,7 +17,6 @@
#include <utils/algorithm.h>
#include <utils/environment.h>
#include <utils/filesystemwatcher.h>
#include <utils/fileutils.h>
#include <utils/qtcassert.h>
#include <utils/stylehelper.h>
@@ -85,16 +84,12 @@ bool AssetsLibraryWidget::eventFilter(QObject *obj, QEvent *event)
AssetsLibraryWidget::AssetsLibraryWidget(AsynchronousImageCache &asynchronousFontImageCache,
SynchronousImageCache &synchronousFontImageCache)
: m_itemIconSize(24, 24)
, m_fontImageCache(synchronousFontImageCache)
, m_assetsIconProvider(new AssetsLibraryIconProvider(synchronousFontImageCache))
, m_fileSystemWatcher(new Utils::FileSystemWatcher(this))
, m_assetsModel(new AssetsLibraryModel(m_fileSystemWatcher, this))
, m_assetsWidget(new QQuickWidget(this))
: m_itemIconSize{24, 24}
, m_fontImageCache{synchronousFontImageCache}
, m_assetsIconProvider{new AssetsLibraryIconProvider(synchronousFontImageCache)}
, m_assetsModel{new AssetsLibraryModel(this)}
, m_assetsWidget{new QQuickWidget(this)}
{
m_assetCompressionTimer.setInterval(200);
m_assetCompressionTimer.setSingleShot(true);
setWindowTitle(tr("Assets Library", "Title of assets library widget"));
setMinimumWidth(250);
@@ -119,21 +114,12 @@ AssetsLibraryWidget::AssetsLibraryWidget(AsynchronousImageCache &asynchronousFon
m_assetsWidget->setClearColor(Theme::getColor(Theme::Color::QmlDesigner_BackgroundColorDarkAlternate));
m_assetsWidget->engine()->addImageProvider("qmldesigner_assets", m_assetsIconProvider);
m_assetsWidget->rootContext()->setContextProperties(QVector<QQmlContext::PropertyPair>{
{{"assetsModel"}, QVariant::fromValue(m_assetsModel.data())},
{{"assetsModel"}, QVariant::fromValue(m_assetsModel)},
{{"rootView"}, QVariant::fromValue(this)},
{{"tooltipBackend"}, QVariant::fromValue(m_fontPreviewTooltipBackend.get())}
});
// If project directory contents change, or one of the asset files is modified, we must
// reconstruct the model to update the icons
connect(m_fileSystemWatcher,
&Utils::FileSystemWatcher::directoryChanged,
[this]([[maybe_unused]] const QString &changedDirPath) {
m_assetCompressionTimer.start();
});
connect(m_fileSystemWatcher, &Utils::FileSystemWatcher::fileChanged,
[](const QString &changeFilePath) {
connect(m_assetsModel, &AssetsLibraryModel::fileChanged, [](const QString &changeFilePath) {
QmlDesignerPlugin::instance()->emitAssetChanged(changeFilePath);
});
@@ -149,23 +135,7 @@ AssetsLibraryWidget::AssetsLibraryWidget(AsynchronousImageCache &asynchronousFon
m_qmlSourceUpdateShortcut = new QShortcut(QKeySequence(Qt::CTRL + Qt::Key_F6), this);
connect(m_qmlSourceUpdateShortcut, &QShortcut::activated, this, &AssetsLibraryWidget::reloadQmlSource);
connect(&m_assetCompressionTimer, &QTimer::timeout, this, [this]() {
// TODO: find a clever way to only refresh the changed directory part of the model
// Don't bother with asset updates after model has detached, project is probably closing
if (!m_model.isNull()) {
if (QApplication::activeModalWidget()) {
// Retry later, as updating file system watchers can crash when there is an active
// modal widget
m_assetCompressionTimer.start();
} else {
m_assetsModel->refresh();
// reload assets qml so that an overridden file's image shows the new image
QTimer::singleShot(100, this, &AssetsLibraryWidget::reloadQmlSource);
}
}
});
connect(this, &AssetsLibraryWidget::extFilesDrop, this, &AssetsLibraryWidget::handleExtFilesDrop, Qt::QueuedConnection);
QmlDesignerPlugin::trackWidgetFocusTime(this, Constants::EVENT_ASSETSLIBRARY_TIME);
@@ -173,7 +143,15 @@ AssetsLibraryWidget::AssetsLibraryWidget(AsynchronousImageCache &asynchronousFon
reloadQmlSource();
}
AssetsLibraryWidget::~AssetsLibraryWidget() = default;
bool AssetsLibraryWidget::qtVersionIsAtLeast6_4() const
{
return (QT_VERSION >= QT_VERSION_CHECK(6, 4, 0));
}
void AssetsLibraryWidget::invalidateThumbnail(const QString &id)
{
m_assetsIconProvider->invalidateThumbnail(id);
}
QList<QToolButton *> AssetsLibraryWidget::createToolBarWidgets()
{
@@ -182,8 +160,9 @@ QList<QToolButton *> AssetsLibraryWidget::createToolBarWidgets()
void AssetsLibraryWidget::handleSearchFilterChanged(const QString &filterText)
{
if (filterText == m_filterText || (m_assetsModel->isEmpty() && filterText.contains(m_filterText)))
return;
if (filterText == m_filterText || (!m_assetsModel->haveFiles()
&& filterText.contains(m_filterText, Qt::CaseInsensitive)))
return;
m_filterText = filterText;
updateSearch();
@@ -194,6 +173,16 @@ void AssetsLibraryWidget::handleAddAsset()
addResources({});
}
void AssetsLibraryWidget::emitExtFilesDrop(const QList<QUrl> &simpleFilePaths,
const QList<QUrl> &complexFilePaths,
const QString &targetDirPath)
{
// workaround for but QDS-8010: we need to postpone the call to handleExtFilesDrop, otherwise
// the TreeViewDelegate might be recreated (therefore, destroyed) while we're still in a handler
// of a QML DropArea which is a child of the delegate being destroyed - this would cause a crash.
emit extFilesDrop(simpleFilePaths, complexFilePaths, targetDirPath);
}
void AssetsLibraryWidget::handleExtFilesDrop(const QList<QUrl> &simpleFilePaths,
const QList<QUrl> &complexFilePaths,
const QString &targetDirPath)
@@ -210,7 +199,7 @@ void AssetsLibraryWidget::handleExtFilesDrop(const QList<QUrl> &simpleFilePaths,
} else {
AddFilesResult result = ModelNodeOperations::addFilesToProject(simpleFilePathStrings,
targetDirPath);
if (result == AddFilesResult::Failed) {
if (result.status() == AddFilesResult::Failed) {
Core::AsynchronousMessageBox::warning(tr("Failed to Add Files"),
tr("Could not add %1 to project.")
.arg(simpleFilePathStrings.join(' ')));
@@ -276,6 +265,7 @@ void AssetsLibraryWidget::updateSearch()
void AssetsLibraryWidget::setResourcePath(const QString &resourcePath)
{
m_assetsModel->setRootPath(resourcePath);
m_assetsIconProvider->clearCache();
updateSearch();
}
@@ -408,10 +398,22 @@ void AssetsLibraryWidget::addResources(const QStringList &files)
if (operation) {
AddFilesResult result = operation(fileNames,
document->fileName().parentDir().toString(), true);
if (result == AddFilesResult::Failed) {
if (result.status() == AddFilesResult::Failed) {
Core::AsynchronousMessageBox::warning(tr("Failed to Add Files"),
tr("Could not add %1 to project.")
.arg(fileNames.join(' ')));
} else {
if (!result.directory().isEmpty()) {
emit directoryCreated(result.directory());
} else if (result.haveDelayedResult()) {
QObject *delayedResult = result.delayedResult();
QObject::connect(delayedResult, &QObject::destroyed, this, [this, delayedResult]() {
QVariant propValue = delayedResult->property(AddFilesResult::directoryPropName);
QString directory = propValue.toString();
if (!directory.isEmpty())
emit directoryCreated(directory);
});
}
}
} else {
Core::AsynchronousMessageBox::warning(tr("Failed to Add Files"),

View File

@@ -21,7 +21,7 @@ class QShortcut;
QT_END_NAMESPACE
namespace Utils {
class FileSystemWatcher;
class QtcProcess;
}
namespace QmlDesigner {
@@ -42,7 +42,7 @@ class AssetsLibraryWidget : public QFrame
public:
AssetsLibraryWidget(AsynchronousImageCache &asynchronousFontImageCache,
SynchronousImageCache &synchronousFontImageCache);
~AssetsLibraryWidget();
~AssetsLibraryWidget() = default;
QList<QToolButton *> createToolBarWidgets();
@@ -59,14 +59,26 @@ public:
Q_INVOKABLE void startDragAsset(const QStringList &assetPaths, const QPointF &mousePos);
Q_INVOKABLE void handleAddAsset();
Q_INVOKABLE void handleSearchFilterChanged(const QString &filterText);
Q_INVOKABLE void handleExtFilesDrop(const QList<QUrl> &simpleFilePaths,
const QList<QUrl> &complexFilePaths,
const QString &targetDirPath = {});
const QString &targetDirPath);
Q_INVOKABLE void emitExtFilesDrop(const QList<QUrl> &simpleFilePaths,
const QList<QUrl> &complexFilePaths,
const QString &targetDirPath = {});
Q_INVOKABLE QSet<QString> supportedAssetSuffixes(bool complex);
Q_INVOKABLE void openEffectMaker(const QString &filePath);
Q_INVOKABLE bool qtVersionIsAtLeast6_4() const;
Q_INVOKABLE void invalidateThumbnail(const QString &id);
signals:
void itemActivated(const QString &itemName);
void extFilesDrop(const QList<QUrl> &simpleFilePaths,
const QList<QUrl> &complexFilePaths,
const QString &targetDirPath);
void directoryCreated(const QString &path);
protected:
bool eventFilter(QObject *obj, QEvent *event) override;
@@ -77,14 +89,12 @@ private:
void addResources(const QStringList &files);
void updateSearch();
QTimer m_assetCompressionTimer;
QSize m_itemIconSize;
SynchronousImageCache &m_fontImageCache;
AssetsLibraryIconProvider *m_assetsIconProvider = nullptr;
Utils::FileSystemWatcher *m_fileSystemWatcher = nullptr;
QPointer<AssetsLibraryModel> m_assetsModel;
AssetsLibraryModel *m_assetsModel = nullptr;
QScopedPointer<QQuickWidget> m_assetsWidget;
std::unique_ptr<PreviewTooltipBackend> m_fontPreviewTooltipBackend;

View File

@@ -277,7 +277,7 @@ QHash<QString, QStringList> DesignerActionManager::handleExternalAssetsDrop(cons
AddResourceOperation operation = categoryOperation.value(category);
QStringList files = categoryFiles.value(category);
AddFilesResult result = operation(files, {}, true);
if (result == AddFilesResult::Succeeded)
if (result.status() == AddFilesResult::Succeeded)
addedCategoryFiles.insert(category, files);
}

View File

@@ -1042,10 +1042,10 @@ AddFilesResult addFilesToProject(const QStringList &fileNames, const QString &de
{
QString directory = showDialog ? AddImagesDialog::getDirectory(fileNames, defaultDir) : defaultDir;
if (directory.isEmpty())
return AddFilesResult::Cancelled;
return AddFilesResult::cancelled(directory);
DesignDocument *document = QmlDesignerPlugin::instance()->currentDesignDocument();
QTC_ASSERT(document, return AddFilesResult::Failed);
QTC_ASSERT(document, return AddFilesResult::failed(directory));
QList<QPair<QString, QString>> copyList;
QStringList removeList;
@@ -1073,7 +1073,7 @@ AddFilesResult addFilesToProject(const QStringList &fileNames, const QString &de
for (const auto &filePair : std::as_const(copyList)) {
const bool success = QFile::copy(filePair.first, filePair.second);
if (!success)
return AddFilesResult::Failed;
return AddFilesResult::failed(directory);
ProjectExplorer::Node *node = ProjectExplorer::ProjectTree::nodeForFile(document->fileName());
if (node) {
@@ -1083,7 +1083,7 @@ AddFilesResult addFilesToProject(const QStringList &fileNames, const QString &de
}
}
return AddFilesResult::Succeeded;
return AddFilesResult::succeeded(directory);
}
static QString getAssetDefaultDirectory(const QString &assetDir, const QString &defaultDirectory)

View File

@@ -9,7 +9,48 @@
namespace QmlDesigner {
enum class AddFilesResult { Succeeded, Failed, Cancelled };
class AddFilesResult
{
public:
enum Status { Succeeded, Failed, Cancelled, Delayed };
static constexpr char directoryPropName[] = "directory";
static AddFilesResult cancelled(const QString &directory = {})
{
return AddFilesResult{Cancelled, directory};
}
static AddFilesResult failed(const QString &directory = {})
{
return AddFilesResult{Failed, directory};
}
static AddFilesResult succeeded(const QString &directory = {})
{
return AddFilesResult{Succeeded, directory};
}
static AddFilesResult delayed(QObject *delayedResult)
{
return AddFilesResult{Delayed, {}, delayedResult};
}
Status status() const { return m_status; }
QString directory() const { return m_directory; }
bool haveDelayedResult() const { return m_delayedResult != nullptr; }
QObject *delayedResult() const { return m_delayedResult; }
private:
AddFilesResult(Status status, const QString &directory, QObject *delayedResult = nullptr)
: m_status{status}
, m_directory{directory}
, m_delayedResult{delayedResult}
{}
Status m_status;
QString m_directory;
QObject *m_delayedResult = nullptr;
};
namespace ModelNodeOperations {

View File

@@ -55,7 +55,7 @@ WidgetInfo ContentLibraryView::widgetInfo()
// copy image to project
AddFilesResult result = ModelNodeOperations::addImageToProject({texPath}, "images", false);
if (result == AddFilesResult::Failed) {
if (result.status() == AddFilesResult::Failed) {
Core::AsynchronousMessageBox::warning(tr("Failed to Add Texture"),
tr("Could not add %1 to project.").arg(texPath));
return;

View File

@@ -156,7 +156,7 @@ void ItemLibraryView::updateImport3DSupport(const QVariantMap &supportMap)
Core::ICore::dialogParent());
int result = importDlg->exec();
return result == QDialog::Accepted ? AddFilesResult::Succeeded : AddFilesResult::Cancelled;
return result == QDialog::Accepted ? AddFilesResult::succeeded() : AddFilesResult::cancelled();
};
auto add3DHandler = [&](const QString &group, const QString &ext) {