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 "transitionitem.h"
27 #include "connectableitem.h"
28 #include "cornergrabberitem.h"
29 #include "finalstateitem.h"
30 #include "graphicsitemprovider.h"
31 #include "graphicsscene.h"
32 #include "parallelitem.h"
33 #include "sceneutils.h"
34 #include "scxmldocument.h"
35 #include "scxmleditorconstants.h"
36 #include "scxmltagutils.h"
37 #include "scxmluifactory.h"
38 #include "serializer.h"
39 #include "stateitem.h"
40 #include "tagtextitem.h"
41
42 #include <QBrush>
43 #include <QDebug>
44 #include <QGraphicsScene>
45 #include <QMenu>
46 #include <QPainter>
47 #include <QPen>
48 #include <QUndoStack>
49 #include <QtMath>
50
51 using namespace ScxmlEditor::PluginInterface;
52
53 const qreal SELECTION_DISTANCE = 10;
54
TransitionItem(BaseItem * parent)55 TransitionItem::TransitionItem(BaseItem *parent)
56 : BaseItem(parent)
57 , m_startTargetFactor(0.5, 0.5)
58 , m_endTargetFactor(0.5, 0.5)
59 {
60 setFlag(ItemIsSelectable, true);
61
62 m_highlightPen = QPen(QColor(0xff, 0x00, 0x60));
63 m_highlightPen.setWidth(8);
64 m_highlightPen.setJoinStyle(Qt::MiterJoin);
65
66 m_pen = QPen(QColor(0x12, 0x12, 0x12));
67 m_pen.setWidth(2);
68
69 m_arrow << QPointF(0, 0)
70 << QPointF(1, 1)
71 << QPointF(0, 1);
72
73 m_eventTagItem = new TagTextItem(this);
74 connect(m_eventTagItem, &TagTextItem::selected, this, [=](bool sel){
75 setItemSelected(sel);
76 });
77 connect(m_eventTagItem, &TagTextItem::textReady, this, &TransitionItem::textHasChanged);
78 connect(m_eventTagItem, &TagTextItem::movePointChanged, this, &TransitionItem::textItemPositionChanged);
79
80 checkWarningItems();
81 }
82
~TransitionItem()83 TransitionItem::~TransitionItem()
84 {
85 setBlockUpdates(true);
86 removeTransition(Start);
87 removeTransition(End);
88 //updateTarget();
89 }
90
checkWarningItems()91 void TransitionItem::checkWarningItems()
92 {
93 ScxmlUiFactory *uifactory = uiFactory();
94 if (uifactory) {
95 auto provider = static_cast<GraphicsItemProvider*>(uifactory->object("graphicsItemProvider"));
96 if (provider) {
97 if (!m_warningItem)
98 m_warningItem = static_cast<TransitionWarningItem*>(provider->createWarningItem(Constants::C_STATE_WARNING_TRANSITION, this));
99 }
100 }
101 }
102
setTag(ScxmlTag * tag)103 void TransitionItem::setTag(ScxmlTag *tag)
104 {
105 BaseItem::setTag(tag);
106 if (tag) {
107 if (tag->tagType() == InitialTransition)
108 m_eventTagItem->setVisible(false);
109 }
110 }
111
textItemPositionChanged()112 void TransitionItem::textItemPositionChanged()
113 {
114 QPointF p = m_eventTagItem->movePoint();
115 QString data;
116 if (p.toPoint() != QPoint(0, 0)) {
117 Serializer s;
118 s.append(p);
119 data = s.data();
120 }
121 setEditorInfo(Constants::C_SCXML_EDITORINFO_MOVEPOINT, data);
122
123 updateComponents();
124 }
125
textHasChanged(const QString & text)126 void TransitionItem::textHasChanged(const QString &text)
127 {
128 setTagValue("event", text);
129 }
130
createGrabbers()131 void TransitionItem::createGrabbers()
132 {
133 if (m_cornerGrabbers.count() != m_cornerPoints.count()) {
134 int selectedGrabberIndex = m_cornerGrabbers.indexOf(m_selectedCornerGrabber);
135
136 if (!m_cornerGrabbers.isEmpty()) {
137 qDeleteAll(m_cornerGrabbers);
138 m_cornerGrabbers.clear();
139 }
140
141 for (int i = 0; i < m_cornerPoints.count(); ++i) {
142 auto cornerGrabber = new CornerGrabberItem(this);
143 cornerGrabber->setGrabberType(CornerGrabberItem::Circle);
144 m_cornerGrabbers << cornerGrabber;
145 }
146
147 if (selectedGrabberIndex >= 0 && selectedGrabberIndex < m_cornerGrabbers.count())
148 m_selectedCornerGrabber = m_cornerGrabbers[selectedGrabberIndex];
149 else
150 m_selectedCornerGrabber = nullptr;
151 }
152
153 m_pen.setStyle(Qt::DotLine);
154
155 m_lineSelected = true;
156 updateGrabberPositions();
157 }
158
removeGrabbers()159 void TransitionItem::removeGrabbers()
160 {
161 if (!m_cornerGrabbers.isEmpty()) {
162 qDeleteAll(m_cornerGrabbers);
163 m_cornerGrabbers.clear();
164 }
165
166 m_lineSelected = false;
167 m_pen.setStyle(Qt::SolidLine);
168 }
169
updateGrabberPositions()170 void TransitionItem::updateGrabberPositions()
171 {
172 for (int i = 0; i < qMin(m_cornerGrabbers.count(), m_cornerPoints.count()); ++i)
173 m_cornerGrabbers[i]->setPos(m_cornerPoints[i]);
174 }
175
removeUnnecessaryPoints()176 void TransitionItem::removeUnnecessaryPoints()
177 {
178 if (m_cornerPoints.count() > 2) {
179 bool found = true;
180 while (found) {
181 found = false;
182 for (int i = 1; i < (m_cornerPoints.count() - 1); ++i) {
183 if (QLineF(m_cornerPoints[i], m_cornerPoints[i + 1]).length() <= 20 || QLineF(m_cornerPoints[i], m_cornerPoints[i - 1]).length() <= 20) {
184 m_cornerPoints.takeAt(i);
185 if (i < m_cornerGrabbers.count())
186 delete m_cornerGrabbers.takeAt(i);
187 found = true;
188 break;
189 }
190 }
191 }
192 }
193 storeValues();
194 updateComponents();
195 }
196
itemChange(GraphicsItemChange change,const QVariant & value)197 QVariant TransitionItem::itemChange(GraphicsItemChange change, const QVariant &value)
198 {
199 QVariant retValue = BaseItem::itemChange(change, value);
200
201 switch (change) {
202 case ItemSelectedChange:
203 if (!m_mouseGrabbed) {
204 if (value.toBool())
205 createGrabbers();
206 else
207 removeGrabbers();
208 }
209 break;
210 case ItemSceneHasChanged:
211 checkWarningItems();
212 break;
213 default:
214 break;
215 }
216
217 return retValue;
218 }
219
snapToAnyPoint(int id,const QPointF & newPoint,int diff)220 void TransitionItem::snapToAnyPoint(int id, const QPointF &newPoint, int diff)
221 {
222 // Check snap to grid
223 bool snappedX = false;
224 bool snappedY = false;
225 for (int i = 0; i < m_cornerPoints.count(); ++i) {
226 if (id != i) {
227 if (qAbs(newPoint.x() - m_cornerPoints[i].x()) < diff) {
228 m_cornerPoints[id].setX(m_cornerPoints[i].x());
229 snappedX = true;
230 }
231 if (qAbs(newPoint.y() - m_cornerPoints[i].y()) < diff) {
232 m_cornerPoints[id].setY(m_cornerPoints[i].y());
233 snappedY = true;
234 }
235 }
236 }
237
238 if (!snappedX)
239 m_cornerPoints[id].setX(newPoint.x());
240
241 if (!snappedY)
242 m_cornerPoints[id].setY(newPoint.y());
243 }
244
snapPointToPoint(int idSnap,const QPointF & p,int diff)245 void TransitionItem::snapPointToPoint(int idSnap, const QPointF &p, int diff)
246 {
247 if (idSnap >= 0 && static_cast<uint>(idSnap) < static_cast<uint>(m_cornerPoints.count())) {
248 if (qAbs(p.x() - m_cornerPoints[idSnap].x()) < diff)
249 m_cornerPoints[idSnap].setX(p.x());
250 if (qAbs(p.y() - m_cornerPoints[idSnap].y()) < diff)
251 m_cornerPoints[idSnap].setY(p.y());
252 }
253 }
254
mousePressEvent(QGraphicsSceneMouseEvent * event)255 void TransitionItem::mousePressEvent(QGraphicsSceneMouseEvent *event)
256 {
257 // We want to pan scene when Shift is pressed -> that's why ignore mouse-event here
258 if (event->modifiers() & Qt::ShiftModifier) {
259 event->ignore();
260 return;
261 }
262
263 QPointF p = event->pos();
264 bool bLeftButton = event->button() == Qt::LeftButton;
265 if (m_mouseGrabbed) {
266 if (bLeftButton) {
267 m_cornerPoints.append(p);
268 snapToAnyPoint(m_cornerPoints.count() - 1, p);
269
270 if (!m_cornerGrabbers.isEmpty()) {
271 auto corner = new CornerGrabberItem(this);
272 corner->setGrabberType(CornerGrabberItem::Circle);
273 corner->setPos(p);
274 m_cornerGrabbers.append(corner);
275 }
276 }
277 event->accept();
278 } else {
279 // Check QuickTransition
280 if (bLeftButton) {
281 // If we found QuickTransition-item or CornerGrabber at this point, we must ignore mouse press here
282 // So we can press QuickTransition/CornerGrabber item although there is transition lines front of these items
283 foreach (QGraphicsItem *item, scene()->items(event->scenePos())) {
284 if (item->type() == QuickTransitionType || (item->type() == CornerGrabberType && item->parentItem() != this)) {
285 event->ignore();
286 return;
287 }
288 }
289 }
290
291 // Check selection
292 bool sel = m_lineSelected;
293 int i;
294 int foundPointIndex = -1;
295 for (i = 0; i < m_cornerPoints.count(); ++i) {
296 if (QLineF(m_cornerPoints[i], p).length() <= SELECTION_DISTANCE) {
297 // Is pressed point close enough of the elbow-point
298 foundPointIndex = i;
299 sel = true;
300 break;
301 } else {
302 if (i < m_cornerPoints.count() - 1) {
303 QLineF line(m_cornerPoints[i], m_cornerPoints[i + 1]);
304
305 // Is pressed point close enough of the line
306 QPointF intersPoint;
307 QLineF line2(p, p + QPointF(SELECTION_DISTANCE, SELECTION_DISTANCE));
308 line2.setAngle(line.angle() + 90);
309 if (line.intersects(line2, &intersPoint) == QLineF::BoundedIntersection) {
310 sel = true;
311 } else {
312 line2.setAngle(line.angle() - 90);
313 sel = line.intersects(line2, &intersPoint) == QLineF::BoundedIntersection;
314 }
315
316 if (sel)
317 break;
318 }
319 }
320 }
321
322 // Create or remove grabbers
323 if (sel != m_lineSelected) {
324 if (sel)
325 createGrabbers();
326 else
327 removeGrabbers();
328
329 if (foundPointIndex > 0 && foundPointIndex < m_cornerGrabbers.count()) {
330 m_selectedCornerGrabber = m_cornerGrabbers[foundPointIndex];
331 m_selectedCornerGrabber->setSelected(true);
332 }
333 } else if (m_lineSelected && bLeftButton) {
334 m_cornerPoints.insert((i + 1), p);
335 m_selectedCornerGrabber = new CornerGrabberItem(this);
336 m_selectedCornerGrabber->setGrabberType(CornerGrabberItem::Circle);
337 m_selectedCornerGrabber->setPos(p);
338 m_cornerGrabbers.insert((i + 1), m_selectedCornerGrabber);
339 m_selectedCornerGrabber->setSelected(true);
340 } else if (m_lineSelected && !bLeftButton) {
341 showContextMenu(event);
342 }
343
344 if (sel)
345 event->accept();
346 else
347 event->ignore();
348 }
349 }
350
mouseMoveEvent(QGraphicsSceneMouseEvent * event)351 void TransitionItem::mouseMoveEvent(QGraphicsSceneMouseEvent *event)
352 {
353 // We want to pan scene when Shift is pressed -> that's why ignore mouse-event here
354 if (event->modifiers() & Qt::ShiftModifier) {
355 event->ignore();
356 return;
357 }
358
359 if (m_mouseGrabbed) {
360 setEndPos(event->pos());
361 event->ignore();
362 } else if (m_selectedCornerGrabber) {
363 snapToAnyPoint(m_cornerGrabbers.indexOf(m_selectedCornerGrabber), event->pos());
364 updateComponents();
365 storeValues();
366 BaseItem::mouseMoveEvent(event);
367 }
368 }
369
mouseReleaseEvent(QGraphicsSceneMouseEvent * event)370 void TransitionItem::mouseReleaseEvent(QGraphicsSceneMouseEvent *event)
371 {
372 // We want to pan scene when Shift is pressed -> that's why ignore mouse-event here
373 if (event->modifiers() & Qt::ShiftModifier) {
374 event->ignore();
375 return;
376 }
377
378 if (m_mouseGrabbed) {
379 if (event->button() == Qt::RightButton) {
380 connectToTopItem(mapToScene(event->pos()), TransitionItem::End, m_grabbedTargetType);
381 setSelected(false);
382 tag()->document()->undoStack()->endMacro();
383 m_mouseGrabbed = false;
384 ungrabMouse();
385 storeValues();
386 }
387 event->accept();
388 } else {
389 if (event->button() == Qt::LeftButton) {
390 if (m_selectedCornerGrabber) {
391 m_selectedCornerGrabber = nullptr;
392 setSelected(true);
393 }
394 removeUnnecessaryPoints();
395 }
396 BaseItem::mouseReleaseEvent(event);
397 }
398 }
399
checkSelectionBeforeContextMenu(QGraphicsSceneMouseEvent * e)400 void TransitionItem::checkSelectionBeforeContextMenu(QGraphicsSceneMouseEvent *e)
401 {
402 int ind = -1;
403 for (int i = 0; i < m_cornerGrabbers.count(); ++i) {
404 if (m_cornerGrabbers[i]->isSelected()) {
405 ind = i;
406 break;
407 }
408 }
409 m_selectedGrabberIndex = ind;
410 BaseItem::checkSelectionBeforeContextMenu(e);
411 }
412
createContextMenu(QMenu * menu)413 void TransitionItem::createContextMenu(QMenu *menu)
414 {
415 QVariantMap data;
416 if (m_selectedGrabberIndex > 0) {
417 data[Constants::C_SCXMLTAG_ACTIONTYPE] = TagUtils::RemovePoint;
418 data["cornerIndex"] = m_selectedGrabberIndex;
419 menu->addAction(tr("Remove Point"))->setData(data);
420 }
421
422 menu->addSeparator();
423 BaseItem::createContextMenu(menu);
424 }
425
selectedMenuAction(const QAction * action)426 void TransitionItem::selectedMenuAction(const QAction *action)
427 {
428 if (action) {
429 QVariantMap data = action->data().toMap();
430 int actionType = data.value(Constants::C_SCXMLTAG_ACTIONTYPE, -1).toInt();
431
432 switch (actionType) {
433 case TagUtils::RemovePoint: {
434 int ind = data.value("cornerIndex", 0).toInt();
435 if (ind > 0) {
436 delete m_cornerGrabbers.takeAt(ind);
437 m_cornerPoints.takeAt(ind);
438 updateComponents();
439 storeValues();
440 }
441 break;
442 }
443 default:
444 BaseItem::selectedMenuAction(action);
445 break;
446 }
447 }
448 }
449
keyPressEvent(QKeyEvent * event)450 void TransitionItem::keyPressEvent(QKeyEvent *event)
451 {
452 if (event->key() == Qt::Key_Delete || event->key() == Qt::Key_Backspace) {
453 bool bFound = false;
454 if (m_cornerGrabbers.count() > 2) {
455 for (int i = m_cornerGrabbers.count() - 1; i >= 1; i--) {
456 if (m_cornerGrabbers[i]->isSelected()) {
457 delete m_cornerGrabbers.takeAt(i);
458 m_cornerPoints.takeAt(i);
459 bFound = true;
460 }
461 }
462 }
463 if (bFound) {
464 updateComponents();
465 storeValues();
466 event->accept();
467 return;
468 }
469 }
470
471 BaseItem::keyPressEvent(event);
472 }
473
sceneEventFilter(QGraphicsItem * watched,QEvent * event)474 bool TransitionItem::sceneEventFilter(QGraphicsItem *watched, QEvent *event)
475 {
476 if (watched->type() == CornerGrabberType) {
477 auto c = qgraphicsitem_cast<CornerGrabberItem*>(watched);
478 auto mouseEvent = dynamic_cast<QGraphicsSceneMouseEvent*>(event);
479 if (!c || !mouseEvent)
480 return BaseItem::sceneEventFilter(watched, event);
481
482 int cid = m_cornerGrabbers.indexOf(c);
483 if (mouseEvent->type() == QEvent::GraphicsSceneMouseMove) {
484 if (mouseEvent->buttons() & Qt::LeftButton) {
485 QPointF movingPoint = c->pressedPoint() - mouseEvent->pos();
486
487 if (cid == 0) {
488 if (!m_movingFirstPoint) {
489 m_movingFirstPoint = true;
490 removeTransition(Start);
491 }
492 } else if (cid == (m_cornerPoints.count() - 1)) {
493 if (!m_movingLastPoint) {
494 m_movingLastPoint = true;
495 if (m_endItem)
496 removeTransition(End);
497 else {
498 updateZValue();
499 updateTargetType();
500 }
501 }
502 }
503
504 if (cid >= 0 && cid < m_cornerPoints.count())
505 snapToAnyPoint(cid, m_cornerPoints[cid] - movingPoint);
506
507 updateComponents();
508 }
509 return true;
510 } else if (mouseEvent->type() == QEvent::GraphicsSceneMouseRelease) {
511 if (mouseEvent->button() == Qt::LeftButton) {
512 if (cid == 0 || (cid == m_cornerPoints.count() - 1)) {
513 m_movingFirstPoint = false;
514 m_movingLastPoint = false;
515 connectToTopItem(watched->mapToScene(mouseEvent->pos()), cid == 0 ? Start : End, UnknownType);
516 }
517 removeUnnecessaryPoints();
518 } else
519 showContextMenu(mouseEvent);
520
521 storeValues();
522 return true;
523 }
524 }
525
526 return BaseItem::sceneEventFilter(watched, event);
527 }
528
removeTransition(TransitionPoint p)529 void TransitionItem::removeTransition(TransitionPoint p)
530 {
531 // Remove transition from the item
532 // Private function. Transition can itself remove connection from the item
533 if (p == Start && m_startItem) {
534 m_oldStartItem = m_startItem;
535 m_startItem->removeOutputTransition(this);
536 m_startItem = nullptr;
537 updateZValue();
538 updateTargetType();
539 if (m_oldStartItem)
540 m_oldStartItem->updateTransitions();
541 } else if (p == End && m_endItem) {
542 m_endItem->removeInputTransition(this);
543 m_endItem = nullptr;
544 updateZValue();
545 updateTargetType();
546 }
547 }
548
disconnectItem(ConnectableItem * item)549 void TransitionItem::disconnectItem(ConnectableItem *item)
550 {
551 // Disconnect item, normally called from the ConnectableItem
552 if (item == m_startItem)
553 removeTransition(Start);
554 if (item == m_endItem)
555 removeTransition(End);
556
557 updateTarget();
558 }
559
setStartItem(ConnectableItem * item)560 void TransitionItem::setStartItem(ConnectableItem *item)
561 {
562 m_oldStartItem = nullptr;
563 m_startItem = item;
564
565 if (item) {
566 if (tag())
567 tag()->document()->changeParent(tag(), m_startItem->tag());
568 item->addOutputTransition(this);
569
570 if (m_cornerPoints.isEmpty()) {
571 m_cornerPoints << sceneTargetPoint(TransitionPoint::Start);
572 m_cornerPoints << sceneTargetPoint(TransitionPoint::End);
573 }
574 }
575
576 updateZValue();
577 updateComponents();
578 storeValues();
579 }
580
startTransitionFrom(ConnectableItem * item,const QPointF & mouseScenePos)581 void TransitionItem::startTransitionFrom(ConnectableItem *item, const QPointF &mouseScenePos)
582 {
583 m_oldStartItem = nullptr;
584 m_startItem = item;
585 m_startItem->addOutputTransition(this);
586 m_cornerPoints.clear();
587 m_cornerPoints << sceneTargetPoint(TransitionPoint::Start);
588 m_cornerPoints << mapFromScene(mouseScenePos);
589
590 createGrabbers();
591 updateZValue();
592 updateComponents();
593 storeValues();
594 m_cornerGrabbers.last()->setSelected(true);
595 }
596
connectToTopItem(const QPointF & pos,TransitionPoint tp,ItemType targetType)597 void TransitionItem::connectToTopItem(const QPointF &pos, TransitionPoint tp, ItemType targetType)
598 {
599 int cornerPoints = m_cornerPoints.count();
600
601 ConnectableItem *parentItem = nullptr;
602 ScxmlTag *parentTag = nullptr;
603 ScxmlDocument *document = tag()->document();
604
605 snapToAnyPoint(m_cornerPoints.count() - 1, pos);
606 QPointF p(m_cornerPoints.last());
607
608 // First try to find parentItem
609 QList<QGraphicsItem*> items = scene()->items(p);
610 if (!items.isEmpty()) {
611 for (int i = 0; i < items.count(); ++i) {
612 ItemType type = ItemType(items[i]->type());
613 if ((targetType == UnknownType && type >= FinalStateType) || type >= StateType) {
614 auto it = qgraphicsitem_cast<ConnectableItem*>(items[i]);
615 if (it) {
616 parentItem = it;
617 parentTag = parentItem->tag();
618 break;
619 }
620 }
621 }
622 }
623
624 if (!parentTag && document)
625 parentTag = document->rootTag();
626
627 // Connect existing item
628 if (targetType == UnknownType) {
629 switch (tp) {
630 case Start:
631 if (parentItem) {
632 m_startTargetFactor = calculateTargetFactor(parentItem, pos);
633 savePoint(m_startTargetFactor * 100, Constants::C_SCXML_EDITORINFO_STARTTARGETFACTORS);
634 }
635 setStartItem(parentItem ? parentItem : m_oldStartItem);
636 break;
637 case End:
638 m_endTargetFactor = calculateTargetFactor(parentItem, pos);
639 savePoint(m_endTargetFactor * 100, Constants::C_SCXML_EDITORINFO_ENDTARGETFACTORS);
640 setEndItem(parentItem);
641 break;
642 default:
643 break;
644 }
645
646 setSelected(false);
647 if (parentItem)
648 parentItem->setSelected(true);
649 removeGrabbers();
650 if (m_startItem == m_endItem && cornerPoints == 2) {
651 setTagValue("type", "internal");
652 setEndItem(nullptr);
653 m_targetType = InternalNoTarget;
654 }
655
656 updateEventName();
657 storeValues();
658 } else {
659 // Create new item and connect to it
660 ConnectableItem *newItem = SceneUtils::createItem(targetType, parentItem ? parentItem->mapFromScene(p) : p);
661 if (newItem) {
662 ScxmlTag *newTag = SceneUtils::createTag(targetType, tag()->document());
663 newItem->setTag(newTag);
664 newItem->setParentItem(parentItem);
665 if (!parentItem)
666 scene()->addItem(newItem);
667
668 newItem->addInputTransition(this);
669 newItem->updateAttributes();
670 newItem->updateEditorInfo();
671 newItem->updateUIProperties();
672
673 if (parentItem)
674 parentItem->updateUIProperties();
675
676 if (document)
677 document->addTag(parentTag, newTag);
678
679 setEndItem(newItem);
680 setSelected(false);
681 newItem->setSelected(true);
682 }
683
684 removeGrabbers();
685 }
686
687 updateTargetType();
688 }
689
setEndPos(const QPointF & endPos,bool snap)690 void TransitionItem::setEndPos(const QPointF &endPos, bool snap)
691 {
692 if (m_cornerPoints.count() >= 2) {
693 m_cornerPoints.last().setX(endPos.x());
694 m_cornerPoints.last().setY(endPos.y());
695
696 if (snap)
697 snapToAnyPoint(m_cornerPoints.count() - 1, endPos);
698 updateComponents();
699 storeValues();
700 }
701 }
702
setEndItem(ConnectableItem * item,bool fixValue)703 void TransitionItem::setEndItem(ConnectableItem *item, bool fixValue)
704 {
705
706 if (item) {
707 m_endItem = item;
708 item->addInputTransition(this);
709 setEndPos(sceneTargetPoint(End), false);
710
711 if (m_cornerPoints.count() > 2)
712 snapPointToPoint(m_cornerPoints.count() - 2, m_cornerPoints.last(), 15);
713 } else {
714 removeTransition(End);
715 updateComponents();
716 storeValues();
717 }
718
719 updateZValue();
720 updateTarget(fixValue);
721 }
722
loadPoint(const QString & name)723 QPointF TransitionItem::loadPoint(const QString &name)
724 {
725 Serializer s;
726 QPointF p;
727 s.setData(editorInfo(name));
728 s.read(p);
729 return p;
730 }
731
savePoint(const QPointF & p,const QString & name)732 void TransitionItem::savePoint(const QPointF &p, const QString &name)
733 {
734 Serializer s;
735 s.append(p);
736 setEditorInfo(name, s.data(), true);
737 }
738
calculateTargetFactor(ConnectableItem * item,const QPointF & pos)739 QPointF TransitionItem::calculateTargetFactor(ConnectableItem *item, const QPointF &pos)
740 {
741 if (item) {
742 QRectF r = item->sceneBoundingRect().adjusted(-8, -8, 8, 8);
743 QPointF pixelFactorPoint = pos - r.topLeft();
744 QPointF normalizedPoint(qBound(0.0, pixelFactorPoint.x() / r.width(), 1.0), qBound(0.0, pixelFactorPoint.y() / r.height(), 1.0));
745
746 // Center point if close enough
747 if (qAbs(normalizedPoint.x() - 0.5) < 0.2 && qAbs(normalizedPoint.y() - 0.5) < 0.2)
748 return QPointF(0.5, 0.5);
749
750 return normalizedPoint;
751 }
752
753 return QPointF(0.5, 0.5);
754 }
755
sceneTargetPoint(TransitionPoint p)756 QPointF TransitionItem::sceneTargetPoint(TransitionPoint p)
757 {
758 ConnectableItem *item = nullptr;
759 QPointF factorPoint;
760 if (p == TransitionPoint::Start) {
761 item = m_startItem;
762 factorPoint = m_startTargetFactor;
763 } else {
764 if (m_endItem) {
765 item = m_endItem;
766 factorPoint = m_endTargetFactor;
767 } else {
768 item = m_startItem;
769 factorPoint = QPointF(0.5, 0.5);
770 }
771 }
772
773 QRectF r;
774 if (item)
775 r = item->sceneBoundingRect();
776
777 return r.topLeft() + QPointF(factorPoint.x() * r.width(), factorPoint.y() * r.height());
778 }
779
findIntersectionPoint(ConnectableItem * item,const QLineF & line,const QPointF & defaultPoint)780 QPointF TransitionItem::findIntersectionPoint(ConnectableItem *item, const QLineF &line, const QPointF &defaultPoint)
781 {
782 // Check circles
783 ItemType t = ItemType(item->type());
784 if (t >= InitialStateType && t <= HistoryType) {
785 QLineF l = item == m_endItem ? line : QLineF(line.p2(), line.p1());
786 l.setLength(l.length() - qMin(item->boundingRect().width(), item->boundingRect().height()) * 0.45);
787 return l.p2();
788 }
789
790 // Find intersection point between line and target item
791 QPolygonF itemPolygon = item->polygonShape();
792 if (!itemPolygon.isEmpty()) {
793 QPointF intersectPoint;
794 QPointF p1 = itemPolygon.at(0) + item->scenePos();
795 QPointF p2;
796 QLineF checkLine;
797 for (int i = 1; i < itemPolygon.count(); ++i) {
798 p2 = itemPolygon.at(i) + item->scenePos();
799 checkLine = QLineF(p1, p2);
800 if (checkLine.intersects(line, &intersectPoint) == QLineF::BoundedIntersection)
801 return intersectPoint;
802 p1 = p2;
803 }
804 }
805
806 return defaultPoint;
807 }
808
updateComponents()809 void TransitionItem::updateComponents()
810 {
811 if (m_cornerPoints.count() < 2)
812 return;
813
814 // Check if we must move all points together
815 bool movePoints = (SceneUtils::isSomeSelected(m_startItem) && SceneUtils::isSomeSelected(m_endItem) && m_cornerPoints.count() > 2) || (m_movingFirstPoint && m_targetType == InternalNoTarget);
816
817 if (!movePoints && !m_mouseGrabbed) {
818 // Check the corners which are in the same line if cornerGrabbers are hidden
819 if (m_cornerGrabbers.isEmpty() && m_cornerPoints.count() > 2) {
820 for (int i = 0; i < m_cornerPoints.count() - 2; ++i) {
821 if (m_cornerPoints[i] != m_cornerPoints[i + 1] && m_cornerPoints[i] != m_cornerPoints[i + 2]) {
822 QLineF l1(m_cornerPoints[i], m_cornerPoints[i + 1]);
823 QLineF l2(m_cornerPoints[i], m_cornerPoints[i + 2]);
824
825 if (qRound(l1.angle()) == qRound(l2.angle())) {
826 m_cornerPoints.takeAt(i + 1);
827 storeValues();
828 break;
829 }
830 }
831 }
832 }
833 }
834
835 if ((m_movingFirstPoint || m_movingLastPoint) && !(m_movingFirstPoint && m_targetType == InternalNoTarget))
836 movePoints = false;
837
838 // Init first line
839 QLineF firstLine(m_cornerPoints[0], m_cornerPoints[1]);
840 if (m_startItem) {
841 if (m_targetType <= InternalNoTarget) {
842 QPointF p = m_startItem->getInternalPosition(this, m_targetType);
843 firstLine.setP1(p);
844 firstLine.setP2(p + QPointF(20, 0));
845 m_cornerPoints[1] = firstLine.p2();
846 if (!(m_movingFirstPoint && m_targetType == InternalNoTarget))
847 movePoints = false;
848 } else {
849 firstLine.setP1(sceneTargetPoint(TransitionPoint::Start));
850 firstLine.setP1(findIntersectionPoint(m_startItem, firstLine, firstLine.p1()));
851 }
852 }
853
854 if (movePoints) {
855 if (m_movingFirstPoint && m_targetType == InternalNoTarget)
856 m_cornerPoints[1] = m_cornerPoints[0] + QPointF(20, 0);
857 else {
858 QPointF movingPoint = firstLine.p1() - m_cornerPoints[0];
859 for (int i = 0; i < m_cornerPoints.count(); ++i)
860 m_cornerPoints[i] += movingPoint;
861
862 for (int i = 0; i < m_arrow.count(); ++i)
863 m_arrow[i] += movingPoint;
864 }
865 }
866
867 m_cornerPoints[0] = firstLine.p1();
868
869 // Init last line
870 int lastIndex = m_cornerPoints.count() - 1;
871 QLineF lastLine(m_cornerPoints[lastIndex - 1], m_cornerPoints[lastIndex]);
872 if (m_endItem && m_targetType > InternalNoTarget) {
873 lastLine.setP2(sceneTargetPoint(TransitionPoint::End));
874 lastLine.setP2(findIntersectionPoint(m_endItem, lastLine, lastLine.p2()));
875 m_cornerPoints[lastIndex] = lastLine.p2();
876 }
877
878 m_arrowAngle = 0;
879 if (m_targetType == InternalSameTarget) {
880 m_arrowAngle = M_PI * 0.6;
881 } else {
882 // Calculate angle of the lastLine and update arrow
883 if (lastLine.length() > 0) {
884 m_arrowAngle = qAcos(lastLine.dx() / lastLine.length()) + M_PI;
885 if (lastLine.dy() >= 0)
886 m_arrowAngle = (2 * M_PI) - m_arrowAngle;
887 }
888 }
889
890 m_arrow[0] = lastLine.p2() + QPointF(qSin(m_arrowAngle + M_PI / 3) * m_arrowSize, qCos(m_arrowAngle + M_PI / 3) * m_arrowSize);
891 m_arrow[1] = lastLine.p2();
892 m_arrow[2] = lastLine.p2() + QPointF(qSin(m_arrowAngle + M_PI - M_PI / 3) * m_arrowSize, qCos(m_arrowAngle + M_PI - M_PI / 3) * m_arrowSize);
893
894 setItemBoundingRect(m_cornerPoints.boundingRect().normalized().adjusted(-SELECTION_DISTANCE, -SELECTION_DISTANCE,
895 SELECTION_DISTANCE, SELECTION_DISTANCE));
896
897 // Set the right place for the name of the transition
898 int ind = m_cornerPoints.count() / 2 - 1;
899 QLineF nameLine(m_cornerPoints[ind], m_cornerPoints[ind + 1]);
900 if (m_targetType <= InternalNoTarget) {
901 m_eventTagItem->setPos(m_cornerPoints[1].x() + 6, m_cornerPoints[1].y() - m_eventTagItem->boundingRect().height() / 3);
902 } else {
903 const qreal w2 = m_eventTagItem->boundingRect().width() / 2;
904 QPointF startPos = nameLine.pointAt(0.5);
905 QLineF targetLine(startPos, startPos + QPointF(SELECTION_DISTANCE, SELECTION_DISTANCE));
906 targetLine.setAngle(nameLine.angle() + 90);
907
908 m_eventTagItem->setPos(targetLine.p2() + m_eventTagItem->movePoint() - QPointF(w2, m_eventTagItem->boundingRect().height() / 2));
909 }
910
911 if (m_warningItem)
912 m_warningItem->setPos(m_eventTagItem->pos() - QPointF(WARNING_ITEM_SIZE, 0));
913 updateGrabberPositions();
914 updateZValue();
915 }
916
grabMouse(ItemType targetType)917 void TransitionItem::grabMouse(ItemType targetType)
918 {
919 m_grabbedTargetType = targetType;
920 m_mouseGrabbed = true;
921 BaseItem::grabMouse();
922 }
923
paint(QPainter * painter,const QStyleOptionGraphicsItem * option,QWidget * widget)924 void TransitionItem::paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget)
925 {
926 Q_UNUSED(option)
927 Q_UNUSED(widget)
928
929 painter->save();
930
931 painter->setRenderHint(QPainter::Antialiasing, true);
932 painter->setPen(m_pen);
933
934 if (m_cornerPoints.count() >= 2) {
935 if (m_targetType == InternalSameTarget) {
936 QRectF rect(m_cornerPoints[0].x(), m_cornerPoints[0].y() - SELECTION_DISTANCE,
937 m_cornerPoints[1].x() - m_cornerPoints[0].x(),
938 SELECTION_DISTANCE * 2);
939 painter->drawArc(rect, 0, 180 * 16);
940 } else {
941 if (highlight()) {
942 painter->setPen(m_highlightPen);
943 painter->drawPolyline(m_cornerPoints);
944 }
945 painter->setPen(m_pen);
946 painter->drawPolyline(m_cornerPoints);
947 }
948 }
949
950 for (int i = 0; i < m_cornerPoints.count() - 1; ++i)
951 painter->drawEllipse(m_cornerPoints[i], 2, 2);
952
953 if (highlight()) {
954 painter->setPen(m_highlightPen);
955 painter->drawPolyline(m_arrow);
956 }
957
958 painter->setPen(m_pen);
959 painter->drawPolyline(m_arrow);
960
961 painter->restore();
962 }
963
updateEditorInfo(bool allChilds)964 void TransitionItem::updateEditorInfo(bool allChilds)
965 {
966 BaseItem::updateEditorInfo(allChilds);
967
968 const QColor fontColor = editorInfo(Constants::C_SCXML_EDITORINFO_FONTCOLOR);
969 m_eventTagItem->setDefaultTextColor(fontColor.isValid() ? fontColor : Qt::black);
970
971 const QColor stateColor = editorInfo(Constants::C_SCXML_EDITORINFO_STATECOLOR);
972 m_pen.setColor(stateColor.isValid() ? stateColor : qRgb(0x12, 0x12, 0x12));
973 }
974
updateTarget(bool fixValue)975 void TransitionItem::updateTarget(bool fixValue)
976 {
977 if (fixValue)
978 setTagValue("target", m_endItem ? m_endItem->itemId() : QString());
979 if (m_endItem)
980 m_endItem->checkInitial(true);
981 }
982
updateAttributes()983 void TransitionItem::updateAttributes()
984 {
985 BaseItem::updateAttributes();
986
987 // Find new target
988 if (!m_endItem || tagValue("target") != m_endItem->itemId()) {
989 if (m_endItem)
990 m_endItem->removeInputTransition(this);
991
992 m_endItem = nullptr;
993 findEndItem();
994 updateTarget(false);
995 updateZValue();
996 }
997
998 // Set event-name
999 updateEventName();
1000 updateTargetType();
1001 }
1002
init(ScxmlTag * tag,BaseItem * parentItem,bool initChildren,bool blockUpdates)1003 void TransitionItem::init(ScxmlTag *tag, BaseItem *parentItem, bool initChildren, bool blockUpdates)
1004 {
1005 Q_UNUSED(initChildren)
1006 setBlockUpdates(blockUpdates);
1007
1008 setTag(tag);
1009 setParentItem(parentItem);
1010 updateEditorInfo();
1011
1012 if (blockUpdates)
1013 setBlockUpdates(false);
1014 }
1015
readUISpecifiedProperties(const ScxmlTag * tag)1016 void TransitionItem::readUISpecifiedProperties(const ScxmlTag *tag)
1017 {
1018 if (tag) {
1019 if (m_cornerPoints.count() >= 2) {
1020 while (m_cornerPoints.count() > 2)
1021 m_cornerPoints.takeAt(1);
1022
1023 Serializer s;
1024
1025 QPointF p = loadPoint(Constants::C_SCXML_EDITORINFO_STARTTARGETFACTORS);
1026 if (p.isNull())
1027 p = QPointF(50, 50);
1028 m_startTargetFactor = p / 100;
1029
1030 p = loadPoint(Constants::C_SCXML_EDITORINFO_ENDTARGETFACTORS);
1031 if (p.isNull())
1032 p = QPointF(50, 50);
1033 m_endTargetFactor = p / 100;
1034
1035 QString localPointsData = editorInfo(Constants::C_SCXML_EDITORINFO_LOCALGEOMETRY);
1036 if (!localPointsData.isEmpty()) {
1037 QPointF startPos = sceneTargetPoint(Start);
1038 QPolygonF localPoints;
1039 s.setData(localPointsData);
1040 s.read(localPoints);
1041 for (int i = 0; i < localPoints.count(); ++i)
1042 m_cornerPoints.insert(i + 1, startPos + localPoints[i]);
1043 } else {
1044 QPolygonF scenePoints;
1045 s.setData(editorInfo(Constants::C_SCXML_EDITORINFO_GEOMETRY));
1046 s.read(scenePoints);
1047 for (int i = 0; i < scenePoints.count(); ++i)
1048 m_cornerPoints.insert(i + 1, scenePoints[i]);
1049 }
1050
1051 m_eventTagItem->resetMovePoint(loadPoint(Constants::C_SCXML_EDITORINFO_MOVEPOINT));
1052
1053 if (m_lineSelected)
1054 createGrabbers();
1055 updateComponents();
1056 }
1057 }
1058 }
1059
finalizeCreation()1060 void TransitionItem::finalizeCreation()
1061 {
1062 bool old = blockUpdates();
1063 setBlockUpdates(true);
1064
1065 updateAttributes();
1066
1067 if (!old)
1068 setBlockUpdates(false);
1069 }
1070
checkVisibility(double scaleFactor)1071 void TransitionItem::checkVisibility(double scaleFactor)
1072 {
1073 m_eventTagItem->setVisible(scaleFactor > 0.5);
1074 }
1075
containsScenePoint(const QPointF & p) const1076 bool TransitionItem::containsScenePoint(const QPointF &p) const
1077 {
1078 QPointF pp = mapFromScene(p);
1079
1080 for (int i = 0; i < m_cornerPoints.count() - 1; ++i) {
1081 QLineF line(m_cornerPoints[i], m_cornerPoints[i + 1]);
1082
1083 // Is point close enough of the line
1084 QPointF intersPoint;
1085 QLineF line2(pp, pp + QPointF(SELECTION_DISTANCE, SELECTION_DISTANCE));
1086 line2.setAngle(line.angle() + 90);
1087 if (line.intersects(line2, &intersPoint) == QLineF::BoundedIntersection) {
1088 return true;
1089 } else {
1090 line2.setAngle(line.angle() - 90);
1091 if (line.intersects(line2, &intersPoint) == QLineF::BoundedIntersection)
1092 return true;
1093 }
1094 }
1095 return false;
1096 }
1097
findEndItem()1098 void TransitionItem::findEndItem()
1099 {
1100 QString targetId = tagValue("target");
1101 if (!m_endItem && !targetId.isEmpty()) {
1102 QList<QGraphicsItem*> items = scene()->items();
1103 for (int i = 0; i < items.count(); ++i) {
1104 if (items[i]->type() >= FinalStateType) {
1105 auto item = qgraphicsitem_cast<ConnectableItem*>(items[i]);
1106 if (item && item->itemId() == targetId) {
1107 setEndItem(item, false);
1108 break;
1109 }
1110 }
1111 }
1112 }
1113 }
1114
updateEventName()1115 void TransitionItem::updateEventName()
1116 {
1117 m_eventTagItem->setText(tagValue("event"));
1118 }
1119
storeGeometry(bool block)1120 void TransitionItem::storeGeometry(bool block)
1121 {
1122 if (tag()) {
1123 if (m_cornerPoints.count() <= 2) {
1124 setEditorInfo(Constants::C_SCXML_EDITORINFO_GEOMETRY, QString(), block);
1125 setEditorInfo(Constants::C_SCXML_EDITORINFO_LOCALGEOMETRY, QString(), block);
1126 } else {
1127 QPolygonF pol = m_cornerPoints;
1128 pol.takeFirst();
1129 pol.takeLast();
1130 Serializer s;
1131 for (int i = 0; i < pol.count(); ++i) {
1132 QPointF spos = sceneTargetPoint(Start);
1133 pol[i].setX(pol[i].x() - spos.x());
1134 pol[i].setY(pol[i].y() - spos.y());
1135 }
1136 s.append(pol);
1137
1138 setEditorInfo(Constants::C_SCXML_EDITORINFO_LOCALGEOMETRY, s.data(), block);
1139 }
1140 }
1141 }
1142
storeMovePoint(bool block)1143 void TransitionItem::storeMovePoint(bool block)
1144 {
1145 if (m_eventTagItem->movePoint().toPoint() == QPoint(0, 0))
1146 setEditorInfo(Constants::C_SCXML_EDITORINFO_MOVEPOINT, QString(), block);
1147 else
1148 savePoint(m_eventTagItem->movePoint(), Constants::C_SCXML_EDITORINFO_MOVEPOINT);
1149 }
1150
storeTargetFactors(bool block)1151 void TransitionItem::storeTargetFactors(bool block)
1152 {
1153 if (m_startTargetFactor == QPointF(0.5, 0.5))
1154 setEditorInfo(Constants::C_SCXML_EDITORINFO_STARTTARGETFACTORS, QString(), block);
1155 else
1156 savePoint(m_startTargetFactor * 100, Constants::C_SCXML_EDITORINFO_STARTTARGETFACTORS);
1157
1158 if (m_endTargetFactor == QPointF(0.5, 0.5))
1159 setEditorInfo(Constants::C_SCXML_EDITORINFO_ENDTARGETFACTORS, QString(), block);
1160 else
1161 savePoint(m_endTargetFactor * 100, Constants::C_SCXML_EDITORINFO_ENDTARGETFACTORS);
1162 }
1163
storeValues(bool block)1164 void TransitionItem::storeValues(bool block)
1165 {
1166 storeGeometry(block);
1167 storeMovePoint(block);
1168 storeTargetFactors(block);
1169 }
1170
updateUIProperties()1171 void TransitionItem::updateUIProperties()
1172 {
1173 if (tag() && isActiveScene())
1174 storeValues();
1175 }
1176
updateTargetType()1177 void TransitionItem::updateTargetType()
1178 {
1179 if (m_movingFirstPoint && m_targetType == InternalNoTarget)
1180 return;
1181
1182 TransitionTargetType type = ExternalTarget;
1183
1184 if (m_startItem && m_startItem == m_endItem)
1185 type = InternalSameTarget;
1186 else if (m_startItem && !m_endItem) {
1187 if (m_movingLastPoint) {
1188 type = ExternalNoTarget;
1189 } else {
1190 QRectF srect = m_startItem->sceneBoundingRect();
1191 if (srect.contains(m_cornerPoints.last()))
1192 type = InternalNoTarget;
1193 else
1194 type = ExternalNoTarget;
1195 }
1196 } else {
1197 type = ExternalTarget;
1198 }
1199
1200 if (type <= InternalNoTarget) {
1201 m_eventTagItem->resetMovePoint();
1202 m_arrowSize = 6;
1203 // Remove extra points
1204 while (m_cornerPoints.count() > 2)
1205 m_cornerPoints.takeAt(1);
1206 while (m_cornerGrabbers.count() > 2)
1207 delete m_cornerGrabbers.takeAt(1);
1208
1209 // Remove editorinfo.geometry
1210 setEditorInfo(Constants::C_SCXML_EDITORINFO_GEOMETRY, QString(), true);
1211 setEditorInfo(Constants::C_SCXML_EDITORINFO_LOCALGEOMETRY, QString(), true);
1212 setEditorInfo(Constants::C_SCXML_EDITORINFO_MOVEPOINT, QString(), true);
1213 setEditorInfo(Constants::C_SCXML_EDITORINFO_STARTTARGETFACTORS, QString(), true);
1214 setEditorInfo(Constants::C_SCXML_EDITORINFO_ENDTARGETFACTORS, QString(), true);
1215 } else {
1216 m_arrowSize = 10;
1217 }
1218
1219 if (m_targetType != type) {
1220 m_targetType = type;
1221 if (m_warningItem)
1222 m_warningItem->check();
1223 if (m_startItem && !m_startItem->blockUpdates()) {
1224 m_startItem->updateOutputTransitions();
1225 if (m_startItem->type() >= StateType)
1226 static_cast<StateItem*>(m_startItem)->updateBoundingRect();
1227 }
1228 }
1229 }
1230
connectedItem(const ConnectableItem * other) const1231 ConnectableItem *TransitionItem::connectedItem(const ConnectableItem *other) const
1232 {
1233 if (other) {
1234 if (other == m_startItem)
1235 return m_endItem;
1236 else if (other == m_endItem)
1237 return m_startItem;
1238 }
1239
1240 return nullptr;
1241 }
1242
isStartItem(const ConnectableItem * item) const1243 bool TransitionItem::isStartItem(const ConnectableItem *item) const
1244 {
1245 return m_startItem == item;
1246 }
1247
isEndItem(const ConnectableItem * item) const1248 bool TransitionItem::isEndItem(const ConnectableItem *item) const
1249 {
1250 return m_endItem == item;
1251 }
1252
hasStartItem() const1253 bool TransitionItem::hasStartItem() const
1254 {
1255 return m_startItem != nullptr;
1256 }
1257
hasEndItem() const1258 bool TransitionItem::hasEndItem() const
1259 {
1260 return m_endItem != nullptr;
1261 }
1262
updateZValue()1263 void TransitionItem::updateZValue()
1264 {
1265 setZValue(qMax(m_startItem ? (m_startItem->zValue() + 1) : 1, m_endItem ? (m_endItem->zValue() + 1) : 1));
1266 }
1267
textWidth() const1268 qreal TransitionItem::textWidth() const
1269 {
1270 return m_eventTagItem->boundingRect().width();
1271 }
1272
wholeBoundingRect() const1273 QRectF TransitionItem::wholeBoundingRect() const
1274 {
1275 return boundingRect().united(m_eventTagItem->sceneBoundingRect());
1276 }
1277