QmlDesigner: Import a model to the default JSON model group

Task-number: QDS-11312
Change-Id: Ib97273a15db4c7fb46ed01debf99602b71ec7630
Reviewed-by: Mahmoud Badri <mahmoud.badri@qt.io>
This commit is contained in:
Ali Kianian
2023-11-18 01:23:24 +02:00
parent 16e06a0af0
commit 9a55e5c3de
13 changed files with 270 additions and 185 deletions

View File

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

View File

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

View File

@@ -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[^*><?|]*)[^/\\:*><?|]$/
}
PlatformWidgets.FileDialog {
id: fileDialog
onAccepted: fileName.text = fileDialog.file
}
Message {
id: creationFailedDialog
title: qsTr("Could not load the file")
message: qsTr("An error occurred while trying to load the file.")
onClosed: root.reject()
}
contentItem: ColumnLayout {
spacing: 2
Text {
text: qsTr("File name: ")
color: StudioTheme.Values.themeTextColor
}
RowLayout {
spacing: StudioTheme.Values.sectionRowSpacing
Layout.fillWidth: true
StudioControls.TextField {
id: fileName
Layout.alignment: Qt.AlignLeft | Qt.AlignVCenter
Layout.fillWidth: true
actionIndicator.visible: false
translationIndicator.visible: false
validator: fileNameValidator
Keys.onEnterPressed: btnCreate.onClicked()
Keys.onReturnPressed: btnCreate.onClicked()
Keys.onEscapePressed: root.reject()
onTextChanged: root.fileExists = root.backendValue.isJsonFile(fileName.text)
}
HelperWidgets.Button {
id: fileDialogButton
Layout.alignment: Qt.AlignRight | Qt.AlignVCenter
text: qsTr("Open")
onClicked: fileDialog.open()
}
}
Item { // spacer
implicitWidth: 1
implicitHeight: StudioTheme.Values.controlLabelGap
}
Label {
Layout.fillWidth: true
padding: 5
text: qsTr("File name cannot be empty.")
wrapMode: Label.WordWrap
color: StudioTheme.Values.themeTextColor
visible: fileName.text === ""
background: Rectangle {
color: "transparent"
border.width: StudioTheme.Values.border
border.color: StudioTheme.Values.themeWarning
}
}
Item { // spacer
implicitWidth: 1
implicitHeight: StudioTheme.Values.sectionColumnSpacing
}
RowLayout {
spacing: StudioTheme.Values.sectionRowSpacing
Layout.alignment: Qt.AlignRight | Qt.AlignVCenter
HelperWidgets.Button {
id: btnCreate
text: qsTr("Import")
enabled: root.fileExists
onClicked: {
let jsonLoaded = root.backendValue.loadJsonFile(fileName.text)
if (jsonLoaded)
root.accept()
else
creationFailedDialog.open()
}
}
HelperWidgets.Button {
text: qsTr("Cancel")
onClicked: root.reject()
}
}
}
}

View File

@@ -839,6 +839,7 @@ extend_qtc_plugin(QmlDesigner
collectiondetailssortfiltermodel.cpp collectiondetailssortfiltermodel.h
collectioneditorconstants.h
collectioneditorutils.cpp collectioneditorutils.h
collectionimporttools.cpp collectionimporttools.h
collectionlistmodel.cpp collectionlistmodel.h
collectionsourcemodel.cpp collectionsourcemodel.h
collectionview.cpp collectionview.h

View File

@@ -72,6 +72,18 @@ bool variantIslessThan(const QVariant &a, const QVariant &b, CollectionDetails::
return std::visit(LessThanVisitor{}, valueToVariant(a, type), valueToVariant(b, type));
}
SourceFormat getSourceCollectionFormat(const ModelNode &node)
{
using namespace QmlDesigner;
if (node.type() == CollectionEditor::JSONCOLLECTIONMODEL_TYPENAME)
return CollectionEditor::SourceFormat::Json;
if (node.type() == CollectionEditor::CSVCOLLECTIONMODEL_TYPENAME)
return CollectionEditor::SourceFormat::Csv;
return CollectionEditor::SourceFormat::Unknown;
}
QString getSourceCollectionType(const ModelNode &node)
{
using namespace QmlDesigner;

View File

@@ -4,6 +4,7 @@
#pragma once
#include "collectiondetails.h"
#include "collectioneditorconstants.h"
QT_BEGIN_NAMESPACE
class QJsonArray;
@@ -13,6 +14,8 @@ namespace QmlDesigner::CollectionEditor {
bool variantIslessThan(const QVariant &a, const QVariant &b, CollectionDetails::DataType type);
SourceFormat getSourceCollectionFormat(const QmlDesigner::ModelNode &node);
QString getSourceCollectionType(const QmlDesigner::ModelNode &node);
QString getSourceCollectionPath(const QmlDesigner::ModelNode &node);

View File

@@ -0,0 +1,105 @@
// Copyright (C) 2023 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
#include "collectionimporttools.h"
#include <QFile>
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
#include <QJsonParseError>
#include <QJsonValue>
#include <QUrl>
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

View File

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

View File

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

View File

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

View File

@@ -23,6 +23,7 @@
#include <utils/qtcassert.h>
namespace {
inline bool isStudioCollectionModel(const QmlDesigner::ModelNode &node)
{
using namespace QmlDesigner::CollectionEditor;

View File

@@ -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<ModelNode>();
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<ModelNode>();
@@ -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("^(?<mainName>[\\w\\d\\.\\_\\-]+)\\_(?<number>\\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

View File

@@ -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<CollectionView> m_view;
QPointer<CollectionSourceModel> m_sourceModel;
QPointer<CollectionDetailsModel> m_collectionDetailsModel;