From 181698b950e63236d77c525964d9af5bbe617932 Mon Sep 17 00:00:00 2001 From: sphrose <82213493+sphrose@users.noreply.github.com> Date: Mon, 16 Aug 2021 10:54:15 +0100 Subject: [PATCH] LY-91616 Dyn Veg: Push (up) and Pop (down) Items in Descriptor List (#3013) * LY-91616 Dyn Veg: Push (up) and Pop (down) Items in Descriptor List Signed-off-by: sphrose <82213493+sphrose@users.noreply.github.com> * Changes as per reviews. Signed-off-by: sphrose <82213493+sphrose@users.noreply.github.com> * Additional review changes. Signed-off-by: sphrose <82213493+sphrose@users.noreply.github.com> * review change Signed-off-by: sphrose <82213493+sphrose@users.noreply.github.com> --- .../Components/FancyDocking.cpp | 5 +- .../PropertyEditor/EntityPropertyEditor.cpp | 1117 ++++++++++++++--- .../PropertyEditor/EntityPropertyEditor.hxx | 70 +- .../UI/PropertyEditor/PropertyRowWidget.cpp | 183 ++- .../UI/PropertyEditor/PropertyRowWidget.hxx | 21 + .../ReflectedPropertyEditor.cpp | 224 +++- .../ReflectedPropertyEditor.hxx | 10 + 7 files changed, 1441 insertions(+), 189 deletions(-) diff --git a/Code/Framework/AzQtComponents/AzQtComponents/Components/FancyDocking.cpp b/Code/Framework/AzQtComponents/AzQtComponents/Components/FancyDocking.cpp index c5aa1a3f88..4ced4ea635 100644 --- a/Code/Framework/AzQtComponents/AzQtComponents/Components/FancyDocking.cpp +++ b/Code/Framework/AzQtComponents/AzQtComponents/Components/FancyDocking.cpp @@ -1882,7 +1882,10 @@ namespace AzQtComponents return; } - QApplication::setOverrideCursor(m_dragCursor); + if (!QApplication::overrideCursor()) + { + QApplication::setOverrideCursor(m_dragCursor); + } QPoint relativePressPos = pressPos; diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/UI/PropertyEditor/EntityPropertyEditor.cpp b/Code/Framework/AzToolsFramework/AzToolsFramework/UI/PropertyEditor/EntityPropertyEditor.cpp index f166e85ff9..2c46bb2d26 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/UI/PropertyEditor/EntityPropertyEditor.cpp +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/UI/PropertyEditor/EntityPropertyEditor.cpp @@ -16,6 +16,7 @@ AZ_POP_DISABLE_WARNING #include #include #include +#include #include #include #include @@ -106,7 +107,11 @@ void initEntityPropertyEditorResources() namespace AzToolsFramework { - static const char* kComponentEditorIndexMimeType = "editor/componentEditorIndices"; + constexpr const char* kComponentEditorIndexMimeType = "editor/componentEditorIndices"; + constexpr const char* kComponentEditorRowWidgetType = "editor/componentEditorRowWidget"; + + constexpr const char* kPropertyEditorMenuActionMoveUp("editor/propertyEditorMoveUp"); + constexpr const char* kPropertyEditorMenuActionMoveDown("editor/propertyEditorMoveDown"); //since component editors are spaced apart to make room for drop indicator, //giving drop logic simple buffer so drops between editors don't go to the bottom @@ -149,6 +154,7 @@ namespace AzToolsFramework : QWidget(parent) , m_editor(editor) , m_dropIndicatorOffset(8) + , m_dropIndicatorRowWidgetOffset(2) { setPalette(Qt::transparent); setWindowFlags(Qt::FramelessWindowHint); @@ -158,22 +164,137 @@ namespace AzToolsFramework } protected: - void paintEvent(QPaintEvent* event) override + static constexpr int TopMargin = 1; + static constexpr int RightMargin = 2; + static constexpr int BottomMargin = 5; + static constexpr int LeftMargin = 2; + static constexpr int RowHighlightIndent = 2; + + void paintDraggingRowWidget(QPainter& painter) { - const int TopMargin = 1; - const int RightMargin = 2; - const int BottomMargin = 5; - const int LeftMargin = 2; + ComponentEditor* rowWidgetEditor = m_editor->GetEditorForCurrentReorderRowWidget(); + PropertyRowWidget* dragRowWidget = m_editor->GetReorderRowWidget(); + PropertyRowWidget* dropTarget = m_editor->GetReorderDropTarget(); + EntityPropertyEditor::DropArea dropArea = m_editor->GetReorderDropArea(); - QWidget::paintEvent(event); + // The user is dragging a row widget. + for (auto componentEditor : m_editor->m_componentEditors) + { + if (!componentEditor->isVisible()) + { + continue; + } - QPainter painter(this); - painter.setCompositionMode(QPainter::CompositionMode_SourceOver); + if (componentEditor != rowWidgetEditor) + { + continue; + } - QRect currRect; + for (auto& [dataNode, rowWidget] : componentEditor->GetPropertyEditor()->GetWidgets()) + { + if (!rowWidget->isVisible()) + { + continue; + } + + QRect globalRect = m_editor->GetWidgetAndVisibleChildrenGlobalRect(rowWidget); + + QRect currRect = QRect( + QPoint(mapFromGlobal(globalRect.topLeft()) + QPoint(LeftMargin, TopMargin)), + QPoint(mapFromGlobal(globalRect.bottomRight()) - QPoint(RightMargin, BottomMargin))); + + currRect.setLeft(LeftMargin + RowHighlightIndent); + currRect.setWidth(rowWidget->GetContainingEditorFrameWidth() - (RightMargin + LeftMargin)); + + if (rowWidget == dragRowWidget) + { + QStyleOption opt; + opt.init(this); + opt.rect = currRect; + qobject_cast (style())->drawDragIndicator(&opt, &painter, this); + } + + if (rowWidget == dropTarget) + { + QRect dropRect = currRect; + if (dropArea == EntityPropertyEditor::DropArea::Above) + { + dropRect.setTop(currRect.top() - m_dropIndicatorRowWidgetOffset); + } + else + { + dropRect.setTop(currRect.bottom()); + } + + dropRect.setHeight(0); + + QStyleOption opt; + opt.init(this); + opt.rect = dropRect; + style()->drawPrimitive(QStyle::PE_IndicatorItemViewItemDrop, &opt, &painter, this); + } + } + } + }; + + void paintMenuHighlight(QPainter& painter, float alpha) + { + // If a RowWidget can be moved up or down, highlight it. + PropertyRowWidget* dragRowWidget = m_editor->GetReorderRowWidget(); + if (!dragRowWidget) + { + return; + } + + PropertyRowWidget* dropTarget = m_editor->GetReorderDropTarget(); + EntityPropertyEditor::DropArea dropArea = m_editor->GetReorderDropArea(); + QPixmap dragImage = m_editor->GetReorderRowWidgetImage(); + + // User has the context menu open with a movable row selected. + QRect globalRect = m_editor->GetWidgetAndVisibleChildrenGlobalRect(dragRowWidget); + + int top = mapFromGlobal(globalRect.topLeft()).y(); + int imageHeight = dragImage.height() / dragImage.devicePixelRatioF(); + int imageWidth = dragImage.width() / dragImage.devicePixelRatioF(); + QRect currRect = QRect(QPoint(LeftMargin + 1, top), QPoint(LeftMargin + 1 + imageWidth, top + imageHeight)); + + painter.setOpacity(alpha); + painter.drawPixmap(currRect, dragImage); + + if (dropTarget) + { + // A move row menu command is highlighted. Draw an indicator to show where the current row will move to. + globalRect = m_editor->GetWidgetAndVisibleChildrenGlobalRect(dropTarget); + QRect dropRect = QRect( + QPoint(mapFromGlobal(globalRect.topLeft()) + QPoint(LeftMargin, TopMargin)), + QPoint(mapFromGlobal(globalRect.bottomRight()) - QPoint(RightMargin, TopMargin))); + + if (dropArea == EntityPropertyEditor::DropArea::Above) + { + dropRect.setTop(dropRect.top() - m_dropIndicatorRowWidgetOffset); + } + else + { + dropRect.setTop(dropRect.bottom()); + } + dropRect.setHeight(0); + + painter.setOpacity(alpha); + QStyleOption lineOpt; + lineOpt.init(this); + lineOpt.rect = dropRect; + style()->drawPrimitive(QStyle::PE_IndicatorItemViewItemDrop, &lineOpt, &painter, this); + painter.setOpacity(1.0f); + } + }; + + void paintDraggingComponent(QPainter& painter) + { bool drag = false; bool drop = false; + QRect currRect; + // Check for a component editor being dragged. for (auto componentEditor : m_editor->m_componentEditors) { if (!componentEditor->isVisible()) @@ -185,8 +306,7 @@ namespace AzToolsFramework currRect = QRect( QPoint(mapFromGlobal(globalRect.topLeft()) + QPoint(LeftMargin, TopMargin)), - QPoint(mapFromGlobal(globalRect.bottomRight()) - QPoint(RightMargin, BottomMargin)) - ); + QPoint(mapFromGlobal(globalRect.bottomRight()) - QPoint(RightMargin, BottomMargin))); currRect.setWidth(currRect.width() - 1); currRect.setHeight(currRect.height() - 1); @@ -226,6 +346,67 @@ namespace AzToolsFramework opt.rect = dropRect; style()->drawPrimitive(QStyle::PE_IndicatorItemViewItemDrop, &opt, &painter, this); } + }; + + void paintMovedRow(QPainter& painter, float alpha) + { + // After a move has been carried out, briefly highlight the moved row. + PropertyRowWidget* rowWidget = m_editor->GetRowToHighlight(); + + if (!rowWidget) + { + return; + } + + QRect globalRect = m_editor->GetWidgetAndVisibleChildrenGlobalRect(rowWidget); + QRect currRect = QRect( + QPoint(mapFromGlobal(globalRect.topLeft()) + QPoint(LeftMargin, TopMargin)), + QPoint(mapFromGlobal(globalRect.bottomRight()) - QPoint(RightMargin, BottomMargin))); + + currRect.setLeft(LeftMargin + 2); + currRect.setWidth(rowWidget->GetContainingEditorFrameWidth() - (RightMargin + LeftMargin)); + + painter.setOpacity(alpha); + + QPen pen; + QColor drawColor = Qt::white; + drawColor.setAlphaF(alpha); + pen.setColor(drawColor); + pen.setWidth(1); + painter.setPen(pen); + painter.drawRect(currRect); + } + + void paintEvent(QPaintEvent* event) override + { + QWidget::paintEvent(event); + + QPainter painter(this); + painter.setCompositionMode(QPainter::CompositionMode_SourceOver); + + EntityPropertyEditor::ReorderState currentState = m_editor->GetReorderState(); + float indicatorAlpha = m_editor->GetMoveIndicatorAlpha(); + + switch (currentState) + { + case EntityPropertyEditor::ReorderState::DraggingRowWidget: + paintDraggingRowWidget(painter); + break; + case EntityPropertyEditor::ReorderState::UsingMenu: + paintMenuHighlight(painter, 1.0f); + break; + case EntityPropertyEditor::ReorderState::MenuOperationInProgress: + paintMenuHighlight(painter, indicatorAlpha); + break; + case EntityPropertyEditor::ReorderState::DraggingComponent: + paintDraggingComponent(painter); + break; + case EntityPropertyEditor::ReorderState::HighlightMovedRow: + paintMovedRow(painter, indicatorAlpha); + break; + default: + break; + } } bool event(QEvent* ev) override @@ -280,6 +461,7 @@ namespace AzToolsFramework private: EntityPropertyEditor* m_editor; int m_dropIndicatorOffset; + int m_dropIndicatorRowWidgetOffset; }; EntityPropertyEditor::SharedComponentInfo::SharedComponentInfo(AZ::Component* component, AZ::Component* sliceReferenceComponent) @@ -382,6 +564,9 @@ namespace AzToolsFramework m_emptyIcon = QIcon(); m_clearIcon = QIcon(":/AssetBrowser/Resources/close.png"); + m_dragIcon = QIcon(QStringLiteral(":/Cursors/Grabbing.svg")); + m_dragCursor = QCursor(m_dragIcon.pixmap(16), 10, 5); + m_serializeContext = nullptr; AZ::ComponentApplicationBus::BroadcastResult(m_serializeContext, &AZ::ComponentApplicationRequests::GetSerializeContext); AZ_Assert(m_serializeContext, "Failed to acquire application serialize context."); @@ -1867,6 +2052,12 @@ namespace AzToolsFramework return; } + // Don't show if a move operation is pending. + if (m_currentReorderState != ReorderState::Inactive) + { + return; + } + // Locate the owning component and class data corresponding to the clicked node. InstanceDataNode* componentNode = node; while (componentNode->GetParent()) @@ -1905,7 +2096,12 @@ namespace AzToolsFramework if (!menu.actions().empty()) { + m_currentReorderState = EntityPropertyEditor::ReorderState::UsingMenu; menu.exec(position); + if (m_currentReorderState != EntityPropertyEditor::ReorderState::MenuOperationInProgress) + { + m_currentReorderState = EntityPropertyEditor::ReorderState::Inactive; + } } } } @@ -1956,129 +2152,179 @@ namespace AzToolsFramework AZ::SliceComponent* rootSlice = nullptr; AzFramework::SliceEntityOwnershipServiceRequestBus::EventResult(rootSlice, contextId, &AzFramework::SliceEntityOwnershipServiceRequestBus::Events::GetRootSlice); - if (!rootSlice) + if (rootSlice) { - return; - } - - AZ::SliceComponent::SliceInstanceAddress address; - AzFramework::SliceEntityRequestBus::EventResult(address, entity->GetId(), - &AzFramework::SliceEntityRequests::GetOwningSlice); - AZ::SliceComponent::SliceReference* sliceReference = address.GetReference(); - if (sliceReference) - { - // This entity is instanced from a slice, so show data push/pull options - AZ::SliceComponent::EntityAncestorList ancestors; - sliceReference->GetInstanceEntityAncestry(entity->GetId(), ancestors); - - AZ_Error("PropertyEditor", !ancestors.empty(), "Entity \"%s\" belongs to a slice, but its source entity could not be located.", entity->GetName().c_str()); - if (!ancestors.empty()) + AZ::SliceComponent::SliceInstanceAddress address; + AzFramework::SliceEntityRequestBus::EventResult(address, entity->GetId(), &AzFramework::SliceEntityRequests::GetOwningSlice); + AZ::SliceComponent::SliceReference* sliceReference = address.GetReference(); + if (sliceReference) { - menu.addSeparator(); + // This entity is instanced from a slice, so show data push/pull options + AZ::SliceComponent::EntityAncestorList ancestors; + sliceReference->GetInstanceEntityAncestry(entity->GetId(), ancestors); - // Populate slice push options. - // Address should start with the fully-addressable component Id to resolve within the target entity. - InstanceDataHierarchy::Address pushFieldAddress; - CalculateAndAdjustNodeAddress(*fieldNode, AddressRootType::RootAtEntity, pushFieldAddress); - if (!pushFieldAddress.empty()) + AZ_Error( + "PropertyEditor", !ancestors.empty(), "Entity \"%s\" belongs to a slice, but its source entity could not be located.", + entity->GetName().c_str()); + if (!ancestors.empty()) { - SliceUtilities::PopulateQuickPushMenu( - menu, - entity->GetId(), - pushFieldAddress, - SliceUtilities::QuickPushMenuOptions("Save field override", SliceUtilities::QuickPushMenuOverrideDisplayCount::ShowOverrideCountOnlyWhenMultiple)); + menu.addSeparator(); + + // Populate slice push options. + // Address should start with the fully-addressable component Id to resolve within the target entity. + InstanceDataHierarchy::Address pushFieldAddress; + CalculateAndAdjustNodeAddress(*fieldNode, AddressRootType::RootAtEntity, pushFieldAddress); + if (!pushFieldAddress.empty()) + { + SliceUtilities::PopulateQuickPushMenu( + menu, entity->GetId(), pushFieldAddress, + SliceUtilities::QuickPushMenuOptions( + "Save field override", + SliceUtilities::QuickPushMenuOverrideDisplayCount::ShowOverrideCountOnlyWhenMultiple)); + } } } - } - menu.addSeparator(); + menu.addSeparator(); - // by leaf node, we mean a visual leaf node in the property editor (ie, we do not have any visible children) - bool isLeafNode = !fieldNode->GetClassMetadata() || !fieldNode->GetClassMetadata()->m_container; + // by leaf node, we mean a visual leaf node in the property editor (ie, we do not have any visible children) + bool isLeafNode = !fieldNode->GetClassMetadata() || !fieldNode->GetClassMetadata()->m_container; - if (isLeafNode) - { - for (const InstanceDataNode& childNode : fieldNode->GetChildren()) + if (isLeafNode) { - if (HasAnyVisibleElements(childNode)) + for (const InstanceDataNode& childNode : fieldNode->GetChildren()) { - // If we have any visible children, we must not be a leaf node - isLeafNode = false; - break; + if (HasAnyVisibleElements(childNode)) + { + // If we have any visible children, we must not be a leaf node + isLeafNode = false; + break; + } } } - } #ifdef ENABLE_SLICE_EDITOR - // Show PreventOverride & HideProperty options - if (GetEntityDataPatchAddress(fieldNode, m_dataPatchAddressBuffer)) - { - AZ::DataPatch::Flags nodeFlags = rootSlice->GetEntityDataFlagsAtAddress(entity->GetId(), m_dataPatchAddressBuffer); + // Show PreventOverride & HideProperty options + if (GetEntityDataPatchAddress(fieldNode, m_dataPatchAddressBuffer)) + { + AZ::DataPatch::Flags nodeFlags = rootSlice->GetEntityDataFlagsAtAddress(entity->GetId(), m_dataPatchAddressBuffer); - if (nodeFlags & AZ::DataPatch::Flag::PreventOverrideSet) - { - QAction* PreventOverrideAction = menu.addAction(tr("Allow property override")); - PreventOverrideAction->setEnabled(isLeafNode); - connect(PreventOverrideAction, &QAction::triggered, this, [this, fieldNode] + if (nodeFlags & AZ::DataPatch::Flag::PreventOverrideSet) { - ContextMenuActionSetDataFlag(fieldNode, AZ::DataPatch::Flag::PreventOverrideSet, false); - InvalidatePropertyDisplay(Refresh_AttributesAndValues); + QAction* PreventOverrideAction = menu.addAction(tr("Allow property override")); + PreventOverrideAction->setEnabled(isLeafNode); + connect( + PreventOverrideAction, &QAction::triggered, this, + [this, fieldNode] + { + ContextMenuActionSetDataFlag(fieldNode, AZ::DataPatch::Flag::PreventOverrideSet, false); + InvalidatePropertyDisplay(Refresh_AttributesAndValues); + }); } - ); - } - else - { - QAction* PreventOverrideAction = menu.addAction(tr("Prevent property override")); - PreventOverrideAction->setEnabled(isLeafNode); - connect(PreventOverrideAction, &QAction::triggered, this, [this, fieldNode] + else { - ContextMenuActionSetDataFlag(fieldNode, AZ::DataPatch::Flag::PreventOverrideSet, true); - InvalidatePropertyDisplay(Refresh_AttributesAndValues); + QAction* PreventOverrideAction = menu.addAction(tr("Prevent property override")); + PreventOverrideAction->setEnabled(isLeafNode); + connect( + PreventOverrideAction, &QAction::triggered, this, + [this, fieldNode] + { + ContextMenuActionSetDataFlag(fieldNode, AZ::DataPatch::Flag::PreventOverrideSet, true); + InvalidatePropertyDisplay(Refresh_AttributesAndValues); + }); } - ); - } - if (nodeFlags & AZ::DataPatch::Flag::HidePropertySet) - { - QAction* HideProperyAction = menu.addAction(tr("Show property on instances")); - HideProperyAction->setEnabled(isLeafNode); - connect(HideProperyAction, &QAction::triggered, this, [this, fieldNode] + if (nodeFlags & AZ::DataPatch::Flag::HidePropertySet) { - ContextMenuActionSetDataFlag(fieldNode, AZ::DataPatch::Flag::HidePropertySet, false); - InvalidatePropertyDisplay(Refresh_AttributesAndValues); + QAction* HideProperyAction = menu.addAction(tr("Show property on instances")); + HideProperyAction->setEnabled(isLeafNode); + connect( + HideProperyAction, &QAction::triggered, this, + [this, fieldNode] + { + ContextMenuActionSetDataFlag(fieldNode, AZ::DataPatch::Flag::HidePropertySet, false); + InvalidatePropertyDisplay(Refresh_AttributesAndValues); + }); } - ); - } - else - { - QAction* HideProperyAction = menu.addAction(tr("Hide property on instances")); - HideProperyAction->setEnabled(isLeafNode); - connect(HideProperyAction, &QAction::triggered, this, [this, fieldNode] + else { - ContextMenuActionSetDataFlag(fieldNode, AZ::DataPatch::Flag::HidePropertySet, true); - InvalidatePropertyDisplay(Refresh_AttributesAndValues); + QAction* HideProperyAction = menu.addAction(tr("Hide property on instances")); + HideProperyAction->setEnabled(isLeafNode); + connect( + HideProperyAction, &QAction::triggered, this, + [this, fieldNode] + { + ContextMenuActionSetDataFlag(fieldNode, AZ::DataPatch::Flag::HidePropertySet, true); + InvalidatePropertyDisplay(Refresh_AttributesAndValues); + }); } - ); } - } #endif - if (sliceReference) - { - // This entity is referenced from a slice, so show property override options - bool hasChanges = fieldNode->HasChangesVersusComparison(false); - - if (!hasChanges && isLeafNode) + if (sliceReference) { - // Add an option to set the ForceOverride flag for this field - menu.setToolTipsVisible(true); - QAction* forceOverrideAction = menu.addAction(tr("Force property override")); - forceOverrideAction->setToolTip(tr("Prevents a property from inheriting from its source slice")); - connect(forceOverrideAction, &QAction::triggered, this, [this, fieldNode]() - { - ContextMenuActionSetDataFlag(fieldNode, AZ::DataPatch::Flag::ForceOverrideSet, true); - } - ); + // This entity is referenced from a slice, so show property override options + bool hasChanges = fieldNode->HasChangesVersusComparison(false); + + if (!hasChanges && isLeafNode) + { + // Add an option to set the ForceOverride flag for this field + menu.setToolTipsVisible(true); + QAction* forceOverrideAction = menu.addAction(tr("Force property override")); + forceOverrideAction->setToolTip(tr("Prevents a property from inheriting from its source slice")); + connect( + forceOverrideAction, &QAction::triggered, this, + [this, fieldNode]() + { + ContextMenuActionSetDataFlag(fieldNode, AZ::DataPatch::Flag::ForceOverrideSet, true); + }); + } + } + } + + m_reorderRowWidget = nullptr; + // Add move up/down actions if appropriate + auto componentEditorIterator = m_componentToEditorMap.find(componentInstance); + AZ_Assert(componentEditorIterator != m_componentToEditorMap.end(), "Unable to find a component editor for the given component"); + if (componentEditorIterator != m_componentToEditorMap.end()) + { + m_reorderRowWidgetEditor = componentEditorIterator->second; + PropertyRowWidget* widget = componentEditorIterator->second->GetPropertyEditor()->GetWidgetFromNode(fieldNode); + if (widget->CanBeReordered()) + { + m_reorderRowWidget = widget; + SetRowWidgetHighlighted(widget); + + QAction* moveUpAction = menu.addAction(tr("Move %1 Up").arg(widget->GetNameLabel()->text())); + moveUpAction->setEnabled(false); + moveUpAction->setData(kPropertyEditorMenuActionMoveUp); + + if (widget->CanMoveUp()) + { + moveUpAction->setEnabled(true); + connect( + moveUpAction, &QAction::triggered, this, + [this, widget] + { + ContextMenuActionMoveItemUp(m_reorderRowWidgetEditor, widget); + }); + } + + QAction* moveDownAction = menu.addAction(tr("Move %1 Down").arg(widget->GetNameLabel()->text())); + moveDownAction->setEnabled(false); + moveDownAction->setData(kPropertyEditorMenuActionMoveDown); + if (widget->CanMoveDown()) + { + moveDownAction->setEnabled(true); + connect( + moveDownAction, &QAction::triggered, this, + [this, widget] + { + ContextMenuActionMoveItemDown(m_reorderRowWidgetEditor, widget); + }); + } + + menu.addSeparator(); } } } @@ -2380,6 +2626,98 @@ namespace AzToolsFramework } } + void EntityPropertyEditor::BeginMoveRowWidgetFade() + { + // Fade out the highlights and indicator bar for two seconds before moving. + m_moveFadeSecondsRemaining = MoveFadeSeconds; + m_currentReorderState = EntityPropertyEditor::ReorderState::MenuOperationInProgress; + AZ::TickBus::Handler::BusConnect(); + } + + void EntityPropertyEditor::HighlightMovedRowWidget() + { + if (m_currentReorderState != ReorderState::WaitForRedraw) + { + return; + } + UpdateOverlay(); + + m_currentReorderState = ReorderState::HighlightMovedRow; + m_moveFadeSecondsRemaining = MoveFadeSeconds; + + AZ::TickBus::Handler::BusConnect(); + m_overlay->setVisible(true); + } + + void EntityPropertyEditor::OnTick(float deltaTime, AZ::ScriptTimePoint /*time*/) + { + m_moveFadeSecondsRemaining -= deltaTime; + m_overlay->setVisible(true); + if (m_moveFadeSecondsRemaining <= 0.0f) + { + m_moveFadeSecondsRemaining = 0.0f; + + if (m_currentReorderState == ReorderState::MenuOperationInProgress) + { + m_reorderRowWidgetEditor->GetPropertyEditor()->MoveNodeToIndex(m_nodeToMove, m_indexMapOfMovedRow[0]); + + // Ensure the highlight gets drawn once the RPE is updated. + m_currentReorderState = ReorderState::WaitForRedraw; + AZ::TickBus::Handler::BusDisconnect(); + ScrollToNewComponent(); + } + else + { + m_currentReorderState = ReorderState::Inactive; + AZ::TickBus::Handler::BusDisconnect(); + m_overlay->setVisible(false); + } + } + + // Force a repaint to show the fade. + repaint(0, 0, -1, -1); + } + + void EntityPropertyEditor::GenerateRowWidgetIndexMapToChildIndex(PropertyRowWidget* parent, int destIndex) + { + m_indexMapOfMovedRow.clear(); + m_indexMapOfMovedRow.push_back(destIndex); + + while (parent) + { + int index = parent->GetIndexInParent(); + if (index < 0) + { + // Top level widget. + break; + } + m_indexMapOfMovedRow.push_back(parent->GetIndexInParent()); + parent = parent->GetParentRow(); + } + } + + void EntityPropertyEditor::ContextMenuActionMoveItemUp(ComponentEditor* componentEditor, PropertyRowWidget* rowWidget) + { + // After the RPE is rebuilt, there'll be no way to work out which is the moved RowWidget. + // Generate a map of the child indices up to the root. + PropertyRowWidget* parent = rowWidget->GetParentRow(); + GenerateRowWidgetIndexMapToChildIndex(parent, rowWidget->GetIndexInParent() - 1); + + m_reorderRowWidgetEditor = componentEditor; + m_nodeToMove = rowWidget->GetNode(); + BeginMoveRowWidgetFade(); + } + + void EntityPropertyEditor::ContextMenuActionMoveItemDown(ComponentEditor* componentEditor, PropertyRowWidget* rowWidget) + { + PropertyRowWidget* parent = rowWidget->GetParentRow(); + GenerateRowWidgetIndexMapToChildIndex(parent, rowWidget->GetIndexInParent() + 1); + + m_reorderRowWidgetEditor = componentEditor; + m_nodeToMove = rowWidget->GetNode(); + BeginMoveRowWidgetFade(); + } + void EntityPropertyEditor::CalculateAndAdjustNodeAddress(const InstanceDataNode& componentFieldNode, AddressRootType rootType, InstanceDataNode::Address& outAddress) const { outAddress = componentFieldNode.ComputeAddress(); @@ -2763,9 +3101,7 @@ namespace AzToolsFramework if (!menu.actions().empty()) { - m_isShowingContextMenu = true; menu.exec(position); - m_isShowingContextMenu = false; } } @@ -3463,6 +3799,76 @@ namespace AzToolsFramework return this == widget || isAncestorOf(widget); } + AZ::u32 EntityPropertyEditor::GetHeightOfRowAndVisibleChildren(const PropertyRowWidget* row) const + { + if (!row->isVisible()) + { + return 0; + } + + QRect rect = QRect(row->mapToGlobal(row->rect().topLeft()), row->mapToGlobal(row->rect().bottomRight())); + + AZ::u32 height = rect.height() + 1; + + for (AZ::u32 childIndex = 0; childIndex < row->GetChildRowCount(); childIndex++) + { + PropertyRowWidget* childRow = row->GetChildRowByIndex(childIndex); + if (childRow->isVisible()) + { + height += GetHeightOfRowAndVisibleChildren(childRow); + } + } + + return height; + } + + QRect EntityPropertyEditor::GetWidgetAndVisibleChildrenGlobalRect(const PropertyRowWidget* widget) const + { + QRect rect = QRect(widget->mapToGlobal(widget->rect().topLeft()), widget->mapToGlobal(widget->rect().bottomRight())); + rect.setHeight(GetHeightOfRowAndVisibleChildren(widget)); + + return rect; + } + + PropertyRowWidget* EntityPropertyEditor::GetRowWidgetAtSameLevelAfter(PropertyRowWidget* widget) const + { + PropertyRowWidget* parent = widget->GetParentRow(); + + bool found = false; + for (PropertyRowWidget* child : parent->GetChildrenRows()) + { + if (found) + { + return child; + } + + if (child == widget) + { + found = true; + } + } + + return nullptr; + } + + PropertyRowWidget* EntityPropertyEditor::GetRowWidgetAtSameLevelBefore(PropertyRowWidget* widget) const + { + PropertyRowWidget* parent = widget->GetParentRow(); + + PropertyRowWidget* previous = nullptr; + for (PropertyRowWidget* child : parent->GetChildrenRows()) + { + if (child == widget) + { + return previous; + } + + previous = child; + } + + return nullptr; + } + QRect EntityPropertyEditor::GetWidgetGlobalRect(const QWidget* widget) const { return QRect( @@ -3757,6 +4163,8 @@ namespace AzToolsFramework m_shouldScrollToNewComponents = false; m_shouldScrollToNewComponentsQueued = false; m_newComponentId.reset(); + + HighlightMovedRowWidget(); } void EntityPropertyEditor::QueueScrollToNewComponent() @@ -3909,9 +4317,64 @@ namespace AzToolsFramework event->accept(); } + bool EntityPropertyEditor::HandleMenuEvent(QObject* /*object*/, QEvent* event) + { + QMenu* menu = qobject_cast(QApplication::activePopupWidget()); + if (!menu) + { + return false; + } + + PropertyRowWidget* lastReorderDropTarget = m_reorderDropTarget; + + switch (event->type()) + { + case QEvent::Leave: + m_reorderDropTarget = nullptr; + break; + case QEvent::Enter: + // Drop through. + case QEvent::MouseMove: + QMouseEvent* originalMouseEvent = static_cast(event); + QAction* action = menu->actionAt(originalMouseEvent->pos()); + if (!action) + { + m_reorderDropTarget = nullptr; + break; + } + + if (action->data() == kPropertyEditorMenuActionMoveUp) + { + m_reorderDropTarget = GetRowWidgetAtSameLevelBefore(m_reorderRowWidget); + + m_reorderDropArea = EntityPropertyEditor::DropArea::Above; + } + else if (action->data() == kPropertyEditorMenuActionMoveDown) + { + m_reorderDropTarget = GetRowWidgetAtSameLevelAfter(m_reorderRowWidget); + + if (m_reorderDropTarget) + { + m_reorderDropArea = EntityPropertyEditor::DropArea::Below; + } + } + + break; + } + + if (lastReorderDropTarget != m_reorderDropTarget) + { + // Force a redraw as the menu is preventing automatic updates. + repaint(0, 0, -1, -1); + } + + return false; + } + //overridden to intercept application level mouse events for component editor selection bool EntityPropertyEditor::eventFilter(QObject* object, QEvent* event) { + HandleMenuEvent(object, event); HandleSelectionEvents(object, event); return false; } @@ -3919,11 +4382,23 @@ namespace AzToolsFramework void EntityPropertyEditor::mousePressEvent(QMouseEvent* event) { ResetDrag(event); + + PropertyRowWidget* rowWidget = FindPropertyRowWidgetAt(event->globalPos()); + if (rowWidget && rowWidget->CanBeReordered() && event->buttons() & Qt::LeftButton) + { + QApplication::setOverrideCursor(m_dragCursor); + } } void EntityPropertyEditor::mouseReleaseEvent(QMouseEvent* event) { ResetDrag(event); + + Qt::MouseButtons realButtons = QApplication::mouseButtons(); + if (QApplication::overrideCursor() && !(event->buttons() & Qt::LeftButton)) + { + QApplication::restoreOverrideCursor(); + } } void EntityPropertyEditor::mouseMoveEvent(QMouseEvent* event) @@ -3963,6 +4438,21 @@ namespace AzToolsFramework void EntityPropertyEditor::dropEvent(QDropEvent* event) { HandleDrop(event); + + if (QApplication::overrideCursor()) + { + QApplication::restoreOverrideCursor(); + } + } + + void EntityPropertyEditor::DragStopped() + { + if (QApplication::overrideCursor()) + { + QApplication::restoreOverrideCursor(); + } + + EndRowWidgetReorder(); } bool EntityPropertyEditor::HandleSelectionEvents(QObject* object, QEvent* event) @@ -4319,11 +4809,110 @@ namespace AzToolsFramework return true; } + bool EntityPropertyEditor::FindAllowedRowWidgetReorderDropTarget(const QPoint& globalPos) + { + const QRect globalRect(globalPos, globalPos); + + AZ_Assert(m_reorderRowWidgetEditor, "Missing editor for row widget drag."); + + AzToolsFramework::ReflectedPropertyEditor::WidgetList widgets = m_reorderRowWidgetEditor->GetPropertyEditor()->GetWidgets(); + for (auto widgetPair : widgets) + { + PropertyRowWidget* widget = widgetPair.second; + if (!widget) + { + continue; + } + + if (DoesIntersectWidget(globalRect, reinterpret_cast(widget))) + { + if (widget->CanBeReordered() && widget->GetParentRow() == m_reorderRowWidget->GetParentRow()) + { + m_reorderDropTarget = widget; + + QRect widgetRect = GetWidgetAndVisibleChildrenGlobalRect(widget); + if (globalPos.y() < widgetRect.center().y()) + { + m_reorderDropArea = EntityPropertyEditor::DropArea::Above; + } + else + { + m_reorderDropArea = EntityPropertyEditor::DropArea::Below; + } + + return true; + } + + // We're hovering over a child of a reorderable ancestor, use the ancestor as the drop target. + PropertyRowWidget* parent = widget->GetParentRow(); + while (parent) + { + if (parent->CanBeReordered() && parent->GetParentRow() == m_reorderRowWidget->GetParentRow()) + { + m_reorderDropTarget = parent; + + QRect widgetRect = GetWidgetAndVisibleChildrenGlobalRect(parent); + if (globalPos.y() < widgetRect.center().y()) + { + m_reorderDropArea = EntityPropertyEditor::DropArea::Above; + } + else + { + m_reorderDropArea = EntityPropertyEditor::DropArea::Below; + } + + return true; + } + parent = parent->GetParentRow(); + } + } + } + + return false; + } + + bool EntityPropertyEditor::UpdateRowWidgetDrag(const QPoint& localPos, Qt::MouseButtons mouseButtons, const QMimeData* /*mimeData*/) + { + const QPoint globalPos(mapToGlobal(localPos)); + const QRect globalRect(globalPos, globalPos); + + if (!m_reorderRowWidget) + { + return false; + } + + if (m_reorderDropTarget) + { + m_reorderDropTarget = nullptr; + } + + UpdateOverlay(); + + // additional checks since handling is done in event filter + if ((mouseButtons & Qt::LeftButton) && DoesIntersectWidget(globalRect, this)) + { + FindAllowedRowWidgetReorderDropTarget(globalPos); + { + UpdateOverlay(); + return true; + } + } + return false; + } + bool EntityPropertyEditor::UpdateDrag(const QPoint& localPos, Qt::MouseButtons mouseButtons, const QMimeData* mimeData) { const QPoint globalPos(mapToGlobal(localPos)); const QRect globalRect(globalPos, globalPos); + if (m_reorderRowWidget) + { + UpdateRowWidgetDrag(localPos, mouseButtons, mimeData); + QueueAutoScroll(); + UpdateOverlay(); + return true; + } + //reset drop indicators for (auto componentEditor : m_componentEditors) { @@ -4357,6 +4946,28 @@ namespace AzToolsFramework return false; } + PropertyRowWidget* EntityPropertyEditor::FindPropertyRowWidgetAt(QPoint globalPos) + { + const QRect globalRect(globalPos, globalPos); + + const bool dragSelected = DoesIntersectSelectedComponentEditor(globalRect); + const auto& componentEditors = dragSelected ? GetSelectedComponentEditors() : GetIntersectingComponentEditors(globalRect); + + for (auto componentEditor : componentEditors) + { + AzToolsFramework::ReflectedPropertyEditor::WidgetList widgets = componentEditor->GetPropertyEditor()->GetWidgets(); + for (auto& [dataNode, rowWidget] : widgets) + { + if (DoesIntersectWidget(globalRect, reinterpret_cast(rowWidget)) && rowWidget->CanBeReordered()) + { + return rowWidget; + } + } + } + + return nullptr; + } + bool EntityPropertyEditor::StartDrag(QMouseEvent* event) { // do not initiate a drag if property editor is disabled @@ -4402,7 +5013,27 @@ namespace AzToolsFramework if (!intersectsHeader) { - return false; + for (auto componentEditor : componentEditors) + { + AzToolsFramework::ReflectedPropertyEditor::WidgetList widgets = componentEditor->GetPropertyEditor()->GetWidgets(); + for (AZStd::pair w : widgets) + { + if (w.second) + { + if (DoesIntersectWidget(dragRect, reinterpret_cast(w.second)) && w.second->CanBeReordered()) + { + m_currentReorderState = EntityPropertyEditor::ReorderState::DraggingRowWidget; + m_reorderRowWidget = w.second; + m_reorderRowWidgetEditor = componentEditor; + if (m_reorderDropTarget) + { + m_reorderDropTarget = nullptr; + } + break; + } + } + } + } } m_dragStarted = true; @@ -4412,105 +5043,166 @@ namespace AzToolsFramework QRect dragImageRect; - AZStd::vector componentEditorIndices; - componentEditorIndices.reserve(componentEditors.size()); - for (auto componentEditor : componentEditors) + if (m_reorderRowWidget) { - //compute the drag image size - if (componentEditorIndices.empty()) - { - dragImageRect = componentEditor->rect(); - } - else - { - dragImageRect.setHeight(dragImageRect.height() + componentEditor->rect().height()); - } + // We're dragging a PropertyRowWidget, grab the image from that. + mimeData->setData(kComponentEditorRowWidgetType, QByteArray()); - //add component editor index to drag data - auto componentEditorIndex = GetComponentEditorIndex(componentEditor); - if (componentEditorIndex >= 0) - { - componentEditorIndices.push_back(GetComponentEditorIndex(componentEditor)); - } + drag->setMimeData(mimeData); + drag->setPixmap(m_reorderRowWidget->createDragImage( + QColor("#8E863E"), QColor("#EAECAA"), 0.5f, PropertyRowWidget::DragImageType::SingleRow)); + drag->setHotSpot(m_dragStartPosition - GetWidgetGlobalRect(m_reorderRowWidget).topLeft()); + drag->setDragCursor(m_dragIcon.pixmap(32), Qt::DropAction::MoveAction); + // Ensure we can tidy up if the drop happens elsewhere. + connect(drag, &QObject::destroyed, this, &EntityPropertyEditor::DragStopped); + drag->exec(Qt::MoveAction, Qt::MoveAction); } - - //build image from dragged editor UI - QImage dragImage(dragImageRect.size(), QImage::Format_ARGB32_Premultiplied); - QPainter painter(&dragImage); - painter.setCompositionMode(QPainter::CompositionMode_Source); - painter.fillRect(dragImageRect, Qt::transparent); - painter.setCompositionMode(QPainter::CompositionMode_SourceOver); - painter.setOpacity(0.5f); - - //render a vertical stack of component editors, may change to render just the headers - QPoint dragImageOffset(0, 0); - for (AZ::s32 index : componentEditorIndices) + else { - auto componentEditor = GetComponentEditorsFromIndex(index); - if (componentEditor) + AZStd::vector componentEditorIndices; + componentEditorIndices.reserve(componentEditors.size()); + for (auto componentEditor : componentEditors) { - if (DoesIntersectWidget(dragRect, componentEditor)) + // compute the drag image size + if (componentEditorIndices.empty()) { - //offset drag image from the drag start position - drag->setHotSpot(dragImageOffset + (m_dragStartPosition - GetWidgetGlobalRect(componentEditor).topLeft())); + dragImageRect = componentEditor->rect(); + } + else + { + dragImageRect.setHeight(dragImageRect.height() + componentEditor->rect().height()); } - //render the component editor to the drag image - componentEditor->render(&painter, dragImageOffset); - - //update the render offset by the component editor height - dragImageOffset.setY(dragImageOffset.y() + componentEditor->rect().height()); + // add component editor index to drag data + auto componentEditorIndex = GetComponentEditorIndex(componentEditor); + if (componentEditorIndex >= 0) + { + componentEditorIndices.push_back(GetComponentEditorIndex(componentEditor)); + } } - } - painter.end(); - //mark dragged components after drag initiated to draw indicators - for (AZ::s32 index : componentEditorIndices) - { - auto componentEditor = GetComponentEditorsFromIndex(index); - if (componentEditor) + // build image from dragged editor UI + QImage dragImage(dragImageRect.size(), QImage::Format_ARGB32_Premultiplied); + QPainter painter(&dragImage); + painter.setCompositionMode(QPainter::CompositionMode_Source); + painter.fillRect(dragImageRect, Qt::transparent); + painter.setCompositionMode(QPainter::CompositionMode_SourceOver); + painter.setOpacity(0.5f); + + // render a vertical stack of component editors, may change to render just the headers + QPoint dragImageOffset(0, 0); + for (AZ::s32 index : componentEditorIndices) { - componentEditor->SetDragged(true); + auto componentEditor = GetComponentEditorsFromIndex(index); + if (componentEditor) + { + if (DoesIntersectWidget(dragRect, componentEditor)) + { + // offset drag image from the drag start position + drag->setHotSpot(dragImageOffset + (m_dragStartPosition - GetWidgetGlobalRect(componentEditor).topLeft())); + } + + // render the component editor to the drag image + componentEditor->render(&painter, dragImageOffset); + + // update the render offset by the component editor height + dragImageOffset.setY(dragImageOffset.y() + componentEditor->rect().height()); + } } - } - UpdateOverlay(); + painter.end(); - //encode component editor indices as internal drag data - mimeData->setData( - kComponentEditorIndexMimeType, - QByteArray(reinterpret_cast(componentEditorIndices.data()), static_cast(componentEditorIndices.size() * sizeof(AZ::s32)))); - - drag->setMimeData(mimeData); - drag->setPixmap(QPixmap::fromImage(dragImage)); - drag->exec(Qt::MoveAction, Qt::MoveAction); - - //mark dragged components after drag completed to stop drawing indicators - for (AZ::s32 index : componentEditorIndices) - { - auto componentEditor = GetComponentEditorsFromIndex(index); - if (componentEditor) + // mark dragged components after drag initiated to draw indicators + for (AZ::s32 index : componentEditorIndices) { - componentEditor->SetDragged(false); + auto componentEditor = GetComponentEditorsFromIndex(index); + if (componentEditor) + { + componentEditor->SetDragged(true); + } + } + UpdateOverlay(); + + // encode component editor indices as internal drag data + mimeData->setData( + kComponentEditorIndexMimeType, + QByteArray( + reinterpret_cast(componentEditorIndices.data()), + static_cast(componentEditorIndices.size() * sizeof(AZ::s32)))); + + drag->setMimeData(mimeData); + drag->setPixmap(QPixmap::fromImage(dragImage)); + drag->exec(Qt::MoveAction, Qt::MoveAction); + + // mark dragged components after drag completed to stop drawing indicators + for (AZ::s32 index : componentEditorIndices) + { + auto componentEditor = GetComponentEditorsFromIndex(index); + if (componentEditor) + { + componentEditor->SetDragged(false); + } } } + UpdateOverlay(); return true; } + void EntityPropertyEditor::SetRowWidgetHighlighted(PropertyRowWidget* rowWidget) + { + m_reorderRowWidget = rowWidget; + m_reorderRowImage = rowWidget->createDragImage( + QColor("#8E863E"), QColor("#EAECAA"), 0.5f, PropertyRowWidget::DragImageType::IncludeVisibleChildren); + } + + void EntityPropertyEditor::EndRowWidgetReorder() + { + m_reorderDropTarget = nullptr; + m_reorderRowWidget = nullptr; + m_reorderDropTarget = nullptr; + m_currentReorderState = EntityPropertyEditor::ReorderState::Inactive; + m_overlay->setVisible(false); + } + bool EntityPropertyEditor::HandleDrop(QDropEvent* event) { const QPoint globalPos(mapToGlobal(event->pos())); const QMimeData* mimeData = event->mimeData(); - if (IsDropAllowed(mimeData, globalPos)) + + if (m_currentReorderState == EntityPropertyEditor::ReorderState::DraggingRowWidget) { - //handle drop for supported mime types - HandleDropForComponentTypes(event); - HandleDropForComponentAssets(event); - HandleDropForAssetBrowserEntries(event); - HandleDropForComponentReorder(event); + if (FindAllowedRowWidgetReorderDropTarget(globalPos)) + { + if (m_reorderDropArea == EntityPropertyEditor::DropArea::Above) + { + m_reorderRowWidgetEditor->GetPropertyEditor()->MoveNodeBefore( + m_reorderRowWidget->GetNode(), m_reorderDropTarget->GetNode()); + } + else + { + m_reorderRowWidgetEditor->GetPropertyEditor()->MoveNodeAfter( + m_reorderRowWidget->GetNode(), m_reorderDropTarget->GetNode()); + } + } + event->acceptProposedAction(); - return true; + + EndRowWidgetReorder(); } + else + { + if (IsDropAllowed(mimeData, globalPos)) + { + // handle drop for supported mime types + HandleDropForComponentTypes(event); + HandleDropForComponentAssets(event); + HandleDropForAssetBrowserEntries(event); + HandleDropForComponentReorder(event); + event->acceptProposedAction(); + return true; + } + } + return false; } @@ -5036,6 +5728,69 @@ namespace AzToolsFramework componentEditor->ActiveComponentModeChanged(componentType); } } + + EntityPropertyEditor::ReorderState EntityPropertyEditor::GetReorderState() const + { + return m_currentReorderState; + } + + ComponentEditor* EntityPropertyEditor::GetEditorForCurrentReorderRowWidget() const + { + return m_reorderRowWidgetEditor; + } + + PropertyRowWidget* EntityPropertyEditor::GetReorderRowWidget() const + { + return m_reorderRowWidget; + } + + PropertyRowWidget* EntityPropertyEditor::GetReorderDropTarget() const + { + return m_reorderDropTarget; + } + + EntityPropertyEditor::DropArea EntityPropertyEditor::GetReorderDropArea() const + { + return m_reorderDropArea; + } + + QPixmap EntityPropertyEditor::GetReorderRowWidgetImage() const + { + return m_reorderRowImage; + } + + float EntityPropertyEditor::GetMoveIndicatorAlpha() const + { + if (m_currentReorderState != ReorderState::MenuOperationInProgress) + { + return 1.0f; + } + + return m_moveFadeSecondsRemaining / MoveFadeSeconds; + } + + PropertyRowWidget* EntityPropertyEditor::GetRowToHighlight() + { + // Use the pregenerated map to find the RowWidget that's in the new position. + QSet rowWidgets = m_reorderRowWidgetEditor->GetPropertyEditor()->GetTopLevelWidgets(); + if (rowWidgets.isEmpty()) + { + return nullptr; + } + + PropertyRowWidget* highlightRow = *rowWidgets.begin(); + + int mapIndex = static_cast(m_indexMapOfMovedRow.size() - 1); + + while (mapIndex >= 0) + { + int mapEntry = m_indexMapOfMovedRow[mapIndex]; + highlightRow = highlightRow->GetChildrenRows()[mapEntry]; + mapIndex--; + } + + return highlightRow; + } } StatusComboBox::StatusComboBox(QWidget* parent) diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/UI/PropertyEditor/EntityPropertyEditor.hxx b/Code/Framework/AzToolsFramework/AzToolsFramework/UI/PropertyEditor/EntityPropertyEditor.hxx index 166d4c4392..3ff69f80e0 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/UI/PropertyEditor/EntityPropertyEditor.hxx +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/UI/PropertyEditor/EntityPropertyEditor.hxx @@ -19,6 +19,7 @@ #include #include #include +#include #include #include #include @@ -110,6 +111,7 @@ namespace AzToolsFramework , public EditorInspectorComponentNotificationBus::MultiHandler , private AzToolsFramework::ComponentModeFramework::EditorComponentModeNotificationBus::Handler , public AZ::EntitySystemBus::Handler + , public AZ::TickBus::Handler , private EditorWindowUIRequestBus::Handler { Q_OBJECT; @@ -117,6 +119,23 @@ namespace AzToolsFramework AZ_CLASS_ALLOCATOR(EntityPropertyEditor, AZ::SystemAllocator, 0) + enum class ReorderState + { + Inactive, // No row widget reordering operation is in progress. + DraggingComponent, // User is dragging a component editor. + DraggingRowWidget, // User is dragging a row widget around. + UsingMenu, // User has the context menu open and may hover over a move up/down operation. + MenuOperationInProgress, // User has selected a move/up down menu item. + WaitForRedraw, // Wait for rebuild of RPE. + HighlightMovedRow // User has moved a row, highlight the new position. + }; + + enum class DropArea + { + Above, + Below + }; + EntityPropertyEditor(QWidget* pParent = NULL, Qt::WindowFlags flags = Qt::WindowFlags(), bool isLevelEntityEditor = false); virtual ~EntityPropertyEditor(); @@ -151,6 +170,16 @@ namespace AzToolsFramework bool IsLockedToSpecificEntities() const { return !m_overrideSelectedEntityIds.empty(); } static bool AreComponentsCopyable(const AZ::Entity::ComponentArrayType& components, const ComponentFilter& filter); + + ReorderState GetReorderState() const; + ComponentEditor* GetEditorForCurrentReorderRowWidget() const; + PropertyRowWidget* GetReorderRowWidget() const; + PropertyRowWidget* GetReorderDropTarget() const; + DropArea GetReorderDropArea() const; + QPixmap GetReorderRowWidgetImage() const; + float GetMoveIndicatorAlpha() const; + PropertyRowWidget* GetRowToHighlight(); + Q_SIGNALS: void SelectedEntityNameChanged(const AZ::EntityId& entityId, const AZStd::string& name); @@ -211,6 +240,9 @@ namespace AzToolsFramework void GetSelectedEntities(EntityIdList& selectedEntityIds) override; void SetNewComponentId(AZ::ComponentId componentId) override; + // TickBus + void OnTick(float deltaTime, AZ::ScriptTimePoint time) override; + // EditorWindowRequestBus overrides void SetEditorUiEnabled(bool enable) override; @@ -253,6 +285,10 @@ namespace AzToolsFramework void ContextMenuActionPullFieldData(AZ::Component* parentComponent, InstanceDataNode* fieldNode); void ContextMenuActionSetDataFlag(InstanceDataNode* node, AZ::DataPatch::Flag flag, bool additive); + void GenerateRowWidgetIndexMapToChildIndex(PropertyRowWidget* parent, int destIndex); + void ContextMenuActionMoveItemUp(ComponentEditor* componentEditor, PropertyRowWidget* rowWidget); + void ContextMenuActionMoveItemDown(ComponentEditor* componentEditor, PropertyRowWidget* rowWidget); + /// Given an InstanceDataNode, calculate a DataPatch address relative to the entity. /// @return true if successful. bool GetEntityDataPatchAddress(const InstanceDataNode* componentFieldNode, AZ::DataPatch::AddressType& dataPatchAddressOut, AZ::EntityId* entityIdOut = nullptr) const; @@ -341,8 +377,6 @@ namespace AzToolsFramework QAction* m_actionToMoveComponentsBottom = nullptr; QAction* m_resetToSliceAction = nullptr; - bool m_isShowingContextMenu = false; - void CreateActions(); void UpdateActions(); @@ -390,6 +424,10 @@ namespace AzToolsFramework void ResetToSlice(); bool DoesOwnFocus() const; + AZ::u32 GetHeightOfRowAndVisibleChildren(const PropertyRowWidget* row) const; + QRect GetWidgetAndVisibleChildrenGlobalRect(const PropertyRowWidget* widget) const; + PropertyRowWidget* GetRowWidgetAtSameLevelAfter(PropertyRowWidget* widget) const; + PropertyRowWidget* GetRowWidgetAtSameLevelBefore(PropertyRowWidget* widget) const; QRect GetWidgetGlobalRect(const QWidget* widget) const; bool DoesIntersectWidget(const QRect& globalRect, const QWidget* widget) const; bool DoesIntersectSelectedComponentEditor(const QRect& globalRect) const; @@ -445,6 +483,8 @@ namespace AzToolsFramework bool HandleSelectionEvents(QObject* object, QEvent* event); bool m_selectionEventAccepted; + bool HandleMenuEvent(QObject* object, QEvent* event); + // drag and drop events QRect GetInflatedRectFromPoint(const QPoint& point, int radius) const; bool GetComponentsAtDropEventPosition(QDropEvent* event, AZ::Entity::ComponentArrayType& targetComponents); @@ -458,8 +498,12 @@ namespace AzToolsFramework ComponentEditor* GetReorderDropTarget(const QRect& globalRect) const; bool ResetDrag(QMouseEvent* event); + bool FindAllowedRowWidgetReorderDropTarget(const QPoint& globalPos); + bool UpdateRowWidgetDrag(const QPoint& localPos, Qt::MouseButtons mouseButtons, const QMimeData* mimeData); + PropertyRowWidget* FindPropertyRowWidgetAt(QPoint globalPos); bool UpdateDrag(const QPoint& localPos, Qt::MouseButtons mouseButtons, const QMimeData* mimeData); bool StartDrag(QMouseEvent* event); + void EndRowWidgetReorder(); bool HandleDrop(QDropEvent* event); bool HandleDropForComponentTypes(QDropEvent* event); bool HandleDropForComponentAssets(QDropEvent* event); @@ -468,6 +512,8 @@ namespace AzToolsFramework bool CanDropForComponentTypes(const QMimeData* mimeData) const; bool CanDropForComponentAssets(const QMimeData* mimeData) const; bool CanDropForAssetBrowserEntries(const QMimeData* mimeData) const; + void SetRowWidgetHighlighted(PropertyRowWidget* rowWidget); + AZStd::vector ExtractComponentEditorIndicesFromMimeData(const QMimeData* mimeData) const; ComponentEditorVector GetComponentEditorsFromIndices(const AZStd::vector& indices) const; ComponentEditor* GetComponentEditorsFromIndex(const AZ::s32 index) const; @@ -559,6 +605,8 @@ namespace AzToolsFramework QIcon m_emptyIcon; QIcon m_clearIcon; + QIcon m_dragIcon; + QCursor m_dragCursor; QStandardItem* m_comboItems[StatusItems]; EntityIdSet m_overrideSelectedEntityIds; @@ -566,6 +614,19 @@ namespace AzToolsFramework Prefab::PrefabPublicInterface* m_prefabPublicInterface = nullptr; bool m_prefabsAreEnabled = false; + // Reordering row widgets within the RPE. + static constexpr float MoveFadeSeconds = 0.5f; + + ReorderState m_currentReorderState = ReorderState::Inactive; + ComponentEditor* m_reorderRowWidgetEditor = nullptr; + InstanceDataNode* m_nodeToMove = nullptr; + PropertyRowWidget* m_reorderRowWidget = nullptr; + PropertyRowWidget* m_reorderDropTarget = nullptr; + DropArea m_reorderDropArea = DropArea::Above; + QPixmap m_reorderRowImage; + float m_moveFadeSecondsRemaining; + AZStd::vector m_indexMapOfMovedRow; + // When m_initiatingPropertyChangeNotification is set to true, it means this EntityPropertyEditor is // broadcasting a change to all listeners about a property change for a given entity. This is needed // so that we don't update the values twice for this inspector @@ -573,6 +634,9 @@ namespace AzToolsFramework void ConnectToEntityBuses(const AZ::EntityId& entityId); void DisconnectFromEntityBuses(const AZ::EntityId& entityId); + void BeginMoveRowWidgetFade(); + void HighlightMovedRowWidget(); + //! Stores a component id to be focused on next time the UI updates. AZStd::optional m_newComponentId; @@ -594,6 +658,8 @@ namespace AzToolsFramework bool SelectedEntitiesAreFromSameSourceSliceEntity() const; + void DragStopped(); + AZ::Entity* GetSelectedEntityById(AZ::EntityId& entityId) const; }; diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/UI/PropertyEditor/PropertyRowWidget.cpp b/Code/Framework/AzToolsFramework/AzToolsFramework/UI/PropertyEditor/PropertyRowWidget.cpp index 05c04872e3..c3da1e69b0 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/UI/PropertyEditor/PropertyRowWidget.cpp +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/UI/PropertyEditor/PropertyRowWidget.cpp @@ -368,10 +368,15 @@ namespace AzToolsFramework delete m_containerAddButton; } + this->unsetCursor(); + if ((m_parentRow) && (m_parentRow->IsContainerEditable())) { if (!m_elementRemoveButton) { + QIcon icon = QIcon(QStringLiteral(":/Cursors/Grab_release.svg")); + this->setCursor(QCursor(icon.pixmap(16), 5, 2)); + static QIcon s_iconRemove(QStringLiteral(":/stylesheet/img/UI20/delete-16.svg")); m_elementRemoveButton = new QToolButton(this); m_elementRemoveButton->setAutoRaise(true); @@ -570,7 +575,12 @@ namespace AzToolsFramework AZ_Assert(m_selectionEnabled, "Property is not selectable"); m_isSelected = selected; m_nameLabel->setProperty("selected", selected); - } + } + + bool PropertyRowWidget::GetSelected() + { + return m_isSelected; + } void PropertyRowWidget::SetSelectionEnabled(bool selectionEnabled) { @@ -1395,6 +1405,21 @@ namespace AzToolsFramework return !m_childrenRows.empty(); } + AZ::u32 PropertyRowWidget::GetChildRowCount() const + { + return static_cast(m_childrenRows.size()); + } + + PropertyRowWidget* PropertyRowWidget::GetChildRowByIndex(AZ::u32 index) const + { + if (index >= m_childrenRows.size()) + { + return nullptr; + } + + return m_childrenRows[index]; + } + bool PropertyRowWidget::ShouldPreValidatePropertyChange() const { return (m_changeValidators.size() > 0); @@ -1722,6 +1747,162 @@ namespace AzToolsFramework return m_parentRow->CanChildrenBeReordered(); } + + int PropertyRowWidget::GetIndexInParent() const + { + if (!GetParentRow()) + { + return -1; + } + + for (int index = 0; index < GetParentRow()->GetChildRowCount(); index++) + { + if (GetParentRow()->GetChildrenRows()[index] == this) + { + return index; + } + } + + return -1; + } + + bool PropertyRowWidget::CanMoveUp() const + { + if (!CanBeReordered()) + { + return false; + } + + return this != m_parentRow->GetChildRowByIndex(0); + } + + bool PropertyRowWidget::CanMoveDown() const + { + if (!CanBeReordered()) + { + return false; + } + + AZ::u32 numChildrenOfParent = m_parentRow->GetChildRowCount(); + + return this != m_parentRow->GetChildRowByIndex(numChildrenOfParent - 1); + } + + int PropertyRowWidget::GetContainingEditorFrameWidth() + { + QWidget* parent = parentWidget(); + + // Find the first ancestor that can be cast to a QFrame, this will be the RPE. + while (!qobject_cast(parent)) + { + parent = parent->parentWidget(); + } + + if (!parent) + { + return 0; + } + + // The parent of the RPE is the size we want. + parent = parent->parentWidget(); + + return parent->rect().width(); + } + + int PropertyRowWidget::GetHeightOfRowAndVisibleChildren() + { + int height = rect().height(); + + if (!GetChildRowCount() || !IsExpanded()) + { + return height; + } + + for (auto childRow : GetChildrenRows()) + { + height += childRow->GetHeightOfRowAndVisibleChildren(); + } + + return height; + } + + int PropertyRowWidget::DrawDragImageAndVisibleChildrenInto(QPainter& painter, int xpos, int ypos) + { + // Render our image into the given painter. + int ystart = ypos; + + render(&painter, QPoint(xpos, ypos)); + + if (!GetChildRowCount() || !IsExpanded()) + { + return rect().height(); + } + + ypos += rect().height(); + + // Recursively draw any children. + for (auto childRow : GetChildrenRows()) + { + ypos += childRow->DrawDragImageAndVisibleChildrenInto(painter, xpos, ypos); + } + + return ypos - ystart; + } + + QPixmap PropertyRowWidget::createDragImage( + const QColor backgroundColor, const QColor borderColor, const float alpha, DragImageType imageType) + { + // Make the drag box as wide as the containing editor minus a gap each side for the border. + static constexpr int ParentEditorBorderSize = 2; + int width = GetContainingEditorFrameWidth() - ParentEditorBorderSize * 2; + int height = 0; + + if (imageType == DragImageType::IncludeVisibleChildren) + { + height = GetHeightOfRowAndVisibleChildren(); + } + else + { + height = rect().height(); + } + + const auto dpr = devicePixelRatioF(); + QPixmap dragImage(width * dpr, height * dpr); + dragImage.setDevicePixelRatio(dpr); + dragImage.fill(Qt::transparent); + + QRect imageRect = QRect(0, 0, width, height); + + QPainter dragPainter(&dragImage); + dragPainter.setCompositionMode(QPainter::CompositionMode_Source); + dragPainter.fillRect(imageRect, Qt::transparent); + dragPainter.setCompositionMode(QPainter::CompositionMode_SourceOver); + dragPainter.setOpacity(alpha); + dragPainter.fillRect(imageRect, backgroundColor); + + dragPainter.setOpacity(1.0f); + + int marginWidth = (imageRect.width() - rect().width()) / 2 + ParentEditorBorderSize - 1; + + if (imageType == DragImageType::IncludeVisibleChildren) + { + DrawDragImageAndVisibleChildrenInto(dragPainter, marginWidth, 0); + } + else + { + render(&dragPainter, QPoint(marginWidth, 0)); + } + + QPen pen; + pen.setColor(QColor(borderColor)); + pen.setWidth(1); + dragPainter.setPen(pen); + dragPainter.drawRect(0, 0, imageRect.width() - 1, imageRect.height() - 1); + + dragPainter.end(); + + return dragImage; + } } #include "UI/PropertyEditor/moc_PropertyRowWidget.cpp" diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/UI/PropertyEditor/PropertyRowWidget.hxx b/Code/Framework/AzToolsFramework/AzToolsFramework/UI/PropertyEditor/PropertyRowWidget.hxx index ebf7c67b21..68cc9b9cf9 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/UI/PropertyEditor/PropertyRowWidget.hxx +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/UI/PropertyEditor/PropertyRowWidget.hxx @@ -45,6 +45,13 @@ namespace AzToolsFramework Q_PROPERTY(bool appendDefaultLabelToName READ GetAppendDefaultLabelToName WRITE AppendDefaultLabelToName) public: AZ_CLASS_ALLOCATOR(PropertyRowWidget, AZ::SystemAllocator, 0) + + enum class DragImageType + { + SingleRow, + IncludeVisibleChildren + }; + PropertyRowWidget(QWidget* pParent); virtual ~PropertyRowWidget(); @@ -86,6 +93,9 @@ namespace AzToolsFramework bool GetAppendDefaultLabelToName(); void AppendDefaultLabelToName(bool doAppend); + AZ::u32 GetChildRowCount() const; + PropertyRowWidget* GetChildRowByIndex(AZ::u32 index) const; + AZStd::vector& GetChildrenRows() { return m_childrenRows; } bool HasChildRows() const; @@ -124,6 +134,7 @@ namespace AzToolsFramework void SetSelectionEnabled(bool selectionEnabled); void SetSelected(bool selected); + bool GetSelected(); bool eventFilter(QObject *watched, QEvent *event) override; void paintEvent(QPaintEvent*) override; @@ -152,9 +163,18 @@ namespace AzToolsFramework bool CanChildrenBeReordered() const; bool CanBeReordered() const; + int GetIndexInParent() const; + bool CanMoveUp() const; + bool CanMoveDown() const; + + int GetContainingEditorFrameWidth(); + QPixmap createDragImage(const QColor backgroundColor, const QColor borderColor, const float alpha, DragImageType imageType); protected: int CalculateLabelWidth() const; + int GetHeightOfRowAndVisibleChildren(); + int DrawDragImageAndVisibleChildrenInto(QPainter& painter, int xpos, int ypos); + bool IsHidden(InstanceDataNode* node) const; struct ChangeNotification; @@ -216,6 +236,7 @@ namespace AzToolsFramework bool m_isMultiSizeContainer = false; bool m_isFixedSizeOrSmartPtrContainer = false; bool m_custom = false; + bool m_canChildrenBeReordered = false; bool m_isSelected = false; bool m_selectionEnabled = false; diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/UI/PropertyEditor/ReflectedPropertyEditor.cpp b/Code/Framework/AzToolsFramework/AzToolsFramework/UI/PropertyEditor/ReflectedPropertyEditor.cpp index 36462bca66..5d5b00e83d 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/UI/PropertyEditor/ReflectedPropertyEditor.cpp +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/UI/PropertyEditor/ReflectedPropertyEditor.cpp @@ -19,6 +19,7 @@ #include #include #include +#include AZ_PUSH_DISABLE_WARNING(4251, "-Wunknown-warning-option") // 'QTextFormat::d': class 'QSharedDataPointer' needs to have dll-interface to be used by clients of class 'QTextFormat' #include AZ_POP_DISABLE_WARNING @@ -1343,7 +1344,7 @@ namespace AzToolsFramework // calculate the index/offset of the instance data node in the container // (useful for notifying which element in a vector was modified/removed) - static size_t CalculateElementIndexInContainer( + static int CalculateElementIndexInContainer( InstanceDataNode* node, void* parentInstanceNode, AZ::SerializeContext::IDataContainer* container, AZStd::vector& nodeInstancesOut) { @@ -1358,7 +1359,7 @@ namespace AzToolsFramework } } - size_t elementIndex = 0; + int elementIndex = 0; void* elementPtr = nodeInstancesOut.empty() ? nullptr : nodeInstancesOut.front(); // find the index of the element we are about to remove @@ -1429,7 +1430,7 @@ namespace AzToolsFramework // if the element being modified exists in a container, calculate // the index to be passed through to PropertyNotify - const auto calculateElementIndex = [](InstanceDataNode* node) -> size_t { + const auto calculateElementIndex = [](InstanceDataNode* node) -> int { if (InstanceDataNode* parent = node->GetParent()) { if (AZ::SerializeContext::IDataContainer* container = parent->GetClassMetadata()->m_container) @@ -1656,6 +1657,221 @@ namespace AzToolsFramework AzToolsFramework::Refresh_EntireTree); } + InstanceDataNode* ReflectedPropertyEditor::FindContainerNodeForNode(InstanceDataNode* node) const + { + // Locate the owning container. There may be a level of indirection due to wrappers, such as DynamicSerializableField. + InstanceDataNode* pContainerNode = node->GetParent(); + if (!pContainerNode) + { + return nullptr; + } + + while (pContainerNode && !pContainerNode->GetClassMetadata()->m_container) + { + pContainerNode = pContainerNode->GetParent(); + node = node->GetParent(); + } + + // Check for pContainerNode again, can happen if a node is deleted during operation. + if (!pContainerNode) + { + return nullptr; + } + + if (IsParentAssociativeContainer(pContainerNode) && IsPairContainer(pContainerNode)) + { + // Go up one more level to the associative container, we'll remove the pair from that container + pContainerNode = pContainerNode->GetParent(); + node = node->GetParent(); + } + + AZ_Assert( + pContainerNode, "Failed to locate parent container for element \"%s\" of type %s.", + node->GetElementMetadata() ? node->GetElementMetadata()->m_name : node->GetClassMetadata()->m_name, + node->GetClassMetadata()->m_typeId.ToString().c_str()); + + return pContainerNode; + } + + InstanceDataNode* ReflectedPropertyEditor::GetNodeAtIndex(int index) + { + if (index >= m_impl->m_widgetsInDisplayOrder.size()) + { + return nullptr; + } + + return GetNodeFromWidget(m_impl->m_widgetsInDisplayOrder[index]); + } + + QSet ReflectedPropertyEditor::GetTopLevelWidgets() + { + return m_impl->getTopLevelWidgets(); + } + + void ReflectedPropertyEditor::ChangeNodeIndex(InstanceDataNode* containerNode, InstanceDataNode* node, int fromIndex, int toIndex) + { + auto container = containerNode->GetElementMetadata() + ? containerNode->GetElementMetadata()->m_genericClassInfo->GetClassData()->m_container + : nullptr; + + if (fromIndex == toIndex) + { + return; + } + + if (!container || container->GetAssociativeContainerInterface()) + { + return; + } + + AZ::Uuid typeId = node->GetClassMetadata()->m_typeId; + + if (m_impl->m_ptrNotify) + { + m_impl->m_ptrNotify->BeforePropertyModified(containerNode); + } + + const AZ::SerializeContext::ClassElement* containerClassElement = container->GetElement(container->GetDefaultElementNameCrc()); + + AZ::SerializeContext* serializeContext = nullptr; + AZ::ComponentApplicationBus::BroadcastResult(serializeContext, &AZ::ComponentApplicationBus::Events::GetSerializeContext); + + // Backup the item we're moving. + void* srcElement = nullptr; + void* destElement = nullptr; + + int destIndex = -1; + int srcIndex = fromIndex; + + srcElement = container->GetElementByIndex(containerNode->GetInstance(0), containerClassElement, srcIndex); + + void* tmpBuffer = serializeContext->CloneObject(srcElement, typeId); + + // Shuffle all intervening items up (or down). + int indexOffset = (toIndex < fromIndex) ? -1 : 1; + + while (destIndex != toIndex - indexOffset) + { + destIndex = srcIndex; + srcIndex += indexOffset; + + destElement = srcElement; + + srcElement = container->GetElementByIndex(containerNode->GetInstance(0), containerClassElement, srcIndex); + + serializeContext->CloneObjectInplace(destElement, srcElement, typeId); + } + + // Now replace the final element with the one backed up previously. + destElement = srcElement; + + serializeContext->CloneObjectInplace(destElement, tmpBuffer, typeId); + + if (m_impl->m_ptrNotify) + { + m_impl->m_ptrNotify->AfterPropertyModified(containerNode); + m_impl->m_ptrNotify->SealUndoStack(); + } + + // Need to refresh any pinned inspectors as well to keep the container state in sync + QueueInvalidation(Refresh_Values); + AzToolsFramework::ToolsApplicationEvents::Bus::Broadcast( + &AzToolsFramework::ToolsApplicationEvents::InvalidatePropertyDisplay, AzToolsFramework::Refresh_Values); + } + + void ReflectedPropertyEditor::MoveNodeToIndex(InstanceDataNode* node, int index) + { + InstanceDataNode* pContainerNode = FindContainerNodeForNode(node); + + if (!pContainerNode) + { + return; + } + + AZ::SerializeContext::IDataContainer* container = pContainerNode->GetClassMetadata()->m_container; + + AZStd::vector nodeInstancesOut; + const int elementIndex = CalculateElementIndexInContainer(node, pContainerNode->GetInstance(0), container, nodeInstancesOut); + + ChangeNodeIndex(pContainerNode, node, elementIndex, index); + } + + void ReflectedPropertyEditor::MoveNodeBefore(InstanceDataNode* nodeToMove, InstanceDataNode* nodeToMoveBefore) + { + InstanceDataNode* pContainerNode = FindContainerNodeForNode(nodeToMove); + InstanceDataNode* pContainerNodeTarget = FindContainerNodeForNode(nodeToMoveBefore); + + if (nodeToMove == nodeToMoveBefore) + { + return; + } + + // Can only move nodes within the same parent. + if (pContainerNode != pContainerNodeTarget) + { + return; + } + + AZ::SerializeContext::IDataContainer* container = pContainerNode->GetClassMetadata()->m_container; + + AZStd::vector nodeInstancesOut; + int elementIndex = CalculateElementIndexInContainer(nodeToMove, pContainerNode->GetInstance(0), container, nodeInstancesOut); + nodeInstancesOut.clear(); + int elementIndexTarget = + CalculateElementIndexInContainer(nodeToMoveBefore, pContainerNode->GetInstance(0), container, nodeInstancesOut); + + if (elementIndex < elementIndexTarget) + { + elementIndexTarget -= 1; + } + + ChangeNodeIndex(pContainerNode, nodeToMove, elementIndex, elementIndexTarget); + } + + void ReflectedPropertyEditor::MoveNodeAfter(InstanceDataNode* nodeToMove, InstanceDataNode* nodeToMoveBefore) + { + InstanceDataNode* pContainerNode = FindContainerNodeForNode(nodeToMove); + InstanceDataNode* pContainerNodeTarget = FindContainerNodeForNode(nodeToMoveBefore); + + if (nodeToMove == nodeToMoveBefore) + { + return; + } + + // Can only move nodes within the same parent. + if (pContainerNode != pContainerNodeTarget) + { + return; + } + + AZ::SerializeContext::IDataContainer* container = pContainerNode->GetClassMetadata()->m_container; + + AZStd::vector nodeInstancesOut; + int elementIndex = CalculateElementIndexInContainer(nodeToMove, pContainerNode->GetInstance(0), container, nodeInstancesOut); + nodeInstancesOut.clear(); + int elementIndexTarget = + CalculateElementIndexInContainer(nodeToMoveBefore, pContainerNode->GetInstance(0), container, nodeInstancesOut); + + if (elementIndex > elementIndexTarget) + { + elementIndexTarget += 1; + } + + ChangeNodeIndex(pContainerNode, nodeToMove, elementIndex, elementIndexTarget); + } + + int ReflectedPropertyEditor::GetNodeIndexInContainer(InstanceDataNode* node) + { + InstanceDataNode* pContainerNode = FindContainerNodeForNode(node); + + AZ::SerializeContext::IDataContainer* container = pContainerNode->GetClassMetadata()->m_container; + + AZStd::vector nodeInstancesOut; + int elementIndex = CalculateElementIndexInContainer(node, pContainerNode->GetInstance(0), container, nodeInstancesOut); + + return elementIndex; + } + void ReflectedPropertyEditor::OnPropertyRowRequestContainerRemoveItem(PropertyRowWidget* widget, InstanceDataNode* node) { // Locate the owning container. There may be a level of indirection due to wrappers, such as DynamicSerializableField. @@ -1690,7 +1906,7 @@ namespace AzToolsFramework // the index of the element being removed AZStd::vector nodeInstancesOut; - const size_t elementIndex = CalculateElementIndexInContainer( + const int elementIndex = CalculateElementIndexInContainer( node, pContainerNode->GetInstance(0), container, nodeInstancesOut); // pass the context as the last parameter to actually delete the related data. diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/UI/PropertyEditor/ReflectedPropertyEditor.hxx b/Code/Framework/AzToolsFramework/AzToolsFramework/UI/PropertyEditor/ReflectedPropertyEditor.hxx index b10e153162..28da540098 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/UI/PropertyEditor/ReflectedPropertyEditor.hxx +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/UI/PropertyEditor/ReflectedPropertyEditor.hxx @@ -155,9 +155,19 @@ namespace AzToolsFramework using VisibilityCallback = AZStd::function; void SetVisibilityCallback(VisibilityCallback callback); + void MoveNodeToIndex(InstanceDataNode* node, int index); + void MoveNodeBefore(InstanceDataNode* nodeToMove, InstanceDataNode* nodeToMoveBefore); + void MoveNodeAfter(InstanceDataNode* nodeToMove, InstanceDataNode* nodeToMoveBefore); + + int GetNodeIndexInContainer(InstanceDataNode* node); + InstanceDataNode* GetNodeAtIndex(int index); + QSet GetTopLevelWidgets(); signals: void OnExpansionContractionDone(); private: + InstanceDataNode* FindContainerNodeForNode(InstanceDataNode* node) const; + void ChangeNodeIndex(InstanceDataNode* containerNode, InstanceDataNode* node, int oldIndex, int newIndex); + class Impl; std::unique_ptr m_impl;