/* * All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or * its licensors. * * For complete copyright and license terms please see the LICENSE at the root of this * distribution (the "License"). All use of this software is governed by the License, * or, if provided, by the license below or the license accompanying this file. Do not * remove or modify any license notices. This file is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * */ #include "precompiled.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include namespace GraphCanvas { ////////////////////////////// // ConnectionVisualComponent ////////////////////////////// void ConnectionVisualComponent::Reflect(AZ::ReflectContext* context) { AZ::SerializeContext* serializeContext = azrtti_cast(context); if (serializeContext) { serializeContext->Class() ->Version(1) ; } } ConnectionVisualComponent::ConnectionVisualComponent() { } void ConnectionVisualComponent::Init() { CreateConnectionVisual(); } void ConnectionVisualComponent::Activate() { if (m_connectionGraphicsItem) { m_connectionGraphicsItem->Activate(); } VisualRequestBus::Handler::BusConnect(GetEntityId()); SceneMemberUIRequestBus::Handler::BusConnect(GetEntityId()); } void ConnectionVisualComponent::Deactivate() { VisualRequestBus::Handler::BusDisconnect(); SceneMemberUIRequestBus::Handler::BusDisconnect(); if (m_connectionGraphicsItem) { m_connectionGraphicsItem->Deactivate(); } } QGraphicsItem* ConnectionVisualComponent::AsGraphicsItem() { return m_connectionGraphicsItem.get(); } bool ConnectionVisualComponent::Contains(const AZ::Vector2&) const { return false; } void ConnectionVisualComponent::SetVisible(bool visible) { m_connectionGraphicsItem->setVisible(visible); } bool ConnectionVisualComponent::IsVisible() const { return m_connectionGraphicsItem->isVisible(); } QGraphicsItem* ConnectionVisualComponent::GetRootGraphicsItem() { return m_connectionGraphicsItem.get(); } QGraphicsLayoutItem* ConnectionVisualComponent::GetRootGraphicsLayoutItem() { return nullptr; } void ConnectionVisualComponent::SetSelected(bool selected) { m_connectionGraphicsItem->setSelected(selected); } bool ConnectionVisualComponent::IsSelected() const { return m_connectionGraphicsItem->isSelected(); } QPainterPath ConnectionVisualComponent::GetOutline() const { return m_connectionGraphicsItem->path(); } void ConnectionVisualComponent::SetZValue(qreal zValue) { m_connectionGraphicsItem->setZValue(zValue); } qreal ConnectionVisualComponent::GetZValue() const { return aznumeric_cast(m_connectionGraphicsItem->zValue()); } void ConnectionVisualComponent::CreateConnectionVisual() { m_connectionGraphicsItem = AZStd::make_unique(GetEntityId()); } //////////////////////////// // ConnectionGraphicsItem //////////////////////////// qreal ConnectionGraphicsItem::VectorLength(QPointF vectorPoint) { return qSqrt(vectorPoint.x() * vectorPoint.x() + vectorPoint.y() * vectorPoint.y()); } ConnectionGraphicsItem::ConnectionGraphicsItem(const AZ::EntityId& connectionEntityId) : RootGraphicsItem(connectionEntityId) , m_trackMove(false) , m_moveSource(false) , m_curveType(Styling::ConnectionCurveType::Straight) , m_offset(0.0) , m_connectionEntityId(connectionEntityId) { setFlags(ItemIsSelectable | ItemIsFocusable); setData(GraphicsItemName, QStringLiteral("DefaultConnectionVisual/%1").arg(static_cast(GetEntityId()), 16, 16, QChar('0'))); } void ConnectionGraphicsItem::Activate() { AZStd::string tooltip; ConnectionRequestBus::EventResult(tooltip, GetEntityId(), &ConnectionRequests::GetTooltip); setToolTip(Tools::qStringFromUtf8(tooltip)); ConnectionNotificationBus::Handler::BusConnect(GetEntityId()); StyleNotificationBus::Handler::BusConnect(GetEntityId()); AZ::EntityId sourceId; ConnectionRequestBus::EventResult(sourceId, GetConnectionEntityId(), &ConnectionRequests::GetSourceSlotId); OnSourceSlotIdChanged(AZ::EntityId(), sourceId); AZ::EntityId targetId; ConnectionRequestBus::EventResult(targetId, GetConnectionEntityId(), &ConnectionRequests::GetTargetSlotId); OnTargetSlotIdChanged(AZ::EntityId(), targetId); ConnectionUIRequestBus::Handler::BusConnect(GetConnectionEntityId()); SceneMemberNotificationBus::Handler::BusConnect(GetConnectionEntityId()); OnStyleChanged(); UpdateConnectionPath(); OnActivate(); } void ConnectionGraphicsItem::Deactivate() { SceneMemberNotificationBus::Handler::BusDisconnect(); ConnectionUIRequestBus::Handler::BusDisconnect(); ConnectionNotificationBus::Handler::BusDisconnect(); StyleNotificationBus::Handler::BusDisconnect(); VisualNotificationBus::MultiHandler::BusDisconnect(); } void ConnectionGraphicsItem::RefreshStyle() { m_style.SetStyle(GetEntityId()); UpdatePen(); } const Styling::StyleHelper& ConnectionGraphicsItem::GetStyle() const { return m_style; } void ConnectionGraphicsItem::UpdateOffset() { auto currentTime = AZStd::chrono::system_clock::now(); auto currentDuration = currentTime.time_since_epoch(); AZStd::chrono::milliseconds currentUpdate = AZStd::chrono::duration_cast(currentDuration); float delta = (currentUpdate - m_lastUpdate).count() * 0.001f; m_lastUpdate = currentUpdate; // This works for all default dash/dot patterns, for now static const double offsetReset = 24.0; // TODO: maybe calculate this based on an "animation-speed" attribute? // For now. 1.35 resets per second? m_offset += offsetReset * 1.35f * delta; if (m_offset >= offsetReset) { m_offset -= offsetReset; } QPen currentPen = pen(); currentPen.setDashOffset(-m_offset); setPen(currentPen); } QRectF ConnectionGraphicsItem::GetBoundingRect() const { return boundingRect(); } void ConnectionGraphicsItem::OnSourceSlotIdChanged(const AZ::EntityId& oldSlotId, const AZ::EntityId& newSlotId) { if (oldSlotId.IsValid()) { VisualNotificationBus::MultiHandler::BusDisconnect(oldSlotId); } if (newSlotId.IsValid()) { VisualNotificationBus::MultiHandler::BusConnect(newSlotId); } UpdateConnectionPath(); } void ConnectionGraphicsItem::OnTargetSlotIdChanged(const AZ::EntityId& oldSlotId, const AZ::EntityId& newSlotId) { if (oldSlotId.IsValid()) { VisualNotificationBus::MultiHandler::BusDisconnect(oldSlotId); } if (newSlotId.IsValid()) { VisualNotificationBus::MultiHandler::BusConnect(newSlotId); } UpdateConnectionPath(); } void ConnectionGraphicsItem::OnTooltipChanged(const AZStd::string& tooltip) { setToolTip(Tools::qStringFromUtf8(tooltip)); } void ConnectionGraphicsItem::OnStyleChanged() { RefreshStyle(); bool animate = m_style.GetAttribute(Styling::Attribute::LineStyle, Qt::SolidLine) != Qt::SolidLine; if (animate) { if (!AZ::SystemTickBus::Handler::BusIsConnected()) { auto currentTime = AZStd::chrono::system_clock::now(); auto currentDuration = currentTime.time_since_epoch(); m_lastUpdate = AZStd::chrono::duration_cast(currentDuration); AZ::SystemTickBus::Handler::BusConnect(); } } else if (AZ::SystemTickBus::Handler::BusIsConnected()) { AZ::SystemTickBus::Handler::BusDisconnect(); } setOpacity(m_style.GetAttribute(Styling::Attribute::Opacity, 1.0f)); UpdateConnectionPath(); } void ConnectionGraphicsItem::OnSystemTick() { UpdateOffset(); } void ConnectionGraphicsItem::OnItemChange(const AZ::EntityId& entityId, QGraphicsItem::GraphicsItemChange change, const QVariant& value) { switch (change) { case QGraphicsItem::ItemScenePositionHasChanged: { UpdateConnectionPath(); break; } } } void ConnectionGraphicsItem::UpdateConnectionPath() { AZ::EntityId sourceId; ConnectionRequestBus::EventResult(sourceId, GetConnectionEntityId(), &ConnectionRequests::GetSourceSlotId); AZ::EntityId targetId; ConnectionRequestBus::EventResult(targetId, GetConnectionEntityId(), &ConnectionRequests::GetTargetSlotId); QPointF start; ConnectionRequestBus::EventResult(start, GetEntityId(), &ConnectionRequests::GetSourcePosition); QPointF startJutDirection; SlotUIRequestBus::EventResult(startJutDirection, sourceId, &SlotUIRequests::GetJutDirection); QPointF end; ConnectionRequestBus::EventResult(end, GetEntityId(), &ConnectionRequests::GetTargetPosition); QPointF endJutDirection; SlotUIRequestBus::EventResult(endJutDirection, targetId, &SlotUIRequests::GetJutDirection); if (!sourceId.IsValid()) { startJutDirection = -endJutDirection; } else if (!targetId.IsValid()) { endJutDirection = -startJutDirection; } bool loopback = false; float nodeHeight = 0; AZ::Vector2 nodePos(0, 0); if (end.isNull()) { end = start; } else { // Determine if this connection is from and to the same node (self-connection) AZ::EntityId sourceNode; SlotRequestBus::EventResult(sourceNode, sourceId, &SlotRequestBus::Events::GetNode); AZ::EntityId targetNode; SlotRequestBus::EventResult(targetNode, targetId, &SlotRequestBus::Events::GetNode); loopback = sourceNode == targetNode; if (loopback) { QGraphicsItem* rootVisual = nullptr; SceneMemberUIRequestBus::EventResult(rootVisual, sourceNode, &SceneMemberUIRequests::GetRootGraphicsItem); if (rootVisual) { nodeHeight = aznumeric_cast(rootVisual->boundingRect().height()); } GeometryRequestBus::EventResult(nodePos, sourceNode, &GeometryRequestBus::Events::GetPosition); } } ConnectionType connectionType = ConnectionType::CT_Invalid; SlotRequestBus::EventResult(connectionType, sourceId, &SlotRequests::GetConnectionType); QPainterPath path = QPainterPath(start); if (m_curveType == Styling::ConnectionCurveType::Curved) { // Scale the control points based on the length of the line to make sure the curve looks pretty QPointF offset = (end - start); QPointF midVector = (start + end) / 2.0 - start; // Mathemagic to make the curvature look nice qreal magnitude = 0; if (offset.x() < 0) { magnitude = AZ::GetMax(qSqrt(VectorLength(offset)) * 5, qFabs(offset.x()) * 0.25f); } else { magnitude = AZ::GetMax(qSqrt(VectorLength(offset)) * 5, offset.x() * 0.5f); } magnitude = AZ::GetClamp(magnitude, (qreal) 10.0f, qMax(VectorLength(midVector), (qreal) 10.0f)); // Makes the line come out horizontally from the start and end points QPointF offsetStart = start + startJutDirection * magnitude; QPointF offsetEnd = end + endJutDirection * magnitude; if (!loopback) { path.cubicTo(offsetStart, offsetEnd, end); } else { // Make the connection wrap around the node, // leaving some space between the connection and the node qreal heightOffset = (qreal)nodePos.GetY() + nodeHeight + 20.0f; path.cubicTo(offsetStart, { offsetStart.x(), heightOffset }, { start.x(), heightOffset }); path.lineTo({ end.x(), heightOffset }); path.cubicTo({ offsetEnd.x(), heightOffset }, offsetEnd, end); } } else { float connectionJut = m_style.GetAttribute(Styling::Attribute::ConnectionJut, 0.0f); QPointF startOffset = start + startJutDirection * connectionJut; QPointF endOffset = end + endJutDirection * connectionJut; path.lineTo(startOffset); path.lineTo(endOffset); path.lineTo(end); } setPath(path); update(); OnPathChanged(); ConnectionVisualNotificationBus::Event(GetEntityId(), &ConnectionVisualNotifications::OnConnectionPathUpdated); } void ConnectionGraphicsItem::SetAltDeletionEnabled(bool enabled) { SetAllowQuickDeletion(enabled); } void ConnectionGraphicsItem::OnSceneMemberHidden() { AZ::EntityId sourceId; ConnectionRequestBus::EventResult(sourceId, GetConnectionEntityId(), &ConnectionRequests::GetSourceSlotId); VisualNotificationBus::MultiHandler::BusDisconnect(sourceId); AZ::EntityId targetId; ConnectionRequestBus::EventResult(targetId, GetConnectionEntityId(), &ConnectionRequests::GetTargetSlotId); VisualNotificationBus::MultiHandler::BusDisconnect(targetId); } void ConnectionGraphicsItem::OnSceneMemberShown() { AZ::EntityId sourceId; ConnectionRequestBus::EventResult(sourceId, GetConnectionEntityId(), &ConnectionRequests::GetSourceSlotId); VisualNotificationBus::MultiHandler::BusConnect(sourceId); AZ::EntityId targetId; ConnectionRequestBus::EventResult(targetId, GetConnectionEntityId(), &ConnectionRequests::GetTargetSlotId); VisualNotificationBus::MultiHandler::BusConnect(targetId); UpdateConnectionPath(); } void ConnectionGraphicsItem::OnSceneSet(const GraphId& graphId) { m_editorId = EditorId(); SceneRequestBus::EventResult(m_editorId, graphId, &SceneRequests::GetEditorId); AssetEditorSettingsNotificationBus::Handler::BusDisconnect(); AssetEditorSettingsNotificationBus::Handler::BusConnect(m_editorId); UpdateCurveStyle(); } void ConnectionGraphicsItem::OnSettingsChanged() { UpdateCurveStyle(); } const AZ::EntityId& ConnectionGraphicsItem::GetConnectionEntityId() const { return m_connectionEntityId; } AZ::EntityId ConnectionGraphicsItem::GetSourceSlotEntityId() const { AZ::EntityId sourceId; ConnectionRequestBus::EventResult(sourceId, GetConnectionEntityId(), &ConnectionRequests::GetSourceSlotId); return sourceId; } AZ::EntityId ConnectionGraphicsItem::GetTargetSlotEntityId() const { AZ::EntityId targetId; ConnectionRequestBus::EventResult(targetId, GetConnectionEntityId(), &ConnectionRequests::GetTargetSlotId); return targetId; } EditorId ConnectionGraphicsItem::GetEditorId() const { return m_editorId; } void ConnectionGraphicsItem::UpdateCurveStyle() { Styling::ConnectionCurveType oldType = m_curveType; m_curveType = GetCurveStyle(); if (m_curveType != oldType) { UpdateConnectionPath(); } } Styling::ConnectionCurveType ConnectionGraphicsItem::GetCurveStyle() const { Styling::ConnectionCurveType curveStyle = Styling::ConnectionCurveType::Straight; AssetEditorSettingsRequestBus::EventResult(curveStyle, GetEditorId(), &AssetEditorSettingsRequests::GetConnectionCurveType); return curveStyle; } void ConnectionGraphicsItem::UpdatePen() { QPen pen = m_style.GetPen(Styling::Attribute::LineWidth, Styling::Attribute::LineStyle, Styling::Attribute::LineColor, Styling::Attribute::CapStyle); setPen(pen); } void ConnectionGraphicsItem::OnActivate() { } void ConnectionGraphicsItem::OnDeactivate() { } void ConnectionGraphicsItem::OnPathChanged() { } QPainterPath ConnectionGraphicsItem::shape() const { // Creates a "selectable area" around the connection QPainterPathStroker stroker; qreal padding = m_style.GetAttribute(Styling::Attribute::LineSelectionPadding, 0); stroker.setWidth(padding); return stroker.createStroke(path()); } void ConnectionGraphicsItem::mousePressEvent(QGraphicsSceneMouseEvent* mouseEvent) { if (mouseEvent->button() == Qt::MouseButton::LeftButton) { m_trackMove = false; if (GetDisplayState() == RootGraphicsItemDisplayState::Inspection) { const QPainterPath painterPath = path(); QPointF clickPoint = mouseEvent->scenePos(); QPointF startPoint = painterPath.pointAtPercent(0); QPointF endPoint = painterPath.pointAtPercent(1); float distanceToSource = aznumeric_cast((clickPoint - startPoint).manhattanLength()); float distanceToTarget = aznumeric_cast((clickPoint - endPoint).manhattanLength()); float maxDistance = m_style.GetAttribute(Styling::Attribute::ConnectionDragMaximumDistance, 100.0f); float dragPercentage = m_style.GetAttribute(Styling::Attribute::ConnectionDragPercent, 0.1f); float acceptanceDistance = AZStd::GetMin(maxDistance, aznumeric_cast(painterPath.length() * dragPercentage)); if (distanceToSource < acceptanceDistance) { m_trackMove = true; m_moveSource = true; m_initialPoint = mouseEvent->scenePos(); } else if (distanceToTarget < acceptanceDistance) { m_trackMove = true; m_moveSource = false; m_initialPoint = mouseEvent->scenePos(); } } else { RootGraphicsItem::mousePressEvent(mouseEvent); } } else { RootGraphicsItem::mousePressEvent(mouseEvent); } } void ConnectionGraphicsItem::mouseMoveEvent(QGraphicsSceneMouseEvent* mouseEvent) { if (m_trackMove) { float maxDistance = m_style.GetAttribute(Styling::Attribute::ConnectionDragMoveBuffer, 0.0f); float distanceFromInitial = aznumeric_cast((m_initialPoint - mouseEvent->scenePos()).manhattanLength()); if (distanceFromInitial > maxDistance) { m_trackMove = false; if (m_moveSource) { ConnectionRequestBus::Event(GetEntityId(), &ConnectionRequests::StartSourceMove); } else { ConnectionRequestBus::Event(GetEntityId(), &ConnectionRequests::StartTargetMove); } } } else { RootGraphicsItem::mouseMoveEvent(mouseEvent); } } void ConnectionGraphicsItem::mouseReleaseEvent(QGraphicsSceneMouseEvent* mouseEvent) { // Handle the case where we clicked but didn't drag. // So we want to select ourselves. if (mouseEvent->button() == Qt::MouseButton::LeftButton && shape().contains(mouseEvent->scenePos())) { if ((mouseEvent->modifiers() & Qt::KeyboardModifier::ControlModifier) == 0) { AZ::EntityId sceneId; SceneMemberRequestBus::EventResult(sceneId, GetEntityId(), &SceneMemberRequests::GetScene); SceneRequestBus::Event(sceneId, &SceneRequests::ClearSelection); setSelected(true); } else { setSelected(!isSelected()); } m_trackMove = false; } else { RootGraphicsItem::mouseReleaseEvent(mouseEvent); } } void ConnectionGraphicsItem::focusOutEvent(QFocusEvent* focusEvent) { if (m_trackMove) { m_trackMove = false; } RootGraphicsItem::focusOutEvent(focusEvent); } void ConnectionGraphicsItem::paint(QPainter* painter, const QStyleOptionGraphicsItem* option, QWidget* widget /*= nullptr*/) { GRAPH_CANVAS_DETAILED_PROFILE_FUNCTION(); bool showDefaultSelector = m_style.GetAttribute(Styling::Attribute::ConnectionDefaultMarquee, false); if (!showDefaultSelector) { // Remove the selected state to get rid of the marquee outline QStyleOptionGraphicsItem myoption = (*option); myoption.state &= !QStyle::State_Selected; QGraphicsPathItem::paint(painter, &myoption, widget); } else { QGraphicsPathItem::paint(painter, option, widget); } } }