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