diff --git a/Code/Editor/Util/StringHelpers.cpp b/Code/Editor/Util/StringHelpers.cpp index 6c819270bd..d0999d6ae2 100644 --- a/Code/Editor/Util/StringHelpers.cpp +++ b/Code/Editor/Util/StringHelpers.cpp @@ -10,8 +10,6 @@ #include "StringHelpers.h" #include "Util.h" -#include - int StringHelpers::CompareIgnoreCase(const AZStd::string& str0, const AZStd::string& str1) { const size_t minLength = Util::getMin(str0.length(), str1.length()); diff --git a/Code/Editor/Util/StringHelpers.h b/Code/Editor/Util/StringHelpers.h index b5c84d7fe3..8a0883b24c 100644 --- a/Code/Editor/Util/StringHelpers.h +++ b/Code/Editor/Util/StringHelpers.h @@ -12,7 +12,7 @@ #pragma once #include -#include +#include namespace StringHelpers { 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..604c6141d7 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 (AZ::u32 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; diff --git a/Code/Legacy/CrySystem/XConsole.cpp b/Code/Legacy/CrySystem/XConsole.cpp index ae07f86312..36443fb9ee 100644 --- a/Code/Legacy/CrySystem/XConsole.cpp +++ b/Code/Legacy/CrySystem/XConsole.cpp @@ -2873,7 +2873,7 @@ void CXConsole::Paste() ////////////////////////////////////////////////////////////////////////// int CXConsole::GetNumVars() { - return (int)m_mapVariables.size(); + return static_cast(m_mapVariables.size()); } ////////////////////////////////////////////////////////////////////////// @@ -3132,7 +3132,6 @@ char* CXConsole::GetCheatVarAt(uint32 nOffset) ////////////////////////////////////////////////////////////////////////// size_t CXConsole::GetSortedVars(AZStd::vector& pszArray, const char* szPrefix) { - size_t i = 0; size_t iPrefixLen = szPrefix ? strlen(szPrefix) : 0; // variables @@ -3140,11 +3139,6 @@ size_t CXConsole::GetSortedVars(AZStd::vector& pszArray, con ConsoleVariablesMap::const_iterator it, end = m_mapVariables.end(); for (it = m_mapVariables.begin(); it != end; ++it) { - if (i >= pszArray.size()) - { - break; - } - if (szPrefix) { if (_strnicmp(it->first, szPrefix, iPrefixLen) != 0) @@ -3158,9 +3152,7 @@ size_t CXConsole::GetSortedVars(AZStd::vector& pszArray, con continue; } - pszArray[i] = it->first; - - i++; + pszArray.push_back(it->first); } } @@ -3169,11 +3161,6 @@ size_t CXConsole::GetSortedVars(AZStd::vector& pszArray, con ConsoleCommandsMap::iterator it, end = m_mapCommands.end(); for (it = m_mapCommands.begin(); it != end; ++it) { - if (i >= pszArray.size()) - { - break; - } - if (szPrefix) { if (_strnicmp(it->first.c_str(), szPrefix, iPrefixLen) != 0) @@ -3187,25 +3174,18 @@ size_t CXConsole::GetSortedVars(AZStd::vector& pszArray, con continue; } - pszArray[i] = it->first.c_str(); - - i++; + pszArray.push_back(it->first.c_str()); } } - if (i != 0) - { - std::sort(pszArray.begin(), pszArray.end()); - } - - return i; + std::sort(pszArray.begin(), pszArray.end()); + return pszArray.size(); } ////////////////////////////////////////////////////////////////////////// void CXConsole::FindVar(const char* substr) { AZStd::vector cmds; - cmds.resize(GetNumVars() + m_mapCommands.size()); size_t cmdCount = GetSortedVars(cmds); for (size_t i = 0; i < cmdCount; i++) @@ -3231,10 +3211,9 @@ const char* CXConsole::AutoComplete(const char* substr) // following code can be optimized AZStd::vector cmds; - cmds.resize(GetNumVars() + m_mapCommands.size()); size_t cmdCount = GetSortedVars(cmds); - size_t substrLen = strlen(substr); + size_t substrLen = substr ? strlen(substr) : 0; // If substring is empty return first command. if (substrLen == 0 && cmdCount > 0) @@ -3246,7 +3225,7 @@ const char* CXConsole::AutoComplete(const char* substr) for (size_t i = 0; i < cmdCount; i++) { const char* szCmd = cmds[i].data(); - size_t cmdlen = strlen(szCmd); + size_t cmdlen = cmds[i].size(); if (cmdlen >= substrLen && memcmp(szCmd, substr, substrLen) == 0) { if (substrLen == cmdlen) @@ -3267,7 +3246,7 @@ const char* CXConsole::AutoComplete(const char* substr) { const char* szCmd = cmds[i].data(); - size_t cmdlen = strlen(szCmd); + size_t cmdlen = cmds[i].size(); if (cmdlen >= substrLen && azstrnicmp(szCmd, substr, substrLen) == 0) { if (substrLen == cmdlen) @@ -3301,27 +3280,19 @@ void CXConsole::SetInputLine(const char* szLine) const char* CXConsole::AutoCompletePrev(const char* substr) { AZStd::vector cmds; - cmds.resize(GetNumVars() + m_mapCommands.size()); - size_t cmdCount = GetSortedVars(cmds); + GetSortedVars(cmds); // If substring is empty return last command. - if (strlen(substr) == 0 && cmds.size() > 0) + if (strlen(substr) == 0 && !cmds.empty()) { - return cmds[cmdCount - 1].data(); + return cmds.back().data(); } - for (unsigned int i = 0; i < cmdCount; i++) + for (const AZStd::string_view& cmd : cmds) { - if (azstricmp(substr, cmds[i].data()) == 0) + if (azstricmp(substr, cmd.data()) == 0) { - if (i > 0) - { - return cmds[i - 1].data(); - } - else - { - return cmds[0].data(); - } + return cmd.data(); } } return AutoComplete(substr); diff --git a/Code/Tools/TestImpactFramework/Frontend/Console/Code/Source/TestImpactRuntimeConfigurationFactory.cpp b/Code/Tools/TestImpactFramework/Frontend/Console/Code/Source/TestImpactRuntimeConfigurationFactory.cpp index 8ea32d6447..c2bc95a906 100644 --- a/Code/Tools/TestImpactFramework/Frontend/Console/Code/Source/TestImpactRuntimeConfigurationFactory.cpp +++ b/Code/Tools/TestImpactFramework/Frontend/Console/Code/Source/TestImpactRuntimeConfigurationFactory.cpp @@ -27,7 +27,7 @@ namespace TestImpact "relative_paths", "artifact_dir", "enumeration_cache_dir", - "test_impact_data_files", + "test_impact_data_file", "temp", "active", "target_sources", @@ -72,7 +72,7 @@ namespace TestImpact RelativePaths, ArtifactDir, EnumerationCacheDir, - TestImpactDataFiles, + TestImpactDataFile, TempWorkspace, ActiveWorkspace, TargetSources, @@ -138,31 +138,18 @@ namespace TestImpact tempWorkspaceConfig.m_artifactDirectory = GetAbsPathFromRelPath( tempWorkspaceConfig.m_root, tempWorkspace[Config::Keys[Config::RelativePaths]][Config::Keys[Config::ArtifactDir]].GetString()); + tempWorkspaceConfig.m_enumerationCacheDirectory = GetAbsPathFromRelPath( + tempWorkspaceConfig.m_root, + tempWorkspace[Config::Keys[Config::RelativePaths]][Config::Keys[Config::EnumerationCacheDir]].GetString()); return tempWorkspaceConfig; } - AZStd::array ParseTestImpactAnalysisDataFiles(const RepoPath& root, const rapidjson::Value& sparTiaFile) - { - AZStd::array sparTiaFiles; - sparTiaFiles[static_cast(SuiteType::Main)] = - GetAbsPathFromRelPath(root, sparTiaFile[SuiteTypeAsString(SuiteType::Main).c_str()].GetString()); - sparTiaFiles[static_cast(SuiteType::Periodic)] = - GetAbsPathFromRelPath(root, sparTiaFile[SuiteTypeAsString(SuiteType::Periodic).c_str()].GetString()); - sparTiaFiles[static_cast(SuiteType::Sandbox)] = - GetAbsPathFromRelPath(root, sparTiaFile[SuiteTypeAsString(SuiteType::Sandbox).c_str()].GetString()); - - return sparTiaFiles; - } - WorkspaceConfig::Active ParseActiveWorkspaceConfig(const rapidjson::Value& activeWorkspace) { WorkspaceConfig::Active activeWorkspaceConfig; const auto& relativePaths = activeWorkspace[Config::Keys[Config::RelativePaths]]; activeWorkspaceConfig.m_root = activeWorkspace[Config::Keys[Config::Root]].GetString(); - activeWorkspaceConfig.m_enumerationCacheDirectory - = GetAbsPathFromRelPath(activeWorkspaceConfig.m_root, relativePaths[Config::Keys[Config::EnumerationCacheDir]].GetString()); - activeWorkspaceConfig.m_sparTiaFiles = - ParseTestImpactAnalysisDataFiles(activeWorkspaceConfig.m_root, relativePaths[Config::Keys[Config::TestImpactDataFiles]]); + activeWorkspaceConfig.m_sparTiaFile = relativePaths[Config::Keys[Config::TestImpactDataFile]].GetString(); return activeWorkspaceConfig; } diff --git a/Code/Tools/TestImpactFramework/Runtime/Code/Include/TestImpactFramework/TestImpactClientSequenceReport.h b/Code/Tools/TestImpactFramework/Runtime/Code/Include/TestImpactFramework/TestImpactClientSequenceReport.h index e86123ddc7..1ddd1042e6 100644 --- a/Code/Tools/TestImpactFramework/Runtime/Code/Include/TestImpactFramework/TestImpactClientSequenceReport.h +++ b/Code/Tools/TestImpactFramework/Runtime/Code/Include/TestImpactFramework/TestImpactClientSequenceReport.h @@ -530,7 +530,7 @@ namespace TestImpact size_t GetTotalNumTimedOutTestRuns() const override; size_t GetTotalNumUnexecutedTestRuns() const override; - //! Returns the report for the discarded test runs. + // ImpactAnalysisSequenceReport overrides ... const TestRunSelection GetDiscardedTestRuns() const; //! Returns the report for the discarded test runs. diff --git a/Code/Tools/TestImpactFramework/Runtime/Code/Include/TestImpactFramework/TestImpactConfiguration.h b/Code/Tools/TestImpactFramework/Runtime/Code/Include/TestImpactFramework/TestImpactConfiguration.h index fcacd90e71..92dd454b01 100644 --- a/Code/Tools/TestImpactFramework/Runtime/Code/Include/TestImpactFramework/TestImpactConfiguration.h +++ b/Code/Tools/TestImpactFramework/Runtime/Code/Include/TestImpactFramework/TestImpactConfiguration.h @@ -37,14 +37,14 @@ namespace TestImpact { RepoPath m_root; //!< Path to the temporary workspace (cleaned prior to use). RepoPath m_artifactDirectory; //!< Path to read and write runtime artifacts to and from. + RepoPath m_enumerationCacheDirectory; //!< Path to the test enumerations cache. }; //! Active persistent data workspace configuration. struct Active { RepoPath m_root; //!< Path to the persistent workspace tracked by the repository. - RepoPath m_enumerationCacheDirectory; //!< Path to the test enumerations cache. - AZStd::array m_sparTiaFiles; //!< Paths to the test impact analysis data files for each test suite. + RepoPath m_sparTiaFile; //!< Paths to the test impact analysis data file. }; Temp m_temp; diff --git a/Code/Tools/TestImpactFramework/Runtime/Code/Source/TestImpactRuntime.cpp b/Code/Tools/TestImpactFramework/Runtime/Code/Source/TestImpactRuntime.cpp index 8a9d96bca8..515118dd7a 100644 --- a/Code/Tools/TestImpactFramework/Runtime/Code/Source/TestImpactRuntime.cpp +++ b/Code/Tools/TestImpactFramework/Runtime/Code/Source/TestImpactRuntime.cpp @@ -275,7 +275,7 @@ namespace TestImpact m_testEngine = AZStd::make_unique( m_config.m_repo.m_root, m_config.m_target.m_outputDirectory, - m_config.m_workspace.m_active.m_enumerationCacheDirectory, + m_config.m_workspace.m_temp.m_enumerationCacheDirectory, m_config.m_workspace.m_temp.m_artifactDirectory, m_config.m_testEngine.m_testRunner.m_binary, m_config.m_testEngine.m_instrumentation.m_binary, @@ -289,7 +289,8 @@ namespace TestImpact } else { - m_sparTiaFile = m_config.m_workspace.m_active.m_sparTiaFiles[static_cast(m_suiteFilter)].String(); + m_sparTiaFile = + m_config.m_workspace.m_active.m_root / RepoPath(SuiteTypeAsString(m_suiteFilter)) / m_config.m_workspace.m_active.m_sparTiaFile; } // Populate the dynamic dependency map with the existing source coverage data (if any) diff --git a/Gems/Atom/Utils/Code/Include/Atom/Utils/StableDynamicArray.h b/Gems/Atom/Utils/Code/Include/Atom/Utils/StableDynamicArray.h index 7c77838330..454a6d4086 100644 --- a/Gems/Atom/Utils/Code/Include/Atom/Utils/StableDynamicArray.h +++ b/Gems/Atom/Utils/Code/Include/Atom/Utils/StableDynamicArray.h @@ -10,6 +10,7 @@ #include #include #include +#include #include #include diff --git a/Gems/PhysX/Code/Source/BallJointComponent.cpp b/Gems/PhysX/Code/Source/BallJointComponent.cpp index 12323e5077..60f7888b0d 100644 --- a/Gems/PhysX/Code/Source/BallJointComponent.cpp +++ b/Gems/PhysX/Code/Source/BallJointComponent.cpp @@ -46,11 +46,26 @@ namespace PhysX JointComponent::LeadFollowerInfo leadFollowerInfo; ObtainLeadFollowerInfo(leadFollowerInfo); - if (!leadFollowerInfo.m_followerActor) + if (leadFollowerInfo.m_followerActor == nullptr || + leadFollowerInfo.m_followerBody == nullptr) { return; } + // if there is no lead body, this will be a constraint of the follower's global position, so use invalid body handle. + AzPhysics::SimulatedBodyHandle parentHandle = AzPhysics::InvalidSimulatedBodyHandle; + if (leadFollowerInfo.m_leadBody != nullptr) + { + parentHandle = leadFollowerInfo.m_leadBody->m_bodyHandle; + } + else + { + AZ_TracePrintf( + "PhysX", "Entity [%s] Ball Joint component missing lead entity. This joint will be a global constraint on the follower's global position.", + GetEntity()->GetName().c_str()); + } + + BallJointConfiguration configuration; configuration.m_parentLocalPosition = leadFollowerInfo.m_leadLocal.GetTranslation(); configuration.m_parentLocalRotation = leadFollowerInfo.m_leadLocal.GetRotation(); @@ -65,7 +80,7 @@ namespace PhysX m_jointHandle = sceneInterface->AddJoint( leadFollowerInfo.m_followerBody->m_sceneOwner, &configuration, - leadFollowerInfo.m_leadBody->m_bodyHandle, + parentHandle, leadFollowerInfo.m_followerBody->m_bodyHandle); m_jointSceneOwner = leadFollowerInfo.m_followerBody->m_sceneOwner; } diff --git a/Gems/PhysX/Code/Source/FixedJointComponent.cpp b/Gems/PhysX/Code/Source/FixedJointComponent.cpp index 53a9946998..82c8cc485c 100644 --- a/Gems/PhysX/Code/Source/FixedJointComponent.cpp +++ b/Gems/PhysX/Code/Source/FixedJointComponent.cpp @@ -54,11 +54,25 @@ namespace PhysX JointComponent::LeadFollowerInfo leadFollowerInfo; ObtainLeadFollowerInfo(leadFollowerInfo); - if (!leadFollowerInfo.m_followerActor) + if (leadFollowerInfo.m_followerActor == nullptr || + leadFollowerInfo.m_followerBody == nullptr) { return; } + // if there is no lead body, this will be a constraint of the follower's global position, so use invalid body handle. + AzPhysics::SimulatedBodyHandle parentHandle = AzPhysics::InvalidSimulatedBodyHandle; + if (leadFollowerInfo.m_leadBody != nullptr) + { + parentHandle = leadFollowerInfo.m_leadBody->m_bodyHandle; + } + else + { + AZ_TracePrintf("PhysX", + "Entity [%s] Fixed Joint component missing lead entity. This joint will be a global constraint on the follower's global position.", + GetEntity()->GetName().c_str()); + } + FixedJointConfiguration configuration; configuration.m_parentLocalPosition = leadFollowerInfo.m_leadLocal.GetTranslation(); configuration.m_parentLocalRotation = leadFollowerInfo.m_leadLocal.GetRotation(); @@ -72,7 +86,7 @@ namespace PhysX m_jointHandle = sceneInterface->AddJoint( leadFollowerInfo.m_followerBody->m_sceneOwner, &configuration, - leadFollowerInfo.m_leadBody->m_bodyHandle, + parentHandle, leadFollowerInfo.m_followerBody->m_bodyHandle); m_jointSceneOwner = leadFollowerInfo.m_followerBody->m_sceneOwner; } diff --git a/Gems/PhysX/Code/Source/HingeJointComponent.cpp b/Gems/PhysX/Code/Source/HingeJointComponent.cpp index f275d32dc7..5edf1803c4 100644 --- a/Gems/PhysX/Code/Source/HingeJointComponent.cpp +++ b/Gems/PhysX/Code/Source/HingeJointComponent.cpp @@ -48,12 +48,24 @@ namespace PhysX JointComponent::LeadFollowerInfo leadFollowerInfo; ObtainLeadFollowerInfo(leadFollowerInfo); if (leadFollowerInfo.m_followerActor == nullptr || - leadFollowerInfo.m_leadBody == nullptr || leadFollowerInfo.m_followerBody == nullptr) { return; } + // if there is no lead body, this will be a constraint of the follower's global position, so use invalid body handle. + AzPhysics::SimulatedBodyHandle parentHandle = AzPhysics::InvalidSimulatedBodyHandle; + if (leadFollowerInfo.m_leadBody != nullptr) + { + parentHandle = leadFollowerInfo.m_leadBody->m_bodyHandle; + } + else + { + AZ_TracePrintf( + "PhysX", "Entity [%s] Hinge Joint component missing lead entity. This joint will be a global constraint on the follower's global position.", + GetEntity()->GetName().c_str()); + } + HingeJointConfiguration configuration; configuration.m_parentLocalPosition = leadFollowerInfo.m_leadLocal.GetTranslation(); configuration.m_parentLocalRotation = leadFollowerInfo.m_leadLocal.GetRotation(); @@ -66,7 +78,9 @@ namespace PhysX if (auto* sceneInterface = AZ::Interface::Get()) { m_jointHandle = sceneInterface->AddJoint( - leadFollowerInfo.m_followerBody->m_sceneOwner, &configuration, leadFollowerInfo.m_leadBody->m_bodyHandle, + leadFollowerInfo.m_followerBody->m_sceneOwner, + &configuration, + parentHandle, leadFollowerInfo.m_followerBody->m_bodyHandle); m_jointSceneOwner = leadFollowerInfo.m_followerBody->m_sceneOwner; } diff --git a/Gems/PhysX/Code/Source/Joint/PhysXJointUtils.cpp b/Gems/PhysX/Code/Source/Joint/PhysXJointUtils.cpp index 5b63a43966..3558c9fb56 100644 --- a/Gems/PhysX/Code/Source/Joint/PhysXJointUtils.cpp +++ b/Gems/PhysX/Code/Source/Joint/PhysXJointUtils.cpp @@ -190,8 +190,9 @@ namespace PhysX { { PxJointActorData actorData = GetJointPxActors(sceneHandle, parentBodyHandle, childBodyHandle); - if (!actorData.parentActor || !actorData.childActor) + if (actorData.parentActor == nullptr && actorData.childActor == nullptr) { + AZ_Warning("PhysX Joint", false, "CreateJoint failed - at least one body must be a PxRigidActor."); return nullptr; } @@ -239,7 +240,8 @@ namespace PhysX { { PxJointActorData actorData = GetJointPxActors(sceneHandle, parentBodyHandle, childBodyHandle); - if (!actorData.parentActor || !actorData.childActor) + //only check the child actor, as a null parent actor means this joint is a global constraint. + if (!actorData.childActor) { return nullptr; } @@ -252,7 +254,8 @@ namespace PhysX { { PHYSX_SCENE_READ_LOCK(actorData.childActor->getScene()); - joint = physx::PxFixedJointCreate(PxGetPhysics(), + joint = physx::PxFixedJointCreate( + PxGetPhysics(), actorData.parentActor, PxMathConvert(parentLocalTM), actorData.childActor, PxMathConvert(childLocalTM)); } @@ -272,7 +275,8 @@ namespace PhysX { { PxJointActorData actorData = GetJointPxActors(sceneHandle, parentBodyHandle, childBodyHandle); - if (!actorData.parentActor || !actorData.childActor) + // only check the child actor, as a null parent actor means this joint is a global constraint. + if (!actorData.childActor) { return nullptr; } @@ -306,7 +310,8 @@ namespace PhysX { { PxJointActorData actorData = GetJointPxActors(sceneHandle, parentBodyHandle, childBodyHandle); - if (!actorData.parentActor || !actorData.childActor) + // only check the child actor, as a null parent actor means this joint is a global constraint. + if (!actorData.childActor) { return nullptr; } diff --git a/Gems/PhysX/Code/Tests/PhysXJointsTest.cpp b/Gems/PhysX/Code/Tests/PhysXJointsTest.cpp index ff9cf11cca..ac5a99e29a 100644 --- a/Gems/PhysX/Code/Tests/PhysXJointsTest.cpp +++ b/Gems/PhysX/Code/Tests/PhysXJointsTest.cpp @@ -123,7 +123,7 @@ namespace PhysX const AZ::Vector3 followerEndPosition = RunJointTest(m_defaultScene, followerEntity->GetId()); - EXPECT_TRUE(followerEndPosition.GetX() > followerPosition.GetX()); + EXPECT_GT(followerEndPosition.GetX(), followerPosition.GetX()); } TEST_F(PhysXJointsTest, Joint_HingeJoint_FollowerSwingsAroundLead) @@ -164,8 +164,8 @@ namespace PhysX const AZ::Vector3 followerEndPosition = RunJointTest(m_defaultScene, followerEntity->GetId()); - EXPECT_TRUE(followerEndPosition.GetX() > followerPosition.GetX()); - EXPECT_TRUE(abs(followerEndPosition.GetZ()) > FLT_EPSILON); + EXPECT_GT(followerEndPosition.GetX(), followerPosition.GetX()); + EXPECT_GT(abs(followerEndPosition.GetZ()), FLT_EPSILON); } TEST_F(PhysXJointsTest, Joint_BallJoint_FollowerSwingsUpAboutLead) @@ -206,7 +206,65 @@ namespace PhysX const AZ::Vector3 followerEndPosition = RunJointTest(m_defaultScene, followerEntity->GetId()); - EXPECT_TRUE(followerEndPosition.GetZ() > followerPosition.GetZ()); + EXPECT_GT(followerEndPosition.GetZ(), followerPosition.GetZ()); + } + + TEST_F(PhysXJointsTest, Joint_BallJoint_GlobalConstraint) + { + // Place an entity in the world with a rigid body, physx collider, and a ball joint components. + // Do not set a lead entity on the ball joint component. + // Set entity's initial velocity to 10 in the X and Y directions on the rigid body component. + // The entity should swing up on the global constraint. + + const AZ::Vector3 followerPosition(0.0f, 0.0f, -1.0f); + const AZ::Vector3 followerInitialLinearVelocity(10.0f, 10.0f, 0.0f); + + const AZ::Vector3 jointLocalPosition(0.0f, 0.0f, 2.0f); + const AZ::Quaternion jointLocalRotation = AZ::Quaternion::CreateRotationY(90.0f); + const AZ::Transform jointLocalTransform = AZ::Transform::CreateFromQuaternionAndTranslation(jointLocalRotation, jointLocalPosition); + + //we want a global constraint, so leave the lead entity unset. + auto jointConfig = AZStd::make_shared(); + jointConfig->m_localTransformFromFollower = jointLocalTransform; + + auto jointLimits = AZStd::make_shared(); + jointLimits->m_isLimited = false; + + auto followerEntity = AddBodyColliderEntity( + m_testSceneHandle, followerPosition, followerInitialLinearVelocity, jointConfig, nullptr, jointLimits); + + const AZ::Vector3 followerEndPosition = RunJointTest(m_defaultScene, followerEntity->GetId()); + + EXPECT_GT(followerEndPosition.GetZ(), followerPosition.GetZ()); + } + + TEST_F(PhysXJointsTest, Joint_HingeJoint_GlobalConstraint) + { + // Place an entity in the world with a rigid body, physx collider, and a hinge joint components. + // Do not set a lead entity on the hinge joint component. + // Set entity's initial velocity to 10 in the X and Y directions on the rigid body component. + // The entity should swing up on the global constraint. + + const AZ::Vector3 followerPosition(0.0f, 0.0f, -1.0f); + const AZ::Vector3 followerInitialLinearVelocity(10.0f, 10.0f, 0.0f); + + const AZ::Vector3 jointLocalPosition(0.0f, 0.0f, 2.0f); + const AZ::Quaternion jointLocalRotation = AZ::Quaternion::CreateFromEulerAnglesDegrees(AZ::Vector3(0.0f, 180.0f, 90.0f)); + const AZ::Transform jointLocalTransform = AZ::Transform::CreateFromQuaternionAndTranslation(jointLocalRotation, jointLocalPosition); + + // do not set the lead entity as that makes this a global constraint + auto jointConfig = AZStd::make_shared(); + jointConfig->m_localTransformFromFollower = jointLocalTransform; + + auto jointLimits = AZStd::make_shared(); + jointLimits->m_isLimited = false; + + auto followerEntity = AddBodyColliderEntity( + m_testSceneHandle, followerPosition, followerInitialLinearVelocity, jointConfig, nullptr, jointLimits); + + const AZ::Vector3 followerEndPosition = RunJointTest(m_defaultScene, followerEntity->GetId()); + + EXPECT_GT(followerEndPosition.GetZ(), followerPosition.GetZ()); } // for some reason TYPED_TEST_CASE with the fixture is not working on Android + Linux diff --git a/cmake/TestImpactFramework/ConsoleFrontendConfig.in b/cmake/TestImpactFramework/ConsoleFrontendConfig.in index 17fbd217d7..3b6318e1e0 100644 --- a/cmake/TestImpactFramework/ConsoleFrontendConfig.in +++ b/cmake/TestImpactFramework/ConsoleFrontendConfig.in @@ -15,19 +15,14 @@ "temp": { "root": "${temp_dir}", "relative_paths": { - "artifact_dir": "RuntimeArtifact" + "artifact_dir": "RuntimeArtifact", + "enumeration_cache_dir": "EnumerationCache" } }, "active": { "root": "${active_dir}", "relative_paths": { - "test_impact_data_files": { - "main": "TestImpactData.main.spartia", - "periodic": "TestImpactData.periodic.spartia", - "sandbox": "TestImpactData.sandbox.spartia" - }, - "enumeration_cache_dir": "EnumerationCache", - "last_build_target_list_file": "LastRunBuildTargets.json" + "test_impact_data_file": "TestImpactData.spartia" } }, "historic": { diff --git a/cmake/Tools/Platform/Android/android_support.py b/cmake/Tools/Platform/Android/android_support.py index d820eb1d8c..77c8a37a35 100755 --- a/cmake/Tools/Platform/Android/android_support.py +++ b/cmake/Tools/Platform/Android/android_support.py @@ -372,9 +372,12 @@ CUSTOM_GRADLE_COPY_NATIVE_CONFIG_BUILD_ARTIFACTS_FORMAT_STR = """ }} compile{config}Sources.dependsOn copyNativeArtifacts{config} +""" + +CUSTOM_GRADLE_COPY_NATIVE_CONFIG_BUILD_ARTIFACTS_DEPENDENCY_FORMAT_STR = """ copyNativeArtifacts{config}.mustRunAfter {{ - tasks.findAll {{ task->task.name.contains('externalNativeBuild{config}') }} + tasks.findAll {{ task->task.name.contains('syncLYLayoutMode{config}') }} }} """ @@ -383,7 +386,13 @@ CUSTOM_APPLY_ASSET_LAYOUT_TASK_FORMAT_STR = """ workingDir '{working_dir}' commandLine '{python_full_path}', 'layout_tool.py', '--project-path', '{project_path}', '-p', 'Android', '-a', '{asset_type}', '-m', '{asset_mode}', '--create-layout-root', '-l', '{asset_layout_folder}' }} + compile{config}Sources.dependsOn syncLYLayoutMode{config} + + syncLYLayoutMode{config}.mustRunAfter {{ + tasks.findAll {{ task->task.name.contains('externalNativeBuild{config}') }} + }} + """ @@ -832,25 +841,28 @@ class AndroidProjectGenerator(object): asset_layout_folder=(self.build_dir / 'app/src/main/assets').resolve().as_posix(), file_includes='Test.Assets/**/*.*') else: - # Copy over settings registry files from the Registry folder with build output directory gradle_build_env[f'CUSTOM_APPLY_ASSET_LAYOUT_{native_config_upper}_TASK'] = \ + CUSTOM_APPLY_ASSET_LAYOUT_TASK_FORMAT_STR.format(working_dir=common.normalize_path_for_settings(self.engine_root / 'cmake/Tools'), + python_full_path=common.normalize_path_for_settings(self.engine_root / 'python' / PYTHON_SCRIPT), + asset_type=self.asset_type, + project_path=self.project_path.as_posix(), + asset_mode=self.asset_mode if native_config != 'Release' else 'PAK', + asset_layout_folder=(self.build_dir / 'app/src/main/assets').resolve().as_posix(), + config=native_config) + # Copy over settings registry files from the Registry folder with build output directory + gradle_build_env[f'CUSTOM_APPLY_ASSET_LAYOUT_{native_config_upper}_TASK'] += \ CUSTOM_GRADLE_COPY_NATIVE_CONFIG_BUILD_ARTIFACTS_FORMAT_STR.format(config=native_config, config_lower=native_config_lower, asset_layout_folder=(self.build_dir / 'app/src/main/assets').resolve().as_posix(), file_includes='**/Registry/*.setreg') - - if self.include_assets_in_apk: - if not self.is_test_project: + if self.include_assets_in_apk: + # This is a dependency of the layout sync only if we are including assets in the APK gradle_build_env[f'CUSTOM_APPLY_ASSET_LAYOUT_{native_config_upper}_TASK'] += \ - CUSTOM_APPLY_ASSET_LAYOUT_TASK_FORMAT_STR.format(working_dir=common.normalize_path_for_settings(self.engine_root / 'cmake/Tools'), - python_full_path=common.normalize_path_for_settings(self.engine_root / 'python' / PYTHON_SCRIPT), - asset_type=self.asset_type, - project_path=self.project_path.as_posix(), - asset_mode=self.asset_mode if native_config != 'Release' else 'PAK', - asset_layout_folder=(self.build_dir / 'app/src/main/assets').resolve().as_posix(), - config=native_config) - else: - gradle_build_env[f'CUSTOM_APPLY_ASSET_LAYOUT_{native_config_upper}_TASK'] = '' + CUSTOM_GRADLE_COPY_NATIVE_CONFIG_BUILD_ARTIFACTS_DEPENDENCY_FORMAT_STR.format(config=native_config) + + + + if self.signing_config: gradle_build_env[f'SIGNING_{native_config_upper}_CONFIG'] = f'signingConfig signingConfigs.{native_config_lower}' if self.signing_config else '' else: diff --git a/scripts/build/TestImpactAnalysis/tiaf.py b/scripts/build/TestImpactAnalysis/tiaf.py index e8faef30e3..27fbaf451f 100644 --- a/scripts/build/TestImpactAnalysis/tiaf.py +++ b/scripts/build/TestImpactAnalysis/tiaf.py @@ -230,7 +230,7 @@ class TestImpact: # Flag for corner case where: # 1. TIAF was already run previously for this commit. - # 2. There was no last commit hash when TIAF last ran on this commit (due to no coverage data existing get for this branch) + # 2. There was no last commit hash when TIAF last ran on this commit (due to no coverage data existing yet for this branch) # 3. TIAF has not been run on any other commits between the run for this commit and the last run for this commit. # The above results in TIAF being stuck in a state of generating an empty change list (and thus doing no work until another # commit comes in) which is problematic if the commit needs to be re-run for whatever reason so in these conditions we revert @@ -323,7 +323,6 @@ class TestImpact: logger.info(f"Args: {unpacked_args}") runtime_result = subprocess.run([str(self._tiaf_bin)] + args) report = None - # If the sequence completed (with or without failures) we will update the historical meta-data if runtime_result.returncode == 0 or runtime_result.returncode == 7: logger.info("Test impact analysis runtime returned successfully.") diff --git a/scripts/build/TestImpactAnalysis/tiaf_persistent_storage.py b/scripts/build/TestImpactAnalysis/tiaf_persistent_storage.py index 3fff05b549..e499b0303c 100644 --- a/scripts/build/TestImpactAnalysis/tiaf_persistent_storage.py +++ b/scripts/build/TestImpactAnalysis/tiaf_persistent_storage.py @@ -17,11 +17,11 @@ logger = get_logger(__file__) class PersistentStorage(ABC): WORKSPACE_KEY = "workspace" - LAST_RUNS_KEY = "last_runs" + HISTORIC_SEQUENCES_KEY = "historic_sequences" ACTIVE_KEY = "active" ROOT_KEY = "root" RELATIVE_PATHS_KEY = "relative_paths" - TEST_IMPACT_DATA_FILES_KEY = "test_impact_data_files" + TEST_IMPACT_DATA_FILE_KEY = "test_impact_data_file" LAST_COMMIT_HASH_KEY = "last_commit_hash" COVERAGE_DATA_KEY = "coverage_data" @@ -35,19 +35,21 @@ class PersistentStorage(ABC): """ # Work on the assumption that there is no historic meta-data (a valid state to be in, should none exist) + self._suite = suite self._last_commit_hash = None self._has_historic_data = False self._has_previous_last_commit_hash = False self._this_commit_hash = commit self._this_commit_hash_last_commit_hash = None self._historic_data = None - logger.info(f"Attempting to access persistent storage for the commit {self._this_commit_hash}") + logger.info(f"Attempting to access persistent storage for the commit '{self._this_commit_hash}' for suite '{self._suite}'") try: # The runtime expects the coverage data to be in the location specified in the config file (unless overridden with # the --datafile command line argument, which the TIAF scripts do not do) self._active_workspace = pathlib.Path(config[self.WORKSPACE_KEY][self.ACTIVE_KEY][self.ROOT_KEY]) - unpacked_coverage_data_file = config[self.WORKSPACE_KEY][self.ACTIVE_KEY][self.RELATIVE_PATHS_KEY][self.TEST_IMPACT_DATA_FILES_KEY][suite] + self._active_workspace = self._active_workspace.joinpath(pathlib.Path(self._suite)) + unpacked_coverage_data_file = config[self.WORKSPACE_KEY][self.ACTIVE_KEY][self.RELATIVE_PATHS_KEY][self.TEST_IMPACT_DATA_FILE_KEY] except KeyError as e: raise SystemError(f"The config does not contain the key {str(e)}.") @@ -70,25 +72,27 @@ class PersistentStorage(ABC): self._last_commit_hash = self._historic_data[self.LAST_COMMIT_HASH_KEY] logger.info(f"Last commit hash '{self._last_commit_hash}' found.") - if self.LAST_RUNS_KEY in self._historic_data: - # Last commit hash for the sequence that was run for this commit previously (if any) - if self._this_commit_hash in self._historic_data[self.LAST_RUNS_KEY]: + # Last commit hash for the sequence that was run for this commit previously (if any) + if self.HISTORIC_SEQUENCES_KEY in self._historic_data: + if self._this_commit_hash in self._historic_data[self.HISTORIC_SEQUENCES_KEY]: # 'None' is a valid value for the previously used last commit hash if there was no coverage data at that time - self._this_commit_hash_last_commit_hash = self._historic_data[self.LAST_RUNS_KEY][self._this_commit_hash] + self._this_commit_hash_last_commit_hash = self._historic_data[self.HISTORIC_SEQUENCES_KEY][self._this_commit_hash] self._has_previous_last_commit_hash = self._this_commit_hash_last_commit_hash is not None if self._has_previous_last_commit_hash: logger.info(f"Last commit hash '{self._this_commit_hash_last_commit_hash}' was used previously for this commit.") else: - logger.info(f"Prior sequence data found for this commit but it is empty (there was no coverage data vailable at that time).") + logger.info(f"Prior sequence data found for this commit but it is empty (there was no coverage data available at that time).") else: logger.info(f"No prior sequence data found for commit '{self._this_commit_hash}', this is the first sequence for this commit.") else: logger.info(f"No prior sequence data found for any commits.") - # Create the active workspace directory where the coverage data file will be placed and unpack the coverage data so - # it is accessible by the runtime + # Create the active workspace directory for the unpacked historic data files so they are accessible by the runtime self._active_workspace.mkdir(exist_ok=True) + + # Coverage file + logger.info(f"Writing coverage data to '{self._unpacked_coverage_data_file}'.") with open(self._unpacked_coverage_data_file, "w", newline='\n') as coverage_data: coverage_data.write(self._historic_data[self.COVERAGE_DATA_KEY]) @@ -117,9 +121,9 @@ class PersistentStorage(ABC): self._historic_data[self.LAST_COMMIT_HASH_KEY] = self._this_commit_hash # Last commit hash for this commit - if not self.LAST_RUNS_KEY in self._historic_data: - self._historic_data[self.LAST_RUNS_KEY] = {} - self._historic_data[self.LAST_RUNS_KEY][self._this_commit_hash] = self._last_commit_hash + if not self.HISTORIC_SEQUENCES_KEY in self._historic_data: + self._historic_data[self.HISTORIC_SEQUENCES_KEY] = {} + self._historic_data[self.HISTORIC_SEQUENCES_KEY][self._this_commit_hash] = self._last_commit_hash # Coverage data for this branch with open(self._unpacked_coverage_data_file, "r") as coverage_data: diff --git a/scripts/build/TestImpactAnalysis/tiaf_persistent_storage_local.py b/scripts/build/TestImpactAnalysis/tiaf_persistent_storage_local.py index c72fafc580..7ad9155a41 100644 --- a/scripts/build/TestImpactAnalysis/tiaf_persistent_storage_local.py +++ b/scripts/build/TestImpactAnalysis/tiaf_persistent_storage_local.py @@ -32,10 +32,12 @@ class PersistentStorageLocal(PersistentStorage): try: # Attempt to obtain the local persistent data location specified in the runtime config file self._historic_workspace = pathlib.Path(config[self.WORKSPACE_KEY][self.HISTORIC_KEY][self.ROOT_KEY]) + self._historic_workspace = self._historic_workspace.joinpath(pathlib.Path(self._suite)) historic_data_file = pathlib.Path(config[self.WORKSPACE_KEY][self.HISTORIC_KEY][self.RELATIVE_PATHS_KEY][self.DATA_KEY]) # Attempt to unpack the local historic data file self._historic_data_file = self._historic_workspace.joinpath(historic_data_file) + logger.info(f"Attempting to retrieve historic data at location '{self._historic_data_file}'...") if self._historic_data_file.is_file(): with open(self._historic_data_file, "r") as historic_data_raw: historic_data_json = historic_data_raw.read() diff --git a/scripts/build/TestImpactAnalysis/tiaf_persistent_storage_s3.py b/scripts/build/TestImpactAnalysis/tiaf_persistent_storage_s3.py index 074caf73a1..175eb3148d 100644 --- a/scripts/build/TestImpactAnalysis/tiaf_persistent_storage_s3.py +++ b/scripts/build/TestImpactAnalysis/tiaf_persistent_storage_s3.py @@ -43,9 +43,9 @@ class PersistentStorageS3(PersistentStorage): # historic_data.json.zip is the file containing the coverage and meta-data of the last TIAF sequence run historic_data_file = f"historic_data.{object_extension}" - # The location of the data is in the form // so the build config of each branch gets its own historic data - self._historic_data_dir = f'{root_dir}/{branch}/{config[self.META_KEY][self.BUILD_CONFIG_KEY]}' - self._historic_data_key = f'{self._historic_data_dir}/{historic_data_file}' + # The location of the data is in the form /// so the build config of each branch gets its own historic data + self._historic_data_dir = f"{root_dir}/{branch}/{config[self.META_KEY][self.BUILD_CONFIG_KEY]}/{self._suite}" + self._historic_data_key = f"{self._historic_data_dir}/{historic_data_file}" logger.info(f"Attempting to retrieve historic data for branch '{branch}' at location '{self._historic_data_key}' on bucket '{s3_bucket}'...") self._s3 = boto3.resource("s3")