1 /****************************************************************************
2 **
3 ** Copyright (C) 2016 The Qt Company Ltd.
4 ** Contact: https://www.qt.io/licensing/
5 **
6 ** This file is part of Qt Creator.
7 **
8 ** Commercial License Usage
9 ** Licensees holding valid commercial Qt licenses may use this file in
10 ** accordance with the commercial license agreement provided with the
11 ** Software or, alternatively, in accordance with the terms contained in
12 ** a written agreement between you and The Qt Company. For licensing terms
13 ** and conditions see https://www.qt.io/terms-conditions. For further
14 ** information use the contact form at https://www.qt.io/contact-us.
15 **
16 ** GNU General Public License Usage
17 ** Alternatively, this file may be used under the terms of the GNU
18 ** General Public License version 3 as published by the Free Software
19 ** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT
20 ** included in the packaging of this file. Please review the following
21 ** information to ensure the GNU General Public License requirements will
22 ** be met: https://www.gnu.org/licenses/gpl-3.0.html.
23 **
24 ****************************************************************************/
25 
26 #include "connectionmodel.h"
27 #include "connectionview.h"
28 
29 #include <bindingproperty.h>
30 #include <variantproperty.h>
31 #include <signalhandlerproperty.h>
32 #include <rewritertransaction.h>
33 #include <nodeabstractproperty.h>
34 #include <exception.h>
35 #include <nodemetainfo.h>
36 #include <nodelistproperty.h>
37 #include <rewriterview.h>
38 #include <nodemetainfo.h>
39 #include <qmldesignerconstants.h>
40 #include <qmldesignerplugin.h>
41 
42 #include <utils/qtcassert.h>
43 
44 #include <QStandardItemModel>
45 #include <QMessageBox>
46 #include <QTableView>
47 #include <QTimer>
48 
49 namespace {
50 
propertyNameListToStringList(const QmlDesigner::PropertyNameList & propertyNameList)51 QStringList propertyNameListToStringList(const QmlDesigner::PropertyNameList &propertyNameList)
52 {
53     QStringList stringList;
54     for (const QmlDesigner::PropertyName &propertyName : propertyNameList)
55         stringList << QString::fromUtf8(propertyName);
56 
57     stringList.removeDuplicates();
58     return stringList;
59 }
60 
isConnection(const QmlDesigner::ModelNode & modelNode)61 bool isConnection(const QmlDesigner::ModelNode &modelNode)
62 {
63     return (modelNode.type() == "Connections"
64             || modelNode.type() == "QtQuick.Connections"
65             || modelNode.type() == "Qt.Connections"
66             || modelNode.type() == "QtQml.Connections");
67 }
68 
69 } //namespace
70 
71 namespace QmlDesigner {
72 
73 namespace Internal {
74 
ConnectionModel(ConnectionView * parent)75 ConnectionModel::ConnectionModel(ConnectionView *parent)
76     : QStandardItemModel(parent)
77     , m_connectionView(parent)
78 {
79     connect(this, &QStandardItemModel::dataChanged, this, &ConnectionModel::handleDataChanged);
80 }
81 
flags(const QModelIndex & modelIndex) const82 Qt::ItemFlags ConnectionModel::flags(const QModelIndex &modelIndex) const
83 {
84     if (!modelIndex.isValid())
85         return Qt::ItemIsEnabled;
86 
87     if (!m_connectionView || !m_connectionView->model())
88         return Qt::ItemIsEnabled;
89 
90     const int internalId = data(index(modelIndex.row(), TargetModelNodeRow), UserRoles::InternalIdRole).toInt();
91     ModelNode modelNode = m_connectionView->modelNodeForInternalId(internalId);
92 
93     if (modelNode.isValid() && ModelNode::isThisOrAncestorLocked(modelNode))
94         return Qt::ItemIsEnabled;
95 
96     return Qt::ItemIsSelectable | Qt::ItemIsEditable | Qt::ItemIsEnabled;
97 }
98 
resetModel()99 void ConnectionModel::resetModel()
100 {
101     beginResetModel();
102     clear();
103     setHorizontalHeaderLabels(QStringList({ tr("Target"), tr("Signal Handler"), tr("Action") }));
104 
105     if (connectionView()->isAttached()) {
106         for (const ModelNode &modelNode : connectionView()->allModelNodes())
107             addModelNode(modelNode);
108     }
109 
110     const int columnWidthTarget = connectionView()->connectionTableView()->columnWidth(0);
111     connectionView()->connectionTableView()->setColumnWidth(0, columnWidthTarget - 80);
112 
113     endResetModel();
114 }
115 
signalHandlerPropertyForRow(int rowNumber) const116 SignalHandlerProperty ConnectionModel::signalHandlerPropertyForRow(int rowNumber) const
117 {
118     const int internalId = data(index(rowNumber, TargetModelNodeRow), UserRoles::InternalIdRole).toInt();
119     const QString targetPropertyName = data(index(rowNumber, TargetModelNodeRow), UserRoles::TargetPropertyNameRole).toString();
120 
121     ModelNode modelNode = connectionView()->modelNodeForInternalId(internalId);
122     if (modelNode.isValid())
123         return modelNode.signalHandlerProperty(targetPropertyName.toUtf8());
124 
125     return SignalHandlerProperty();
126 }
127 
addModelNode(const ModelNode & modelNode)128 void ConnectionModel::addModelNode(const ModelNode &modelNode)
129 {
130     if (isConnection(modelNode))
131         addConnection(modelNode);
132 }
133 
addConnection(const ModelNode & modelNode)134 void ConnectionModel::addConnection(const ModelNode &modelNode)
135 {
136     for (const AbstractProperty &property : modelNode.properties()) {
137         if (property.isSignalHandlerProperty() && property.name() != "target") {
138             addSignalHandler(property.toSignalHandlerProperty());
139         }
140     }
141 }
142 
addSignalHandler(const SignalHandlerProperty & signalHandlerProperty)143 void ConnectionModel::addSignalHandler(const SignalHandlerProperty &signalHandlerProperty)
144 {
145     QStandardItem *targetItem;
146     QStandardItem *signalItem;
147     QStandardItem *actionItem;
148 
149     QString idLabel;
150 
151     ModelNode connectionsModelNode = signalHandlerProperty.parentModelNode();
152 
153     if (connectionsModelNode.bindingProperty("target").isValid()) {
154         idLabel = connectionsModelNode.bindingProperty("target").expression();
155     }
156 
157     targetItem = new QStandardItem(idLabel);
158     updateCustomData(targetItem, signalHandlerProperty);
159     const QString propertyName = QString::fromUtf8(signalHandlerProperty.name());
160     const QString source = signalHandlerProperty.source();
161 
162     signalItem = new QStandardItem(propertyName);
163     QList<QStandardItem*> items;
164 
165     items.append(targetItem);
166     items.append(signalItem);
167 
168     actionItem = new QStandardItem(source);
169 
170     items.append(actionItem);
171 
172     appendRow(items);
173 }
174 
removeModelNode(const ModelNode & modelNode)175 void ConnectionModel::removeModelNode(const ModelNode &modelNode)
176 {
177     if (isConnection(modelNode))
178         removeConnection(modelNode);
179 }
180 
removeConnection(const ModelNode &)181 void ConnectionModel::removeConnection(const ModelNode & /*modelNode*/)
182 {
183     Q_ASSERT_X(false, "not implemented", Q_FUNC_INFO);
184 }
185 
updateSource(int row)186 void ConnectionModel::updateSource(int row)
187 {
188     SignalHandlerProperty signalHandlerProperty = signalHandlerPropertyForRow(row);
189 
190     const QString sourceString = data(index(row, SourceRow)).toString();
191 
192     RewriterTransaction transaction =
193         connectionView()->beginRewriterTransaction(QByteArrayLiteral("ConnectionModel::updateSource"));
194 
195     try {
196         signalHandlerProperty.setSource(sourceString);
197         transaction.commit();
198     }
199     catch (Exception &e) {
200         m_exceptionError = e.description();
201         QTimer::singleShot(200, this, &ConnectionModel::handleException);
202     }
203 }
204 
updateSignalName(int rowNumber)205 void ConnectionModel::updateSignalName(int rowNumber)
206 {
207     SignalHandlerProperty signalHandlerProperty = signalHandlerPropertyForRow(rowNumber);
208     ModelNode connectionNode = signalHandlerProperty.parentModelNode();
209 
210     const PropertyName newName = data(index(rowNumber, TargetPropertyNameRow)).toString().toUtf8();
211     if (!newName.isEmpty()) {
212         connectionView()->executeInTransaction("ConnectionModel::updateSignalName", [=, &connectionNode](){
213 
214             const QString source = signalHandlerProperty.source();
215 
216             connectionNode.signalHandlerProperty(newName).setSource(source);
217             connectionNode.removeProperty(signalHandlerProperty.name());
218         });
219 
220         QStandardItem* idItem = item(rowNumber, 0);
221         SignalHandlerProperty newSignalHandlerProperty = connectionNode.signalHandlerProperty(newName);
222         updateCustomData(idItem, newSignalHandlerProperty);
223     } else {
224         qWarning() << "BindingModel::updatePropertyName invalid property name";
225     }
226 }
227 
updateTargetNode(int rowNumber)228 void ConnectionModel::updateTargetNode(int rowNumber)
229 {
230     SignalHandlerProperty signalHandlerProperty = signalHandlerPropertyForRow(rowNumber);
231     const QString newTarget = data(index(rowNumber, TargetModelNodeRow)).toString();
232     ModelNode connectionNode = signalHandlerProperty.parentModelNode();
233 
234     const bool isAlias = newTarget.contains(".");
235     bool isSingleton = false;
236 
237     if (RewriterView* rv = connectionView()->rewriterView()) {
238         for (const QmlTypeData &data : rv->getQMLTypes()) {
239             if (!data.typeName.isEmpty()) {
240                 if (data.typeName == newTarget) {
241                     if (connectionView()->model()->metaInfo(data.typeName.toUtf8()).isValid()) {
242                         isSingleton = true;
243                         break;
244                     }
245                 } else if (isAlias) {
246                     if (data.typeName == newTarget.split(".").constFirst()) {
247                         if (connectionView()->model()->metaInfo(data.typeName.toUtf8()).isValid()) {
248                             isSingleton = true;
249                             break;
250                         }
251                     }
252                 }
253             }
254         }
255     }
256 
257     if (!newTarget.isEmpty()) {
258         //if it's a singleton, then let's reparent connections to rootNode,
259         //if it's an alias, then reparent to alias property owner:
260         const ModelNode parent = connectionView()->modelNodeForId((isSingleton || (isSingleton && isAlias))
261                                                                   ? connectionView()->rootModelNode().id()
262                                                                   : isAlias
263                                                                   ? newTarget.split(".").constFirst()
264                                                                   : newTarget);
265 
266         if (parent.isValid() && QmlItemNode::isValidQmlItemNode(parent))
267             parent.nodeListProperty("data").reparentHere(connectionNode);
268 
269         connectionView()->executeInTransaction("ConnectionModel::updateTargetNode", [= ,&connectionNode](){
270             connectionNode.bindingProperty("target").setExpression(newTarget);
271         });
272 
273     } else {
274         qWarning() << "BindingModel::updatePropertyName invalid target id";
275     }
276 }
277 
updateCustomData(QStandardItem * item,const SignalHandlerProperty & signalHandlerProperty)278 void ConnectionModel::updateCustomData(QStandardItem *item, const SignalHandlerProperty &signalHandlerProperty)
279 {
280     item->setData(signalHandlerProperty.parentModelNode().internalId(), UserRoles::InternalIdRole);
281     item->setData(signalHandlerProperty.name(), UserRoles::TargetPropertyNameRole);
282 }
283 
getTargetNodeForConnection(const ModelNode & connection) const284 ModelNode ConnectionModel::getTargetNodeForConnection(const ModelNode &connection) const
285 {
286     ModelNode result;
287 
288     const BindingProperty bindingProperty = connection.bindingProperty("target");
289     const QString bindExpression = bindingProperty.expression();
290 
291     if (bindingProperty.isValid()) {
292         if (bindExpression.contains(".")) {
293             QStringList substr = bindExpression.split(".");
294             const QString itemId = substr.constFirst();
295             if (substr.size() > 1) {
296                 const ModelNode aliasParent = connectionView()->modelNodeForId(itemId);
297                 substr.removeFirst(); //remove id, only alias pieces left
298                 const QString aliasBody = substr.join(".");
299                 if (aliasParent.isValid() && aliasParent.hasBindingProperty(aliasBody.toUtf8())) {
300                     const BindingProperty binding = aliasParent.bindingProperty(aliasBody.toUtf8());
301                     if (binding.isValid() && connectionView()->hasId(binding.expression())) {
302                         result = connectionView()->modelNodeForId(binding.expression());
303                     }
304                 }
305             }
306         } else {
307             result = connectionView()->modelNodeForId(bindExpression);
308         }
309     }
310 
311     return result;
312 }
313 
addConnection()314 void ConnectionModel::addConnection()
315 {
316     QmlDesignerPlugin::emitUsageStatistics(Constants::EVENT_CONNECTION_ADDED);
317 
318     ModelNode rootModelNode = connectionView()->rootModelNode();
319 
320     if (rootModelNode.isValid() && rootModelNode.metaInfo().isValid()) {
321 
322         NodeMetaInfo nodeMetaInfo = connectionView()->model()->metaInfo("QtQuick.Connections");
323 
324         if (nodeMetaInfo.isValid()) {
325             connectionView()->executeInTransaction("ConnectionModel::addConnection", [=, &rootModelNode](){
326                 ModelNode newNode = connectionView()->createModelNode("QtQuick.Connections",
327                                                                       nodeMetaInfo.majorVersion(),
328                                                                       nodeMetaInfo.minorVersion());
329                 QString source = "console.log(\"clicked\")";
330 
331                 if (connectionView()->selectedModelNodes().count() == 1) {
332                     ModelNode selectedNode = connectionView()->selectedModelNodes().constFirst();
333                     if (QmlItemNode::isValidQmlItemNode(selectedNode))
334                         selectedNode.nodeAbstractProperty("data").reparentHere(newNode);
335                     else
336                         rootModelNode.nodeAbstractProperty(rootModelNode.metaInfo().defaultPropertyName()).reparentHere(newNode);
337 
338                     if (QmlItemNode(selectedNode).isFlowActionArea() || QmlVisualNode(selectedNode).isFlowTransition())
339                         source = selectedNode.validId() + ".trigger()";
340 
341                     if (!connectionView()->selectedModelNodes().constFirst().id().isEmpty())
342                         newNode.bindingProperty("target").setExpression(selectedNode.validId());
343                 } else {
344                     rootModelNode.nodeAbstractProperty(rootModelNode.metaInfo().defaultPropertyName()).reparentHere(newNode);
345                     newNode.bindingProperty("target").setExpression(rootModelNode.validId());
346                 }
347 
348                 newNode.signalHandlerProperty("onClicked").setSource(source);
349             });
350         }
351     }
352 }
353 
bindingPropertyChanged(const BindingProperty & bindingProperty)354 void ConnectionModel::bindingPropertyChanged(const BindingProperty &bindingProperty)
355 {
356     if (isConnection(bindingProperty.parentModelNode()))
357         resetModel();
358 }
359 
variantPropertyChanged(const VariantProperty & variantProperty)360 void ConnectionModel::variantPropertyChanged(const VariantProperty &variantProperty)
361 {
362     if (isConnection(variantProperty.parentModelNode()))
363         resetModel();
364 }
365 
abstractPropertyChanged(const AbstractProperty & abstractProperty)366 void ConnectionModel::abstractPropertyChanged(const AbstractProperty &abstractProperty)
367 {
368     if (isConnection(abstractProperty.parentModelNode()))
369         resetModel();
370 }
371 
deleteConnectionByRow(int currentRow)372 void ConnectionModel::deleteConnectionByRow(int currentRow)
373 {
374     SignalHandlerProperty targetSignal = signalHandlerPropertyForRow(currentRow);
375     QTC_ASSERT(targetSignal.isValid(), return );
376     QmlDesigner::ModelNode node = targetSignal.parentModelNode();
377     QTC_ASSERT(node.isValid(), return );
378 
379     QList<SignalHandlerProperty> allSignals = node.signalProperties();
380     if (allSignals.size() > 1) {
381         if (allSignals.contains(targetSignal))
382             node.removeProperty(targetSignal.name());
383     } else {
384         node.destroy();
385     }
386 }
387 
removeRowFromTable(const SignalHandlerProperty & property)388 void ConnectionModel::removeRowFromTable(const SignalHandlerProperty &property)
389 {
390     for (int currentRow = 0; currentRow < rowCount(); currentRow++) {
391         SignalHandlerProperty targetSignal = signalHandlerPropertyForRow(currentRow);
392 
393         if (targetSignal == property) {
394             removeRow(currentRow);
395             break;
396         }
397     }
398 }
399 
handleException()400 void ConnectionModel::handleException()
401 {
402     QMessageBox::warning(nullptr, tr("Error"), m_exceptionError);
403     resetModel();
404 }
405 
handleDataChanged(const QModelIndex & topLeft,const QModelIndex & bottomRight)406 void ConnectionModel::handleDataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight)
407 {
408     if (topLeft != bottomRight) {
409         qWarning() << "ConnectionModel::handleDataChanged multi edit?";
410         return;
411     }
412 
413     m_lock = true;
414 
415     int currentColumn = topLeft.column();
416     int currentRow = topLeft.row();
417 
418     switch (currentColumn) {
419     case TargetModelNodeRow: {
420         updateTargetNode(currentRow);
421     } break;
422     case TargetPropertyNameRow: {
423         updateSignalName(currentRow);
424     } break;
425     case SourceRow: {
426         updateSource(currentRow);
427     } break;
428 
429     default: qWarning() << "ConnectionModel::handleDataChanged column" << currentColumn;
430     }
431 
432     m_lock = false;
433 }
434 
connectionView() const435 ConnectionView *ConnectionModel::connectionView() const
436 {
437     return m_connectionView;
438 }
439 
getSignalsForRow(int row) const440 QStringList ConnectionModel::getSignalsForRow(int row) const
441 {
442     QStringList stringList;
443     SignalHandlerProperty signalHandlerProperty = signalHandlerPropertyForRow(row);
444 
445     if (signalHandlerProperty.isValid())
446         stringList.append(getPossibleSignalsForConnection(signalHandlerProperty.parentModelNode()));
447 
448     return stringList;
449 }
450 
getflowActionTriggerForRow(int row) const451 QStringList ConnectionModel::getflowActionTriggerForRow(int row) const
452 {
453     QStringList stringList;
454     SignalHandlerProperty signalHandlerProperty = signalHandlerPropertyForRow(row);
455 
456     if (signalHandlerProperty.isValid()) {
457         const ModelNode parentModelNode = signalHandlerProperty.parentModelNode();
458         ModelNode targetNode = getTargetNodeForConnection(parentModelNode);
459         if (!targetNode.isValid() && !parentModelNode.isRootNode())
460              targetNode = parentModelNode.parentProperty().parentModelNode();
461          if (targetNode.isValid()) {
462              for (auto &node : targetNode.allSubModelNodesAndThisNode()) {
463                  if (QmlItemNode(node).isFlowActionArea() && node.hasId())
464                      stringList.append(node.id() + ".trigger()");
465              }
466          }
467      }
468      return stringList;
469 }
470 
getPossibleSignalsForConnection(const ModelNode & connection) const471 QStringList ConnectionModel::getPossibleSignalsForConnection(const ModelNode &connection) const
472 {
473     QStringList stringList;
474 
475     auto getAliasMetaSignals = [&](QString aliasPart, NodeMetaInfo metaInfo) {
476         if (metaInfo.isValid() && metaInfo.hasProperty(aliasPart.toUtf8())) {
477             NodeMetaInfo propertyMetaInfo = connectionView()->model()->metaInfo(
478                         metaInfo.propertyTypeName(aliasPart.toUtf8()));
479             if (propertyMetaInfo.isValid()) {
480                 return propertyNameListToStringList(propertyMetaInfo.signalNames());
481             }
482         }
483         return QStringList();
484     };
485 
486     if (connection.isValid()) {
487         //separate check for singletons
488         if (connection.hasBindingProperty("target")) {
489             const BindingProperty bp = connection.bindingProperty("target");
490 
491             if (bp.isValid()) {
492                 const QString bindExpression = bp.expression();
493 
494                 if (const RewriterView * const rv = connectionView()->rewriterView()) {
495                     for (const QmlTypeData &data : rv->getQMLTypes()) {
496                         if (!data.typeName.isEmpty()) {
497                             if (data.typeName == bindExpression) {
498                                 NodeMetaInfo metaInfo = connectionView()->model()->metaInfo(data.typeName.toUtf8());
499                                 if (metaInfo.isValid()) {
500                                     stringList << propertyNameListToStringList(metaInfo.signalNames());
501                                     break;
502                                 }
503                             } else if (bindExpression.contains(".")) {
504                                 //if it doesn't fit the same name, maybe it's an alias?
505                                 QStringList expression = bindExpression.split(".");
506                                 if ((expression.size() > 1) && (expression.constFirst() == data.typeName)) {
507                                     expression.removeFirst();
508 
509                                     stringList << getAliasMetaSignals(
510                                                       expression.join("."),
511                                                       connectionView()->model()->metaInfo(data.typeName.toUtf8()));
512                                     break;
513                                 }
514                             }
515                         }
516                     }
517                 }
518             }
519         }
520 
521         ModelNode targetNode = getTargetNodeForConnection(connection);
522         if (targetNode.isValid() && targetNode.metaInfo().isValid()) {
523             stringList.append(propertyNameListToStringList(targetNode.metaInfo().signalNames()));
524         } else {
525             //most likely it's component's internal alias:
526 
527             if (connection.hasBindingProperty("target")) {
528                 const BindingProperty bp = connection.bindingProperty("target");
529 
530                 if (bp.isValid()) {
531                     QStringList expression = bp.expression().split(".");
532                     if (expression.size() > 1) {
533                         const QString itemId = expression.constFirst();
534                         if (connectionView()->hasId(itemId)) {
535                             ModelNode parentItem = connectionView()->modelNodeForId(itemId);
536                             if (parentItem.isValid()
537                                     && parentItem.hasMetaInfo()
538                                     && parentItem.metaInfo().isValid()) {
539                                 expression.removeFirst();
540                                 stringList << getAliasMetaSignals(expression.join("."),
541                                                                   parentItem.metaInfo());
542                             }
543                         }
544                     }
545                 }
546             }
547         }
548     }
549 
550     return stringList;
551 }
552 
553 } // namespace Internal
554 
555 } // namespace QmlDesigner
556