/* * All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or * its licensors. * * For complete copyright and license terms please see the LICENSE at the root of this * distribution (the "License"). All use of this software is governed by the License, * or, if provided, by the license below or the license accompanying this file. Do not * remove or modify any license notices. This file is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * */ #include "stdafx.h" #include "EditorCommon.h" #include #include #include #define UICANVASEDITOR_HIERARCHY_ICON_OPEN ":/Icons/Eye_Open.tif" #define UICANVASEDITOR_HIERARCHY_ICON_OPEN_HIDDEN ":/Icons/Eye_Open_Hidden.tif" #define UICANVASEDITOR_HIERARCHY_ICON_OPEN_HOVER ":/Icons/Eye_Open_Hover.tif" #define UICANVASEDITOR_HIERARCHY_ICON_PADLOCK_ENABLED_HOVER ":/Icons/Padlock_Enabled_Hover.tif" #define UICANVASEDITOR_HIERARCHY_ICON_PADLOCK_ENABLED ":/Icons/Padlock_Enabled.tif" HierarchyItem::HierarchyItem(EditorWindow* editWindow, QTreeWidgetItem& parent, int childIndex, const QString label, AZ::Entity* optionalElement) : QObject() , QTreeWidgetItem(static_cast(nullptr), QStringList(label)) , m_editorWindow(editWindow) , m_elementId(optionalElement ? optionalElement->GetId() : AZ::EntityId()) , m_mark(false) , m_preMoveChildRow(-1) , m_mouseIsHovering(false) , m_nonSnappedOffsets() , m_nonSnappedZRotation(0.0f) { // Add this hierarchy item to its parent at the specified child index QTreeWidgetItem* child = static_cast(this); if (childIndex >= 0) { parent.insertChild(childIndex, child); } else { parent.addChild(child); } // If an optional existing element is specified, we don't need to create a // new element. This occurs when building the tree from an existing canvas if (!optionalElement) { // Create a new element as the last child of the specified parent element AZ::Entity* element = nullptr; HierarchyItem* parentHierarchyItem = dynamic_cast(&parent); if (parentHierarchyItem) { EBUS_EVENT_ID_RESULT(element, parentHierarchyItem->GetEntityId(), UiElementBus, CreateChildElement, label.toStdString().c_str()); } else { EBUS_EVENT_ID_RESULT(element, editWindow->GetCanvas(), UiCanvasBus, CreateChildElement, label.toStdString().c_str()); } if (element->GetState() == AZ::Entity::ES_ACTIVE) { element->Deactivate(); // deactivate so that we can add components } // add a transform component to the element - all UI elements have a transform element->CreateComponent(LyShine::UiTransform2dComponentUuid); if (element->GetState() == AZ::Entity::ES_CONSTRUCTED) { element->Init(); // init } if (element->GetState() == AZ::Entity::ES_INIT) { element->Activate(); // activate } m_elementId = element->GetId(); // Move the new child element to the desired child index if (childIndex >= 0) { AZ::EntityId parentEntityId; if (parentHierarchyItem) { parentEntityId = parentHierarchyItem->GetEntityId(); } AZ::EntityId insertBeforeEntityId; EBUS_EVENT_ID_RESULT(insertBeforeEntityId, parentEntityId, UiElementBus, GetChildEntityId, childIndex); EBUS_EVENT_ID(m_elementId, UiElementBus, ReparentByEntityId, parentEntityId, insertBeforeEntityId); } } AZ_Assert(m_elementId.IsValid(), "Invalid element ID"); // Connect signals. { // Register with the entity map for quick lookup. QObject::connect(this, SIGNAL(SignalItemAdd(HierarchyItem*)), m_editorWindow->GetHierarchy(), SLOT(HandleItemAdd(HierarchyItem*))); QObject::connect(this, SIGNAL(SignalItemRemove(HierarchyItem*)), m_editorWindow->GetHierarchy(), SLOT(HandleItemRemove(HierarchyItem*))); } // Add to the entity map for quick lookup. // // IMPORTANT: This MUST be done BEFORE changing the // behavior and look of this class. SignalItemAdd(this); // Behavior and look. // // IMPORTANT: This MUST be done AFTER SignalItemAdd(). { setFlags(flags() | Qt::ItemIsEditable | Qt::ItemIsDragEnabled | Qt::ItemIsDropEnabled); UpdateIcon(); UpdateSliceInfo(); UpdateEditorOnlyInfo(); } } HierarchyItem::~HierarchyItem() { DeleteElement(); // Remove from quick lookup entity map. SignalItemRemove(this); } void HierarchyItem::DeleteElement() { // IMPORTANT: DeleteElement() can be called from ~HierarchyItem(). // Parent HierarchyItem are destroyed BEFORE their children. // When a parent HierarchyItem is destroyed, all its AZ::Entity // children are destroyed. Therefore, it's NECESSARY to use // SAFE_DELETE(). That's because our AZ::Entity might have already // been deleted. In which case GetElement() will return nullptr. // ~HierarchyItem() is the ONLY place where GetElement() is allowed // return nullptr. EBUS_EVENT_ID(m_elementId, UiElementBus, DestroyElement); } AZ::Entity* HierarchyItem::GetElement() const { // IMPORTANT: "element" will NEVER be nullptr, EXCEPT in ~HierarchyItem(). // In the ~HierarchyItem(), deleting the parent of our element will cause // our own element to be destroyed. ~HierarchyItem() is the ONLY place // where we CAN'T assume that our m_elementId is always valid. return EntityHelpers::GetEntity(m_elementId); } AZ::EntityId HierarchyItem::GetEntityId() const { return m_elementId; } void HierarchyItem::ClearEntityId() { m_elementId.SetInvalid(); } void HierarchyItem::SetMouseIsHovering(bool isHovering) { m_mouseIsHovering = isHovering; UpdateIcon(); } void HierarchyItem::SetIsExpanded(bool isExpanded) { // Runtime-side. EBUS_EVENT_ID(m_elementId, UiEditorBus, SetIsExpanded, isExpanded); // Editor-side. setExpanded(isExpanded); } void HierarchyItem::ApplyElementIsExpanded() { bool isExpanded = false; EBUS_EVENT_ID_RESULT(isExpanded, m_elementId, UiEditorBus, GetIsExpanded); setExpanded(isExpanded); } void HierarchyItem::SetIsSelectable(bool isSelectable) { // Runtime-side. EBUS_EVENT_ID(m_elementId, UiEditorBus, SetIsSelectable, isSelectable); // Editor-side. UpdateIcon(); UpdateChildIcon(); m_editorWindow->GetViewport()->Refresh(); } void HierarchyItem::SetIsSelected(bool isSelected) { // Runtime-side. EBUS_EVENT_ID(m_elementId, UiEditorBus, SetIsSelected, isSelected); // Editor-side. setSelected(isSelected); UpdateIcon(); m_editorWindow->GetViewport()->Refresh(); } void HierarchyItem::SetIsVisible(bool isVisible) { // Runtime-side. EBUS_EVENT_ID(m_elementId, UiEditorBus, SetIsVisible, isVisible); // Editor-side. UpdateIcon(); UpdateChildIcon(); m_editorWindow->GetViewport()->Refresh(); } void HierarchyItem::UpdateIcon() { // Eye icon. { const char* textureName = nullptr; bool isVisible = false; EBUS_EVENT_ID_RESULT(isVisible, m_elementId, UiEditorBus, GetIsVisible); if (isVisible) { // This item is visible. bool areAllAncestorsVisible = true; EBUS_EVENT_ID_RESULT(areAllAncestorsVisible, m_elementId, UiEditorBus, AreAllAncestorsVisible); textureName = (m_mouseIsHovering ? UICANVASEDITOR_HIERARCHY_ICON_OPEN_HOVER : (areAllAncestorsVisible ? UICANVASEDITOR_HIERARCHY_ICON_OPEN : UICANVASEDITOR_HIERARCHY_ICON_OPEN_HIDDEN)); } else { // This item is NOT visible. textureName = (m_mouseIsHovering ? UICANVASEDITOR_HIERARCHY_ICON_OPEN_HIDDEN : ""); } setIcon(kHierarchyColumnIsVisible, QIcon(textureName).pixmap(UICANVASEDITOR_HIERARCHY_HEADER_ICON_SIZE, UICANVASEDITOR_HIERARCHY_HEADER_ICON_SIZE)); } // Padlock icon. { const char* textureName = nullptr; bool isSelectable = false; EBUS_EVENT_ID_RESULT(isSelectable, m_elementId, UiEditorBus, GetIsSelectable); if (isSelectable) { // This item is NOT locked. textureName = (m_mouseIsHovering ? UICANVASEDITOR_HIERARCHY_ICON_PADLOCK_ENABLED : ""); } else { // This item is locked. textureName = (m_mouseIsHovering ? UICANVASEDITOR_HIERARCHY_ICON_PADLOCK_ENABLED_HOVER : UICANVASEDITOR_HIERARCHY_ICON_PADLOCK_ENABLED); } setIcon(kHierarchyColumnIsSelectable, QIcon(textureName).pixmap(UICANVASEDITOR_HIERARCHY_HEADER_ICON_SIZE, UICANVASEDITOR_HIERARCHY_HEADER_ICON_SIZE)); } } void HierarchyItem::UpdateChildIcon() { // Seed the list. HierarchyItemRawPtrList items; HierarchyHelpers::AppendAllChildrenToEndOfList(this, items); // Update child icons. HierarchyHelpers::TraverseListAndAllChildren(items, [](HierarchyItem* childItem) { childItem->UpdateIcon(); }); } HierarchyItem* HierarchyItem::Parent() const { // It's ok to return a nullptr. // nullptr normally happens when we've reached the invisibleRootItem(), // We DON'T consider the invisibleRootItem() the parent of a HierarchyItem. return dynamic_cast(QTreeWidgetItem::parent()); } HierarchyItem* HierarchyItem::Child(int i) const { HierarchyItem* item = dynamic_cast(QTreeWidgetItem::child(i)); AZ_Assert(item, "There's an item in the Hierarchy that isn't a HierarchyItem."); return item; } void HierarchyItem::SetMark(bool m) { m_mark = m; } bool HierarchyItem::GetMark() { return m_mark; } void HierarchyItem::SetPreMove(AZ::EntityId parentId, int childRow) { m_preMoveParentId = parentId; m_preMoveChildRow = childRow; } AZ::EntityId HierarchyItem::GetPreMoveParentId() { return m_preMoveParentId; } int HierarchyItem::GetPreMoveChildRow() { return m_preMoveChildRow; } void HierarchyItem::ReplaceElement(const AZStd::string& xml, const AZStd::unordered_set& referencedSliceAssets) { AZ_Assert(!xml.empty(), "XML is empty"); AZ::Entity* parentEntity = Parent() ? Parent()->GetElement() : nullptr; AZ::Entity* replaceEntity = GetElement(); // find the element after the one to be replaced AZ::Entity* insertBeforeEntity = nullptr; { LyShine::EntityArray childElements; if (parentEntity) { EBUS_EVENT_ID_RESULT(childElements, parentEntity->GetId(), UiElementBus, GetChildElements); } else { EBUS_EVENT_ID_RESULT(childElements, m_editorWindow->GetCanvas(), UiCanvasBus, GetChildElements); } // find the enity we are replacing in the list (it must exist) auto iter = std::find(childElements.begin(), childElements.end(), replaceEntity); AZ_Assert(iter != childElements.end(), "Entity not found"); // if there is an element after the replace element then that is the one to insert before if (++iter != childElements.end()) { insertBeforeEntity = *iter; } } // If restoring to a slice, keep a reference to the slice asset so it isn't released when the entity // is deleted, only to immediately reload upon restoring. AZStd::vector> preservedAssetsRefs; for (auto assetId : referencedSliceAssets) { preservedAssetsRefs.push_back(AZ::Data::AssetManager::Instance().FindAsset(assetId)); } // Discard the old element. DeleteElement(); // Load the new element. SerializeHelpers::RestoreSerializedElements(m_editorWindow->GetCanvas(), parentEntity, insertBeforeEntity, m_editorWindow->GetEntityContext(), xml, false, nullptr); // Update any visual information that may have changed with this element or any of its descendants UpdateEditorOnlyInfoRecursive(); } void HierarchyItem::UpdateSliceInfo() { // This is deliberately slightly different to the blue color used for hover, so that we can still see a change on hover static const QColor sliceForegroundColor(117, 156, 254); //determine if entity belongs to a slice AZ::SliceComponent::SliceInstanceAddress sliceAddress; AzFramework::EntityIdContextQueryBus::EventResult(sliceAddress, m_elementId, &AzFramework::EntityIdContextQueries::GetOwningSlice); bool isSliceEntity = sliceAddress.IsValid(); AZStd::string sliceAssetName; if (isSliceEntity) { auto sliceReference = sliceAddress.GetReference(); auto sliceInstance = sliceAddress.GetInstance(); // determine slice asset name (for tooltip display) AZ::Data::AssetCatalogRequestBus::BroadcastResult(sliceAssetName, &AZ::Data::AssetCatalogRequests::GetAssetPathById, sliceReference->GetSliceAsset().GetId()); //determine if entity parent belongs to a slice AZ::EntityId parentId; EBUS_EVENT_ID_RESULT(parentId, m_elementId, UiElementBus, GetParentEntityId); AZ::SliceComponent::SliceInstanceAddress parentSliceAddress; AzFramework::EntityIdContextQueryBus::EventResult(parentSliceAddress, parentId, &AzFramework::EntityIdContextQueries::GetOwningSlice); //we're a slice root if our parent doesn't have a slice reference or instance or our parent slice reference or instances don't match ours auto parentSliceReference = parentSliceAddress.GetReference(); auto parentSliceInstance = parentSliceAddress.GetInstance(); bool isSliceRoot = !parentSliceReference || !parentSliceInstance || (sliceReference != parentSliceReference) || (sliceInstance->GetId() != parentSliceInstance->GetId()); // set the text color to the slice color setForeground(0, sliceForegroundColor); // use bold or italic to indicate whether this is the root of the slice instance or a child entity within an instance auto itemFont = font(0); if (isSliceRoot) { itemFont.setBold(true); } else { itemFont.setItalic(true); } setFont(0, itemFont); } else { // get the normal text color from the palette auto parentWidgetPtr = static_cast(m_editorWindow); setForeground(0, QBrush(parentWidgetPtr->palette().color(QPalette::Text))); // set to normal font auto itemFont = font(0); itemFont.setBold(false); itemFont.setItalic(false); setFont(0, itemFont); } // Set tooltip to indicate which slice this is part of (if any) QString tooltip = !sliceAssetName.empty() ? QString("Slice asset: %1").arg(sliceAssetName.data()) : QString("Slice asset: This entity is not part of a slice."); setToolTip(0, tooltip); } void HierarchyItem::UpdateEditorOnlyInfo() { bool isEditorOnly = false; AzToolsFramework::EditorOnlyEntityComponentRequestBus::EventResult(isEditorOnly, m_elementId, &AzToolsFramework::EditorOnlyEntityComponentRequests::IsEditorOnlyEntity); if (isEditorOnly) { static const QColor editorOnlyBackgroundColor(60, 0, 0); setBackground(0, editorOnlyBackgroundColor); } else { setBackground(0, Qt::transparent); } } void HierarchyItem::UpdateEditorOnlyInfoRecursive() { UpdateEditorOnlyInfo(); for (int i = 0; i < childCount(); ++i) { HierarchyItem* item = dynamic_cast(child(i)); item->UpdateEditorOnlyInfoRecursive(); } } void HierarchyItem::SetNonSnappedOffsets(UiTransform2dInterface::Offsets offsets) { m_nonSnappedOffsets = offsets; } UiTransform2dInterface::Offsets HierarchyItem::GetNonSnappedOffsets() { return m_nonSnappedOffsets; } void HierarchyItem::SetNonSnappedZRotation(float rotation) { m_nonSnappedZRotation = rotation; } float HierarchyItem::GetNonSnappedZRotation() { return m_nonSnappedZRotation; } #include