diff --git a/share/qtcreator/qmldesigner/collectionEditorQmlSource/CollectionView.qml b/share/qtcreator/qmldesigner/collectionEditorQmlSource/CollectionView.qml index c6873460e4b..363d8bab309 100644 --- a/share/qtcreator/qmldesigner/collectionEditorQmlSource/CollectionView.qml +++ b/share/qtcreator/qmldesigner/collectionEditorQmlSource/CollectionView.qml @@ -24,15 +24,8 @@ Item { warningDialog.open() } - JsonImport { - id: jsonImporter - - backendValue: root.rootView - anchors.centerIn: parent - } - - CsvImport { - id: csvImporter + ImportDialog { + id: importDialog backendValue: root.rootView anchors.centerIn: parent @@ -82,26 +75,14 @@ Item { leftPadding: 15 } - IconTextButton { + HelperWidgets.IconButton { Layout.alignment: Qt.AlignRight | Qt.AlignVCenter icon: StudioTheme.Constants.import_medium - text: qsTr("JSON") - tooltip: qsTr("Import JSON") + tooltip: qsTr("Import a model") radius: StudioTheme.Values.smallRadius - onClicked: jsonImporter.open() - } - - IconTextButton { - Layout.alignment: Qt.AlignRight | Qt.AlignVCenter - - icon: StudioTheme.Constants.import_medium - text: qsTr("CSV") - tooltip: qsTr("Import CSV") - radius: StudioTheme.Values.smallRadius - - onClicked: csvImporter.open() + onClicked: importDialog.open() } } diff --git a/share/qtcreator/qmldesigner/collectionEditorQmlSource/CsvImport.qml b/share/qtcreator/qmldesigner/collectionEditorQmlSource/ImportDialog.qml similarity index 83% rename from share/qtcreator/qmldesigner/collectionEditorQmlSource/CsvImport.qml rename to share/qtcreator/qmldesigner/collectionEditorQmlSource/ImportDialog.qml index 6bcd6e97a3a..bf28695ec9b 100644 --- a/share/qtcreator/qmldesigner/collectionEditorQmlSource/CsvImport.qml +++ b/share/qtcreator/qmldesigner/collectionEditorQmlSource/ImportDialog.qml @@ -13,7 +13,7 @@ import StudioTheme as StudioTheme StudioControls.Dialog { id: root - title: qsTr("Import A CSV File") + title: qsTr("Import a model") anchors.centerIn: parent closePolicy: Popup.CloseOnEscape modal: true @@ -23,8 +23,8 @@ StudioControls.Dialog { property bool fileExists: false onOpened: { - collectionName.text = "Collection_" - fileName.text = qsTr("New CSV File") + collectionName.text = "Model" + fileName.text = qsTr("Model path") fileName.selectAll() fileName.forceActiveFocus() } @@ -40,6 +40,14 @@ StudioControls.Dialog { PlatformWidgets.FileDialog { id: fileDialog + nameFilters : ["All Model Files (*.json *.csv)", + "JSON Files (*.json)", + "Comma-Separated Values (*.csv)"] + + title: qsTr("Select a model file") + fileMode: PlatformWidgets.FileDialog.OpenFile + acceptLabel: qsTr("Open") + onAccepted: fileName.text = fileDialog.file } @@ -61,7 +69,7 @@ StudioControls.Dialog { spacing: 2 Text { - text: qsTr("File name: ") + text: qsTr("File name") color: StudioTheme.Values.themeTextColor } @@ -80,11 +88,11 @@ StudioControls.Dialog { translationIndicator.visible: false validator: fileNameValidator - Keys.onEnterPressed: btnCreate.onClicked() - Keys.onReturnPressed: btnCreate.onClicked() + Keys.onEnterPressed: btnImport.onClicked() + Keys.onReturnPressed: btnImport.onClicked() Keys.onEscapePressed: root.reject() - onTextChanged: root.fileExists = root.backendValue.isCsvFile(fileName.text) + onTextChanged: root.fileExists = root.backendValue.isValidUrlToImport(fileName.text) } HelperWidgets.Button { @@ -100,7 +108,7 @@ StudioControls.Dialog { Spacer {} Text { - text: qsTr("The model name: ") + text: qsTr("The model name") color: StudioTheme.Values.themeTextColor } @@ -115,8 +123,8 @@ StudioControls.Dialog { regularExpression: /^\w+$/ } - Keys.onEnterPressed: btnCreate.onClicked() - Keys.onReturnPressed: btnCreate.onClicked() + Keys.onEnterPressed: btnImport.onClicked() + Keys.onReturnPressed: btnImport.onClicked() Keys.onEscapePressed: root.reject() } @@ -179,15 +187,17 @@ StudioControls.Dialog { Layout.alignment: Qt.AlignRight | Qt.AlignVCenter HelperWidgets.Button { - id: btnCreate + id: btnImport text: qsTr("Import") enabled: root.fileExists && collectionName.text !== "" onClicked: { - let csvLoaded = root.backendValue.loadCsvFile(fileName.text, collectionName.text) + let collectionImported = root.backendValue.importCollectionToDataStore( + collectionName.text, + fileName.text) - if (csvLoaded) + if (collectionImported) root.accept() else creationFailedDialog.open() diff --git a/share/qtcreator/qmldesigner/collectionEditorQmlSource/JsonImport.qml b/share/qtcreator/qmldesigner/collectionEditorQmlSource/JsonImport.qml deleted file mode 100644 index fec1155927b..00000000000 --- a/share/qtcreator/qmldesigner/collectionEditorQmlSource/JsonImport.qml +++ /dev/null @@ -1,145 +0,0 @@ -// Copyright (C) 2023 The Qt Company Ltd. -// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 - -import QtQuick -import QtQuick.Controls -import QtQuick.Layouts -import QtQuickDesignerTheme 1.0 -import Qt.labs.platform as PlatformWidgets -import HelperWidgets 2.0 as HelperWidgets -import StudioControls 1.0 as StudioControls -import StudioTheme as StudioTheme - -StudioControls.Dialog { - id: root - - title: qsTr("Import Models") - anchors.centerIn: parent - closePolicy: Popup.CloseOnEscape - modal: true - - required property var backendValue - property bool fileExists: false - - onOpened: { - fileName.text = qsTr("New JSON File") - fileName.selectAll() - fileName.forceActiveFocus() - } - - onRejected: { - fileName.text = "" - } - - RegularExpressionValidator { - id: fileNameValidator - regularExpression: /^(\w[^*> +#include +#include +#include +#include +#include +#include + +namespace QmlDesigner::CollectionEditor::ImportTools { + +QJsonArray loadAsSingleJsonCollection(const QUrl &url) +{ + QFile file(url.isLocalFile() ? url.toLocalFile() : url.toString()); + QJsonArray collection; + QByteArray jsonData; + if (file.open(QFile::ReadOnly)) + jsonData = file.readAll(); + + file.close(); + if (jsonData.isEmpty()) + return {}; + + QJsonParseError parseError; + QJsonDocument document = QJsonDocument::fromJson(jsonData, &parseError); + if (parseError.error != QJsonParseError::NoError) + return {}; + + auto refineJsonArray = [](const QJsonArray &array) -> QJsonArray { + QJsonArray resultArray; + for (const QJsonValue &collectionData : array) { + if (!collectionData.isObject()) + resultArray.push_back(collectionData); + } + return resultArray; + }; + + if (document.isArray()) { + collection = refineJsonArray(document.array()); + } else if (document.isObject()) { + QJsonObject documentObject = document.object(); + const QStringList mainKeys = documentObject.keys(); + + bool arrayFound = false; + for (const QString &key : mainKeys) { + const QJsonValue &value = documentObject.value(key); + if (value.isArray()) { + arrayFound = true; + collection = refineJsonArray(value.toArray()); + break; + } + } + + if (!arrayFound) { + QJsonObject singleObject; + for (const QString &key : mainKeys) { + const QJsonValue value = documentObject.value(key); + + if (!value.isObject()) + singleObject.insert(key, value); + } + collection.push_back(singleObject); + } + } + return collection; +} + +QJsonArray loadAsCsvCollection(const QUrl &url) +{ + QFile sourceFile(url.isLocalFile() ? url.toLocalFile() : url.toString()); + QStringList headers; + QJsonArray elements; + + if (sourceFile.open(QFile::ReadOnly)) { + QTextStream stream(&sourceFile); + + if (!stream.atEnd()) + headers = stream.readLine().split(','); + + for (QString &header : headers) + header = header.trimmed(); + + if (!headers.isEmpty()) { + while (!stream.atEnd()) { + const QStringList recordDataList = stream.readLine().split(','); + int column = -1; + QJsonObject recordData; + for (const QString &cellData : recordDataList) { + if (++column == headers.size()) + break; + recordData.insert(headers.at(column), cellData); + } + elements.append(recordData); + } + } + } + + return elements; +} + +} // namespace QmlDesigner::CollectionEditor::ImportTools diff --git a/src/plugins/qmldesigner/components/collectioneditor/collectionimporttools.h b/src/plugins/qmldesigner/components/collectioneditor/collectionimporttools.h new file mode 100644 index 00000000000..4ee98284893 --- /dev/null +++ b/src/plugins/qmldesigner/components/collectioneditor/collectionimporttools.h @@ -0,0 +1,16 @@ +// Copyright (C) 2023 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +#pragma once + +QT_BEGIN_NAMESPACE +class QJsonArray; +class QUrl; +QT_END_NAMESPACE + +namespace QmlDesigner::CollectionEditor::ImportTools { + +QJsonArray loadAsSingleJsonCollection(const QUrl &url); +QJsonArray loadAsCsvCollection(const QUrl &url); + +} // namespace QmlDesigner::CollectionEditor::ImportTools diff --git a/src/plugins/qmldesigner/components/collectioneditor/collectionsourcemodel.cpp b/src/plugins/qmldesigner/components/collectioneditor/collectionsourcemodel.cpp index 12f8dd099c9..098bbdaaa70 100644 --- a/src/plugins/qmldesigner/components/collectioneditor/collectionsourcemodel.cpp +++ b/src/plugins/qmldesigner/components/collectioneditor/collectionsourcemodel.cpp @@ -268,6 +268,7 @@ bool CollectionSourceModel::collectionExists(const ModelNode &node, const QStrin bool CollectionSourceModel::addCollectionToSource(const ModelNode &node, const QString &collectionName, + const QJsonArray &newCollectionData, QString *errorString) { auto returnError = [errorString](const QString &msg) -> bool { @@ -284,7 +285,7 @@ bool CollectionSourceModel::addCollectionToSource(const ModelNode &node, return returnError(tr("Node should be a JSON model.")); if (collectionExists(node, collectionName)) - return returnError(tr("Model does not exist.")); + return returnError(tr("A model with the identical name already exists.")); QString sourceFileAddress = CollectionEditor::getSourceCollectionPath(node); @@ -305,7 +306,7 @@ bool CollectionSourceModel::addCollectionToSource(const ModelNode &node, if (document.isObject()) { QJsonObject sourceObject = document.object(); - sourceObject.insert(collectionName, CollectionEditor::defaultCollectionArray()); + sourceObject.insert(collectionName, newCollectionData); document.setObject(sourceObject); if (!jsonFile.resize(0)) return returnError(tr("Can't clean \"%1\".").arg(sourceFileInfo.absoluteFilePath())); diff --git a/src/plugins/qmldesigner/components/collectioneditor/collectionsourcemodel.h b/src/plugins/qmldesigner/components/collectioneditor/collectionsourcemodel.h index 28c36d03e01..f0dd517ee10 100644 --- a/src/plugins/qmldesigner/components/collectioneditor/collectionsourcemodel.h +++ b/src/plugins/qmldesigner/components/collectioneditor/collectionsourcemodel.h @@ -54,6 +54,7 @@ public: bool collectionExists(const ModelNode &node, const QString &collectionName) const; bool addCollectionToSource(const ModelNode &node, const QString &collectionName, + const QJsonArray &newCollectionData, QString *errorString = nullptr); ModelNode sourceNodeAt(int idx); diff --git a/src/plugins/qmldesigner/components/collectioneditor/collectionview.cpp b/src/plugins/qmldesigner/components/collectioneditor/collectionview.cpp index f9354ddfb0f..de82734b353 100644 --- a/src/plugins/qmldesigner/components/collectioneditor/collectionview.cpp +++ b/src/plugins/qmldesigner/components/collectioneditor/collectionview.cpp @@ -23,6 +23,7 @@ #include namespace { + inline bool isStudioCollectionModel(const QmlDesigner::ModelNode &node) { using namespace QmlDesigner::CollectionEditor; diff --git a/src/plugins/qmldesigner/components/collectioneditor/collectionwidget.cpp b/src/plugins/qmldesigner/components/collectioneditor/collectionwidget.cpp index 54eeedf7352..2aae651c4ee 100644 --- a/src/plugins/qmldesigner/components/collectioneditor/collectionwidget.cpp +++ b/src/plugins/qmldesigner/components/collectioneditor/collectionwidget.cpp @@ -6,6 +6,7 @@ #include "collectiondetailsmodel.h" #include "collectiondetailssortfiltermodel.h" #include "collectioneditorutils.h" +#include "collectionimporttools.h" #include "collectionsourcemodel.h" #include "collectionview.h" #include "qmldesignerconstants.h" @@ -188,6 +189,20 @@ bool CollectionWidget::isCsvFile(const QUrl &url) const return file.exists() && file.fileName().endsWith(".csv"); } +bool CollectionWidget::isValidUrlToImport(const QUrl &url) const +{ + using Utils::FilePath; + FilePath fileInfo = FilePath::fromUserInput(url.isLocalFile() ? url.toLocalFile() + : url.toString()); + if (fileInfo.suffix() == "json") + return isJsonFile(url); + + if (fileInfo.suffix() == "csv") + return isCsvFile(url); + + return false; +} + bool CollectionWidget::addCollection(const QString &collectionName, const QString &collectionType, const QUrl &sourceUrl, @@ -243,7 +258,10 @@ bool CollectionWidget::addCollection(const QString &collectionName, } } else if (collectionType == "json") { QString errorMsg; - bool added = m_sourceModel->addCollectionToSource(node, collectionName, &errorMsg); + bool added = m_sourceModel->addCollectionToSource(node, + collectionName, + CollectionEditor::defaultCollectionArray(), + &errorMsg); if (!added) warn(tr("Can not add a model to the JSON file"), errorMsg); return added; @@ -252,6 +270,50 @@ bool CollectionWidget::addCollection(const QString &collectionName, return false; } +bool CollectionWidget::importToJson(const QVariant &sourceNode, + const QString &collectionName, + const QUrl &url) +{ + using CollectionEditor::SourceFormat; + using Utils::FilePath; + const ModelNode node = sourceNode.value(); + const SourceFormat nodeFormat = CollectionEditor::getSourceCollectionFormat(node); + QTC_ASSERT(node.isValid() && nodeFormat == SourceFormat::Json, return false); + + FilePath fileInfo = FilePath::fromUserInput(url.isLocalFile() ? url.toLocalFile() + : url.toString()); + bool added = false; + QString errorMsg; + QJsonArray loadedCollection; + + if (fileInfo.suffix() == "json") + loadedCollection = CollectionEditor::ImportTools::loadAsSingleJsonCollection(url); + else if (fileInfo.suffix() == "csv") + loadedCollection = CollectionEditor::ImportTools::loadAsCsvCollection(url); + + if (!loadedCollection.isEmpty()) { + const QString newCollectionName = generateUniqueCollectionName(node, collectionName); + added = m_sourceModel->addCollectionToSource(node, newCollectionName, loadedCollection, &errorMsg); + } else { + errorMsg = tr("The imported model is empty or is not supported."); + } + + if (!added) + warn(tr("Can not add a model to the JSON file"), errorMsg); + return added; +} + +bool CollectionWidget::importCollectionToDataStore(const QString &collectionName, const QUrl &url) +{ + using Utils::FilePath; + const ModelNode node = dataStoreNode(); + if (node.isValid()) + return importToJson(QVariant::fromValue(node), collectionName, url); + + warn(tr("Can not import to the main model"), tr("The data store is not available.")); + return false; +} + void CollectionWidget::assignSourceNodeToSelectedItem(const QVariant &sourceNode) { ModelNode sourceModel = sourceNode.value(); @@ -268,6 +330,16 @@ void CollectionWidget::assignSourceNodeToSelectedItem(const QVariant &sourceNode CollectionEditor::assignCollectionSourceToNode(m_view, targetNode, sourceModel); } +ModelNode CollectionWidget::dataStoreNode() const +{ + for (int i = 0; i < m_sourceModel->rowCount(); ++i) { + const ModelNode node = m_sourceModel->sourceNodeAt(i); + if (CollectionEditor::getSourceCollectionFormat(node) == CollectionEditor::SourceFormat::Json) + return node; + } + return {}; +} + void CollectionWidget::warn(const QString &title, const QString &body) { QMetaObject::invokeMethod(m_quickWidget->rootObject(), @@ -285,4 +357,20 @@ void CollectionWidget::setTargetNodeSelected(bool selected) emit targetNodeSelectedChanged(m_targetNodeSelected); } +QString CollectionWidget::generateUniqueCollectionName(const ModelNode &node, const QString &name) +{ + if (!m_sourceModel->collectionExists(node, name)) + return name; + + static QRegularExpression reg("^(?[\\w\\d\\.\\_\\-]+)\\_(?\\d+)$"); + QRegularExpressionMatch match = reg.match(name); + if (match.hasMatch()) { + int nextNumber = match.captured("number").toInt() + 1; + return generateUniqueCollectionName( + node, QString("%1_%2").arg(match.captured("mainName")).arg(nextNumber)); + } else { + return generateUniqueCollectionName(node, QString("%1_1").arg(name)); + } +} + } // namespace QmlDesigner diff --git a/src/plugins/qmldesigner/components/collectioneditor/collectionwidget.h b/src/plugins/qmldesigner/components/collectioneditor/collectionwidget.h index 6be4fabd6ac..142fe9fbd51 100644 --- a/src/plugins/qmldesigner/components/collectioneditor/collectionwidget.h +++ b/src/plugins/qmldesigner/components/collectioneditor/collectionwidget.h @@ -38,13 +38,22 @@ public: Q_INVOKABLE bool loadCsvFile(const QUrl &url, const QString &collectionName = {}); Q_INVOKABLE bool isJsonFile(const QUrl &url) const; Q_INVOKABLE bool isCsvFile(const QUrl &url) const; + Q_INVOKABLE bool isValidUrlToImport(const QUrl &url) const; Q_INVOKABLE bool addCollection(const QString &collectionName, const QString &collectionType, const QUrl &sourceUrl, const QVariant &sourceNode); + Q_INVOKABLE bool importToJson(const QVariant &sourceNode, + const QString &collectionName, + const QUrl &url); + + Q_INVOKABLE bool importCollectionToDataStore(const QString &collectionName, const QUrl &url); + Q_INVOKABLE void assignSourceNodeToSelectedItem(const QVariant &sourceNode); + Q_INVOKABLE ModelNode dataStoreNode() const; + void warn(const QString &title, const QString &body); void setTargetNodeSelected(bool selected); @@ -52,6 +61,8 @@ signals: void targetNodeSelectedChanged(bool); private: + QString generateUniqueCollectionName(const ModelNode &node, const QString &name); + QPointer m_view; QPointer m_sourceModel; QPointer m_collectionDetailsModel;