You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1037 lines
35 KiB
C++
1037 lines
35 KiB
C++
/*
|
|
* Copyright (c) Contributors to the Open 3D Engine Project.
|
|
* For complete copyright and license terms please see the LICENSE at the root of this distribution.
|
|
*
|
|
* SPDX-License-Identifier: Apache-2.0 OR MIT
|
|
*
|
|
*/
|
|
#include "EditorCommon.h"
|
|
#include "CanvasHelpers.h"
|
|
#include <AzQtComponents/Components/Style.h>
|
|
#include <AzToolsFramework/Slice/SliceUtilities.h>
|
|
#include <AzToolsFramework/Entity/EditorEntityInfoBus.h>
|
|
#include <AzToolsFramework/ToolsComponents/EditorOnlyEntityComponent.h>
|
|
#include <LyShine/Bus/UiSystemBus.h>
|
|
|
|
#include <QApplication>
|
|
#include <QBoxLayout>
|
|
#include <QCheckBox>
|
|
#include <QLineEdit>
|
|
#include <QMouseEvent>
|
|
#include <QPainter>
|
|
#include <QContextMenuEvent>
|
|
//-------------------------------------------------------------------------------
|
|
|
|
//we require an overlay widget to act as a canvas to draw on top of everything in the inspector
|
|
//attaching to inspector rather than component editors so we can draw outside of bounds
|
|
class PropertyContainerOverlay : public QWidget
|
|
{
|
|
public:
|
|
PropertyContainerOverlay(PropertiesContainer* editor, QWidget* parent)
|
|
: QWidget(parent)
|
|
, m_editor(editor)
|
|
, m_dropIndicatorOffset(8)
|
|
{
|
|
setPalette(Qt::transparent);
|
|
setWindowFlags(Qt::FramelessWindowHint);
|
|
setAttribute(Qt::WA_NoSystemBackground);
|
|
setAttribute(Qt::WA_TranslucentBackground);
|
|
setAttribute(Qt::WA_TransparentForMouseEvents);
|
|
}
|
|
|
|
protected:
|
|
void paintEvent(QPaintEvent* event) override
|
|
{
|
|
const int TopMargin = 1;
|
|
const int RightMargin = 2;
|
|
const int BottomMargin = 5;
|
|
const int LeftMargin = 2;
|
|
|
|
QWidget::paintEvent(event);
|
|
|
|
QPainter painter(this);
|
|
painter.setCompositionMode(QPainter::CompositionMode_SourceOver);
|
|
|
|
QRect currRect;
|
|
bool drag = false;
|
|
bool drop = false;
|
|
|
|
for (auto componentEditor : m_editor->m_componentEditors)
|
|
{
|
|
if (!componentEditor->isVisible())
|
|
{
|
|
continue;
|
|
}
|
|
|
|
QRect globalRect = m_editor->GetWidgetGlobalRect(componentEditor);
|
|
|
|
currRect = QRect(
|
|
QPoint(mapFromGlobal(globalRect.topLeft()) + QPoint(LeftMargin, TopMargin)),
|
|
QPoint(mapFromGlobal(globalRect.bottomRight()) - QPoint(RightMargin, BottomMargin))
|
|
);
|
|
|
|
currRect.setWidth(currRect.width() - 1);
|
|
currRect.setHeight(currRect.height() - 1);
|
|
|
|
if (componentEditor->IsDragged())
|
|
{
|
|
QStyleOption opt;
|
|
opt.init(this);
|
|
opt.rect = currRect;
|
|
static_cast<AzQtComponents::Style*>(style())->drawDragIndicator(&opt, &painter, this);
|
|
drag = true;
|
|
}
|
|
|
|
if (componentEditor->IsDropTarget())
|
|
{
|
|
QRect dropRect = currRect;
|
|
dropRect.setTop(currRect.top() - m_dropIndicatorOffset);
|
|
dropRect.setHeight(0);
|
|
|
|
QStyleOption opt;
|
|
opt.init(this);
|
|
opt.rect = dropRect;
|
|
style()->drawPrimitive(QStyle::PE_IndicatorItemViewItemDrop, &opt, &painter, this);
|
|
|
|
drop = true;
|
|
}
|
|
}
|
|
|
|
if (drag && !drop)
|
|
{
|
|
QRect dropRect = currRect;
|
|
dropRect.setTop(currRect.top() - m_dropIndicatorOffset);
|
|
dropRect.setHeight(0);
|
|
|
|
QStyleOption opt;
|
|
opt.init(this);
|
|
opt.rect = dropRect;
|
|
style()->drawPrimitive(QStyle::PE_IndicatorItemViewItemDrop, &opt, &painter, this);
|
|
}
|
|
}
|
|
|
|
private:
|
|
PropertiesContainer* m_editor;
|
|
int m_dropIndicatorOffset;
|
|
};
|
|
|
|
//-------------------------------------------------------------------------------
|
|
|
|
PropertiesContainer::PropertiesContainer(PropertiesWidget* propertiesWidget,
|
|
EditorWindow* editorWindow)
|
|
: QScrollArea(propertiesWidget)
|
|
, m_propertiesWidget(propertiesWidget)
|
|
, m_editorWindow(editorWindow)
|
|
, m_selectedEntityDisplayNameWidget(nullptr)
|
|
, m_selectionHasChanged(false)
|
|
, m_isCanvasSelected(false)
|
|
, m_selectionEventAccepted(false)
|
|
, m_componentEditorLastSelectedIndex(-1)
|
|
{
|
|
setFocusPolicy(Qt::ClickFocus);
|
|
setFrameShape(QFrame::NoFrame);
|
|
setFrameShadow(QFrame::Plain);
|
|
setLineWidth(0);
|
|
setWidgetResizable(true);
|
|
|
|
m_componentListContents = new QWidget();
|
|
m_componentListContents->setGeometry(QRect(0, 0, 382, 537));
|
|
QSizePolicy sizePolicy(QSizePolicy::Preferred, QSizePolicy::Expanding);
|
|
sizePolicy.setHeightForWidth(m_componentListContents->sizePolicy().hasHeightForWidth());
|
|
m_componentListContents->setSizePolicy(sizePolicy);
|
|
m_rowLayout = new QVBoxLayout(m_componentListContents);
|
|
m_rowLayout->setSpacing(10);
|
|
m_rowLayout->setContentsMargins(0, 0, 0, 0);
|
|
m_rowLayout->setAlignment(Qt::AlignTop);
|
|
|
|
setWidget(m_componentListContents);
|
|
|
|
m_overlay = new PropertyContainerOverlay(this, m_componentListContents);
|
|
UpdateOverlay();
|
|
|
|
CreateActions();
|
|
|
|
// Get the serialize context.
|
|
EBUS_EVENT_RESULT(m_serializeContext, AZ::ComponentApplicationBus, GetSerializeContext);
|
|
AZ_Assert(m_serializeContext, "We should have a valid context!");
|
|
|
|
QObject::connect(m_editorWindow->GetHierarchy(),
|
|
&HierarchyWidget::editorOnlyStateChangedOnSelectedElements,
|
|
[this]()
|
|
{
|
|
UpdateEditorOnlyCheckbox();
|
|
});
|
|
}
|
|
|
|
void PropertiesContainer::resizeEvent(QResizeEvent* event)
|
|
{
|
|
QScrollArea::resizeEvent(event);
|
|
UpdateOverlay();
|
|
}
|
|
|
|
void PropertiesContainer::contextMenuEvent(QContextMenuEvent* event)
|
|
{
|
|
OnDisplayUiComponentEditorMenu(event->globalPos());
|
|
event->accept();
|
|
}
|
|
|
|
//overridden to intercept application level mouse events for component editor selection
|
|
bool PropertiesContainer::eventFilter(QObject* object, QEvent* event)
|
|
{
|
|
HandleSelectionEvents(object, event);
|
|
return false;
|
|
}
|
|
|
|
bool PropertiesContainer::HandleSelectionEvents(QObject* object, QEvent* event)
|
|
{
|
|
(void)object;
|
|
if (m_selectedEntities.empty())
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (event->type() != QEvent::MouseButtonPress &&
|
|
event->type() != QEvent::MouseButtonDblClick &&
|
|
event->type() != QEvent::MouseButtonRelease)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
QMouseEvent* mouseEvent = static_cast<QMouseEvent*>(event);
|
|
|
|
//selection now occurs on mouse released
|
|
//reset selection flag when mouse is clicked to allow additional selection changes
|
|
if (event->type() == QEvent::MouseButtonPress)
|
|
{
|
|
m_selectionEventAccepted = false;
|
|
return false;
|
|
}
|
|
|
|
//reject input if selection already occurred for this click
|
|
if (m_selectionEventAccepted)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
//reject input if a popup or modal window is active
|
|
if (QApplication::activeModalWidget() != nullptr ||
|
|
QApplication::activePopupWidget() != nullptr)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
const QRect globalRect(mouseEvent->globalPos(), mouseEvent->globalPos());
|
|
|
|
//reject input outside of the inspector's component list
|
|
if (!DoesOwnFocus() ||
|
|
!DoesIntersectWidget(globalRect, this))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
//reject input from other buttons
|
|
if ((mouseEvent->button() != Qt::LeftButton) &&
|
|
(mouseEvent->button() != Qt::RightButton))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
//right click is allowed if the component editor under the mouse is not selected
|
|
if (mouseEvent->button() == Qt::RightButton)
|
|
{
|
|
if (DoesIntersectSelectedComponentEditor(globalRect))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
ClearComponentEditorSelection();
|
|
SelectIntersectingComponentEditors(globalRect, true);
|
|
}
|
|
else if (mouseEvent->button() == Qt::LeftButton)
|
|
{
|
|
//if shift or control is pressed this is a multi=-select operation, otherwise reset the selection
|
|
if (mouseEvent->modifiers() & Qt::ControlModifier)
|
|
{
|
|
ToggleIntersectingComponentEditors(globalRect);
|
|
}
|
|
else if (mouseEvent->modifiers() & Qt::ShiftModifier)
|
|
{
|
|
ComponentEditorVector intersections = GetIntersectingComponentEditors(globalRect);
|
|
if (!intersections.empty())
|
|
{
|
|
SelectRangeOfComponentEditors(m_componentEditorLastSelectedIndex, GetComponentEditorIndex(intersections.front()), true);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
ClearComponentEditorSelection();
|
|
SelectIntersectingComponentEditors(globalRect, true);
|
|
}
|
|
}
|
|
|
|
UpdateInternalState();
|
|
|
|
//ensure selection logic executes only once per click since eventFilter may execute multiple times for a single click
|
|
m_selectionEventAccepted = true;
|
|
return true;
|
|
}
|
|
|
|
AZ::Entity::ComponentArrayType PropertiesContainer::GetSelectedComponents()
|
|
{
|
|
ComponentEditorVector selectedComponentEditors;
|
|
|
|
selectedComponentEditors.reserve(m_componentEditors.size());
|
|
for (auto componentEditor : m_componentEditors)
|
|
{
|
|
if (componentEditor->isVisible() && componentEditor->IsSelected())
|
|
{
|
|
selectedComponentEditors.push_back(componentEditor);
|
|
}
|
|
}
|
|
|
|
AZ::Entity::ComponentArrayType selectedComponents;
|
|
|
|
for (auto componentEditor : selectedComponentEditors)
|
|
{
|
|
const auto& components = componentEditor->GetComponents();
|
|
selectedComponents.insert(selectedComponents.end(), components.begin(), components.end());
|
|
}
|
|
|
|
return selectedComponents;
|
|
}
|
|
|
|
void PropertiesContainer::BuildSharedComponentList(ComponentTypeMap& sharedComponentsByType, const AzToolsFramework::EntityIdList& entitiesShown)
|
|
{
|
|
// For single selection of a slice-instanced entity, gather the direct slice ancestor
|
|
// so we can visualize per-component differences.
|
|
m_compareToEntity.reset();
|
|
if (1 == entitiesShown.size())
|
|
{
|
|
AZ::SliceComponent::SliceInstanceAddress address;
|
|
AzFramework::SliceEntityRequestBus::EventResult(address, entitiesShown[0],
|
|
&AzFramework::SliceEntityRequestBus::Events::GetOwningSlice);
|
|
if (address.IsValid())
|
|
{
|
|
AZ::SliceComponent::EntityAncestorList ancestors;
|
|
address.GetReference()->GetInstanceEntityAncestry(entitiesShown[0], ancestors, 1);
|
|
|
|
if (!ancestors.empty())
|
|
{
|
|
m_compareToEntity = AzToolsFramework::SliceUtilities::CloneSliceEntityForComparison(*ancestors[0].m_entity, *address.GetInstance(), *m_serializeContext);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Create a SharedComponentInfo for each component
|
|
// that selected entities have in common.
|
|
// See comments on SharedComponentInfo for more details
|
|
for (AZ::EntityId entityId : entitiesShown)
|
|
{
|
|
AZ::Entity* entity = nullptr;
|
|
EBUS_EVENT_RESULT(entity, AZ::ComponentApplicationBus, FindEntity, entityId);
|
|
AZ_Assert(entity, "Entity was selected but no such entity exists?");
|
|
if (!entity)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
// Track how many of each component-type we've seen on this entity
|
|
AZStd::unordered_map<AZ::Uuid, size_t> entityComponentCounts;
|
|
|
|
for (AZ::Component* component : entity->GetComponents())
|
|
{
|
|
const AZ::Uuid& componentType = azrtti_typeid(component);
|
|
const AZ::SerializeContext::ClassData* classData = m_serializeContext->FindClassData(componentType);
|
|
|
|
// Skip components without edit data
|
|
if (!classData || !classData->m_editData)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
// Skip components that are set to invisible.
|
|
if (const AZ::Edit::ElementData* editorDataElement = classData->m_editData->FindElementData(AZ::Edit::ClassElements::EditorData))
|
|
{
|
|
if (AZ::Edit::Attribute* visibilityAttribute = editorDataElement->FindAttribute(AZ::Edit::Attributes::Visibility))
|
|
{
|
|
AzToolsFramework::PropertyAttributeReader reader(component, visibilityAttribute);
|
|
AZ::u32 visibilityValue;
|
|
if (reader.Read<AZ::u32>(visibilityValue))
|
|
{
|
|
if (visibilityValue == AZ_CRC("PropertyVisibility_Hide", 0x32ab90f7))
|
|
{
|
|
continue;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// The sharedComponentList is created based on the first entity.
|
|
if (entityId == *entitiesShown.begin())
|
|
{
|
|
// Add new SharedComponentInfo
|
|
SharedComponentInfo sharedComponent;
|
|
sharedComponent.m_classData = classData;
|
|
sharedComponentsByType[componentType].push_back(sharedComponent);
|
|
}
|
|
|
|
// Skip components that don't correspond to a type from the first entity.
|
|
if (sharedComponentsByType.find(componentType) == sharedComponentsByType.end())
|
|
{
|
|
continue;
|
|
}
|
|
|
|
// Update entityComponentCounts (may be multiple components of this type)
|
|
auto entityComponentCountsIt = entityComponentCounts.find(componentType);
|
|
size_t componentIndex = (entityComponentCountsIt == entityComponentCounts.end())
|
|
? 0
|
|
: entityComponentCountsIt->second;
|
|
entityComponentCounts[componentType] = componentIndex + 1;
|
|
|
|
// Skip component if the first entity didn't have this many.
|
|
if (componentIndex >= sharedComponentsByType[componentType].size())
|
|
{
|
|
continue;
|
|
}
|
|
|
|
// Component accepted! Add it as an instance
|
|
SharedComponentInfo& sharedComponent = sharedComponentsByType[componentType][componentIndex];
|
|
sharedComponent.m_instances.push_back(component);
|
|
|
|
// If specified, locate the corresponding component in the comparison entity to
|
|
// visualize differences.
|
|
if (m_compareToEntity && !sharedComponent.m_compareInstance)
|
|
{
|
|
size_t compareComponentIndex = 0;
|
|
for (AZ::Component* compareComponent : m_compareToEntity.get()->GetComponents())
|
|
{
|
|
const AZ::Uuid& compareComponentType = azrtti_typeid(compareComponent);
|
|
if (componentType == compareComponentType)
|
|
{
|
|
if (componentIndex == compareComponentIndex)
|
|
{
|
|
sharedComponent.m_compareInstance = compareComponent;
|
|
break;
|
|
}
|
|
compareComponentIndex++;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Cull any SharedComponentInfo that doesn't fit all our requirements
|
|
ComponentTypeMap::iterator sharedComponentsByTypeIterator = sharedComponentsByType.begin();
|
|
while (sharedComponentsByTypeIterator != sharedComponentsByType.end())
|
|
{
|
|
AZStd::vector<SharedComponentInfo>& sharedComponents = sharedComponentsByTypeIterator->second;
|
|
|
|
// Remove component if it doesn't exist on every entity
|
|
AZStd::vector<SharedComponentInfo>::iterator sharedComponentIterator = sharedComponents.begin();
|
|
while (sharedComponentIterator != sharedComponents.end())
|
|
{
|
|
if (sharedComponentIterator->m_instances.size() != entitiesShown.size()
|
|
|| sharedComponentIterator->m_instances.empty())
|
|
{
|
|
sharedComponentIterator = sharedComponents.erase(sharedComponentIterator);
|
|
}
|
|
else
|
|
{
|
|
++sharedComponentIterator;
|
|
}
|
|
}
|
|
|
|
// Remove entry if all its components were culled
|
|
if (sharedComponents.size() == 0)
|
|
{
|
|
sharedComponentsByTypeIterator = sharedComponentsByType.erase(sharedComponentsByTypeIterator);
|
|
}
|
|
else
|
|
{
|
|
++sharedComponentsByTypeIterator;
|
|
}
|
|
}
|
|
}
|
|
|
|
void PropertiesContainer::BuildSharedComponentUI(ComponentTypeMap& sharedComponentsByType, const AzToolsFramework::EntityIdList& entitiesShown)
|
|
{
|
|
(void)entitiesShown;
|
|
|
|
// At this point in time:
|
|
// - Each SharedComponentInfo should contain one component instance
|
|
// from each selected entity.
|
|
// - Any pre-existing m_componentEditor entries should be
|
|
// cleared of component instances.
|
|
|
|
// Add each component instance to its corresponding editor
|
|
// We add them in the order that the component factories were registered in, this provides
|
|
// a consistent order of components. It doesn't appear to be the case that components always
|
|
// stay in the order they were added to the entity in, some of our slices do not have the
|
|
// UiElementComponent first for example.
|
|
const AZStd::vector<AZ::Uuid>* componentTypes;
|
|
EBUS_EVENT_RESULT(componentTypes, UiSystemBus, GetComponentTypesForMenuOrdering);
|
|
|
|
// There could be components that were not registered for component ordering. We don't
|
|
// want to hide them. So add them at the end of the list
|
|
AZStd::vector<AZ::Uuid> componentOrdering;
|
|
componentOrdering = *componentTypes;
|
|
for (auto sharedComponentMapEntry : sharedComponentsByType)
|
|
{
|
|
if (AZStd::find(componentOrdering.begin(), componentOrdering.end(), sharedComponentMapEntry.first) == componentOrdering.end())
|
|
{
|
|
componentOrdering.push_back(sharedComponentMapEntry.first);
|
|
}
|
|
}
|
|
|
|
m_componentEditors.clear();
|
|
|
|
for (auto& componentType : componentOrdering)
|
|
{
|
|
if (sharedComponentsByType.count(componentType) <= 0)
|
|
{
|
|
continue; // there are no components of this type in the sharedComponentsByType map
|
|
}
|
|
|
|
const auto& sharedComponents = sharedComponentsByType[componentType];
|
|
|
|
for (size_t sharedComponentIndex = 0; sharedComponentIndex < sharedComponents.size(); ++sharedComponentIndex)
|
|
{
|
|
const SharedComponentInfo& sharedComponent = sharedComponents[sharedComponentIndex];
|
|
|
|
AZ_Assert(sharedComponent.m_instances.size() == entitiesShown.size()
|
|
&& !sharedComponent.m_instances.empty(),
|
|
"sharedComponentsByType should only contain valid entries at this point");
|
|
|
|
// Create an editor if necessary
|
|
ComponentEditorVector& componentEditors = m_componentEditorsByType[componentType];
|
|
if (sharedComponentIndex >= componentEditors.size())
|
|
{
|
|
componentEditors.push_back(CreateComponentEditor(*sharedComponent.m_instances[0]));
|
|
}
|
|
else
|
|
{
|
|
// Place existing editor in correct order
|
|
m_rowLayout->removeWidget(componentEditors[sharedComponentIndex]);
|
|
m_rowLayout->addWidget(componentEditors[sharedComponentIndex]);
|
|
}
|
|
|
|
AzToolsFramework::ComponentEditor* componentEditor = componentEditors[sharedComponentIndex];
|
|
|
|
// Save a list of components in order shown
|
|
m_componentEditors.push_back(componentEditor);
|
|
|
|
// Add instances to componentEditor
|
|
auto& componentInstances = sharedComponent.m_instances;
|
|
for (AZ::Component* componentInstance : componentInstances)
|
|
{
|
|
// non-first instances are aggregated under the first instance
|
|
AZ::Component* aggregateInstance = componentInstance != componentInstances.front() ? componentInstances.front() : nullptr;
|
|
|
|
// Reference the slice entity if we are a slice so we can indicate differences from base
|
|
AZ::Component* compareInstance = sharedComponent.m_compareInstance;
|
|
|
|
componentEditor->AddInstance(componentInstance, aggregateInstance, compareInstance);
|
|
}
|
|
|
|
// Refresh editor
|
|
componentEditor->InvalidateAll();
|
|
componentEditor->show();
|
|
}
|
|
}
|
|
}
|
|
|
|
AzToolsFramework::ComponentEditor* PropertiesContainer::CreateComponentEditor([[maybe_unused]] const AZ::Component& componentInstance)
|
|
{
|
|
AzToolsFramework::ComponentEditor* editor = new AzToolsFramework::ComponentEditor(m_serializeContext, m_propertiesWidget, this);
|
|
connect(editor, &AzToolsFramework::ComponentEditor::OnDisplayComponentEditorMenu, this, &PropertiesContainer::OnDisplayUiComponentEditorMenu);
|
|
|
|
m_rowLayout->addWidget(editor);
|
|
editor->hide();
|
|
|
|
return editor;
|
|
}
|
|
|
|
bool PropertiesContainer::DoesOwnFocus() const
|
|
{
|
|
QWidget* widget = QApplication::focusWidget();
|
|
return this == widget || isAncestorOf(widget);
|
|
}
|
|
|
|
QRect PropertiesContainer::GetWidgetGlobalRect(const QWidget* widget) const
|
|
{
|
|
return QRect(
|
|
widget->mapToGlobal(widget->rect().topLeft()),
|
|
widget->mapToGlobal(widget->rect().bottomRight()));
|
|
}
|
|
|
|
bool PropertiesContainer::DoesIntersectWidget(const QRect& globalRect, const QWidget* widget) const
|
|
{
|
|
return widget->isVisible() && globalRect.intersects(GetWidgetGlobalRect(widget));
|
|
}
|
|
|
|
bool PropertiesContainer::DoesIntersectSelectedComponentEditor(const QRect& globalRect) const
|
|
{
|
|
for (auto componentEditor : GetIntersectingComponentEditors(globalRect))
|
|
{
|
|
if (componentEditor->IsSelected())
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
bool PropertiesContainer::DoesIntersectNonSelectedComponentEditor(const QRect& globalRect) const
|
|
{
|
|
for (auto componentEditor : GetIntersectingComponentEditors(globalRect))
|
|
{
|
|
if (!componentEditor->IsSelected())
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
void PropertiesContainer::ClearComponentEditorSelection()
|
|
{
|
|
AZ_PROFILE_FUNCTION(AzToolsFramework);
|
|
for (auto componentEditor : m_componentEditors)
|
|
{
|
|
componentEditor->SetSelected(false);
|
|
}
|
|
|
|
UpdateInternalState();
|
|
}
|
|
|
|
void PropertiesContainer::SelectRangeOfComponentEditors(const AZ::s32 index1, const AZ::s32 index2, bool selected)
|
|
{
|
|
if (index1 >= 0 && index2 >= 0)
|
|
{
|
|
const AZ::s32 min = AZStd::min(index1, index2);
|
|
const AZ::s32 max = AZStd::max(index1, index2);
|
|
for (AZ::s32 index = min; index <= max; ++index)
|
|
{
|
|
m_componentEditors[index]->SetSelected(selected);
|
|
}
|
|
m_componentEditorLastSelectedIndex = index2;
|
|
}
|
|
|
|
UpdateInternalState();
|
|
}
|
|
|
|
void PropertiesContainer::SelectIntersectingComponentEditors(const QRect& globalRect, bool selected)
|
|
{
|
|
for (auto componentEditor : GetIntersectingComponentEditors(globalRect))
|
|
{
|
|
componentEditor->SetSelected(selected);
|
|
m_componentEditorLastSelectedIndex = GetComponentEditorIndex(componentEditor);
|
|
}
|
|
|
|
UpdateInternalState();
|
|
}
|
|
|
|
void PropertiesContainer::ToggleIntersectingComponentEditors(const QRect& globalRect)
|
|
{
|
|
for (auto componentEditor : GetIntersectingComponentEditors(globalRect))
|
|
{
|
|
componentEditor->SetSelected(!componentEditor->IsSelected());
|
|
m_componentEditorLastSelectedIndex = GetComponentEditorIndex(componentEditor);
|
|
}
|
|
|
|
UpdateInternalState();
|
|
}
|
|
|
|
AZ::s32 PropertiesContainer::GetComponentEditorIndex(const AzToolsFramework::ComponentEditor* componentEditor) const
|
|
{
|
|
AZ::s32 index = 0;
|
|
for (auto componentEditorToCompare : m_componentEditors)
|
|
{
|
|
if (componentEditorToCompare == componentEditor)
|
|
{
|
|
return index;
|
|
}
|
|
++index;
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
AZStd::vector<AzToolsFramework::ComponentEditor*> PropertiesContainer::GetIntersectingComponentEditors(const QRect& globalRect) const
|
|
{
|
|
ComponentEditorVector intersectingComponentEditors;
|
|
intersectingComponentEditors.reserve(m_componentEditors.size());
|
|
for (auto componentEditor : m_componentEditors)
|
|
{
|
|
if (DoesIntersectWidget(globalRect, componentEditor))
|
|
{
|
|
intersectingComponentEditors.push_back(componentEditor);
|
|
}
|
|
}
|
|
return intersectingComponentEditors;
|
|
}
|
|
|
|
void PropertiesContainer::CreateActions()
|
|
{
|
|
QAction* seperator1 = new QAction(this);
|
|
seperator1->setSeparator(true);
|
|
addAction(seperator1);
|
|
|
|
m_actionToAddComponents = new QAction(tr("Add component"), this);
|
|
m_actionToAddComponents->setShortcutContext(Qt::WidgetWithChildrenShortcut);
|
|
connect(m_actionToAddComponents, &QAction::triggered, this, &PropertiesContainer::OnAddComponent);
|
|
addAction(m_actionToAddComponents);
|
|
|
|
m_actionToDeleteComponents = ComponentHelpers::CreateRemoveComponentsAction(this);
|
|
addAction(m_actionToDeleteComponents);
|
|
|
|
QAction* seperator2 = new QAction(this);
|
|
seperator2->setSeparator(true);
|
|
addAction(seperator2);
|
|
|
|
m_actionToCutComponents = ComponentHelpers::CreateCutComponentsAction(this);
|
|
addAction(m_actionToCutComponents);
|
|
|
|
m_actionToCopyComponents = ComponentHelpers::CreateCopyComponentsAction(this);
|
|
addAction(m_actionToCopyComponents);
|
|
|
|
m_actionToPasteComponents = ComponentHelpers::CreatePasteComponentsAction(this);
|
|
addAction(m_actionToPasteComponents);
|
|
|
|
UpdateInternalState();
|
|
}
|
|
|
|
void PropertiesContainer::UpdateActions()
|
|
{
|
|
ComponentHelpers::UpdateRemoveComponentsAction(m_actionToDeleteComponents);
|
|
ComponentHelpers::UpdateCutComponentsAction(m_actionToCutComponents);
|
|
ComponentHelpers::UpdateCopyComponentsAction(m_actionToCopyComponents);
|
|
// The paste components action always remains enabled except for when the context menu is up
|
|
// This is so a paste can be performed after a copy operation was made via the shortcut keys (since we don't know when a copy was performed)
|
|
}
|
|
|
|
void PropertiesContainer::UpdateOverlay()
|
|
{
|
|
m_overlay->setVisible(true);
|
|
m_overlay->setGeometry(m_componentListContents->rect());
|
|
m_overlay->raise();
|
|
m_overlay->update();
|
|
}
|
|
|
|
void PropertiesContainer::UpdateInternalState()
|
|
{
|
|
UpdateActions();
|
|
UpdateOverlay();
|
|
}
|
|
|
|
void PropertiesContainer::OnAddComponent()
|
|
{
|
|
HierarchyMenu contextMenu(m_editorWindow->GetHierarchy(),
|
|
HierarchyMenu::Show::kAddComponents,
|
|
true);
|
|
|
|
contextMenu.exec(QCursor::pos());
|
|
}
|
|
|
|
void PropertiesContainer::OnDisplayUiComponentEditorMenu(const QPoint& position)
|
|
{
|
|
ShowContextMenu(position);
|
|
}
|
|
|
|
void PropertiesContainer::ShowContextMenu(const QPoint& position)
|
|
{
|
|
UpdateInternalState();
|
|
// The paste components action is only updated right before the context menu appears, otherwise it remains enabled
|
|
ComponentHelpers::UpdatePasteComponentsAction(m_actionToPasteComponents);
|
|
|
|
HierarchyMenu contextMenu(m_editorWindow->GetHierarchy(),
|
|
HierarchyMenu::Show::kPushToSlice,
|
|
false);
|
|
|
|
contextMenu.addActions(actions());
|
|
|
|
if (!contextMenu.isEmpty())
|
|
{
|
|
contextMenu.exec(position);
|
|
}
|
|
|
|
// Keep the paste components action enabled when the context menu is not showing in order to handle pastes after a copy was performed
|
|
m_actionToPasteComponents->setEnabled(true);
|
|
}
|
|
|
|
void PropertiesContainer::Update()
|
|
{
|
|
size_t selectedEntitiesAmount = m_selectedEntities.size();
|
|
QString displayName;
|
|
|
|
if (selectedEntitiesAmount == 0)
|
|
{
|
|
displayName = "No Canvas Loaded";
|
|
}
|
|
else if (selectedEntitiesAmount == 1)
|
|
{
|
|
// Either only one element selected, or none (still is 1 because it selects the canvas instead)
|
|
|
|
// If the canvas was selected
|
|
if (m_isCanvasSelected)
|
|
{
|
|
displayName = "Canvas";
|
|
}
|
|
// Else one element was selected
|
|
else
|
|
{
|
|
// Set the name in the properties pane to the name of the element
|
|
AZ::EntityId selectedElement = m_selectedEntities.front();
|
|
AZStd::string selectedEntityName;
|
|
EBUS_EVENT_ID_RESULT(selectedEntityName, selectedElement, UiElementBus, GetName);
|
|
displayName = selectedEntityName.c_str();
|
|
}
|
|
}
|
|
else // more than one entity selected
|
|
{
|
|
displayName = QString::number(selectedEntitiesAmount) + " elements selected";
|
|
}
|
|
|
|
// Update the selected element display name
|
|
if (m_selectedEntityDisplayNameWidget != nullptr)
|
|
{
|
|
m_selectedEntityDisplayNameWidget->setText(displayName);
|
|
|
|
// Preventing renaming entities if the canvas entity is selected or
|
|
// multiple entities are selected.
|
|
if (m_isCanvasSelected || selectedEntitiesAmount > 1)
|
|
{
|
|
m_selectedEntityDisplayNameWidget->setEnabled(false);
|
|
}
|
|
else
|
|
{
|
|
m_selectedEntityDisplayNameWidget->setEnabled(true);
|
|
}
|
|
}
|
|
|
|
// Clear content.
|
|
{
|
|
for (int j = m_rowLayout->count(); j > 0; --j)
|
|
{
|
|
AzToolsFramework::ComponentEditor* editor = static_cast<AzToolsFramework::ComponentEditor*>(m_rowLayout->itemAt(j - 1)->widget());
|
|
|
|
editor->hide();
|
|
editor->ClearInstances(true);
|
|
}
|
|
|
|
m_compareToEntity.reset();
|
|
}
|
|
|
|
UpdateEditorOnlyCheckbox();
|
|
|
|
if (m_selectedEntities.empty())
|
|
{
|
|
return; // nothing to do
|
|
}
|
|
|
|
ComponentTypeMap sharedComponentList;
|
|
BuildSharedComponentList(sharedComponentList, m_selectedEntities);
|
|
BuildSharedComponentUI(sharedComponentList, m_selectedEntities);
|
|
|
|
UpdateInternalState();
|
|
}
|
|
|
|
void PropertiesContainer::UpdateEditorOnlyCheckbox()
|
|
{
|
|
if (m_isCanvasSelected)
|
|
{
|
|
// The canvas itself can't be editor-only, so don't show the checkbox when the
|
|
// canvas is displayed in the properties pane.
|
|
m_editorOnlyCheckbox->setVisible(false);
|
|
}
|
|
else
|
|
{
|
|
QSignalBlocker noSignals(m_editorOnlyCheckbox);
|
|
|
|
if (m_selectedEntities.empty())
|
|
{
|
|
m_editorOnlyCheckbox->setVisible(false);
|
|
return;
|
|
}
|
|
|
|
m_editorOnlyCheckbox->setVisible(true);
|
|
|
|
bool allEditorOnly = true;
|
|
bool noneEditorOnly = true;
|
|
|
|
for (AZ::EntityId id : m_selectedEntities)
|
|
{
|
|
// If any of the entities is a slice root, grey out the checkbox.
|
|
bool isSliceRoot = false;
|
|
AzToolsFramework::EditorEntityInfoRequestBus::EventResult(isSliceRoot, id, &AzToolsFramework::EditorEntityInfoRequestBus::Events::IsSliceRoot);
|
|
if (isSliceRoot)
|
|
{
|
|
m_editorOnlyCheckbox->setChecked(false);
|
|
m_editorOnlyCheckbox->setEnabled(false);
|
|
return;
|
|
}
|
|
|
|
bool isEditorOnly = false;
|
|
AzToolsFramework::EditorOnlyEntityComponentRequestBus::EventResult(isEditorOnly, id, &AzToolsFramework::EditorOnlyEntityComponentRequests::IsEditorOnlyEntity);
|
|
|
|
allEditorOnly &= isEditorOnly;
|
|
noneEditorOnly &= !isEditorOnly;
|
|
}
|
|
|
|
m_editorOnlyCheckbox->setEnabled(true);
|
|
|
|
if (allEditorOnly)
|
|
{
|
|
m_editorOnlyCheckbox->setCheckState(Qt::CheckState::Checked);
|
|
}
|
|
else if (noneEditorOnly)
|
|
{
|
|
m_editorOnlyCheckbox->setCheckState(Qt::CheckState::Unchecked);
|
|
}
|
|
else // Some marked editor-only, some not
|
|
{
|
|
m_editorOnlyCheckbox->setCheckState(Qt::CheckState::PartiallyChecked);
|
|
}
|
|
}
|
|
}
|
|
|
|
void PropertiesContainer::Refresh(AzToolsFramework::PropertyModificationRefreshLevel refreshLevel, const AZ::Uuid* componentType)
|
|
{
|
|
if (m_selectionHasChanged)
|
|
{
|
|
Update();
|
|
m_selectionHasChanged = false;
|
|
}
|
|
else
|
|
{
|
|
for (auto& componentEditorsPair : m_componentEditorsByType)
|
|
{
|
|
if (!componentType || (*componentType == componentEditorsPair.first))
|
|
{
|
|
for (AzToolsFramework::ComponentEditor* editor : componentEditorsPair.second)
|
|
{
|
|
if (editor->isVisible())
|
|
{
|
|
editor->QueuePropertyEditorInvalidation(refreshLevel);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// If the selection has not changed, but a refresh was prompted then the name of the currently selected entity might
|
|
// have changed.
|
|
size_t selectedEntitiesAmount = m_selectedEntities.size();
|
|
// Check if only one entity is selected and that it is an element
|
|
if (selectedEntitiesAmount == 1 && !m_isCanvasSelected)
|
|
{
|
|
// Set the name in the properties pane to the name of the element
|
|
AZ::EntityId selectedElement = m_selectedEntities.front();
|
|
AZStd::string selectedEntityName;
|
|
EBUS_EVENT_ID_RESULT(selectedEntityName, selectedElement, UiElementBus, GetName);
|
|
|
|
// Update the selected element display name
|
|
if (m_selectedEntityDisplayNameWidget != nullptr)
|
|
{
|
|
m_selectedEntityDisplayNameWidget->setText(selectedEntityName.c_str());
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void PropertiesContainer::SelectionChanged(HierarchyItemRawPtrList* items)
|
|
{
|
|
ClearComponentEditorSelection();
|
|
|
|
m_selectedEntities.clear();
|
|
if (items)
|
|
{
|
|
for (auto i : *items)
|
|
{
|
|
m_selectedEntities.push_back(i->GetEntityId());
|
|
}
|
|
}
|
|
|
|
m_isCanvasSelected = false;
|
|
|
|
if (m_selectedEntities.empty())
|
|
{
|
|
// Add the canvas
|
|
AZ::EntityId canvasId = m_editorWindow->GetCanvas();
|
|
if (canvasId.IsValid())
|
|
{
|
|
m_selectedEntities.push_back(canvasId);
|
|
m_isCanvasSelected = true;
|
|
}
|
|
}
|
|
|
|
m_selectionHasChanged = true;
|
|
}
|
|
|
|
void PropertiesContainer::SelectedEntityPointersChanged()
|
|
{
|
|
m_selectionHasChanged = true;
|
|
Refresh();
|
|
}
|
|
|
|
void PropertiesContainer::RequestPropertyContextMenu([[maybe_unused]] AzToolsFramework::InstanceDataNode* node, const QPoint& globalPos)
|
|
{
|
|
ShowContextMenu(globalPos);
|
|
}
|
|
|
|
void PropertiesContainer::SetSelectedEntityDisplayNameWidget(QLineEdit* selectedEntityDisplayNameWidget)
|
|
{
|
|
if (selectedEntityDisplayNameWidget)
|
|
{
|
|
if (m_selectedEntityDisplayNameWidget)
|
|
{
|
|
QObject::disconnect(m_selectedEntityDisplayNameWidget);
|
|
}
|
|
|
|
m_selectedEntityDisplayNameWidget = selectedEntityDisplayNameWidget;
|
|
|
|
// Listen for changes to the line edit field
|
|
QObject::connect(m_selectedEntityDisplayNameWidget,
|
|
&QLineEdit::editingFinished,
|
|
[this]()
|
|
{
|
|
// Ignore editing when this field is read-only or if there is more than one
|
|
// entity selected.
|
|
if (!m_selectedEntityDisplayNameWidget->isEnabled() || m_selectedEntities.size() != 1)
|
|
{
|
|
return;
|
|
}
|
|
|
|
AZ::EntityId selectedEntityId = m_selectedEntities.front();
|
|
AZ::Entity* selectedEntity = nullptr;
|
|
EBUS_EVENT_RESULT(selectedEntity, AZ::ComponentApplicationBus, FindEntity, selectedEntityId);
|
|
if (selectedEntity)
|
|
{
|
|
AZStd::string currentName = selectedEntity->GetName();
|
|
AZStd::string newName = m_selectedEntityDisplayNameWidget->text().toUtf8().constData();
|
|
|
|
CommandHierarchyItemRename::Push(m_editorWindow->GetActiveStack(),
|
|
m_editorWindow->GetHierarchy(),
|
|
selectedEntityId,
|
|
currentName.c_str(),
|
|
newName.c_str());
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
void PropertiesContainer::SetEditorOnlyCheckbox(QCheckBox* editorOnlyCheckbox)
|
|
{
|
|
m_editorOnlyCheckbox = editorOnlyCheckbox;
|
|
|
|
QObject::connect(m_editorOnlyCheckbox,
|
|
&QCheckBox::stateChanged,
|
|
[this](int value)
|
|
{
|
|
QSignalBlocker blocker(this);
|
|
|
|
QMetaObject::invokeMethod(m_editorWindow->GetHierarchy(), "SetEditorOnlyForSelectedItems", Qt::QueuedConnection, Q_ARG(bool, value));
|
|
}
|
|
);
|
|
}
|
|
|
|
#include <moc_PropertiesContainer.cpp>
|