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 var selectedAssets: ({})
property int allExpandedState: 0 property int allExpandedState: 0
property string delFilePath: "" property string contextFilePath: ""
property var contextDir: undefined
property bool isDirContextMenu: false
DropArea { DropArea {
id: 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 // called from C++ to close context menu on focus out
function handleViewFocusOut() function handleViewFocusOut()
{ {
@@ -75,9 +90,139 @@ Item {
selectedAssetsChanged() 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 ScrollView { // TODO: experiment using ListView instead of ScrollView + Column
id: assetsView id: assetsView
anchors.fill: parent anchors.fill: parent
interactive: assetsView.verticalScrollBarVisible
Item { Item {
StudioControls.Menu { StudioControls.Menu {
@@ -86,7 +231,7 @@ Item {
StudioControls.MenuItem { StudioControls.MenuItem {
text: qsTr("Expand All") text: qsTr("Expand All")
enabled: allExpandedState !== 1 enabled: allExpandedState !== 1
visible: !delFilePath visible: isDirContextMenu
height: visible ? implicitHeight : 0 height: visible ? implicitHeight : 0
onTriggered: assetsModel.toggleExpandAll(true) onTriggered: assetsModel.toggleExpandAll(true)
} }
@@ -94,22 +239,51 @@ Item {
StudioControls.MenuItem { StudioControls.MenuItem {
text: qsTr("Collapse All") text: qsTr("Collapse All")
enabled: allExpandedState !== 2 enabled: allExpandedState !== 2
visible: !delFilePath visible: isDirContextMenu
height: visible ? implicitHeight : 0 height: visible ? implicitHeight : 0
onTriggered: assetsModel.toggleExpandAll(false) onTriggered: assetsModel.toggleExpandAll(false)
} }
StudioControls.MenuSeparator {
visible: isDirContextMenu
height: visible ? StudioTheme.Values.border : 0
}
StudioControls.MenuItem { StudioControls.MenuItem {
text: qsTr("Delete File") text: qsTr("Delete File")
visible: delFilePath visible: contextFilePath
height: visible ? implicitHeight : 0 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 { Column {
spacing: 2
Repeater { Repeater {
model: assetsModel // context property model: assetsModel // context property
delegate: dirSection delegate: dirSection
@@ -120,31 +294,35 @@ Item {
Section { Section {
width: assetsView.width - width: assetsView.width -
(assetsView.verticalScrollBarVisible ? assetsView.verticalThickness : 0) (assetsView.verticalScrollBarVisible ? assetsView.verticalThickness : 0) - 5
caption: dirName caption: dirName
sectionHeight: 30 sectionHeight: 30
sectionFontSize: 15 sectionFontSize: 15
levelShift: 20
leftPadding: 0 leftPadding: 0
topPadding: dirDepth > 0 ? 5 : 0
bottomPadding: 0
hideHeader: dirDepth === 0 hideHeader: dirDepth === 0
showLeftBorder: true showLeftBorder: dirDepth > 0
expanded: dirExpanded expanded: dirExpanded
visible: dirVisible visible: !assetsModel.isEmpty && dirVisible
expandOnClick: false expandOnClick: false
useDefaulContextMenu: false useDefaulContextMenu: false
onToggleExpand: { onToggleExpand: {
dirExpanded = !dirExpanded dirExpanded = !dirExpanded
} }
onShowContextMenu: { onShowContextMenu: {
delFilePath = "" contextFilePath = ""
contextDir = model
isDirContextMenu = true
allExpandedState = assetsModel.getAllExpandedState() allExpandedState = assetsModel.getAllExpandedState()
contextMenu.popup() contextMenu.popup()
} }
Column { Column {
spacing: 5 spacing: 5
leftPadding: 15 leftPadding: 5
Repeater { Repeater {
model: dirsModel model: dirsModel
@@ -155,6 +333,25 @@ Item {
model: filesModel model: filesModel
delegate: fileSection 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) if (currFileSelected)
rootView.startDragAsset(selectedAssetsArr, mapToGlobal(mouse.x, mouse.y)) rootView.startDragAsset(selectedAssetsArr, mapToGlobal(mouse.x, mouse.y))
} else { } else {
delFilePath = filePath contextFilePath = filePath
contextDir = model.fileDir
tooltipBackend.hideTooltip() tooltipBackend.hideTooltip()
isDirContextMenu = false
contextMenu.popup() contextMenu.popup()
} }
} }
@@ -263,7 +463,7 @@ Item {
// Placeholder when the assets panel is empty // Placeholder when the assets panel is empty
Column { Column {
id: colNoAssets id: colNoAssets
visible: assetsModel.isEmpty visible: assetsModel.isEmpty && !rootView.searchActive
spacing: 20 spacing: 20
x: 20 x: 20
@@ -307,4 +507,13 @@ Item {
wrapMode: Text.WordWrap 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 id: leftBorder
visible: false visible: false
width: 1 width: 1
height: parent.height - 15 height: parent.height - bottomPadding
color: header.color color: header.color
} }

View File

@@ -34,6 +34,7 @@ ItemLibraryAssetsFilesModel::ItemLibraryAssetsFilesModel(QObject *parent)
// add roles // add roles
m_roleNames.insert(FileNameRole, "fileName"); m_roleNames.insert(FileNameRole, "fileName");
m_roleNames.insert(FilePathRole, "filePath"); m_roleNames.insert(FilePathRole, "filePath");
m_roleNames.insert(FileDirRole, "fileDir");
} }
QVariant ItemLibraryAssetsFilesModel::data(const QModelIndex &index, int role) const 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) if (role == FilePathRole)
return m_files[index.row()]; return m_files[index.row()];
if (role == FileDirRole)
return QVariant::fromValue(parent());
qWarning() << Q_FUNC_INFO << "Invalid role requested: " << QString::number(role); qWarning() << Q_FUNC_INFO << "Invalid role requested: " << QString::number(role);
return {}; return {};
} }

View File

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

View File

@@ -99,7 +99,7 @@ void ItemLibraryAssetsModel::toggleExpandAll(bool expand)
endResetModel(); endResetModel();
} }
void ItemLibraryAssetsModel::removeFile(const QString &filePath) void ItemLibraryAssetsModel::deleteFile(const QString &filePath)
{ {
bool askBeforeDelete = DesignerSettings::getValue( bool askBeforeDelete = DesignerSettings::getValue(
DesignerSettingsKey::ASK_BEFORE_DELETING_ASSET).toBool(); 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() const QStringList &ItemLibraryAssetsModel::supportedImageSuffixes()
{ {
static QStringList retList; static QStringList retList;
@@ -270,7 +316,7 @@ void ItemLibraryAssetsModel::setRootPath(const QString &path)
isEmpty &= parseDirRecursive(assetsDir, currDepth + 1); isEmpty &= parseDirRecursive(assetsDir, currDepth + 1);
} }
if (isEmpty) if (!m_searchText.isEmpty() && isEmpty)
currAssetsDir->setDirVisible(false); currAssetsDir->setDirVisible(false);
return isEmpty; return isEmpty;

View File

@@ -83,7 +83,10 @@ public:
Q_INVOKABLE void toggleExpandAll(bool expand); Q_INVOKABLE void toggleExpandAll(bool expand);
Q_INVOKABLE DirExpandState getAllExpandedState() const; 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: signals:
void isEmptyChanged(); void isEmptyChanged();

View File

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