forked from qt-creator/qt-creator
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:
@@ -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
|
||||
}
|
||||
}
|
||||
|
@@ -173,7 +173,7 @@ Item {
|
||||
id: leftBorder
|
||||
visible: false
|
||||
width: 1
|
||||
height: parent.height - 15
|
||||
height: parent.height - bottomPadding
|
||||
color: header.color
|
||||
}
|
||||
|
||||
|
@@ -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 {};
|
||||
}
|
||||
|
@@ -44,7 +44,8 @@ public:
|
||||
|
||||
private:
|
||||
enum Roles {FileNameRole = Qt::UserRole + 1,
|
||||
FilePathRole};
|
||||
FilePathRole,
|
||||
FileDirRole};
|
||||
|
||||
QStringList m_files;
|
||||
QHash<int, QByteArray> m_roleNames;
|
||||
|
@@ -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;
|
||||
|
@@ -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();
|
||||
|
@@ -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;
|
||||
}
|
||||
|
||||
|
Reference in New Issue
Block a user