QmlDesigner: Implement adding and removing asset folders

Also corrected assets view margins and few relevants tweaks.

Task-number: QDS-5795
Change-Id: Ieeb68584bcb261422f48ec1a865f510a00c251f5
Reviewed-by: Miikka Heikkinen <miikka.heikkinen@qt.io>
Reviewed-by: Thomas Hartmann <thomas.hartmann@qt.io>
This commit is contained in:
Mahmoud Badri
2022-01-28 16:12:23 +02:00
parent 01f33fe4f1
commit 25fa5540ea
7 changed files with 288 additions and 19 deletions

View File

@@ -36,7 +36,9 @@ Item {
property var selectedAssets: ({})
property int allExpandedState: 0
property string delFilePath: ""
property string contextFilePath: ""
property var contextDir: undefined
property bool isDirContextMenu: false
DropArea {
id: dropArea
@@ -67,6 +69,19 @@ Item {
}
}
MouseArea { // right clicking the empty area of the view
anchors.fill: parent
acceptedButtons: Qt.RightButton
onClicked: {
if (!assetsModel.isEmpty) {
contextFilePath = ""
contextDir = assetsModel.rootDir()
isDirContextMenu = false
contextMenu.popup()
}
}
}
// called from C++ to close context menu on focus out
function handleViewFocusOut()
{
@@ -75,9 +90,139 @@ Item {
selectedAssetsChanged()
}
Dialog {
id: newFolderDialog
title: qsTr("Create new folder")
anchors.centerIn: parent
closePolicy: Popup.CloseOnEscape
modal: true
contentItem: Column {
spacing: 2
Row {
Text {
text: qsTr("Folder Name: ")
anchors.verticalCenter: parent.verticalCenter
color: StudioTheme.Values.themeTextColor
}
StudioControls.TextField {
id: folderName
actionIndicator.visible: false
translationIndicator.visible: false
Keys.onEnterPressed: btnCreate.onClicked()
Keys.onReturnPressed: btnCreate.onClicked()
}
}
Text {
text: qsTr("Folder Name cannot be empty.")
color: "#ff0000"
anchors.right: parent.right
visible: folderName.text === ""
}
Item { // spacer
width: 1
height: 20
}
Row {
anchors.right: parent.right
Button {
id: btnCreate
text: qsTr("Create")
enabled: folderName.text !== ""
onClicked: {
assetsModel.addNewFolder(contextDir.dirPath + '/' + folderName.text)
newFolderDialog.accept()
}
}
Button {
text: qsTr("Cancel")
onClicked: newFolderDialog.reject()
}
}
}
onOpened: {
folderName.text = "New folder"
folderName.selectAll()
folderName.forceActiveFocus()
}
}
Dialog {
id: confirmDeleteFolderDialog
title: qsTr("Folder not empty")
anchors.centerIn: parent
closePolicy: Popup.CloseOnEscape
implicitWidth: 300
modal: true
contentItem: Column {
spacing: 20
width: parent.width
Text {
id: folderNotEmpty
text: qsTr("Folder '%1' is not empty. Are you sure you want to delete it?")
.arg(contextDir ? contextDir.dirName : "")
color: StudioTheme.Values.themeTextColor
wrapMode: Text.WordWrap
width: confirmDeleteFolderDialog.width
leftPadding: 10
rightPadding: 10
Keys.onEnterPressed: btnDelete.onClicked()
Keys.onReturnPressed: btnDelete.onClicked()
}
Text {
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
leftPadding: 10
rightPadding: 10
}
Row {
anchors.right: parent.right
Button {
id: btnDelete
text: qsTr("Delete")
onClicked: {
assetsModel.deleteFolder(contextDir.dirPath)
confirmDeleteFolderDialog.accept()
}
}
Button {
text: qsTr("Cancel")
onClicked: confirmDeleteFolderDialog.reject()
}
}
}
onOpened: folderNotEmpty.forceActiveFocus()
}
ScrollView { // TODO: experiment using ListView instead of ScrollView + Column
id: assetsView
anchors.fill: parent
interactive: assetsView.verticalScrollBarVisible
Item {
StudioControls.Menu {
@@ -86,7 +231,7 @@ Item {
StudioControls.MenuItem {
text: qsTr("Expand All")
enabled: allExpandedState !== 1
visible: !delFilePath
visible: isDirContextMenu
height: visible ? implicitHeight : 0
onTriggered: assetsModel.toggleExpandAll(true)
}
@@ -94,22 +239,51 @@ Item {
StudioControls.MenuItem {
text: qsTr("Collapse All")
enabled: allExpandedState !== 2
visible: !delFilePath
visible: isDirContextMenu
height: visible ? implicitHeight : 0
onTriggered: assetsModel.toggleExpandAll(false)
}
StudioControls.MenuSeparator {
visible: isDirContextMenu
height: visible ? StudioTheme.Values.border : 0
}
StudioControls.MenuItem {
text: qsTr("Delete File")
visible: delFilePath
visible: contextFilePath
height: visible ? implicitHeight : 0
onTriggered: assetsModel.removeFile(delFilePath)
onTriggered: assetsModel.deleteFile(contextFilePath)
}
StudioControls.MenuSeparator {
visible: contextFilePath
height: visible ? StudioTheme.Values.border : 0
}
StudioControls.MenuItem {
text: qsTr("New Folder")
onTriggered: newFolderDialog.open()
}
StudioControls.MenuItem {
text: qsTr("Delete Folder")
visible: isDirContextMenu
height: visible ? implicitHeight : 0
onTriggered: {
var dirEmpty = !(contextDir.dirsModel && contextDir.dirsModel.rowCount() > 0)
&& !(contextDir.filesModel && contextDir.filesModel.rowCount() > 0);
if (dirEmpty)
assetsModel.deleteFolder(contextDir.dirPath)
else
confirmDeleteFolderDialog.open()
}
}
}
}
Column {
spacing: 2
Repeater {
model: assetsModel // context property
delegate: dirSection
@@ -120,31 +294,35 @@ Item {
Section {
width: assetsView.width -
(assetsView.verticalScrollBarVisible ? assetsView.verticalThickness : 0)
(assetsView.verticalScrollBarVisible ? assetsView.verticalThickness : 0) - 5
caption: dirName
sectionHeight: 30
sectionFontSize: 15
levelShift: 20
leftPadding: 0
topPadding: dirDepth > 0 ? 5 : 0
bottomPadding: 0
hideHeader: dirDepth === 0
showLeftBorder: true
showLeftBorder: dirDepth > 0
expanded: dirExpanded
visible: dirVisible
visible: !assetsModel.isEmpty && dirVisible
expandOnClick: false
useDefaulContextMenu: false
onToggleExpand: {
dirExpanded = !dirExpanded
}
onShowContextMenu: {
delFilePath = ""
contextFilePath = ""
contextDir = model
isDirContextMenu = true
allExpandedState = assetsModel.getAllExpandedState()
contextMenu.popup()
}
Column {
spacing: 5
leftPadding: 15
leftPadding: 5
Repeater {
model: dirsModel
@@ -155,6 +333,25 @@ Item {
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: {
contextFilePath = ""
contextDir = model
isDirContextMenu = true
contextMenu.popup()
}
}
}
}
}
}
@@ -222,8 +419,11 @@ Item {
if (currFileSelected)
rootView.startDragAsset(selectedAssetsArr, mapToGlobal(mouse.x, mouse.y))
} else {
delFilePath = filePath
contextFilePath = filePath
contextDir = model.fileDir
tooltipBackend.hideTooltip()
isDirContextMenu = false
contextMenu.popup()
}
}
@@ -263,7 +463,7 @@ Item {
// Placeholder when the assets panel is empty
Column {
id: colNoAssets
visible: assetsModel.isEmpty
visible: assetsModel.isEmpty && !rootView.searchActive
spacing: 20
x: 20
@@ -307,4 +507,13 @@ Item {
wrapMode: Text.WordWrap
}
}
Text {
text: qsTr("No match found.")
x: 20
y: 10
color: StudioTheme.Values.themeTextColor
font.pixelSize: 12
visible: assetsModel.isEmpty && rootView.searchActive
}
}

View File

@@ -173,7 +173,7 @@ Item {
id: leftBorder
visible: false
width: 1
height: parent.height - 15
height: parent.height - bottomPadding
color: header.color
}

View File

@@ -34,6 +34,7 @@ ItemLibraryAssetsFilesModel::ItemLibraryAssetsFilesModel(QObject *parent)
// add roles
m_roleNames.insert(FileNameRole, "fileName");
m_roleNames.insert(FilePathRole, "filePath");
m_roleNames.insert(FileDirRole, "fileDir");
}
QVariant ItemLibraryAssetsFilesModel::data(const QModelIndex &index, int role) const
@@ -49,6 +50,9 @@ QVariant ItemLibraryAssetsFilesModel::data(const QModelIndex &index, int role) c
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 {};
}

View File

@@ -44,7 +44,8 @@ public:
private:
enum Roles {FileNameRole = Qt::UserRole + 1,
FilePathRole};
FilePathRole,
FileDirRole};
QStringList m_files;
QHash<int, QByteArray> m_roleNames;

View File

@@ -99,7 +99,7 @@ void ItemLibraryAssetsModel::toggleExpandAll(bool expand)
endResetModel();
}
void ItemLibraryAssetsModel::removeFile(const QString &filePath)
void ItemLibraryAssetsModel::deleteFile(const QString &filePath)
{
bool askBeforeDelete = DesignerSettings::getValue(
DesignerSettingsKey::ASK_BEFORE_DELETING_ASSET).toBool();
@@ -134,6 +134,52 @@ void ItemLibraryAssetsModel::removeFile(const QString &filePath)
}
}
void ItemLibraryAssetsModel::addNewFolder(const QString &folderPath)
{
QString iterPath = folderPath;
QRegularExpression rgx("\\d+$"); // matches a number at the end of a string
QDir dir{folderPath};
while (dir.exists()) {
// if the folder name ends with a number, increment it
QRegularExpressionMatch match = rgx.match(iterPath);
if (match.hasMatch()) { // ends with a number
QString numStr = match.captured(0);
int num = match.captured(0).toInt();
// get number of padding zeros, ex: for "005" = 2
int nPaddingZeros = 0;
for (; nPaddingZeros < numStr.size() && numStr[nPaddingZeros] == '0'; ++nPaddingZeros);
++num;
// if the incremented number's digits increased, decrease the padding zeros
if (std::fmod(std::log10(num), 1.0) == 0)
--nPaddingZeros;
iterPath = folderPath.mid(0, match.capturedStart())
+ QString('0').repeated(nPaddingZeros)
+ QString::number(num);
} else {
iterPath = folderPath + '1';
}
dir.setPath(iterPath);
}
dir.mkpath(iterPath);
}
void ItemLibraryAssetsModel::deleteFolder(const QString &folderPath)
{
QDir{folderPath}.removeRecursively();
}
QObject *ItemLibraryAssetsModel::rootDir() const
{
return m_assetsDir;
}
const QStringList &ItemLibraryAssetsModel::supportedImageSuffixes()
{
static QStringList retList;
@@ -270,7 +316,7 @@ void ItemLibraryAssetsModel::setRootPath(const QString &path)
isEmpty &= parseDirRecursive(assetsDir, currDepth + 1);
}
if (isEmpty)
if (!m_searchText.isEmpty() && isEmpty)
currAssetsDir->setDirVisible(false);
return isEmpty;

View File

@@ -83,7 +83,10 @@ public:
Q_INVOKABLE void toggleExpandAll(bool expand);
Q_INVOKABLE DirExpandState getAllExpandedState() const;
Q_INVOKABLE void removeFile(const QString &filePath);
Q_INVOKABLE void deleteFile(const QString &filePath);
Q_INVOKABLE void addNewFolder(const QString &folderPath);
Q_INVOKABLE void deleteFolder(const QString &folderPath);
Q_INVOKABLE QObject *rootDir() const;
signals:
void isEmptyChanged();

View File

@@ -504,8 +504,14 @@ bool DocumentManager::belongsToQmakeProject()
Utils::FilePath DocumentManager::currentResourcePath()
{
Utils::FilePath resourcePath = currentProjectDirPath();
if (resourcePath.isEmpty())
return currentFilePath().absolutePath();
FilePath contentFilePath = resourcePath.pathAppended("content");
if (contentFilePath.exists())
return contentFilePath;
return resourcePath;
}