forked from qt-creator/qt-creator
This is still disabled by default until the UI gets polished. Add roles to models. ConnectionModel, BindingModel and DynamicPropertiesModel. In QML roles are used in the ListView to "mimic" columns. The columns are only relevant for the "old" cpp QTableView and can be removed. Making currentIndex part of the model. If rows are removed and added again we have to "reset/keep" the currentIndex as part of the model, because the index is temporarly invalid on the view. Implementing DynamicPropertiesModelBackendDelegate as reference. This is a pure "backend delegate" that exposes all relevant properties for a current row to QML. Adding StudioQuickWidget and exposing the backend. Adding StudioQmlTextBackend and StudioQmlComboBoxBackend to StudioQuickWidget (should be moved into their own files). Those helper classes make is easy to expose a property to a combobox or textfield. The backend has to know nothing about the actual frontend and those classes act as a mini-model for a view in QML. The API is similar to UI controls. Change-Id: I7a2c6ad951306fbca1d586fb8f278acdd91a064b Reviewed-by: Qt CI Patch Build Bot <ci_patchbuild_bot@qt.io> Reviewed-by: Knud Dollereder <knud.dollereder@qt.io>
557 lines
20 KiB
C++
557 lines
20 KiB
C++
// Copyright (C) 2016 The Qt Company Ltd.
|
|
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
|
|
|
|
#include "connectionmodel.h"
|
|
#include "connectionview.h"
|
|
|
|
#include <bindingproperty.h>
|
|
#include <variantproperty.h>
|
|
#include <signalhandlerproperty.h>
|
|
#include <rewritertransaction.h>
|
|
#include <nodeabstractproperty.h>
|
|
#include <exception.h>
|
|
#include <nodemetainfo.h>
|
|
#include <nodelistproperty.h>
|
|
#include <rewriterview.h>
|
|
#include <nodemetainfo.h>
|
|
#include <qmldesignerconstants.h>
|
|
#include <qmldesignerplugin.h>
|
|
|
|
#include <utils/qtcassert.h>
|
|
|
|
#include <QStandardItemModel>
|
|
#include <QMessageBox>
|
|
#include <QTableView>
|
|
#include <QTimer>
|
|
|
|
namespace {
|
|
|
|
QStringList propertyNameListToStringList(const QmlDesigner::PropertyNameList &propertyNameList)
|
|
{
|
|
QStringList stringList;
|
|
for (const QmlDesigner::PropertyName &propertyName : propertyNameList)
|
|
stringList << QString::fromUtf8(propertyName);
|
|
|
|
stringList.removeDuplicates();
|
|
return stringList;
|
|
}
|
|
|
|
bool isConnection(const QmlDesigner::ModelNode &modelNode)
|
|
{
|
|
return (modelNode.metaInfo().simplifiedTypeName() == "Connections");
|
|
}
|
|
|
|
} //namespace
|
|
|
|
namespace QmlDesigner {
|
|
|
|
ConnectionModel::ConnectionModel(ConnectionView *parent)
|
|
: QStandardItemModel(parent)
|
|
, m_connectionView(parent)
|
|
{
|
|
connect(this, &QStandardItemModel::dataChanged, this, &ConnectionModel::handleDataChanged);
|
|
}
|
|
|
|
Qt::ItemFlags ConnectionModel::flags(const QModelIndex &modelIndex) const
|
|
{
|
|
if (!modelIndex.isValid())
|
|
return Qt::ItemIsEnabled;
|
|
|
|
if (!m_connectionView || !m_connectionView->model())
|
|
return Qt::ItemIsEnabled;
|
|
|
|
const int internalId = data(index(modelIndex.row(), TargetModelNodeRow), UserRoles::InternalIdRole).toInt();
|
|
ModelNode modelNode = m_connectionView->modelNodeForInternalId(internalId);
|
|
|
|
if (modelNode.isValid() && ModelNode::isThisOrAncestorLocked(modelNode))
|
|
return Qt::ItemIsEnabled;
|
|
|
|
return Qt::ItemIsSelectable | Qt::ItemIsEditable | Qt::ItemIsEnabled;
|
|
}
|
|
|
|
void ConnectionModel::resetModel()
|
|
{
|
|
beginResetModel();
|
|
clear();
|
|
setHorizontalHeaderLabels(QStringList({ tr("Target"), tr("Signal Handler"), tr("Action") }));
|
|
|
|
if (connectionView()->isAttached()) {
|
|
for (const ModelNode &modelNode : connectionView()->allModelNodes())
|
|
addModelNode(modelNode);
|
|
}
|
|
|
|
const int columnWidthTarget = connectionView()->connectionTableView()->columnWidth(0);
|
|
connectionView()->connectionTableView()->setColumnWidth(0, columnWidthTarget - 80);
|
|
|
|
endResetModel();
|
|
}
|
|
|
|
SignalHandlerProperty ConnectionModel::signalHandlerPropertyForRow(int rowNumber) const
|
|
{
|
|
const int internalId = data(index(rowNumber, TargetModelNodeRow), UserRoles::InternalIdRole).toInt();
|
|
const QString targetPropertyName = data(index(rowNumber, TargetModelNodeRow), UserRoles::TargetPropertyNameRole).toString();
|
|
|
|
ModelNode modelNode = connectionView()->modelNodeForInternalId(internalId);
|
|
if (modelNode.isValid())
|
|
return modelNode.signalHandlerProperty(targetPropertyName.toUtf8());
|
|
|
|
return SignalHandlerProperty();
|
|
}
|
|
|
|
void ConnectionModel::addModelNode(const ModelNode &modelNode)
|
|
{
|
|
if (isConnection(modelNode))
|
|
addConnection(modelNode);
|
|
}
|
|
|
|
void ConnectionModel::addConnection(const ModelNode &modelNode)
|
|
{
|
|
for (const AbstractProperty &property : modelNode.properties()) {
|
|
if (property.isSignalHandlerProperty() && property.name() != "target") {
|
|
addSignalHandler(property.toSignalHandlerProperty());
|
|
}
|
|
}
|
|
}
|
|
|
|
void ConnectionModel::addSignalHandler(const SignalHandlerProperty &signalHandlerProperty)
|
|
{
|
|
QStandardItem *targetItem;
|
|
QStandardItem *signalItem;
|
|
QStandardItem *actionItem;
|
|
|
|
QString idLabel;
|
|
|
|
ModelNode connectionsModelNode = signalHandlerProperty.parentModelNode();
|
|
|
|
if (connectionsModelNode.bindingProperty("target").isValid()) {
|
|
idLabel = connectionsModelNode.bindingProperty("target").expression();
|
|
}
|
|
|
|
targetItem = new QStandardItem(idLabel);
|
|
updateCustomData(targetItem, signalHandlerProperty);
|
|
const QString propertyName = QString::fromUtf8(signalHandlerProperty.name());
|
|
const QString source = signalHandlerProperty.source();
|
|
|
|
signalItem = new QStandardItem(propertyName);
|
|
QList<QStandardItem*> items;
|
|
|
|
items.append(targetItem);
|
|
items.append(signalItem);
|
|
|
|
actionItem = new QStandardItem(source);
|
|
|
|
items.append(actionItem);
|
|
|
|
appendRow(items);
|
|
}
|
|
|
|
void ConnectionModel::removeModelNode(const ModelNode &modelNode)
|
|
{
|
|
if (isConnection(modelNode))
|
|
removeConnection(modelNode);
|
|
}
|
|
|
|
void ConnectionModel::removeConnection(const ModelNode & /*modelNode*/)
|
|
{
|
|
Q_ASSERT_X(false, "not implemented", Q_FUNC_INFO);
|
|
}
|
|
|
|
void ConnectionModel::updateSource(int row)
|
|
{
|
|
SignalHandlerProperty signalHandlerProperty = signalHandlerPropertyForRow(row);
|
|
|
|
const QString sourceString = data(index(row, SourceRow)).toString();
|
|
|
|
RewriterTransaction transaction =
|
|
connectionView()->beginRewriterTransaction(QByteArrayLiteral("ConnectionModel::updateSource"));
|
|
|
|
try {
|
|
signalHandlerProperty.setSource(sourceString);
|
|
transaction.commit();
|
|
}
|
|
catch (Exception &e) {
|
|
m_exceptionError = e.description();
|
|
QTimer::singleShot(200, this, &ConnectionModel::handleException);
|
|
}
|
|
}
|
|
|
|
void ConnectionModel::updateSignalName(int rowNumber)
|
|
{
|
|
SignalHandlerProperty signalHandlerProperty = signalHandlerPropertyForRow(rowNumber);
|
|
ModelNode connectionNode = signalHandlerProperty.parentModelNode();
|
|
|
|
const PropertyName newName = data(index(rowNumber, TargetPropertyNameRow)).toString().toUtf8();
|
|
if (!newName.isEmpty()) {
|
|
connectionView()->executeInTransaction("ConnectionModel::updateSignalName", [=, &connectionNode](){
|
|
|
|
const QString source = signalHandlerProperty.source();
|
|
|
|
connectionNode.signalHandlerProperty(newName).setSource(source);
|
|
connectionNode.removeProperty(signalHandlerProperty.name());
|
|
});
|
|
|
|
QStandardItem* idItem = item(rowNumber, 0);
|
|
SignalHandlerProperty newSignalHandlerProperty = connectionNode.signalHandlerProperty(newName);
|
|
updateCustomData(idItem, newSignalHandlerProperty);
|
|
} else {
|
|
qWarning() << "BindingModel::updatePropertyName invalid property name";
|
|
}
|
|
}
|
|
|
|
void ConnectionModel::updateTargetNode(int rowNumber)
|
|
{
|
|
SignalHandlerProperty signalHandlerProperty = signalHandlerPropertyForRow(rowNumber);
|
|
const QString newTarget = data(index(rowNumber, TargetModelNodeRow)).toString();
|
|
ModelNode connectionNode = signalHandlerProperty.parentModelNode();
|
|
|
|
const bool isAlias = newTarget.contains(".");
|
|
bool isSingleton = false;
|
|
|
|
if (RewriterView* rv = connectionView()->rewriterView()) {
|
|
for (const QmlTypeData &data : rv->getQMLTypes()) {
|
|
if (!data.typeName.isEmpty()) {
|
|
if (data.typeName == newTarget) {
|
|
if (connectionView()->model()->metaInfo(data.typeName.toUtf8()).isValid()) {
|
|
isSingleton = true;
|
|
break;
|
|
}
|
|
} else if (isAlias) {
|
|
if (data.typeName == newTarget.split(".").constFirst()) {
|
|
if (connectionView()->model()->metaInfo(data.typeName.toUtf8()).isValid()) {
|
|
isSingleton = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!newTarget.isEmpty()) {
|
|
//if it's a singleton, then let's reparent connections to rootNode,
|
|
//if it's an alias, then reparent to alias property owner:
|
|
const ModelNode parent = connectionView()->modelNodeForId((isSingleton || (isSingleton && isAlias))
|
|
? connectionView()->rootModelNode().id()
|
|
: isAlias
|
|
? newTarget.split(".").constFirst()
|
|
: newTarget);
|
|
|
|
if (parent.isValid() && QmlItemNode::isValidQmlItemNode(parent))
|
|
parent.nodeListProperty("data").reparentHere(connectionNode);
|
|
|
|
connectionView()->executeInTransaction("ConnectionModel::updateTargetNode", [= ,&connectionNode](){
|
|
connectionNode.bindingProperty("target").setExpression(newTarget);
|
|
});
|
|
|
|
} else {
|
|
qWarning() << "BindingModel::updatePropertyName invalid target id";
|
|
}
|
|
}
|
|
|
|
void ConnectionModel::updateCustomData(QStandardItem *item, const SignalHandlerProperty &signalHandlerProperty)
|
|
{
|
|
item->setData(signalHandlerProperty.parentModelNode().internalId(), UserRoles::InternalIdRole);
|
|
item->setData(signalHandlerProperty.name(), UserRoles::TargetPropertyNameRole);
|
|
item->setData(signalHandlerProperty.parentModelNode()
|
|
.bindingProperty("target")
|
|
.resolveToModelNode()
|
|
.id(),
|
|
UserRoles::TargetNameRole);
|
|
|
|
// TODO signalHandlerProperty.source() contains a statement that defines the type.
|
|
// foo.bar() <- function call
|
|
// foo.state = "literal" //state change
|
|
//anything else is assignment
|
|
// e.g. foo.bal = foo2.bula ; foo.bal = "literal" ; goo.gal = true
|
|
|
|
item->setData("Assignment", UserRoles::ActionTypeRole);
|
|
}
|
|
|
|
ModelNode ConnectionModel::getTargetNodeForConnection(const ModelNode &connection) const
|
|
{
|
|
ModelNode result;
|
|
|
|
const BindingProperty bindingProperty = connection.bindingProperty("target");
|
|
const QString bindExpression = bindingProperty.expression();
|
|
|
|
if (bindingProperty.isValid()) {
|
|
if (bindExpression.contains(".")) {
|
|
QStringList substr = bindExpression.split(".");
|
|
const QString itemId = substr.constFirst();
|
|
if (substr.size() > 1) {
|
|
const ModelNode aliasParent = connectionView()->modelNodeForId(itemId);
|
|
substr.removeFirst(); //remove id, only alias pieces left
|
|
const QString aliasBody = substr.join(".");
|
|
if (aliasParent.isValid() && aliasParent.hasBindingProperty(aliasBody.toUtf8())) {
|
|
const BindingProperty binding = aliasParent.bindingProperty(aliasBody.toUtf8());
|
|
if (binding.isValid() && connectionView()->hasId(binding.expression())) {
|
|
result = connectionView()->modelNodeForId(binding.expression());
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
result = connectionView()->modelNodeForId(bindExpression);
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
void ConnectionModel::addConnection()
|
|
{
|
|
QmlDesignerPlugin::emitUsageStatistics(Constants::EVENT_CONNECTION_ADDED);
|
|
|
|
ModelNode rootModelNode = connectionView()->rootModelNode();
|
|
|
|
if (rootModelNode.isValid() && rootModelNode.metaInfo().isValid()) {
|
|
|
|
NodeMetaInfo nodeMetaInfo = connectionView()->model()->qtQuickConnectionsMetaInfo();
|
|
|
|
if (nodeMetaInfo.isValid()) {
|
|
connectionView()->executeInTransaction("ConnectionModel::addConnection", [=, &rootModelNode](){
|
|
ModelNode newNode = connectionView()->createModelNode("QtQuick.Connections",
|
|
nodeMetaInfo.majorVersion(),
|
|
nodeMetaInfo.minorVersion());
|
|
QString source = "console.log(\"clicked\")";
|
|
|
|
if (connectionView()->selectedModelNodes().size() == 1) {
|
|
ModelNode selectedNode = connectionView()->selectedModelNodes().constFirst();
|
|
if (QmlItemNode::isValidQmlItemNode(selectedNode))
|
|
selectedNode.nodeAbstractProperty("data").reparentHere(newNode);
|
|
else
|
|
rootModelNode.nodeAbstractProperty(rootModelNode.metaInfo().defaultPropertyName()).reparentHere(newNode);
|
|
|
|
if (QmlItemNode(selectedNode).isFlowActionArea() || QmlVisualNode(selectedNode).isFlowTransition())
|
|
source = selectedNode.validId() + ".trigger()";
|
|
|
|
if (!connectionView()->selectedModelNodes().constFirst().id().isEmpty())
|
|
newNode.bindingProperty("target").setExpression(selectedNode.validId());
|
|
} else {
|
|
rootModelNode.nodeAbstractProperty(rootModelNode.metaInfo().defaultPropertyName()).reparentHere(newNode);
|
|
newNode.bindingProperty("target").setExpression(rootModelNode.validId());
|
|
}
|
|
|
|
newNode.signalHandlerProperty("onClicked").setSource(source);
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
void ConnectionModel::bindingPropertyChanged(const BindingProperty &bindingProperty)
|
|
{
|
|
if (isConnection(bindingProperty.parentModelNode()))
|
|
resetModel();
|
|
}
|
|
|
|
void ConnectionModel::variantPropertyChanged(const VariantProperty &variantProperty)
|
|
{
|
|
if (isConnection(variantProperty.parentModelNode()))
|
|
resetModel();
|
|
}
|
|
|
|
void ConnectionModel::abstractPropertyChanged(const AbstractProperty &abstractProperty)
|
|
{
|
|
if (isConnection(abstractProperty.parentModelNode()))
|
|
resetModel();
|
|
}
|
|
|
|
void ConnectionModel::deleteConnectionByRow(int currentRow)
|
|
{
|
|
SignalHandlerProperty targetSignal = signalHandlerPropertyForRow(currentRow);
|
|
QTC_ASSERT(targetSignal.isValid(), return );
|
|
ModelNode node = targetSignal.parentModelNode();
|
|
QTC_ASSERT(node.isValid(), return );
|
|
|
|
QList<SignalHandlerProperty> allSignals = node.signalProperties();
|
|
if (allSignals.size() > 1) {
|
|
if (allSignals.contains(targetSignal))
|
|
node.removeProperty(targetSignal.name());
|
|
} else {
|
|
node.destroy();
|
|
}
|
|
}
|
|
|
|
void ConnectionModel::removeRowFromTable(const SignalHandlerProperty &property)
|
|
{
|
|
for (int currentRow = 0; currentRow < rowCount(); currentRow++) {
|
|
SignalHandlerProperty targetSignal = signalHandlerPropertyForRow(currentRow);
|
|
|
|
if (targetSignal == property) {
|
|
removeRow(currentRow);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
void ConnectionModel::add()
|
|
{
|
|
addConnection();
|
|
}
|
|
|
|
void ConnectionModel::remove(int row)
|
|
{
|
|
deleteConnectionByRow(row);
|
|
}
|
|
|
|
void ConnectionModel::handleException()
|
|
{
|
|
QMessageBox::warning(nullptr, tr("Error"), m_exceptionError);
|
|
resetModel();
|
|
}
|
|
|
|
void ConnectionModel::handleDataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight)
|
|
{
|
|
if (topLeft != bottomRight) {
|
|
qWarning() << "ConnectionModel::handleDataChanged multi edit?";
|
|
return;
|
|
}
|
|
|
|
m_lock = true;
|
|
|
|
int currentColumn = topLeft.column();
|
|
int currentRow = topLeft.row();
|
|
|
|
switch (currentColumn) {
|
|
case TargetModelNodeRow: {
|
|
updateTargetNode(currentRow);
|
|
} break;
|
|
case TargetPropertyNameRow: {
|
|
updateSignalName(currentRow);
|
|
} break;
|
|
case SourceRow: {
|
|
updateSource(currentRow);
|
|
} break;
|
|
|
|
default: qWarning() << "ConnectionModel::handleDataChanged column" << currentColumn;
|
|
}
|
|
|
|
m_lock = false;
|
|
}
|
|
|
|
ConnectionView *ConnectionModel::connectionView() const
|
|
{
|
|
return m_connectionView;
|
|
}
|
|
|
|
QStringList ConnectionModel::getSignalsForRow(int row) const
|
|
{
|
|
QStringList stringList;
|
|
SignalHandlerProperty signalHandlerProperty = signalHandlerPropertyForRow(row);
|
|
|
|
if (signalHandlerProperty.isValid())
|
|
stringList.append(getPossibleSignalsForConnection(signalHandlerProperty.parentModelNode()));
|
|
|
|
return stringList;
|
|
}
|
|
|
|
QStringList ConnectionModel::getflowActionTriggerForRow(int row) const
|
|
{
|
|
QStringList stringList;
|
|
SignalHandlerProperty signalHandlerProperty = signalHandlerPropertyForRow(row);
|
|
|
|
if (signalHandlerProperty.isValid()) {
|
|
const ModelNode parentModelNode = signalHandlerProperty.parentModelNode();
|
|
ModelNode targetNode = getTargetNodeForConnection(parentModelNode);
|
|
if (!targetNode.isValid() && !parentModelNode.isRootNode())
|
|
targetNode = parentModelNode.parentProperty().parentModelNode();
|
|
if (targetNode.isValid()) {
|
|
for (auto &node : targetNode.allSubModelNodesAndThisNode()) {
|
|
if (QmlItemNode(node).isFlowActionArea() && node.hasId())
|
|
stringList.append(node.id() + ".trigger()");
|
|
}
|
|
}
|
|
}
|
|
return stringList;
|
|
}
|
|
|
|
QStringList ConnectionModel::getPossibleSignalsForConnection(const ModelNode &connection) const
|
|
{
|
|
QStringList stringList;
|
|
|
|
auto getAliasMetaSignals = [&](QString aliasPart, NodeMetaInfo metaInfo) {
|
|
if (metaInfo.isValid() && metaInfo.hasProperty(aliasPart.toUtf8())) {
|
|
NodeMetaInfo propertyMetaInfo = metaInfo.property(aliasPart.toUtf8()).propertyType();
|
|
if (propertyMetaInfo.isValid()) {
|
|
return propertyNameListToStringList(propertyMetaInfo.signalNames());
|
|
}
|
|
}
|
|
return QStringList();
|
|
};
|
|
|
|
if (connection.isValid()) {
|
|
//separate check for singletons
|
|
if (connection.hasBindingProperty("target")) {
|
|
const BindingProperty bp = connection.bindingProperty("target");
|
|
|
|
if (bp.isValid()) {
|
|
const QString bindExpression = bp.expression();
|
|
|
|
if (const RewriterView * const rv = connectionView()->rewriterView()) {
|
|
for (const QmlTypeData &data : rv->getQMLTypes()) {
|
|
if (!data.typeName.isEmpty()) {
|
|
if (data.typeName == bindExpression) {
|
|
NodeMetaInfo metaInfo = connectionView()->model()->metaInfo(data.typeName.toUtf8());
|
|
if (metaInfo.isValid()) {
|
|
stringList << propertyNameListToStringList(metaInfo.signalNames());
|
|
break;
|
|
}
|
|
} else if (bindExpression.contains(".")) {
|
|
//if it doesn't fit the same name, maybe it's an alias?
|
|
QStringList expression = bindExpression.split(".");
|
|
if ((expression.size() > 1) && (expression.constFirst() == data.typeName)) {
|
|
expression.removeFirst();
|
|
|
|
stringList << getAliasMetaSignals(
|
|
expression.join("."),
|
|
connectionView()->model()->metaInfo(data.typeName.toUtf8()));
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
ModelNode targetNode = getTargetNodeForConnection(connection);
|
|
if (targetNode.isValid() && targetNode.metaInfo().isValid()) {
|
|
stringList.append(propertyNameListToStringList(targetNode.metaInfo().signalNames()));
|
|
} else {
|
|
//most likely it's component's internal alias:
|
|
|
|
if (connection.hasBindingProperty("target")) {
|
|
const BindingProperty bp = connection.bindingProperty("target");
|
|
|
|
if (bp.isValid()) {
|
|
QStringList expression = bp.expression().split(".");
|
|
if (expression.size() > 1) {
|
|
const QString itemId = expression.constFirst();
|
|
if (connectionView()->hasId(itemId)) {
|
|
ModelNode parentItem = connectionView()->modelNodeForId(itemId);
|
|
if (parentItem.isValid()
|
|
&& parentItem.hasMetaInfo()
|
|
&& parentItem.metaInfo().isValid()) {
|
|
expression.removeFirst();
|
|
stringList << getAliasMetaSignals(expression.join("."),
|
|
parentItem.metaInfo());
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return stringList;
|
|
}
|
|
|
|
QHash<int, QByteArray> ConnectionModel::roleNames() const
|
|
{
|
|
static QHash<int, QByteArray> roleNames{{TargetPropertyNameRole, "signal"},
|
|
{TargetNameRole, "target"},
|
|
{ActionTypeRole, "action"}};
|
|
return roleNames;
|
|
}
|
|
|
|
} // namespace QmlDesigner
|