diff --git a/share/qtcreator/qmldesigner/collectionEditorQmlSource/CollectionItem.qml b/share/qtcreator/qmldesigner/collectionEditorQmlSource/CollectionItem.qml new file mode 100644 index 00000000000..5be2dacb8d7 --- /dev/null +++ b/share/qtcreator/qmldesigner/collectionEditorQmlSource/CollectionItem.qml @@ -0,0 +1,288 @@ +// 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 Qt.labs.platform as PlatformWidgets +import HelperWidgets 2.0 as HelperWidgets +import StudioControls 1.0 as StudioControls +import StudioTheme as StudioTheme + +Item { + id: root + + implicitWidth: 300 + implicitHeight: innerRect.height + 6 + + property color textColor + + signal selectItem(int itemIndex) + signal deleteItem() + + Item { + id: boundingRect + + anchors.centerIn: root + width: root.width - 24 + height: nameHolder.height + clip: true + + MouseArea { + id: itemMouse + + anchors.fill: parent + acceptedButtons: Qt.LeftButton + propagateComposedEvents: true + hoverEnabled: true + onClicked: (event) => { + if (!collectionIsSelected) { + collectionIsSelected = true + event.accepted = true + } + } + } + + Rectangle { + id: innerRect + anchors.fill: parent + } + + Row { + width: parent.width - threeDots.width + leftPadding: 20 + + Text { + id: moveTool + + property StudioTheme.ControlStyle style: StudioTheme.Values.viewBarButtonStyle + + width: moveTool.style.squareControlSize.width + height: nameHolder.height + + text: StudioTheme.Constants.dragmarks + font.family: StudioTheme.Constants.iconFont.family + font.pixelSize: moveTool.style.baseIconFontSize + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + } + + Text { + id: nameHolder + + text: collectionName + font.pixelSize: StudioTheme.Values.baseFontSize + color: textColor + leftPadding: 5 + topPadding: 8 + rightPadding: 8 + bottomPadding: 8 + elide: Text.ElideMiddle + verticalAlignment: Text.AlignVCenter + } + } + + Text { + id: threeDots + + text: "..." + font.pixelSize: StudioTheme.Values.baseFontSize + color: textColor + anchors.right: boundingRect.right + anchors.verticalCenter: parent.verticalCenter + rightPadding: 12 + topPadding: nameHolder.topPadding + bottomPadding: nameHolder.bottomPadding + verticalAlignment: Text.AlignVCenter + + MouseArea { + anchors.fill: parent + acceptedButtons: Qt.RightButton + Qt.LeftButton + onClicked: (event) => { + collectionMenu.open() + event.accepted = true + } + } + } + } + + PlatformWidgets.Menu { + id: collectionMenu + + PlatformWidgets.MenuItem { + text: qsTr("Delete") + shortcut: StandardKey.Delete + onTriggered: deleteDialog.open() + } + + PlatformWidgets.MenuItem { + text: qsTr("Rename") + shortcut: StandardKey.Replace + onTriggered: renameDialog.open() + } + } + + StudioControls.Dialog { + id: deleteDialog + + title: qsTr("Deleting whole collection") + + contentItem: Column { + spacing: 2 + + Text { + text: qsTr("Are you sure that you want to delete collection \"" + collectionName + "\"?") + color: StudioTheme.Values.themeTextColor + } + + Item { // spacer + width: 1 + height: 20 + } + + Row { + anchors.right: parent.right + spacing: 10 + + HelperWidgets.Button { + id: btnDelete + anchors.verticalCenter: parent.verticalCenter + + text: qsTr("Delete") + onClicked: root.deleteItem(index) + } + + HelperWidgets.Button { + text: qsTr("Cancel") + anchors.verticalCenter: parent.verticalCenter + onClicked: deleteDialog.reject() + } + } + } + } + + StudioControls.Dialog { + id: renameDialog + + title: qsTr("Rename collection") + + onAccepted: { + if (newNameField.text !== "") + collectionName = newNameField.text + } + + onOpened: { + newNameField.text = collectionName + } + + contentItem: Column { + spacing: 2 + + Text { + text: qsTr("Previous name: " + collectionName) + color: StudioTheme.Values.themeTextColor + } + + Row { + spacing: 10 + Text { + text: qsTr("New name:") + color: StudioTheme.Values.themeTextColor + } + + StudioControls.TextField { + id: newNameField + + anchors.verticalCenter: parent.verticalCenter + actionIndicator.visible: false + translationIndicator.visible: false + validator: newNameValidator + + Keys.onEnterPressed: renameDialog.accept() + Keys.onReturnPressed: renameDialog.accept() + Keys.onEscapePressed: renameDialog.reject() + + onTextChanged: { + btnRename.enabled = newNameField.text !== "" + } + } + } + + Item { // spacer + width: 1 + height: 20 + } + + Row { + anchors.right: parent.right + spacing: 10 + + HelperWidgets.Button { + id: btnRename + + anchors.verticalCenter: parent.verticalCenter + text: qsTr("Rename") + onClicked: renameDialog.accept() + } + + HelperWidgets.Button { + text: qsTr("Cancel") + anchors.verticalCenter: parent.verticalCenter + onClicked: renameDialog.reject() + } + } + } + } + + HelperWidgets.RegExpValidator { + id: newNameValidator + regExp: /^\w+$/ + } + + states: [ + State { + name: "default" + when: !collectionIsSelected && !itemMouse.containsMouse + + PropertyChanges { + target: innerRect + opacity: 0.6 + color: StudioTheme.Values.themeControlBackground + } + + PropertyChanges { + target: root + textColor: StudioTheme.Values.themeTextColor + } + }, + State { + name: "hovered" + when: !collectionIsSelected && itemMouse.containsMouse + + PropertyChanges { + target: innerRect + opacity: 0.8 + color: StudioTheme.Values.themeControlBackgroundHover + } + + PropertyChanges { + target: root + textColor: StudioTheme.Values.themeTextColor + } + }, + State { + name: "selected" + when: collectionIsSelected + + PropertyChanges { + target: innerRect + opacity: 1 + color: StudioTheme.Values.themeControlBackgroundInteraction + } + + PropertyChanges { + target: root + textColor: StudioTheme.Values.themeIconColorSelected + } + } + ] +} diff --git a/share/qtcreator/qmldesigner/collectionEditorQmlSource/CollectionView.qml b/share/qtcreator/qmldesigner/collectionEditorQmlSource/CollectionView.qml new file mode 100644 index 00000000000..17ac70167d2 --- /dev/null +++ b/share/qtcreator/qmldesigner/collectionEditorQmlSource/CollectionView.qml @@ -0,0 +1,156 @@ +// 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 QtQuickDesignerTheme 1.0 +import HelperWidgets 2.0 as HelperWidgets +import StudioTheme 1.0 as StudioTheme +import CollectionEditorBackend + +Item { + id: root + focus: true + + property var rootView: CollectionEditorBackend.rootView + property var model: CollectionEditorBackend.model + + function showWarning(title, message) { + warningDialog.title = title + warningDialog.message = message + warningDialog.open() + } + + JsonImport { + id: jsonImporter + + backendValue: root.rootView + anchors.centerIn: parent + } + + CsvImport { + id: csvImporter + + backendValue: root.rootView + anchors.centerIn: parent + } + + NewCollectionDialog { + id: newCollection + + backendValue: root.rootView + anchors.centerIn: parent + } + + Message { + id: warningDialog + + title: "" + message: "" + } + + Rectangle { + id: collectionsRect + + color: StudioTheme.Values.themeToolbarBackground + width: 300 + height: root.height + + Column { + width: parent.width + + Rectangle { + height: StudioTheme.Values.height + 5 + color: StudioTheme.Values.themeToolbarBackground + width: parent.width + + Text { + id: collectionText + + anchors.verticalCenter: parent.verticalCenter + text: qsTr("Collections") + font.pixelSize: StudioTheme.Values.mediumIconFont + color: StudioTheme.Values.themeTextColor + leftPadding: 15 + } + + Row { + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + rightPadding: 12 + spacing: 2 + + HelperWidgets.IconButton { + icon: StudioTheme.Constants.translationImport + tooltip: qsTr("Import Json") + + onClicked: jsonImporter.open() + } + + HelperWidgets.IconButton { + icon: StudioTheme.Constants.translationImport + tooltip: qsTr("Import CSV") + + onClicked: csvImporter.open() + } + } + } + + Rectangle { // Collections + width: parent.width + color: StudioTheme.Values.themeBackgroundColorNormal + height: 330 + + MouseArea { + anchors.fill: parent + propagateComposedEvents: true + onClicked: (event) => { + root.model.deselect() + event.accepted = true + } + } + + ListView { + id: collectionListView + + width: parent.width + height: contentHeight + model: root.model + + delegate: CollectionItem { + onDeleteItem: root.model.removeRow(index) + } + + } + } + + Rectangle { + width: parent.width + height: addCollectionButton.height + color: StudioTheme.Values.themeBackgroundColorNormal + + IconTextButton { + id: addCollectionButton + + anchors.centerIn: parent + text: qsTr("Add new collection") + icon: StudioTheme.Constants.create_medium + onClicked: newCollection.open() + } + } + } + } + + Rectangle { + id: collectionRect + + color: StudioTheme.Values.themeBackgroundColorAlternate + + anchors { + left: collectionsRect.right + right: parent.right + top: parent.top + bottom: parent.bottom + } + } +} diff --git a/share/qtcreator/qmldesigner/collectionEditorQmlSource/CsvImport.qml b/share/qtcreator/qmldesigner/collectionEditorQmlSource/CsvImport.qml new file mode 100644 index 00000000000..5323d6759e9 --- /dev/null +++ b/share/qtcreator/qmldesigner/collectionEditorQmlSource/CsvImport.qml @@ -0,0 +1,188 @@ +// 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 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 A CSV File") + anchors.centerIn: parent + closePolicy: Popup.CloseOnEscape + modal: true + + required property var backendValue + + property bool fileExists: false + + onOpened: { + collectionName.text = "Collection_" + fileName.text = qsTr("New CSV File") + fileName.selectAll() + fileName.forceActiveFocus() + } + + onRejected: { + fileName.text = "" + } + + HelperWidgets.RegExpValidator { + id: fileNameValidator + regExp: /^(\w[^*> + +namespace QmlDesigner { +CollectionModel::CollectionModel() {} + +int CollectionModel::rowCount(const QModelIndex &) const +{ + return m_collections.size(); +} + +QVariant CollectionModel::data(const QModelIndex &index, int role) const +{ + QTC_ASSERT(index.isValid(), return {}); + + const ModelNode *collection = &m_collections.at(index.row()); + + switch (role) { + case IdRole: + return collection->id(); + case NameRole: + return collection->variantProperty("objectName").value(); + case SelectedRole: + return index.row() == m_selectedIndex; + } + + return {}; +} + +bool CollectionModel::setData(const QModelIndex &index, const QVariant &value, int role) +{ + if (!index.isValid()) + return false; + + ModelNode collection = m_collections.at(index.row()); + switch (role) { + case IdRole: { + if (collection.id() == value) + return false; + + bool duplicatedId = Utils::anyOf(std::as_const(m_collections), + [&collection, &value](const ModelNode &otherCollection) { + return (otherCollection.id() == value + && otherCollection != collection); + }); + if (duplicatedId) + return false; + + collection.setIdWithRefactoring(value.toString()); + } break; + case Qt::DisplayRole: + case NameRole: { + auto collectionName = collection.variantProperty("objectName"); + if (collectionName.value() == value) + return false; + + collectionName.setValue(value.toString()); + } break; + case SelectedRole: { + if (value.toBool() != index.data(SelectedRole).toBool()) + setSelectedIndex(value.toBool() ? index.row() : -1); + else + return false; + } break; + default: + return false; + } + + return true; +} + +bool CollectionModel::removeRows(int row, int count, [[maybe_unused]] const QModelIndex &parent) +{ + const int rowMax = std::min(row + count, rowCount()); + + if (row >= rowMax || row < 0) + return false; + + AbstractView *view = m_collections.at(row).view(); + if (!view) + return false; + + count = rowMax - row; + + bool selectionUpdateNeeded = m_selectedIndex >= row && m_selectedIndex < rowMax; + + // It's better to remove the group of nodes here because of the performance issue for the list, + // and update issue for the view + beginRemoveRows({}, row, rowMax - 1); + + view->executeInTransaction(Q_FUNC_INFO, [row, count, this]() { + for (ModelNode node : Utils::span(m_collections).subspan(row, count)) { + m_collectionsIndexHash.remove(node.internalId()); + node.destroy(); + } + }); + + m_collections.remove(row, count); + + int idx = row; + for (const ModelNode &node : Utils::span(m_collections).subspan(row)) + m_collectionsIndexHash.insert(node.internalId(), ++idx); + + endRemoveRows(); + + if (selectionUpdateNeeded) + updateSelectedCollection(); + + updateEmpty(); + return true; +} + +QHash CollectionModel::roleNames() const +{ + static QHash roles; + if (roles.isEmpty()) { + roles.insert(Super::roleNames()); + roles.insert({ + {IdRole, "collectionId"}, + {NameRole, "collectionName"}, + {SelectedRole, "collectionIsSelected"}, + }); + } + return roles; +} + +void CollectionModel::setCollections(const ModelNodes &collections) +{ + beginResetModel(); + bool wasEmpty = isEmpty(); + m_collections = collections; + m_collectionsIndexHash.clear(); + int i = 0; + for (const ModelNode &collection : collections) + m_collectionsIndexHash.insert(collection.internalId(), i++); + + if (wasEmpty != isEmpty()) + emit isEmptyChanged(isEmpty()); + + endResetModel(); + + updateSelectedCollection(true); +} + +void CollectionModel::removeCollection(const ModelNode &node) +{ + int nodePlace = m_collectionsIndexHash.value(node.internalId(), -1); + if (nodePlace < 0) + return; + + removeRow(nodePlace); +} + +int CollectionModel::collectionIndex(const ModelNode &node) const +{ + return m_collectionsIndexHash.value(node.internalId(), -1); +} + +void CollectionModel::selectCollection(const ModelNode &node) +{ + int nodePlace = m_collectionsIndexHash.value(node.internalId(), -1); + if (nodePlace < 0) + return; + + selectCollectionIndex(nodePlace, true); +} + +bool CollectionModel::isEmpty() const +{ + return m_collections.isEmpty(); +} + +void CollectionModel::selectCollectionIndex(int idx, bool selectAtLeastOne) +{ + int collectionCount = m_collections.size(); + int prefferedIndex = -1; + if (collectionCount) { + if (selectAtLeastOne) + prefferedIndex = std::max(0, std::min(idx, collectionCount - 1)); + else if (idx > -1 && idx < collectionCount) + prefferedIndex = idx; + } + + setSelectedIndex(prefferedIndex); +} + +void CollectionModel::deselect() +{ + setSelectedIndex(-1); +} + +void CollectionModel::updateSelectedCollection(bool selectAtLeastOne) +{ + int idx = m_selectedIndex; + m_selectedIndex = -1; + selectCollectionIndex(idx, selectAtLeastOne); +} + +void CollectionModel::updateNodeName(const ModelNode &node) +{ + QModelIndex index = indexOfNode(node); + emit dataChanged(index, index, {NameRole, Qt::DisplayRole}); +} + +void CollectionModel::updateNodeId(const ModelNode &node) +{ + QModelIndex index = indexOfNode(node); + emit dataChanged(index, index, {IdRole}); +} + +void CollectionModel::setSelectedIndex(int idx) +{ + idx = (idx > -1 && idx < m_collections.count()) ? idx : -1; + + if (m_selectedIndex != idx) { + QModelIndex previousIndex = index(m_selectedIndex); + QModelIndex newIndex = index(idx); + + m_selectedIndex = idx; + + if (previousIndex.isValid()) + emit dataChanged(previousIndex, previousIndex, {SelectedRole}); + + if (newIndex.isValid()) + emit dataChanged(newIndex, newIndex, {SelectedRole}); + + emit selectedIndexChanged(idx); + } +} + +void CollectionModel::updateEmpty() +{ + bool isEmptyNow = isEmpty(); + if (m_isEmpty != isEmptyNow) { + m_isEmpty = isEmptyNow; + emit isEmptyChanged(m_isEmpty); + + if (m_isEmpty) + setSelectedIndex(-1); + } +} + +QModelIndex CollectionModel::indexOfNode(const ModelNode &node) const +{ + return index(m_collectionsIndexHash.value(node.internalId(), -1)); +} +} // namespace QmlDesigner diff --git a/src/plugins/qmldesigner/components/collectioneditor/collectionmodel.h b/src/plugins/qmldesigner/components/collectioneditor/collectionmodel.h new file mode 100644 index 00000000000..f3a1e7de786 --- /dev/null +++ b/src/plugins/qmldesigner/components/collectioneditor/collectionmodel.h @@ -0,0 +1,70 @@ +// Copyright (C) 2023 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0+ OR GPL-3.0 WITH Qt-GPL-exception-1.0 +#pragma once + +#include "modelnode.h" + +#include +#include + +QT_BEGIN_NAMESPACE +class QJsonArray; +QT_END_NAMESPACE + +namespace QmlDesigner { + +class CollectionModel : public QAbstractListModel +{ + Q_OBJECT + + Q_PROPERTY(int selectedIndex MEMBER m_selectedIndex NOTIFY selectedIndexChanged) + +public: + enum Roles { IdRole = Qt::UserRole + 1, NameRole, SelectedRole }; + + explicit CollectionModel(); + + virtual int rowCount(const QModelIndex &parent = QModelIndex()) const override; + virtual QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; + virtual bool setData(const QModelIndex &index, + const QVariant &value, + int role = Qt::EditRole) override; + + Q_INVOKABLE virtual bool removeRows(int row, + int count = 1, + const QModelIndex &parent = QModelIndex()) override; + + virtual QHash roleNames() const override; + + void setCollections(const ModelNodes &collections); + void removeCollection(const ModelNode &node); + int collectionIndex(const ModelNode &node) const; + void selectCollection(const ModelNode &node); + + Q_INVOKABLE bool isEmpty() const; + Q_INVOKABLE void selectCollectionIndex(int idx, bool selectAtLeastOne = false); + Q_INVOKABLE void deselect(); + Q_INVOKABLE void updateSelectedCollection(bool selectAtLeastOne = false); + void updateNodeName(const ModelNode &node); + void updateNodeId(const ModelNode &node); + +signals: + void selectedIndexChanged(int idx); + void renameCollectionTriggered(const QmlDesigner::ModelNode &collection, const QString &newName); + void addNewCollectionTriggered(); + void isEmptyChanged(bool); + +private: + void setSelectedIndex(int idx); + void updateEmpty(); + + using Super = QAbstractListModel; + + QModelIndex indexOfNode(const ModelNode &node) const; + ModelNodes m_collections; + QHash m_collectionsIndexHash; // internalId -> index + int m_selectedIndex = -1; + bool m_isEmpty = true; +}; + +} // namespace QmlDesigner diff --git a/src/plugins/qmldesigner/components/collectioneditor/collectionview.cpp b/src/plugins/qmldesigner/components/collectioneditor/collectionview.cpp new file mode 100644 index 00000000000..e967e850d19 --- /dev/null +++ b/src/plugins/qmldesigner/components/collectioneditor/collectionview.cpp @@ -0,0 +1,598 @@ +// 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 "collectionview.h" +#include "collectionmodel.h" +#include "collectionwidget.h" +#include "designmodecontext.h" +#include "nodelistproperty.h" +#include "nodemetainfo.h" +#include "qmldesignerconstants.h" +#include "qmldesignerplugin.h" +#include "variantproperty.h" + +#include +#include +#include + +#include +#include +#include + +namespace { +using Data = std::variant; +using DataRecord = QMap; + +struct DataHeader +{ + enum class Type { Unknown, Bool, Numeric, String, DateTime }; + Type type; + QString name; +}; + +using DataHeaderMap = QMap; // Lowercase Name - Header Data + +inline constexpr QStringView BoolDataType{u"Bool"}; +inline constexpr QStringView NumberDataType{u"Number"}; +inline constexpr QStringView StringDataType{u"String"}; +inline constexpr QStringView DateTimeDataType{u"Date/Time"}; + +QString removeSpaces(QString string) +{ + string.replace(" ", "_"); + string.replace("-", "_"); + return string; +} + +DataHeader getDataType(const QString &type, const QString &name) +{ + static const QMap typeMap = { + {BoolDataType.toString().toLower(), DataHeader::Type::Bool}, + {NumberDataType.toString().toLower(), DataHeader::Type::Numeric}, + {StringDataType.toString().toLower(), DataHeader::Type::String}, + {DateTimeDataType.toString().toLower(), DataHeader::Type::DateTime}}; + if (name.isEmpty()) + return {}; + + if (type.isEmpty()) + return {DataHeader::Type::String, removeSpaces(name)}; + + return {typeMap.value(type.toLower(), DataHeader::Type::Unknown), removeSpaces(name)}; +} + +struct JsonDocumentError : public std::exception +{ + enum Error { + InvalidDocumentType, + InvalidCollectionName, + InvalidCollectionId, + InvalidCollectionObject, + InvalidArrayPosition, + InvalidLiteralType, + InvalidCollectionHeader, + IsNotJsonArray, + CollectionHeaderNotFound + }; + + const Error error; + + JsonDocumentError(Error error) + : error(error) + {} + + const char *what() const noexcept override + { + switch (error) { + case InvalidDocumentType: + return "Current JSON document contains errors."; + case InvalidCollectionName: + return "Invalid collection name."; + case InvalidCollectionId: + return "Invalid collection Id."; + case InvalidCollectionObject: + return "A collection should be a json object."; + case InvalidArrayPosition: + return "Arrays are not supported inside the collection."; + case InvalidLiteralType: + return "Invalid literal type for collection items"; + case InvalidCollectionHeader: + return "Invalid Collection Header"; + case IsNotJsonArray: + return "Json file should be an array"; + case CollectionHeaderNotFound: + return "Collection Header not found"; + default: + return "Unknown Json Error"; + } + } +}; + +struct CsvDocumentError : public std::exception +{ + enum Error { + HeaderNotFound, + DataNotFound, + }; + + const Error error; + + CsvDocumentError(Error error) + : error(error) + {} + + const char *what() const noexcept override + { + switch (error) { + case HeaderNotFound: + return "CSV Header not found"; + case DataNotFound: + return "CSV data not found"; + default: + return "Unknown CSV Error"; + } + } +}; + +Data getLiteralDataValue(const QVariant &value, const DataHeader &header, bool *typeWarningCheck = nullptr) +{ + if (header.type == DataHeader::Type::Bool) + return value.toBool(); + + if (header.type == DataHeader::Type::Numeric) + return value.toDouble(); + + if (header.type == DataHeader::Type::String) + return value.toString(); + + if (header.type == DataHeader::Type::DateTime) { + QDateTime dateTimeStr = QDateTime::fromString(value.toString()); + if (dateTimeStr.isValid()) + return dateTimeStr; + } + + if (typeWarningCheck) + *typeWarningCheck = true; + + return value.toString(); +} + +void loadJsonHeaders(QList &collectionHeaders, + DataHeaderMap &headerDataMap, + const QJsonObject &collectionJsonObject) +{ + const QJsonArray collectionHeader = collectionJsonObject.value("headers").toArray(); + for (const QJsonValue &headerValue : collectionHeader) { + const QJsonObject headerJsonObject = headerValue.toObject(); + DataHeader dataHeader = getDataType(headerJsonObject.value("type").toString(), + headerJsonObject.value("name").toString()); + + if (dataHeader.type == DataHeader::Type::Unknown) + throw JsonDocumentError{JsonDocumentError::InvalidCollectionHeader}; + + collectionHeaders.append(dataHeader); + headerDataMap.insert(dataHeader.name.toLower(), dataHeader); + } + + if (collectionHeaders.isEmpty()) + throw JsonDocumentError{JsonDocumentError::CollectionHeaderNotFound}; +} + +void loadJsonRecords(QList &collectionItems, + DataHeaderMap &headerDataMap, + const QJsonObject &collectionJsonObject) +{ + auto addItemFromValue = [&headerDataMap, &collectionItems](const QJsonValue &jsonValue) { + const QVariantMap dataMap = jsonValue.toObject().toVariantMap(); + DataRecord recordData; + for (const auto &dataPair : dataMap.asKeyValueRange()) { + const DataHeader correspondingHeader = headerDataMap.value(removeSpaces( + dataPair.first.toLower()), + {}); + + const QString &fieldName = correspondingHeader.name; + if (fieldName.size()) + recordData.insert(fieldName, + getLiteralDataValue(dataPair.second, correspondingHeader)); + } + if (!recordData.isEmpty()) + collectionItems.append(recordData); + }; + + const QJsonValue jsonDataValue = collectionJsonObject.value("data"); + if (jsonDataValue.isObject()) { + addItemFromValue(jsonDataValue); + } else if (jsonDataValue.isArray()) { + const QJsonArray jsonDataArray = jsonDataValue.toArray(); + for (const QJsonValue &jsonItem : jsonDataArray) { + if (jsonItem.isObject()) + addItemFromValue(jsonItem); + } + } +} + +inline bool isCollectionLib(const QmlDesigner::ModelNode &node) +{ + return node.parentProperty().parentModelNode().isRootNode() + && node.id() == QmlDesigner::Constants::COLLECTION_LIB_ID; +} + +inline bool isListModel(const QmlDesigner::ModelNode &node) +{ + return node.metaInfo().isQtQuickListModel(); +} + +inline bool isListElement(const QmlDesigner::ModelNode &node) +{ + return node.metaInfo().isQtQuickListElement(); +} + +inline bool isCollection(const QmlDesigner::ModelNode &node) +{ + return isCollectionLib(node.parentProperty().parentModelNode()) && isListModel(node); +} + +inline bool isCollectionElement(const QmlDesigner::ModelNode &node) +{ + return isListElement(node) && isCollection(node.parentProperty().parentModelNode()); +} + +} // namespace + +namespace QmlDesigner { + +struct Collection +{ + QString name; + QString id; + QList headers; + QList items; +}; + +CollectionView::CollectionView(ExternalDependenciesInterface &externalDependencies) + : AbstractView(externalDependencies) +{} + +bool CollectionView::loadJson(const QByteArray &data) +{ + try { + QJsonParseError parseError; + QJsonDocument document = QJsonDocument::fromJson(data, &parseError); + if (parseError.error != QJsonParseError::NoError) + throw JsonDocumentError{JsonDocumentError::InvalidDocumentType}; + + QList collections; + if (document.isArray()) { + const QJsonArray collectionsJsonArray = document.array(); + + for (const QJsonValue &collectionJson : collectionsJsonArray) { + Collection collection; + if (!collectionJson.isObject()) + throw JsonDocumentError{JsonDocumentError::InvalidCollectionObject}; + + QJsonObject collectionJsonObject = collectionJson.toObject(); + + const QString &collectionName = collectionJsonObject.value(u"name").toString(); + if (!collectionName.size()) + throw JsonDocumentError{JsonDocumentError::InvalidCollectionName}; + + const QString &collectionId = collectionJsonObject.value(u"id").toString(); + if (!collectionId.size()) + throw JsonDocumentError{JsonDocumentError::InvalidCollectionId}; + + DataHeaderMap headerDataMap; + + loadJsonHeaders(collection.headers, headerDataMap, collectionJsonObject); + loadJsonRecords(collection.items, headerDataMap, collectionJsonObject); + + if (collection.items.count()) + collections.append(collection); + } + } else { + throw JsonDocumentError{JsonDocumentError::InvalidDocumentType}; + } + + addLoadedModel(collections); + } catch (const std::exception &error) { + m_widget->warn("Json Import Problem", QString::fromLatin1(error.what())); + return false; + } + + return true; +} + +bool CollectionView::loadCsv(const QString &collectionName, const QByteArray &data) +{ + QTextStream stream(data); + Collection collection; + collection.name = collectionName; + + try { + if (!stream.atEnd()) { + const QStringList recordData = stream.readLine().split(','); + for (const QString &name : recordData) + collection.headers.append(getDataType({}, name)); + } + if (collection.headers.isEmpty()) + throw CsvDocumentError{CsvDocumentError::HeaderNotFound}; + + while (!stream.atEnd()) { + const QStringList recordDataList = stream.readLine().split(','); + DataRecord recordData; + int column = -1; + for (const QString &cellData : recordDataList) { + if (++column == collection.headers.size()) + break; + recordData.insert(collection.headers.at(column).name, cellData); + } + if (recordData.count()) + collection.items.append(recordData); + } + + if (collection.items.isEmpty()) + throw CsvDocumentError{CsvDocumentError::DataNotFound}; + + addLoadedModel({collection}); + } catch (const std::exception &error) { + m_widget->warn("Json Import Problem", QString::fromLatin1(error.what())); + return false; + } + + return true; +} + +bool CollectionView::hasWidget() const +{ + return true; +} + +QmlDesigner::WidgetInfo CollectionView::widgetInfo() +{ + if (m_widget.isNull()) { + m_widget = new CollectionWidget(this); + + auto collectionEditorContext = new Internal::CollectionEditorContext(m_widget.data()); + Core::ICore::addContextObject(collectionEditorContext); + } + + return createWidgetInfo(m_widget.data(), + "CollectionEditor", + WidgetInfo::LeftPane, + 0, + tr("Collection Editor"), + tr("Collection Editor view")); +} + +void CollectionView::modelAttached(Model *model) +{ + AbstractView::modelAttached(model); + refreshModel(); +} + +void CollectionView::nodeReparented(const ModelNode &node, + const NodeAbstractProperty &newPropertyParent, + const NodeAbstractProperty &oldPropertyParent, + [[maybe_unused]] PropertyChangeFlags propertyChange) +{ + if (!isListModel(node)) + return; + + ModelNode newParentNode = newPropertyParent.parentModelNode(); + ModelNode oldParentNode = oldPropertyParent.parentModelNode(); + bool added = isCollectionLib(newParentNode); + bool removed = isCollectionLib(oldParentNode); + + if (!added && !removed) + return; + + refreshModel(); + + if (isCollection(node)) + m_widget->collectionModel()->selectCollection(node); +} + +void CollectionView::nodeAboutToBeRemoved(const ModelNode &removedNode) +{ + // removing the collections lib node + if (isCollectionLib(removedNode)) { + m_widget->collectionModel()->setCollections({}); + return; + } + + if (isCollection(removedNode)) + m_widget->collectionModel()->removeCollection(removedNode); +} + +void CollectionView::nodeRemoved([[maybe_unused]] const ModelNode &removedNode, + const NodeAbstractProperty &parentProperty, + [[maybe_unused]] PropertyChangeFlags propertyChange) +{ + if (parentProperty.parentModelNode().id() != Constants::COLLECTION_LIB_ID) + return; + + m_widget->collectionModel()->updateSelectedCollection(true); +} + +void CollectionView::variantPropertiesChanged(const QList &propertyList, + [[maybe_unused]] PropertyChangeFlags propertyChange) +{ + for (const VariantProperty &property : propertyList) { + ModelNode node(property.parentModelNode()); + if (isCollection(node)) { + if (property.name() == "objectName") + m_widget->collectionModel()->updateNodeName(node); + else if (property.name() == "id") + m_widget->collectionModel()->updateNodeId(node); + } + } +} + +void CollectionView::selectedNodesChanged(const QList &selectedNodeList, + [[maybe_unused]] const QList &lastSelectedNodeList) +{ + QList selectedCollections = Utils::filtered(selectedNodeList, &isCollection); + + // More than one collections are selected. So ignore them + if (selectedCollections.size() > 1) + return; + + if (selectedCollections.size() == 1) { // If exactly one collection is selected + m_widget->collectionModel()->selectCollection(selectedCollections.first()); + return; + } + + // If no collection is selected, check the elements + QList selectedElements = Utils::filtered(selectedNodeList, &isCollectionElement); + if (selectedElements.size()) { + const ModelNode parentElement = selectedElements.first().parentProperty().parentModelNode(); + bool haveSameParent = Utils::allOf(selectedElements, [&parentElement](const ModelNode &element) { + return element.parentProperty().parentModelNode() == parentElement; + }); + if (haveSameParent) + m_widget->collectionModel()->selectCollection(parentElement); + } +} + +void CollectionView::addNewCollection(const QString &name) +{ + executeInTransaction(__FUNCTION__, [&] { + ensureCollectionLibraryNode(); + ModelNode collectionLib = collectionLibraryNode(); + if (!collectionLib.isValid()) + return; + + NodeMetaInfo listModelMetaInfo = model()->qtQmlModelsListModelMetaInfo(); + ModelNode collectionNode = createModelNode(listModelMetaInfo.typeName(), + listModelMetaInfo.majorVersion(), + listModelMetaInfo.minorVersion()); + QString collectionName = name.isEmpty() ? "Collection" : name; + renameCollection(collectionNode, collectionName); + + QmlDesignerPlugin::emitUsageStatistics(Constants::EVENT_PROPERTY_ADDED); + + auto headersProperty = collectionNode.variantProperty("headers"); + headersProperty.setDynamicTypeNameAndValue("string", {}); + + collectionLib.defaultNodeListProperty().reparentHere(collectionNode); + }); +} + +void CollectionView::refreshModel() +{ + if (!model()) + return; + + ModelNode collectionLib = modelNodeForId(Constants::COLLECTION_LIB_ID); + ModelNodes collections; + + if (collectionLib.isValid()) { + const QList collectionLibNodes = collectionLib.directSubModelNodes(); + for (const ModelNode &node : collectionLibNodes) { + if (isCollection(node)) + collections.append(node); + } + } + + m_widget->collectionModel()->setCollections(collections); +} + +ModelNode CollectionView::getNewCollectionNode(const Collection &collection) +{ + QTC_ASSERT(model(), return {}); + ModelNode collectionNode; + executeInTransaction(__FUNCTION__, [&] { + NodeMetaInfo listModelMetaInfo = model()->qtQmlModelsListModelMetaInfo(); + collectionNode = createModelNode(listModelMetaInfo.typeName(), + listModelMetaInfo.majorVersion(), + listModelMetaInfo.minorVersion()); + QString collectionName = collection.name.isEmpty() ? "Collection" : collection.name; + renameCollection(collectionNode, collectionName); + QStringList headers; + for (const DataHeader &header : collection.headers) + headers.append(header.name); + + QmlDesignerPlugin::emitUsageStatistics(Constants::EVENT_PROPERTY_ADDED); + + auto headersProperty = collectionNode.variantProperty("headers"); + headersProperty.setDynamicTypeNameAndValue("string", headers.join(",")); + + NodeMetaInfo listElementMetaInfo = model()->qtQmlModelsListElementMetaInfo(); + for (const DataRecord &item : collection.items) { + ModelNode elementNode = createModelNode(listElementMetaInfo.typeName(), + listElementMetaInfo.majorVersion(), + listElementMetaInfo.minorVersion()); + for (const auto &headerMapElement : item.asKeyValueRange()) { + auto property = elementNode.variantProperty(headerMapElement.first.toLatin1()); + QVariant value = std::visit([](const auto &data) + -> QVariant { return QVariant::fromValue(data); }, + headerMapElement.second); + property.setValue(value); + } + collectionNode.defaultNodeListProperty().reparentHere(elementNode); + } + }); + return collectionNode; +} + +void CollectionView::addLoadedModel(const QList &newCollection) +{ + executeInTransaction(__FUNCTION__, [&] { + ensureCollectionLibraryNode(); + ModelNode collectionLib = collectionLibraryNode(); + if (!collectionLib.isValid()) + return; + + for (const Collection &collection : newCollection) { + ModelNode collectionNode = getNewCollectionNode(collection); + collectionLib.defaultNodeListProperty().reparentHere(collectionNode); + } + }); +} + +void CollectionView::renameCollection(ModelNode &collection, const QString &newName) +{ + QTC_ASSERT(collection.isValid(), return); + + QVariant objName = collection.variantProperty("objectName").value(); + if (objName.isValid() && objName.toString() == newName) + return; + + executeInTransaction(__FUNCTION__, [&] { + collection.setIdWithRefactoring(model()->generateIdFromName(newName, "collection")); + + VariantProperty objNameProp = collection.variantProperty("objectName"); + objNameProp.setValue(newName); + }); +} + +void CollectionView::ensureCollectionLibraryNode() +{ + ModelNode collectionLib = modelNodeForId(Constants::COLLECTION_LIB_ID); + if (collectionLib.isValid() + || (!rootModelNode().metaInfo().isQtQuick3DNode() + && !rootModelNode().metaInfo().isQtQuickItem())) { + return; + } + + executeInTransaction(__FUNCTION__, [&] { + // Create collection library node +#ifdef QDS_USE_PROJECTSTORAGE + TypeName nodeTypeName = rootModelNode().metaInfo().isQtQuick3DNode() ? "Node" : "Item"; + matLib = createModelNode(nodeTypeName, -1, -1); +#else + auto nodeType = rootModelNode().metaInfo().isQtQuick3DNode() + ? model()->qtQuick3DNodeMetaInfo() + : model()->qtQuickItemMetaInfo(); + collectionLib = createModelNode(nodeType.typeName(), + nodeType.majorVersion(), + nodeType.minorVersion()); +#endif + collectionLib.setIdWithoutRefactoring(Constants::COLLECTION_LIB_ID); + rootModelNode().defaultNodeListProperty().reparentHere(collectionLib); + }); +} + +ModelNode CollectionView::collectionLibraryNode() +{ + return modelNodeForId(Constants::COLLECTION_LIB_ID); +} +} // namespace QmlDesigner diff --git a/src/plugins/qmldesigner/components/collectioneditor/collectionview.h b/src/plugins/qmldesigner/components/collectioneditor/collectionview.h new file mode 100644 index 00000000000..9372c74150f --- /dev/null +++ b/src/plugins/qmldesigner/components/collectioneditor/collectionview.h @@ -0,0 +1,59 @@ +// 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 + +#include "abstractview.h" +#include "modelnode.h" + +#include + +namespace QmlDesigner { + +struct Collection; +class CollectionWidget; + +class CollectionView : public AbstractView +{ + Q_OBJECT + +public: + explicit CollectionView(ExternalDependenciesInterface &externalDependencies); + + bool loadJson(const QByteArray &data); + bool loadCsv(const QString &collectionName, const QByteArray &data); + + bool hasWidget() const override; + WidgetInfo widgetInfo() override; + + void modelAttached(Model *model) override; + + void nodeReparented(const ModelNode &node, + const NodeAbstractProperty &newPropertyParent, + const NodeAbstractProperty &oldPropertyParent, + PropertyChangeFlags propertyChange) override; + + void nodeAboutToBeRemoved(const ModelNode &removedNode) override; + + void nodeRemoved(const ModelNode &removedNode, + const NodeAbstractProperty &parentProperty, + PropertyChangeFlags propertyChange) override; + + void variantPropertiesChanged(const QList &propertyList, + PropertyChangeFlags propertyChange) override; + + void selectedNodesChanged(const QList &selectedNodeList, + const QList &lastSelectedNodeList) override; + + void addNewCollection(const QString &name); + +private: + void refreshModel(); + ModelNode getNewCollectionNode(const Collection &collection); + void addLoadedModel(const QList &newCollection); + void renameCollection(ModelNode &material, const QString &newName); + void ensureCollectionLibraryNode(); + ModelNode collectionLibraryNode(); + + QPointer m_widget; +}; +} // namespace QmlDesigner diff --git a/src/plugins/qmldesigner/components/collectioneditor/collectionwidget.cpp b/src/plugins/qmldesigner/components/collectioneditor/collectionwidget.cpp new file mode 100644 index 00000000000..df119654d08 --- /dev/null +++ b/src/plugins/qmldesigner/components/collectioneditor/collectionwidget.cpp @@ -0,0 +1,162 @@ +// 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 "collectionwidget.h" +#include "collectionmodel.h" +#include "collectionview.h" +#include "qmldesignerconstants.h" +#include "qmldesignerplugin.h" +#include "theme.h" + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { +QString collectionViewResourcesPath() +{ +#ifdef SHARE_QML_PATH + if (qEnvironmentVariableIsSet("LOAD_QML_FROM_SOURCE")) + return QLatin1String(SHARE_QML_PATH) + "/collectionEditorQmlSource"; +#endif + return Core::ICore::resourcePath("qmldesigner/collectionEditorQmlSource").toString(); +} +} // namespace + +namespace QmlDesigner { +CollectionWidget::CollectionWidget(CollectionView *view) + : QFrame() + , m_view(view) + , m_model(new CollectionModel) + , m_quickWidget(new StudioQuickWidget(this)) +{ + setWindowTitle(tr("Collection View", "Title of collection view widget")); + + Core::IContext *icontext = nullptr; + Core::Context context(Constants::C_QMLMATERIALBROWSER); + icontext = new Core::IContext(this); + icontext->setContext(context); + icontext->setWidget(this); + + m_quickWidget->quickWidget()->setObjectName(Constants::OBJECT_NAME_COLLECTION_EDITOR); + m_quickWidget->setResizeMode(QQuickWidget::SizeRootObjectToView); + m_quickWidget->engine()->addImportPath(collectionViewResourcesPath() + "/imports"); + m_quickWidget->setClearColor(Theme::getColor(Theme::Color::DSpanelBackground)); + + Theme::setupTheme(m_quickWidget->engine()); + m_quickWidget->quickWidget()->installEventFilter(this); + + auto layout = new QVBoxLayout(this); + layout->setContentsMargins({}); + layout->setSpacing(0); + layout->addWidget(m_quickWidget.data()); + + qmlRegisterAnonymousType("CollectionEditorBackend", 1); + auto map = m_quickWidget->registerPropertyMap("CollectionEditorBackend"); + map->setProperties( + {{"rootView", QVariant::fromValue(this)}, {"model", QVariant::fromValue(m_model.data())}}); + + auto hotReloadShortcut = new QShortcut(QKeySequence(Qt::CTRL + Qt::Key_F4), this); + connect(hotReloadShortcut, &QShortcut::activated, this, &CollectionWidget::reloadQmlSource); + + reloadQmlSource(); +} + +void CollectionWidget::contextHelp(const Core::IContext::HelpCallback &callback) const +{ + if (m_view) + QmlDesignerPlugin::contextHelp(callback, m_view->contextHelpId()); + else + callback({}); +} + +QPointer CollectionWidget::collectionModel() const +{ + return m_model; +} + +void CollectionWidget::reloadQmlSource() +{ + const QString collectionViewQmlPath = collectionViewResourcesPath() + "/CollectionView.qml"; + + QTC_ASSERT(QFileInfo::exists(collectionViewQmlPath), return); + + m_quickWidget->setSource(QUrl::fromLocalFile(collectionViewQmlPath)); +} + +bool CollectionWidget::loadJsonFile(const QString &jsonFileAddress) +{ + QUrl jsonUrl(jsonFileAddress); + QString fileAddress = jsonUrl.isLocalFile() ? jsonUrl.toLocalFile() : jsonUrl.toString(); + QFile file(fileAddress); + if (file.open(QFile::ReadOnly)) + return m_view->loadJson(file.readAll()); + + warn("Unable to open the file", file.errorString()); + return false; +} + +bool CollectionWidget::loadCsvFile(const QString &collectionName, const QString &csvFileAddress) +{ + QUrl csvUrl(csvFileAddress); + QString fileAddress = csvUrl.isLocalFile() ? csvUrl.toLocalFile() : csvUrl.toString(); + QFile file(fileAddress); + if (file.open(QFile::ReadOnly)) + return m_view->loadCsv(collectionName, file.readAll()); + + warn("Unable to open the file", file.errorString()); + return false; +} + +bool CollectionWidget::isJsonFile(const QString &jsonFileAddress) const +{ + QUrl jsonUrl(jsonFileAddress); + QString fileAddress = jsonUrl.isLocalFile() ? jsonUrl.toLocalFile() : jsonUrl.toString(); + QFile file(fileAddress); + + if (!file.exists() || !file.open(QFile::ReadOnly)) + return false; + + QJsonParseError error; + QJsonDocument::fromJson(file.readAll(), &error); + if (error.error) + return false; + + return true; +} + +bool CollectionWidget::isCsvFile(const QString &csvFileAddress) const +{ + QUrl csvUrl(csvFileAddress); + QString fileAddress = csvUrl.isLocalFile() ? csvUrl.toLocalFile() : csvUrl.toString(); + QFile file(fileAddress); + + if (!file.exists()) + return false; + + // TODO: Evaluate the csv file + return true; +} + +bool CollectionWidget::addCollection(const QString &collectionName) const +{ + m_view->addNewCollection(collectionName); + return true; +} + +void CollectionWidget::warn(const QString &title, const QString &body) +{ + QMetaObject::invokeMethod(m_quickWidget->rootObject(), + "showWarning", + Q_ARG(QVariant, title), + Q_ARG(QVariant, body)); +} +} // namespace QmlDesigner diff --git a/src/plugins/qmldesigner/components/collectioneditor/collectionwidget.h b/src/plugins/qmldesigner/components/collectioneditor/collectionwidget.h new file mode 100644 index 00000000000..1f6f57470c2 --- /dev/null +++ b/src/plugins/qmldesigner/components/collectioneditor/collectionwidget.h @@ -0,0 +1,42 @@ +// 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 + +#include + +#include + +class StudioQuickWidget; + +namespace QmlDesigner { + +class CollectionModel; +class CollectionView; + +class CollectionWidget : public QFrame +{ + Q_OBJECT + +public: + CollectionWidget(CollectionView *view); + void contextHelp(const Core::IContext::HelpCallback &callback) const; + + QPointer collectionModel() const; + + void reloadQmlSource(); + + Q_INVOKABLE bool loadJsonFile(const QString &jsonFileAddress); + Q_INVOKABLE bool loadCsvFile(const QString &collectionName, const QString &csvFileAddress); + Q_INVOKABLE bool isJsonFile(const QString &jsonFileAddress) const; + Q_INVOKABLE bool isCsvFile(const QString &csvFileAddress) const; + Q_INVOKABLE bool addCollection(const QString &collectionName) const; + + void warn(const QString &title, const QString &body); + +private: + QPointer m_view; + QPointer m_model; + QScopedPointer m_quickWidget; +}; + +} // namespace QmlDesigner diff --git a/src/plugins/qmldesigner/components/componentcore/viewmanager.cpp b/src/plugins/qmldesigner/components/componentcore/viewmanager.cpp index b61f09885d7..8c06085393c 100644 --- a/src/plugins/qmldesigner/components/componentcore/viewmanager.cpp +++ b/src/plugins/qmldesigner/components/componentcore/viewmanager.cpp @@ -7,6 +7,7 @@ #include #include #include +#include #include #include #include @@ -23,11 +24,11 @@ #include #include #include -#include #include #include #include #include +#include #include @@ -51,6 +52,7 @@ public: : connectionManager, externalDependencies, true) + , collectionView{externalDependencies} , contentLibraryView{externalDependencies} , componentView{externalDependencies} , edit3DView{externalDependencies} @@ -73,6 +75,7 @@ public: Internal::DebugView debugView; DesignerActionManagerView designerActionManagerView; NodeInstanceView nodeInstanceView; + CollectionView collectionView; ContentLibraryView contentLibraryView; ComponentView componentView; Edit3DView edit3DView; @@ -212,6 +215,9 @@ QList ViewManager::standardViews() const if (qEnvironmentVariableIsSet("ENABLE_QDS_EFFECTMAKER")) list.append(&d->effectMakerView); + if (qEnvironmentVariableIsSet("ENABLE_QDS_COLLECTIONVIEW")) + list.append(&d->collectionView); + #ifdef CHECK_LICENSE if (checkLicense() == FoundLicense::enterprise) list.append(&d->contentLibraryView); @@ -390,6 +396,9 @@ QList ViewManager::widgetInfos() const if (qEnvironmentVariableIsSet("ENABLE_QDS_EFFECTMAKER")) widgetInfoList.append(d->effectMakerView.widgetInfo()); + if (qEnvironmentVariableIsSet("ENABLE_QDS_COLLECTIONVIEW")) + widgetInfoList.append(d->collectionView.widgetInfo()); + #ifdef CHECK_LICENSE if (checkLicense() == FoundLicense::enterprise) widgetInfoList.append(d->contentLibraryView.widgetInfo()); diff --git a/src/plugins/qmldesigner/designmodecontext.cpp b/src/plugins/qmldesigner/designmodecontext.cpp index fade9e9a953..13a03bc10b1 100644 --- a/src/plugins/qmldesigner/designmodecontext.cpp +++ b/src/plugins/qmldesigner/designmodecontext.cpp @@ -3,6 +3,7 @@ #include "designmodecontext.h" #include "assetslibrarywidget.h" +#include "collectionwidget.h" #include "designmodewidget.h" #include "edit3dwidget.h" #include "effectmakerwidget.h" @@ -12,11 +13,10 @@ #include "qmldesignerconstants.h" #include "texteditorwidget.h" -namespace QmlDesigner { -namespace Internal { +namespace QmlDesigner::Internal { DesignModeContext::DesignModeContext(QWidget *widget) - : IContext(widget) + : IContext(widget) { setWidget(widget); setContext(Core::Context(Constants::C_QMLDESIGNER, Constants::C_QT_QUICK_TOOLS_MENU)); @@ -111,6 +111,15 @@ void EffectMakerContext::contextHelp(const HelpCallback &callback) const qobject_cast(m_widget)->contextHelp(callback); } -} +CollectionEditorContext::CollectionEditorContext(QWidget *widget) + : IContext(widget) +{ + setWidget(widget); + setContext(Core::Context(Constants::C_QMLCOLLECTIONEDITOR, Constants::C_QT_QUICK_TOOLS_MENU)); } +void CollectionEditorContext::contextHelp(const HelpCallback &callback) const +{ + qobject_cast(m_widget)->contextHelp(callback); +} +} // namespace QmlDesigner::Internal diff --git a/src/plugins/qmldesigner/designmodecontext.h b/src/plugins/qmldesigner/designmodecontext.h index 0829b2a8222..72c0a195239 100644 --- a/src/plugins/qmldesigner/designmodecontext.h +++ b/src/plugins/qmldesigner/designmodecontext.h @@ -83,5 +83,13 @@ public: void contextHelp(const Core::IContext::HelpCallback &callback) const override; }; -} -} +class CollectionEditorContext : public Core::IContext +{ + Q_OBJECT + +public: + CollectionEditorContext(QWidget *widget); + void contextHelp(const Core::IContext::HelpCallback &callback) const override; +}; +} // namespace Internal +} // namespace QmlDesigner diff --git a/src/plugins/qmldesigner/qmldesignerconstants.h b/src/plugins/qmldesigner/qmldesignerconstants.h index c2003441e0a..60e1ded8474 100644 --- a/src/plugins/qmldesigner/qmldesignerconstants.h +++ b/src/plugins/qmldesigner/qmldesignerconstants.h @@ -11,14 +11,15 @@ const char C_DELETE[] = "QmlDesigner.Delete"; const char C_DUPLICATE[] = "QmlDesigner.Duplicate"; // Context -const char C_QMLDESIGNER[] = "QmlDesigner::QmlDesignerMain"; -const char C_QMLFORMEDITOR[] = "QmlDesigner::FormEditor"; -const char C_QMLEDITOR3D[] = "QmlDesigner::Editor3D"; -const char C_QMLEFFECTMAKER[] = "QmlDesigner::EffectMaker"; -const char C_QMLNAVIGATOR[] = "QmlDesigner::Navigator"; -const char C_QMLTEXTEDITOR[] = "QmlDesigner::TextEditor"; -const char C_QMLMATERIALBROWSER[] = "QmlDesigner::MaterialBrowser"; -const char C_QMLASSETSLIBRARY[] = "QmlDesigner::AssetsLibrary"; +const char C_QMLDESIGNER[] = "QmlDesigner::QmlDesignerMain"; +const char C_QMLFORMEDITOR[] = "QmlDesigner::FormEditor"; +const char C_QMLEDITOR3D[] = "QmlDesigner::Editor3D"; +const char C_QMLEFFECTMAKER[] = "QmlDesigner::EffectMaker"; +const char C_QMLNAVIGATOR[] = "QmlDesigner::Navigator"; +const char C_QMLTEXTEDITOR[] = "QmlDesigner::TextEditor"; +const char C_QMLMATERIALBROWSER[] = "QmlDesigner::MaterialBrowser"; +const char C_QMLASSETSLIBRARY[] = "QmlDesigner::AssetsLibrary"; +const char C_QMLCOLLECTIONEDITOR[] = "QmlDesigner::CollectionEditor"; // Special context for preview menu, shared b/w designer and text editor const char C_QT_QUICK_TOOLS_MENU[] = "QmlDesigner::ToolsMenu"; @@ -77,6 +78,7 @@ const char QUICK_3D_ASSET_IMPORT_DATA_OPTIONS_KEY[] = "import_options"; const char QUICK_3D_ASSET_IMPORT_DATA_SOURCE_KEY[] = "source_scene"; const char DEFAULT_ASSET_IMPORT_FOLDER[] = "/asset_imports"; const char MATERIAL_LIB_ID[] = "__materialLibrary__"; +const char COLLECTION_LIB_ID[] = "__collectionLibrary__"; const char MIME_TYPE_ITEM_LIBRARY_INFO[] = "application/vnd.qtdesignstudio.itemlibraryinfo"; const char MIME_TYPE_ASSETS[] = "application/vnd.qtdesignstudio.assets"; @@ -159,6 +161,7 @@ const char OBJECT_NAME_EFFECT_MAKER[] = "QQuickWidgetEffectMaker"; const char OBJECT_NAME_MATERIAL_BROWSER[] = "QQuickWidgetMaterialBrowser"; const char OBJECT_NAME_MATERIAL_EDITOR[] = "QQuickWidgetMaterialEditor"; const char OBJECT_NAME_PROPERTY_EDITOR[] = "QQuickWidgetPropertyEditor"; +const char OBJECT_NAME_COLLECTION_EDITOR[] = "QQuickWidgetQDSCollectionEditor"; const char OBJECT_NAME_STATES_EDITOR[] = "QQuickWidgetStatesEditor"; const char OBJECT_NAME_TEXTURE_EDITOR[] = "QQuickWidgetTextureEditor"; const char OBJECT_NAME_TOP_TOOLBAR[] = "QQuickWidgetTopToolbar";