/**************************************************************************** ** ** Copyright (C) 2016 The Qt Company Ltd. ** Contact: https://www.qt.io/licensing/ ** ** This file is part of Qt Creator. ** ** Commercial License Usage ** Licensees holding valid commercial Qt licenses may use this file in ** accordance with the commercial license agreement provided with the ** Software or, alternatively, in accordance with the terms contained in ** a written agreement between you and The Qt Company. For licensing terms ** and conditions see https://www.qt.io/terms-conditions. For further ** information use the contact form at https://www.qt.io/contact-us. ** ** GNU General Public License Usage ** Alternatively, this file may be used under the terms of the GNU ** General Public License version 3 as published by the Free Software ** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT ** included in the packaging of this file. Please review the following ** information to ensure the GNU General Public License requirements will ** be met: https://www.gnu.org/licenses/gpl-3.0.html. ** ****************************************************************************/ #include "graphicsscene.h" #include "actionhandler.h" #include "actionprovider.h" #include "finalstateitem.h" #include "initialstateitem.h" #include "parallelitem.h" #include "sceneutils.h" #include "scxmleditorconstants.h" #include "scxmltagutils.h" #include "scxmluifactory.h" #include "snapline.h" #include "stateitem.h" #include "transitionitem.h" #include "utilsprovider.h" #include "warning.h" #include "warningitem.h" #include #include #include #include #include #include #include #include using namespace ScxmlEditor::PluginInterface; GraphicsScene::GraphicsScene(QObject *parent) : QGraphicsScene(parent) { //setMinimumRenderSize(5); setItemIndexMethod(QGraphicsScene::NoIndex); } GraphicsScene::~GraphicsScene() { clear(); } void GraphicsScene::unselectAll() { const QList selectedItems = this->selectedItems(); foreach (QGraphicsItem *it, selectedItems) it->setSelected(false); if (m_document) m_document->setCurrentTag(0); } void GraphicsScene::unhighlightAll() { foreach (BaseItem *it, m_baseItems) it->setHighlight(false); } void GraphicsScene::highlightItems(const QVector &lstIds) { foreach (BaseItem *it, m_baseItems) it->setHighlight(lstIds.contains(it->tag())); } QRectF GraphicsScene::selectedBoundingRect() const { QRectF r; foreach (BaseItem *item, m_baseItems) { if (item->isSelected()) r = r.united(item->sceneBoundingRect()); } return r; } qreal GraphicsScene::selectedMaxWidth() const { qreal maxw = 0; foreach (BaseItem *item, m_baseItems) { if (item->isSelected() && item->type() >= InitialStateType) maxw = qMax(maxw, item->sceneBoundingRect().width()); } return maxw; } qreal GraphicsScene::selectedMaxHeight() const { qreal maxh = 0; foreach (BaseItem *item, m_baseItems) { if (item->isSelected() && item->type() >= InitialStateType) maxh = qMax(maxh, item->sceneBoundingRect().height()); } return maxh; } void GraphicsScene::alignStates(int alignType) { if (alignType >= ActionAlignLeft && alignType <= ActionAlignVertical) { m_document->undoStack()->beginMacro(tr("Align states")); QRectF r = selectedBoundingRect(); if (r.isValid()) { switch (alignType) { case ActionAlignLeft: foreach (BaseItem *item, m_baseItems) { if (item->isSelected() && item->type() >= InitialStateType) item->moveStateBy(r.left() - item->sceneBoundingRect().left(), 0); } break; case ActionAlignRight: foreach (BaseItem *item, m_baseItems) { if (item->isSelected() && item->type() >= InitialStateType) item->moveStateBy(r.right() - item->sceneBoundingRect().right(), 0); } break; case ActionAlignTop: foreach (BaseItem *item, m_baseItems) { if (item->isSelected() && item->type() >= InitialStateType) item->moveStateBy(0, r.top() - item->sceneBoundingRect().top()); } break; case ActionAlignBottom: foreach (BaseItem *item, m_baseItems) { if (item->isSelected() && item->type() >= InitialStateType) item->moveStateBy(0, r.bottom() - item->sceneBoundingRect().bottom()); } break; case ActionAlignHorizontal: foreach (BaseItem *item, m_baseItems) { if (item->isSelected() && item->type() >= InitialStateType) item->moveStateBy(0, r.center().y() - item->sceneBoundingRect().center().y()); } break; case ActionAlignVertical: foreach (BaseItem *item, m_baseItems) { if (item->isSelected() && item->type() >= InitialStateType) item->moveStateBy(r.center().x() - item->sceneBoundingRect().center().x(), 0); } break; default: break; } } m_document->undoStack()->endMacro(); } } void GraphicsScene::adjustStates(int adjustType) { if (adjustType >= ActionAdjustWidth && adjustType <= ActionAdjustSize) { m_document->undoStack()->beginMacro(tr("Adjust states")); qreal maxw = selectedMaxWidth(); qreal maxh = selectedMaxHeight(); foreach (BaseItem *item, m_baseItems) { if (item->isSelected() && item->type() >= InitialStateType) { QRectF rr = item->boundingRect(); if ((adjustType == ActionAdjustWidth || adjustType == ActionAdjustSize) && !qFuzzyCompare(rr.width(), maxw)) rr.setWidth(maxw); if ((adjustType == ActionAdjustHeight || adjustType == ActionAdjustSize) && !qFuzzyCompare(rr.height(), maxh)) rr.setHeight(maxh); item->setItemBoundingRect(rr); qgraphicsitem_cast(item)->updateTransitions(true); } } m_document->undoStack()->endMacro(); } } void GraphicsScene::cut() { m_document->undoStack()->beginMacro(tr("Cut")); copy(); removeSelectedItems(); m_document->undoStack()->endMacro(); } void GraphicsScene::removeSelectedItems() { QVector tags = SceneUtils::findRemovedTags(m_baseItems); if (tags.count() > 0) { m_document->undoStack()->beginMacro(tr("Remove item(s)")); // Then remove found tags for (int i = tags.count(); i--;) { m_document->setCurrentTag(tags[i]); m_document->removeTag(tags[i]); } m_document->setCurrentTag(0); m_document->undoStack()->endMacro(); } } void GraphicsScene::copy() { QPointF minPos; QVector tags; if (m_document->currentTag()->tagType() == Scxml) { QVector items; foreach (BaseItem *item, m_baseItems) { if (!item->parentItem()) items << item; } tags = SceneUtils::findCopyTags(items, minPos); } else { tags = SceneUtils::findCopyTags(m_baseItems, minPos); } if (tags.isEmpty() && m_document->currentTag()) tags << m_document->currentTag(); if (tags.count() > 0) { auto mime = new QMimeData; QByteArray result = m_document->content(tags); mime->setText(QLatin1String(result)); mime->setData("StateChartEditor/StateData", result); QStringList strTypes; foreach (const ScxmlTag *tag, tags) { strTypes << tag->tagName(false); } mime->setData("StateChartEditor/CopiedTagTypes", strTypes.join(",").toLocal8Bit()); mime->setData("StateChartEditor/CopiedMinPos", QString::fromLatin1("%1:%2").arg(minPos.x()).arg(minPos.y()).toLocal8Bit()); QGuiApplication::clipboard()->setMimeData(mime); } checkPaste(); } void GraphicsScene::checkPaste() { const QMimeData *mimeData = QGuiApplication::clipboard()->mimeData(); const QString copiedTagTypes = QLatin1String(mimeData->data("StateChartEditor/CopiedTagTypes")); emit pasteAvailable(TagUtils::checkPaste(copiedTagTypes, m_document->currentTag())); } void GraphicsScene::paste(const QPointF &targetPos) { const QMimeData *mimeData = QGuiApplication::clipboard()->mimeData(); QPointF startPos(targetPos); BaseItem *targetItem = nullptr; foreach (BaseItem *item, m_baseItems) { if (item->isSelected() && item->type() >= StateType) { targetItem = item; break; } } if (m_lastPasteTargetItem != targetItem) m_pasteCounter = 0; m_lastPasteTargetItem = targetItem; if (m_lastPasteTargetItem) startPos = m_lastPasteTargetItem->boundingRect().topLeft(); QPointF pastedPos = startPos + QPointF(m_pasteCounter * 30, m_pasteCounter * 30); m_pasteCounter++; QString strMinPos = QLatin1String(mimeData->data("StateChartEditor/CopiedMinPos")); QPointF minPos(0, 0); if (!strMinPos.isEmpty()) { QStringList coords = strMinPos.split(":", QString::SkipEmptyParts); if (coords.count() == 2) minPos = QPointF(coords[0].toDouble(), coords[1].toDouble()); } m_document->pasteData(mimeData->data("StateChartEditor/StateData"), minPos, pastedPos); } void GraphicsScene::setEditorInfo(const QString &key, const QString &value) { foreach (BaseItem *item, m_baseItems) { if (item->isSelected() && item->type() >= TransitionType) item->setEditorInfo(key, value); } } void GraphicsScene::setDocument(ScxmlDocument *document) { if (m_document) disconnect(m_document, 0, this, 0); m_document = document; init(); connectDocument(); } void GraphicsScene::connectDocument() { if (m_document) { connect(m_document.data(), &ScxmlDocument::beginTagChange, this, &GraphicsScene::beginTagChange); connect(m_document.data(), &ScxmlDocument::endTagChange, this, &GraphicsScene::endTagChange); } } void GraphicsScene::disconnectDocument() { if (m_document) m_document->disconnect(this); } void GraphicsScene::init() { m_initializing = true; disconnectDocument(); clear(); addItem(m_lineX = new SnapLine); addItem(m_lineY = new SnapLine); if (m_document) { const ScxmlTag *rootTag = m_document->rootTag(); if (rootTag) { for (int i = 0; i < rootTag->childCount(); ++i) { ScxmlTag *child = rootTag->child(i); ConnectableItem *newItem = SceneUtils::createItemByTagType(child->tagType()); if (newItem) { addItem(newItem); newItem->init(child); } } const QList items = this->items(); for (int i = 0; i < items.count(); ++i) { if (items[i]->type() >= TransitionType) { auto item = qgraphicsitem_cast(items[i]); if (item) item->finalizeCreation(); } } } } m_initializing = false; warningVisibilityChanged(0, 0); emit selectedStateCountChanged(0); emit selectedBaseItemCountChanged(0); } void GraphicsScene::runLayoutToSelectedStates() { m_document->undoStack()->beginMacro(tr("Relayout")); QVector selectedItems; foreach (BaseItem *node, m_baseItems) { if (node->isSelected()) { int index = 0; for (int i = 0; i < selectedItems.count(); ++i) { if (node->depth() <= selectedItems[i]->depth()) { index = i; break; } } selectedItems.insert(index, node); } } // Layout selected items for (int i = 0; i < selectedItems.count(); ++i) selectedItems[i]->doLayout(selectedItems[i]->depth()); // Layout scene items if necessary if (selectedItems.isEmpty()) { QList sceneItems; foreach (BaseItem *item, m_baseItems) { if (item->type() >= InitialStateType && !item->parentItem()) sceneItems << item; } SceneUtils::layout(sceneItems); foreach (QGraphicsItem *item, sceneItems) { if (item->type() >= StateType) static_cast(item)->shrink(); } } // Update properties foreach (BaseItem *node, selectedItems) { node->updateUIProperties(); } m_document->undoStack()->endMacro(); } void GraphicsScene::runAutomaticLayout() { m_autoLayoutRunning = true; // 1. Find max depth int maxDepth = 0; foreach (BaseItem *node, m_baseItems) { maxDepth = qMax(maxDepth, node->depth()); node->setBlockUpdates(true); } // 2. Layout every depth-level separately for (int d = (maxDepth + 1); d--;) { foreach (BaseItem *node, m_baseItems) node->doLayout(d); } // 3. Layout scene items QList sceneItems; foreach (BaseItem *item, m_baseItems) { if (item->type() >= InitialStateType && !item->parentItem()) sceneItems << item; } SceneUtils::layout(sceneItems); foreach (QGraphicsItem *item, sceneItems) { if (item->type() >= StateType) static_cast(item)->shrink(); } foreach (BaseItem *node, m_baseItems) { node->updateUIProperties(); node->setBlockUpdates(false); } m_autoLayoutRunning = false; } void GraphicsScene::beginTagChange(ScxmlDocument::TagChange change, ScxmlTag *tag, const QVariant &value) { switch (change) { case ScxmlDocument::TagRemoveChild: { if (tag) removeItems(tag->child(value.toInt())); break; } default: break; } } void GraphicsScene::endTagChange(ScxmlDocument::TagChange change, ScxmlTag *tag, const QVariant &value) { Q_UNUSED(value) QTC_ASSERT(tag, return); switch (change) { case ScxmlDocument::TagAttributesChanged: { foreach (BaseItem *item, m_baseItems) { if (item->tag() == tag) item->updateAttributes(); } break; } case ScxmlDocument::TagEditorInfoChanged: { foreach (BaseItem *item, m_baseItems) { if (item->tag() == tag) item->updateEditorInfo(); } break; } case ScxmlDocument::TagCurrentChanged: { foreach (BaseItem *item, m_baseItems) { if (!item->isSelected() && item->tag() == tag) item->setSelected(true); } checkPaste(); break; } case ScxmlDocument::TagChangeParent: { auto childItem = qobject_cast(findItem(tag)); if (childItem) { BaseItem *newParentItem = findItem(tag->parentTag()); BaseItem *oldParentItem = childItem ? childItem->parentBaseItem() : nullptr; QPointF sPos = childItem->scenePos(); if (oldParentItem) { childItem->setParentItem(nullptr); childItem->setPos(sPos); } if (newParentItem) childItem->setPos(newParentItem->mapFromScene(childItem->sceneBoundingRect().center()) - childItem->boundingRect().center()); childItem->setParentItem(newParentItem); childItem->updateUIProperties(); childItem->updateTransitions(true); childItem->updateTransitionAttributes(true); childItem->checkWarnings(); childItem->checkInitial(); if (newParentItem) { newParentItem->checkInitial(); newParentItem->updateAttributes(); newParentItem->checkWarnings(); newParentItem->checkOverlapping(); newParentItem->updateUIProperties(); } if (oldParentItem) oldParentItem->checkInitial(); if (!oldParentItem || !newParentItem) checkInitialState(); } break; } case ScxmlDocument::TagAddTags: { // Finalize transitions QVector childTransitionTags; if (tag->tagName(false) == "transition") childTransitionTags << tag; TagUtils::findAllTransitionChildren(tag, childTransitionTags); for (int i = 0; i < childTransitionTags.count(); ++i) { BaseItem *item = findItem(childTransitionTags[i]); if (item) item->finalizeCreation(); } } // FIXME: intended fallthrough? case ScxmlDocument::TagAddChild: { ScxmlTag *childTag = tag->child(value.toInt()); if (childTag) { // Check that there is no any item with this tag BaseItem *childItem = findItem(childTag); BaseItem *parentItem = findItem(tag); if (!childItem) { if (childTag->tagType() == Transition || childTag->tagType() == InitialTransition) { auto transition = new TransitionItem; addItem(transition); transition->setStartItem(qgraphicsitem_cast(parentItem)); transition->init(childTag, 0, false, false); transition->updateAttributes(); } else { childItem = SceneUtils::createItemByTagType(childTag->tagType(), QPointF()); if (childItem) { childItem->init(childTag, parentItem, false); if (!parentItem) addItem(childItem); childItem->finalizeCreation(); childItem->updateUIProperties(); } } } if (parentItem) { parentItem->updateAttributes(); parentItem->updateUIProperties(); parentItem->checkInitial(); } else checkInitialState(); } break; } case ScxmlDocument::TagRemoveChild: { BaseItem *parentItem = findItem(tag); if (parentItem) { parentItem->updateAttributes(); parentItem->checkInitial(); } else { checkInitialState(); } break; } case ScxmlDocument::TagChangeOrder: { BaseItem *parentItem = findItem(tag->parentTag()); if (parentItem) parentItem->updateAttributes(); else checkInitialState(); } default: break; } } void GraphicsScene::setTopMostScene(bool topmost) { m_topMostScene = topmost; } bool GraphicsScene::topMostScene() const { return m_topMostScene; } void GraphicsScene::setActionHandler(ActionHandler *mgr) { m_actionHandler = mgr; } void GraphicsScene::setWarningModel(ScxmlEditor::OutputPane::WarningModel *model) { m_warningModel = model; } void GraphicsScene::setUiFactory(ScxmlUiFactory *uifactory) { m_uiFactory = uifactory; } ActionHandler *GraphicsScene::actionHandler() const { return m_actionHandler; } ScxmlEditor::OutputPane::WarningModel *GraphicsScene::warningModel() const { return m_warningModel; } ScxmlUiFactory *GraphicsScene::uiFactory() const { return m_uiFactory; } void GraphicsScene::addConnectableItem(ItemType type, const QPointF &pos, BaseItem *parentItem) { m_document->undoStack()->beginMacro(tr("Add new state")); ConnectableItem *newItem = SceneUtils::createItem(type, pos); if (newItem) { ScxmlTag *newTag = SceneUtils::createTag(type, m_document); ScxmlTag *parentTag = parentItem ? parentItem->tag() : m_document->rootTag(); newItem->setTag(newTag); newItem->setParentItem(parentItem); if (!parentItem) addItem(newItem); newItem->updateAttributes(); newItem->updateEditorInfo(); newItem->updateUIProperties(); if (parentItem) parentItem->updateUIProperties(); m_document->addTag(parentTag, newTag); unselectAll(); newItem->setSelected(true); } m_document->undoStack()->endMacro(); } void GraphicsScene::keyPressEvent(QKeyEvent *event) { QGraphicsItem *focusItem = this->focusItem(); if (!focusItem || focusItem->type() != TextType) { if (event->key() == Qt::Key_Delete) removeSelectedItems(); } QGraphicsScene::keyPressEvent(event); } void GraphicsScene::mousePressEvent(QGraphicsSceneMouseEvent *event) { QGraphicsItem *it = itemAt(event->scenePos(), QTransform()); if (!it || it->type() == LayoutType) { if (event->button() == Qt::LeftButton) { QGraphicsScene::mousePressEvent(event); m_document->setCurrentTag(m_document->rootTag()); return; } else if (m_actionHandler && event->button() == Qt::RightButton) { event->accept(); QMenu menu; menu.addAction(m_actionHandler->action(ActionCopy)); menu.addAction(m_actionHandler->action(ActionPaste)); menu.addAction(m_actionHandler->action(ActionScreenshot)); menu.addAction(m_actionHandler->action(ActionExportToImage)); menu.addSeparator(); menu.addAction(m_actionHandler->action(ActionZoomIn)); menu.addAction(m_actionHandler->action(ActionZoomOut)); menu.addAction(m_actionHandler->action(ActionFitToView)); if (m_uiFactory) { auto actionProvider = static_cast(m_uiFactory->object(Constants::C_UI_FACTORY_OBJECT_ACTIONPROVIDER)); if (actionProvider) { menu.addSeparator(); actionProvider->initStateMenu(m_document->rootTag(), &menu); } } menu.exec(event->screenPos()); return; } } QGraphicsScene::mousePressEvent(event); } BaseItem *GraphicsScene::findItem(const ScxmlTag *tag) const { if (!tag) return nullptr; foreach (BaseItem *it, m_baseItems) { if (it->tag() == tag) return it; } return nullptr; } void GraphicsScene::removeItems(const ScxmlTag *tag) { if (tag) { // Find right items QVector items; foreach (BaseItem *it, m_baseItems) { if (it->tag() == tag) items << it; } // Then delete them for (int i = items.count(); i--;) { items[i]->setTag(0); delete items[i]; } } } QPair GraphicsScene::checkSnapToItem(BaseItem *item, const QPointF &p, QPointF &pp) { if (m_selectedStateCount > 1) return QPair(false, false); QGraphicsItem *parentItem = item->parentItem(); qreal diffX = 8; qreal diffXdY = 2000; qreal diffY = 8; qreal diffYdX = 2000; foreach (BaseItem *it, m_baseItems) { if (!it->isSelected() && it != item && it->parentItem() == parentItem && it->type() >= InitialStateType) { QPointF c = it->sceneCenter(); qreal dX = qAbs(c.x() - p.x()); qreal dY = qAbs(c.y() - p.y()); if (dX < 7 && dY < diffXdY) { pp.setX(c.x()); diffX = dX; diffXdY = dY; m_lineY->show(c.x(), c.y(), c.x(), p.y()); } if (dY < 7 && dX < diffYdX) { pp.setY(c.y()); diffY = dY; diffYdX = dX; m_lineX->show(c.x(), c.y(), p.x(), c.y()); } } } if (qFuzzyCompare(diffX, 8)) m_lineY->hideLine(); if (qFuzzyCompare(diffY, 8)) m_lineX->hideLine(); return QPair(diffX < 8, diffY < 8); } void GraphicsScene::selectionChanged(bool para) { Q_UNUSED(para) int count = 0; int baseCount = 0; int stateTypeCount = 0; foreach (BaseItem *item, m_baseItems) { if (item->isSelected()) { if (item->type() >= TransitionType) baseCount++; if (item->type() >= InitialStateType) count++; if (item->type() >= StateType) stateTypeCount++; } } m_selectedStateTypeCount = stateTypeCount; if (count != m_selectedStateCount) { m_selectedStateCount = count; emit selectedStateCountChanged(m_selectedStateCount); } if (baseCount != m_selectedBaseItemCount) { m_selectedBaseItemCount = baseCount; emit selectedBaseItemCountChanged(m_selectedBaseItemCount); } } void GraphicsScene::addWarningItem(WarningItem *item) { if (!m_allWarnings.contains(item)) { m_allWarnings << item; if (!m_autoLayoutRunning && !m_initializing) QMetaObject::invokeMethod(this, "warningVisibilityChanged", Qt::QueuedConnection, Q_ARG(int, 0)); } } void GraphicsScene::removeWarningItem(WarningItem *item) { m_allWarnings.removeAll(item); if (!m_autoLayoutRunning && !m_initializing) QMetaObject::invokeMethod(this, "warningVisibilityChanged", Qt::QueuedConnection, Q_ARG(int, 0)); } void GraphicsScene::warningVisibilityChanged(int type, WarningItem *item) { if (!m_autoLayoutRunning && !m_initializing) { foreach (WarningItem *it, m_allWarnings) { if (it != item && (type == 0 || it->type() == type)) it->check(); } } } ScxmlTag *GraphicsScene::tagByWarning(const ScxmlEditor::OutputPane::Warning *w) const { ScxmlTag *tag = nullptr; foreach (WarningItem *it, m_allWarnings) { if (it->warning() == w) { tag = it->tag(); break; } } return tag; } void GraphicsScene::highlightWarningItem(const ScxmlEditor::OutputPane::Warning *w) { ScxmlTag *tag = tagByWarning(w); if (tag) highlightItems(QVector() << tag); else unhighlightAll(); } void GraphicsScene::selectWarningItem(const ScxmlEditor::OutputPane::Warning *w) { ScxmlTag *tag = tagByWarning(w); if (tag) { unselectAll(); m_document->setCurrentTag(tag); } } QList GraphicsScene::sceneItems(Qt::SortOrder order) const { QList children; QList items = this->items(order); for (int i = 0; i < items.count(); ++i) { if (!items[i]->parentItem() && items[i]->type() >= InitialStateType) children << items[i]; } return children; } void GraphicsScene::addChild(BaseItem *item) { if (!m_baseItems.contains(item)) { connect(item, &BaseItem::selectedStateChanged, this, &GraphicsScene::selectionChanged); connect(item, &BaseItem::openToDifferentView, this, [=](BaseItem *item){ openStateView(item); }, Qt::QueuedConnection); m_baseItems << item; } } void GraphicsScene::removeChild(BaseItem *item) { if (item) disconnect(item, 0, this, 0); m_baseItems.removeAll(item); selectionChanged(false); } void GraphicsScene::checkItemsVisibility(double scaleFactor) { foreach (BaseItem *item, m_baseItems) { item->checkVisibility(scaleFactor); } } void GraphicsScene::checkInitialState() { if (m_document) { QList sceneItems; foreach (BaseItem *item, m_baseItems) { if (item->type() >= InitialStateType && !item->parentItem()) sceneItems << item; } if (m_uiFactory) { auto utilsProvider = static_cast(m_uiFactory->object("utilsProvider")); if (utilsProvider) utilsProvider->checkInitialState(sceneItems, m_document->rootTag()); } } } void GraphicsScene::clearAllTags() { foreach (BaseItem *it, m_baseItems) { it->setTag(0); } } void GraphicsScene::setBlockUpdates(bool block) { foreach (BaseItem *it, m_baseItems) { it->setBlockUpdates(block); } }