// Copyright (C) 2016 The Qt Company Ltd. // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 #include "stateitem.h" #include "eventitem.h" #include "graphicsitemprovider.h" #include "graphicsscene.h" #include "idwarningitem.h" #include "imageprovider.h" #include "sceneutils.h" #include "scxmleditorconstants.h" #include "scxmleditortr.h" #include "scxmltagutils.h" #include "scxmluifactory.h" #include "statewarningitem.h" #include "textitem.h" #include "transitionitem.h" #include "utilsprovider.h" #include #include #include #include #include #include #include #include #include using namespace ScxmlEditor::PluginInterface; const qreal TEXT_ITEM_HEIGHT = 20; const qreal TEXT_ITEM_CAP = 10; const qreal STATE_RADIUS = 10; StateItem::StateItem(const QPointF &pos, BaseItem *parent) : ConnectableItem(pos, parent) { m_stateNameItem = new TextItem(this); m_stateNameItem->setParentItem(this); checkWarningItems(); connect(m_stateNameItem, &TextItem::selected, this, [=](bool sel){ setItemSelected(sel); }); connect(m_stateNameItem, &TextItem::textChanged, this, &StateItem::updateTextPositions); connect(m_stateNameItem, &TextItem::textReady, this, &StateItem::titleHasChanged); m_pen = QPen(QColor(0x45, 0x45, 0x45)); updateColors(); updatePolygon(); } void StateItem::checkWarningItems() { ScxmlUiFactory *uifactory = uiFactory(); if (uifactory) { auto provider = static_cast(uifactory->object("graphicsItemProvider")); if (provider) { if (!m_idWarningItem) m_idWarningItem = static_cast(provider->createWarningItem(Constants::C_STATE_WARNING_ID, this)); if (!m_stateWarningItem) m_stateWarningItem = static_cast(provider->createWarningItem(Constants::C_STATE_WARNING_STATE, this)); if (m_idWarningItem && m_stateWarningItem) m_stateWarningItem->setIdWarning(m_idWarningItem); checkWarnings(); if (m_idWarningItem || m_stateWarningItem) updateAttributes(); } } } QVariant StateItem::itemChange(GraphicsItemChange change, const QVariant &value) { QVariant retValue = ConnectableItem::itemChange(change, value); switch (change) { case QGraphicsItem::ItemSceneHasChanged: checkWarningItems(); break; default: break; } return retValue; } void StateItem::updateAttributes() { if (tag()) { ConnectableItem::updateAttributes(); // Check initial attribute QString strNewId = tagValue("id", true); if (!m_parallelState) { QStringList NSIDs = strNewId.split(tag()->document()->nameSpaceDelimiter(), Qt::SkipEmptyParts); if (!NSIDs.isEmpty()) { NSIDs[NSIDs.count() - 1] = m_stateNameItem->toPlainText(); QString strOldId = NSIDs.join(tag()->document()->nameSpaceDelimiter()); ScxmlTag *parentTag = tag()->parentTag(); if (parentTag && !strOldId.isEmpty() && parentTag->attribute("initial") == strOldId) parentTag->setAttribute("initial", strNewId); } } m_stateNameItem->setText(tagValue("id")); if (m_idWarningItem) m_idWarningItem->setId(strNewId); updateTextPositions(); if (m_parallelState) checkInitial(true); } updateBoundingRect(); } void StateItem::updateEditorInfo(bool allChildren) { ConnectableItem::updateEditorInfo(allChildren); QString color = editorInfo(Constants::C_SCXML_EDITORINFO_FONTCOLOR); m_stateNameItem->setDefaultTextColor(color.isEmpty() ? QColor(Qt::black) : QColor(color)); // Update child too if necessary if (allChildren) { QList children = childItems(); for (int i = 0; i < children.count(); ++i) { if (children[i]->type() >= InitialStateType) { auto child = qgraphicsitem_cast(children[i]); if (child) child->updateEditorInfo(allChildren); } } } } void StateItem::updateColors() { updateDepth(); m_parallelState = parentItem() && parentItem()->type() == ParallelType; if (m_parallelState) m_pen.setStyle(Qt::DashLine); else m_pen.setStyle(Qt::SolidLine); // Update child color-indices too QList children = childItems(); for (int i = 0; i < children.count(); ++i) { if (children[i]->type() >= StateType) { auto child = qgraphicsitem_cast(children[i]); if (child) child->updateColors(); } } update(); } void StateItem::updateBoundingRect() { QRectF r2 = childItemsBoundingRect(); // Check if we need to increase parent boundingrect if (!r2.isNull()) { positionOnExitItems(); QRectF r = boundingRect(); QRectF r3 = r.united(r2); if (r != r3) { setItemBoundingRect(r3); updateTransitions(); updateUIProperties(); checkOverlapping(); } } } void StateItem::shrink() { QRectF trect; const QVector items = outputTransitions(); for (TransitionItem *item : items) { if (item->targetType() == TransitionItem::InternalSameTarget || item->targetType() == TransitionItem::InternalNoTarget) { trect = trect.united(item->wholeBoundingRect()); } } QRectF r = boundingRect(); QRectF r2 = childItemsBoundingRect(); double minWidth = qMax(120.0, r2.width()); double minH = qMax((double)minHeight(), r2.height()); minWidth = qMax(minWidth, m_stateNameItem->boundingRect().width() + (double)WARNING_ITEM_SIZE + (double)TEXT_ITEM_CAP); if (!m_backgroundImage.isNull()) { minWidth = qMax(minWidth, (double)m_backgroundImage.width() + 3 * STATE_RADIUS + trect.width()); minH = qMax(minH, ((double)m_backgroundImage.height() + 3 * STATE_RADIUS + TEXT_ITEM_HEIGHT) / 0.94); } r2 = QRectF(0, 0, minWidth, minH); r2.moveCenter(r.center()); // Check if we need to increase parent boundingrect if (r != r2) { setItemBoundingRect(r2); updateTransitions(); updateUIProperties(); } } void StateItem::transitionsChanged() { QRectF rr = boundingRect(); QRectF rectInternalTransitions; const QVector internalTransitions = outputTransitions(); for (TransitionItem *item : internalTransitions) { if (item->targetType() <= TransitionItem::InternalNoTarget) { QRectF br = mapFromItem(item, item->boundingRect()).boundingRect(); br.setLeft(rr.left() + 20); br.setTop(br.top() + 4); br.setWidth(br.width() + item->textWidth()); rectInternalTransitions = rectInternalTransitions.united(br); } } m_transitionRect = rectInternalTransitions; updateBoundingRect(); } void StateItem::transitionCountChanged() { ConnectableItem::transitionsChanged(); checkWarnings(); transitionsChanged(); } QRectF StateItem::childItemsBoundingRect() const { QRectF r; QList children = childItems(); for (int i = 0; i < children.count(); ++i) { if (children[i]->type() >= InitialStateType) { QRectF br = children[i]->boundingRect(); QPointF p = children[i]->pos() + br.topLeft(); br.moveTopLeft(p); r = r.united(br); } } if (m_onEntryItem) { QRectF br = m_onEntryItem->childBoundingRect(); QPointF p = m_onEntryItem->pos() + br.topLeft(); br.moveTopLeft(p); r = r.united(br); } if (m_onExitItem) { QRectF br = m_onExitItem->childBoundingRect(); QPointF p = m_onExitItem->pos() + br.topLeft(); br.moveTopLeft(p); r = r.united(br); } if (m_transitionRect.isValid()) { r.setLeft(r.left() - m_transitionRect.width()); r.setHeight(qMax(r.height(), m_transitionRect.height())); r.moveBottom(qMax(r.bottom(), m_transitionRect.bottom())); } return r; } void StateItem::mouseDoubleClickEvent(QGraphicsSceneMouseEvent *event) { ConnectableItem::mouseDoubleClickEvent(event); shrink(); updateUIProperties(); } void StateItem::createContextMenu(QMenu *menu) { QVariantMap data; if (!m_parallelState) { data[Constants::C_SCXMLTAG_ACTIONTYPE] = TagUtils::SetAsInitial; menu->addAction(Tr::tr("Set as Initial"))->setData(data); } data[Constants::C_SCXMLTAG_ACTIONTYPE] = TagUtils::ZoomToState; menu->addAction(Tr::tr("Zoom to State"))->setData(data); if (type() == ParallelType) { data[Constants::C_SCXMLTAG_ACTIONTYPE] = TagUtils::Relayout; menu->addAction(Tr::tr("Re-Layout"))->setData(data); } menu->addSeparator(); ConnectableItem::createContextMenu(menu); } void StateItem::selectedMenuAction(const QAction *action) { if (!action) return; const ScxmlTag *tag = this->tag(); if (tag) { const QVariantMap data = action->data().toMap(); const int actionType = data.value(Constants::C_SCXMLTAG_ACTIONTYPE, -1).toInt(); ScxmlDocument *document = tag->document(); switch (actionType) { case TagUtils::SetAsInitial: { ScxmlTag *parentTag = tag->parentTag(); if (parentTag) { document->undoStack()->beginMacro(Tr::tr("Change initial state")); ScxmlTag *initialTag = parentTag->child("initial"); if (initialTag) { ScxmlTag *initialTransitionTag = initialTag->child("transition"); if (initialTransitionTag) document->setValue(initialTransitionTag, "target", tag->attribute("id", true)); else { auto newTransition = new ScxmlTag(Transition, document); newTransition->setAttribute("target", tag->attribute("id", true)); document->addTag(initialTag, newTransition); } } else document->setValue(parentTag, "initial", tag->attribute("id", true)); checkInitial(true); document->undoStack()->endMacro(); } break; } case TagUtils::Relayout: { document->undoStack()->beginMacro(Tr::tr("Re-Layout")); doLayout(depth()); document->undoStack()->endMacro(); break; } case TagUtils::ZoomToState: { emit BaseItem::openToDifferentView(this); break; } default: ConnectableItem::selectedMenuAction(action); break; } } } void StateItem::checkInitial(bool parent) { QList items; ScxmlTag *tag = nullptr; if (parent) { if (parentItem()) { items = parentItem()->childItems(); if (parentBaseItem()) tag = parentBaseItem()->tag(); } else { auto sc = static_cast(scene()); if (sc) { sc->checkInitialState(); return; } } } else { items = childItems(); tag = this->tag(); } if (!items.isEmpty() && tag && uiFactory()) { auto utilsProvider = static_cast(uiFactory()->object("utilsProvider")); if (utilsProvider) utilsProvider->checkInitialState(items, tag); } } void StateItem::titleHasChanged(const QString &text) { QString strOldId = tagValue("id", true); setTagValue("id", text); // Check initial attribute if (tag() && !m_parallelState) { ScxmlTag *parentTag = tag()->parentTag(); if (!strOldId.isEmpty() && parentTag->attribute("initial") == strOldId) parentTag->setAttribute("initial", tagValue("id", true)); } } void StateItem::updateTextPositions() { if (m_parallelState) { m_stateNameItem->setPos(m_titleRect.x(), m_titleRect.top()); m_stateNameItem->setItalic(true); } else { m_stateNameItem->setPos(m_titleRect.center().x() - m_stateNameItem->boundingRect().width() / 2, m_titleRect.top()); m_stateNameItem->setItalic(false); } QPointF p(m_stateNameItem->pos().x() - WARNING_ITEM_SIZE, m_titleRect.center().y() - WARNING_ITEM_SIZE / 2); if (m_idWarningItem) m_idWarningItem->setPos(p); if (m_stateWarningItem) m_stateWarningItem->setPos(p); } void StateItem::updatePolygon() { m_drawingRect = boundingRect().adjusted(5, 5, -5, -5); m_polygon.clear(); m_polygon << m_drawingRect.topLeft() << m_drawingRect.topRight() << m_drawingRect.bottomRight() << m_drawingRect.bottomLeft() << m_drawingRect.topLeft(); m_titleRect = QRectF(m_drawingRect.left(), m_drawingRect.top(), m_drawingRect.width(), TEXT_ITEM_HEIGHT + m_drawingRect.height() * 0.06); QFont f = m_stateNameItem->font(); f.setPixelSize(m_titleRect.height() * 0.65); m_stateNameItem->setFont(f); if (m_onEntryItem) m_onEntryItem->setPos(m_titleRect.x(), m_titleRect.bottom()); positionOnExitItems(); updateTextPositions(); } bool StateItem::canStartTransition(ItemType /*type*/) const { return !m_parallelState; } void StateItem::connectToParent(BaseItem *parentItem) { ConnectableItem::connectToParent(parentItem); updateTextPositions(); checkInitial(true); } void StateItem::paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) { ConnectableItem::paint(painter, option, widget); painter->save(); painter->setRenderHint(QPainter::Antialiasing, true); // Set opacity and color painter->setOpacity(getOpacity()); m_pen.setColor(overlapping() ? qRgb(0xff, 0x00, 0x60) : qRgb(0x45, 0x45, 0x45)); painter->setPen(m_pen); QColor stateColor(editorInfo(Constants::C_SCXML_EDITORINFO_STATECOLOR)); if (!stateColor.isValid()) stateColor = tag() ? tag()->document()->getColor(depth()) : QColor(0x12, 0x34, 0x56); // Draw basic frame QRectF r = boundingRect(); QLinearGradient grad(r.topLeft(), r.bottomLeft()); grad.setColorAt(0, stateColor.lighter(115)); grad.setColorAt(1, stateColor); painter->setBrush(QBrush(grad)); painter->drawRoundedRect(m_drawingRect, STATE_RADIUS, STATE_RADIUS); // Background image ScxmlUiFactory *uifactory = uiFactory(); if (uifactory) { auto imageProvider = static_cast(uifactory->object("imageProvider")); if (imageProvider) { const QImage *image = imageProvider->backgroundImage(tag()); if (image) { int w = m_drawingRect.width() - 2 * STATE_RADIUS; int h = m_drawingRect.height() - m_titleRect.height() - 2 * STATE_RADIUS; int left = m_drawingRect.left() + STATE_RADIUS; int top = m_drawingRect.top() + m_titleRect.height() + STATE_RADIUS; if (m_transitionRect.isValid()) { w = m_drawingRect.right() - m_transitionRect.right() - 2 * STATE_RADIUS; left = m_transitionRect.right() + STATE_RADIUS; } m_backgroundImage = image->scaled(w, h, Qt::KeepAspectRatio); painter->drawImage(QPointF(left + (w - m_backgroundImage.width()) / 2, top + (h - m_backgroundImage.height()) / 2), m_backgroundImage); } else m_backgroundImage = QImage(); } } // Draw title rect if (!m_parallelState) { painter->drawLine(QLineF(m_titleRect.bottomLeft(), m_titleRect.bottomRight())); if (m_initial) { double size = m_titleRect.height() * 0.3; painter->setBrush(QColor(0x4d, 0x4d, 0x4d)); painter->drawEllipse(QPointF(m_titleRect.left() + 2 * size, m_titleRect.center().y()), size, size); } } // Transition part if (m_transitionRect.isValid()) { qreal tx = m_transitionRect.right(); painter->drawLine(QLineF(QPointF(tx, m_titleRect.bottom() + STATE_RADIUS), QPointF(tx, m_drawingRect.bottom() - STATE_RADIUS))); } painter->restore(); } void StateItem::init(ScxmlTag *tag, BaseItem *parentItem, bool initChildren, bool blockUpdates) { setBlockUpdates(blockUpdates); ConnectableItem::init(tag, parentItem); if (initChildren) { for (int i = 0; i < tag->childCount(); ++i) { ScxmlTag *child = tag->child(i); ConnectableItem *newItem = SceneUtils::createItemByTagType(child->tagType(), QPointF()); if (newItem) { newItem->init(child, this, initChildren, blockUpdates); newItem->finalizeCreation(); } else addChild(child); } } if (blockUpdates) setBlockUpdates(false); } void StateItem::addChild(ScxmlTag *child) { if (child->tagName() == "onentry") { OnEntryExitItem *item = new OnEntryExitItem(this); m_onEntryItem = item; item->setTag(child); item->finalizeCreation(); item->updateAttributes(); m_onEntryItem->setPos(m_titleRect.x(), m_titleRect.bottom()); } else if (child->tagName() == "onexit") { OnEntryExitItem *item = new OnEntryExitItem(this); m_onExitItem = item; item->setTag(child); item->finalizeCreation(); item->updateAttributes(); positionOnExitItems(); } } void StateItem::positionOnExitItems() { int offset = m_onEntryItem ? m_onEntryItem->boundingRect().height() : 0; if (m_onExitItem) m_onExitItem->setPos(m_titleRect.x(), m_titleRect.bottom() + offset); } QString StateItem::itemId() const { return m_stateNameItem ? m_stateNameItem->toPlainText() : QString(); } void StateItem::doLayout(int d) { if (depth() != d) return; SceneUtils::layout(childItems()); updateBoundingRect(); shrink(); } void StateItem::setInitial(bool initial) { m_initial = initial; update(); checkWarnings(); } void StateItem::checkWarnings() { if (m_idWarningItem) m_idWarningItem->check(); if (m_stateWarningItem) m_stateWarningItem->check(); if (parentItem() && parentItem()->type() == StateType) qgraphicsitem_cast(parentItem())->checkWarnings(); } bool StateItem::isInitial() const { return m_initial; }