Merge branch 'development' of https://github.com/o3de/o3de into daimini/FocusMode/breadcrumbs

Signed-off-by: Danilo Aimini <82231674+AMZN-daimini@users.noreply.github.com>

# Conflicts:
#	Code/Framework/AzToolsFramework/AzToolsFramework/UI/Outliner/EntityOutlinerTreeView.cpp
This commit is contained in:
Danilo Aimini
2021-09-27 21:23:38 -07:00
110 changed files with 3299 additions and 1637 deletions
@@ -138,7 +138,13 @@ namespace AzToolsFramework
m_indexMap[row] = index;
m_rowMap[index] = row;
++row;
++m_displayedItemsCounter;
// We only want to increase the displayed counter if it is a parent (Source)
// so we don't cut children entries.
if (entry->GetEntryType() == AssetBrowserEntry::AssetEntryType::Source)
{
++m_displayedItemsCounter;
}
}
if (model->hasChildren(index))
@@ -29,20 +29,20 @@ namespace AzToolsFramework
{
AssetBrowserTableView::AssetBrowserTableView(QWidget* parent)
: AzQtComponents::TableView(parent)
, m_delegate(new EntryDelegate(this))
, m_delegate(new SearchEntryDelegate(this))
{
setSortingEnabled(true);
setSortingEnabled(false);
setItemDelegate(m_delegate);
setRootIsDecorated(false);
//Styling the header aligning text to the left and using a bold font.
header()->setDefaultAlignment(Qt::AlignLeft);
header()->setStyleSheet("QHeaderView { font-weight: bold; }");
header()->setStyleSheet("QHeaderView { font-weight: bold; };");
setContextMenuPolicy(Qt::CustomContextMenu);
setMouseTracking(true);
setSortingEnabled(false);
setSelectionMode(QAbstractItemView::SingleSelection);
connect(this, &AzQtComponents::TableView::customContextMenuRequested, this, &AssetBrowserTableView::OnContextMenu);
@@ -67,6 +67,8 @@ namespace AzToolsFramework
header()->setSectionResizeMode(0, QHeaderView::ResizeMode::Stretch);
header()->setSectionResizeMode(1, QHeaderView::ResizeMode::Stretch);
header()->setSortIndicatorShown(false);
header()->setSectionsClickable(false);
}
void AssetBrowserTableView::SetName(const QString& name)
@@ -26,7 +26,7 @@ namespace AzToolsFramework
class AssetBrowserEntry;
class AssetBrowserTableModel;
class AssetBrowserFilterModel;
class EntryDelegate;
class SearchEntryDelegate;
class AssetBrowserTableView //! Table view that displays the asset browser entries in a list.
: public AzQtComponents::TableView
@@ -67,9 +67,9 @@ namespace AzToolsFramework
private:
QString m_name;
QPointer<AssetBrowserTableModel> m_tableModel = nullptr;
QPointer<AssetBrowserFilterModel> m_sourceFilterModel = nullptr;
EntryDelegate* m_delegate = nullptr;
QPointer<AssetBrowserTableModel> m_tableModel;
QPointer<AssetBrowserFilterModel> m_sourceFilterModel;
SearchEntryDelegate* m_delegate = nullptr;
private Q_SLOTS:
void OnContextMenu(const QPoint& point);
@@ -11,12 +11,13 @@
#include <AzToolsFramework/AssetBrowser/AssetBrowserModel.h>
#include <AzToolsFramework/Thumbnails/ThumbnailerBus.h>
#include <AzToolsFramework/AssetBrowser/Views/EntryDelegate.h>
#include <AzCore/Utils/Utils.h>
#include <AzQtComponents/Components/StyledBusyLabel.h>
#include <QApplication>
AZ_PUSH_DISABLE_WARNING(4251 4800, "-Wunknown-warning-option") // 4251: class 'QScopedPointer<QBrushData,QBrushDataPointerDeleter>' needs to have dll-interface to be used by clients of class 'QBrush'
// 4800: 'uint': forcing value to bool 'true' or 'false' (performance warning)
#include <QAbstractItemView>
#include <QPainter>
AZ_POP_DISABLE_WARNING
@@ -24,8 +25,13 @@ namespace AzToolsFramework
{
namespace AssetBrowser
{
const int ENTRY_SPACING_LEFT_PIXELS = 8;
const int ENTRY_ICON_MARGIN_LEFT_PIXELS = 2;
static constexpr const char* TreeIconPathFirst = "Assets/Editor/Icons/AssetBrowser/TreeBranch_First.svg";
static constexpr const char* TreeIconPathMiddle = "Assets/Editor/Icons/AssetBrowser/TreeBranch_Middle.svg";
static constexpr const char* TreeIconPathLast = "Assets/Editor/Icons/AssetBrowser/TreeBranch_Last.svg";
static constexpr const char* TreeIconPathOneChild = "Assets/Editor/Icons/AssetBrowser/TreeBranch_OneChild.svg";
const int EntrySpacingLeftPixels = 8;
const int EntryIconMarginLeftPixels = 2;
EntryDelegate::EntryDelegate(QWidget* parent)
: QStyledItemDelegate(parent)
@@ -62,7 +68,7 @@ namespace AzToolsFramework
// Draw main entry thumbnail.
QRect remainingRect(option.rect);
remainingRect.adjust(ENTRY_ICON_MARGIN_LEFT_PIXELS, 0, 0, 0); // bump it rightwards to give some margin to the icon.
remainingRect.adjust(EntryIconMarginLeftPixels, 0, 0, 0); // bump it rightwards to give some margin to the icon.
QSize iconSize(m_iconSize, m_iconSize);
// Note that the thumbnail might actually be smaller than the row if theres a lot of padding or font size
@@ -89,7 +95,7 @@ namespace AzToolsFramework
}
remainingRect.adjust(thumbX, 0, 0, 0); // bump it to the right by the size of the thumbnail
remainingRect.adjust(ENTRY_SPACING_LEFT_PIXELS, 0, 0, 0); // bump it to the right by the spacing.
remainingRect.adjust(EntrySpacingLeftPixels, 0, 0, 0); // bump it to the right by the spacing.
}
QString displayString = index.column() == aznumeric_cast<int>(AssetBrowserEntry::Column::Name)
? qvariant_cast<QString>(entry->data(aznumeric_cast<int>(AssetBrowserEntry::Column::Name)))
@@ -148,7 +154,162 @@ namespace AzToolsFramework
return m_iconSize;
}
} // namespace Thumbnailer
SearchEntryDelegate::SearchEntryDelegate(QWidget* parent)
: EntryDelegate(parent)
{
LoadBranchPixMaps();
}
void SearchEntryDelegate::paint(QPainter* painter, const QStyleOptionViewItem& option, const QModelIndex& index) const
{
auto data = index.data(AssetBrowserModel::Roles::EntryRole);
if (data.canConvert<const AssetBrowserEntry*>())
{
bool isEnabled = (option.state & QStyle::State_Enabled) != 0;
bool isSelected = (option.state & QStyle::State_Selected) != 0;
QStyle* style = option.widget ? option.widget->style() : QApplication::style();
// draw the background
style->drawPrimitive(QStyle::PE_PanelItemViewItem, &option, painter, option.widget);
// Draw main entry thumbnail.
QRect remainingRect(option.rect);
QSize iconSize(m_iconSize, m_iconSize);
// Note that the thumbnail might actually be smaller than the row if theres a lot of padding or font size
// so it needs to center vertically with padding in that case:
QPoint iconTopLeft;
QPoint branchIconTopLeft = QPoint();
auto entry = qvariant_cast<const AssetBrowserEntry*>(data);
auto sourceEntry = azrtti_cast<const SourceAssetBrowserEntry*>(entry);
//If it is a SourceEntry or it is not the column name we don't want to add space for the branch Icon
if (sourceEntry || index.column() != aznumeric_cast<int>(AssetBrowserEntry::Column::Name))
{
remainingRect.adjust(EntryIconMarginLeftPixels, 0, 0, 0); // bump it rightwards to give some margin to the icon.
iconTopLeft = QPoint(remainingRect.x(), remainingRect.y() + (remainingRect.height() / 2) - (m_iconSize / 2));
}
else
{
remainingRect.adjust(EntryIconMarginLeftPixels + m_iconSize, 0, 0, 0); // bump it rightwards to give some margin to the icon.
iconTopLeft = QPoint(remainingRect.x() / 2 + m_iconSize, remainingRect.y() + (remainingRect.height() / 2) - (m_iconSize / 2));
branchIconTopLeft = QPoint((remainingRect.x() / 2) - 2, remainingRect.y() + (remainingRect.height() / 2) - (m_iconSize / 2));
}
QPalette actualPalette(option.palette);
if (index.column() == aznumeric_cast<int>(AssetBrowserEntry::Column::Name))
{
int thumbX = DrawThumbnail(painter, iconTopLeft, iconSize, entry->GetThumbnailKey());
if (sourceEntry)
{
if (m_showSourceControl)
{
DrawThumbnail(painter, iconTopLeft, iconSize, sourceEntry->GetSourceControlThumbnailKey());
}
// sources with no children should be greyed out.
if (sourceEntry->GetChildCount() == 0)
{
isEnabled = false; // draw in disabled style.
actualPalette.setCurrentColorGroup(QPalette::Disabled);
}
}
else
{
//Get the indexes above and below our entry to see what type are they.
QAbstractItemView* view = qobject_cast<QAbstractItemView*>(option.styleObject);
const QAbstractItemModel* viewModel = view->model();
const QModelIndex indexBelow = viewModel->index(index.row() + 1, index.column());
const QModelIndex indexAbove = viewModel->index(index.row() - 1, index.column());
auto aboveEntry = qvariant_cast<const AssetBrowserEntry*>(indexBelow.data(AssetBrowserModel::Roles::EntryRole));
auto belowEntry = qvariant_cast<const AssetBrowserEntry*>(indexAbove.data(AssetBrowserModel::Roles::EntryRole));
auto aboveSourceEntry = azrtti_cast<const SourceAssetBrowserEntry*>(aboveEntry);
auto belowSourceEntry = azrtti_cast<const SourceAssetBrowserEntry*>(belowEntry);
// if current index is the last entry in the view
// or the index above it is a Source Entry and
// the index below is invalid or is valid but it is also a source entry
// then the current index is the only child.
if (index.row() == viewModel->rowCount() - 1 ||
(indexBelow.isValid() && aboveSourceEntry &&
(!indexAbove.isValid() || (indexAbove.isValid() && belowSourceEntry))))
{
DrawBranchPixMap(EntryBranchType::OneChild, painter, branchIconTopLeft, iconSize); // Draw One Child Icon
}
else if (indexBelow.isValid() && aboveSourceEntry) // The index above is a source entry
{
DrawBranchPixMap(EntryBranchType::Last, painter, branchIconTopLeft, iconSize); // Draw First child Icon
}
else if (indexAbove.isValid() && belowSourceEntry) // The index below is a source entry
{
DrawBranchPixMap(EntryBranchType::First, painter, branchIconTopLeft, iconSize); // Draw Last Child Icon
}
else //the index above and below are also child entries
{
DrawBranchPixMap(EntryBranchType::Middle, painter, branchIconTopLeft, iconSize); // Draw Default child Icon.
}
}
remainingRect.adjust(thumbX, 0, 0, 0); // bump it to the right by the size of the thumbnail
remainingRect.adjust(EntrySpacingLeftPixels, 0, 0, 0); // bump it to the right by the spacing.
}
QString displayString = index.column() == aznumeric_cast<int>(AssetBrowserEntry::Column::Name)
? qvariant_cast<QString>(entry->data(aznumeric_cast<int>(AssetBrowserEntry::Column::Name)))
: qvariant_cast<QString>(entry->data(aznumeric_cast<int>(AssetBrowserEntry::Column::Path)));
style->drawItemText(
painter, remainingRect, option.displayAlignment, actualPalette, isEnabled, displayString,
isSelected ? QPalette::HighlightedText : QPalette::Text);
}
}
void SearchEntryDelegate::LoadBranchPixMaps()
{
AZ::IO::BasicPath<AZ::IO::FixedMaxPathString> absoluteIconPath;
for (int branchType = EntryBranchType::First; branchType != EntryBranchType::Count; ++branchType)
{
QPixmap pixmap;
switch (branchType)
{
case AzToolsFramework::AssetBrowser::EntryBranchType::First:
absoluteIconPath = AZ::IO::FixedMaxPath(AZ::Utils::GetEnginePath()) / TreeIconPathFirst;
break;
case AzToolsFramework::AssetBrowser::EntryBranchType::Middle:
absoluteIconPath = AZ::IO::FixedMaxPath(AZ::Utils::GetEnginePath()) / TreeIconPathMiddle;
break;
case AzToolsFramework::AssetBrowser::EntryBranchType::Last:
absoluteIconPath = AZ::IO::FixedMaxPath(AZ::Utils::GetEnginePath()) / TreeIconPathLast;
break;
case AzToolsFramework::AssetBrowser::EntryBranchType::OneChild:
default:
absoluteIconPath = AZ::IO::FixedMaxPath(AZ::Utils::GetEnginePath()) / TreeIconPathOneChild;
break;
}
bool pixmapLoadedSuccess = pixmap.load(absoluteIconPath.c_str());
AZ_Assert(pixmapLoadedSuccess, "Error loading Branch Icons in SearchEntryDelegate");
m_branchIcons[static_cast<EntryBranchType>(branchType)] = pixmap;
}
}
void SearchEntryDelegate::DrawBranchPixMap(
EntryBranchType branchType, QPainter* painter, const QPoint& point, const QSize& size) const
{
const QPixmap& pixmap = m_branchIcons[branchType];
pixmap.scaled(size, Qt::KeepAspectRatio, Qt::SmoothTransformation);
const QSize sizeDelta = size - pixmap.size();
const QPoint pointDelta = QPoint(sizeDelta.width() / 2, sizeDelta.height() / 2);
painter->drawPixmap(point + pointDelta, pixmap);
}
} // namespace AssetBrowser
} // namespace AzToolsFramework
#include "AssetBrowser/Views/moc_EntryDelegate.cpp"
@@ -27,9 +27,19 @@ namespace AzToolsFramework
{
namespace AssetBrowser
{
//! Type of branch icon the delegate should paint.
enum EntryBranchType
{
First,
Middle,
Last,
OneChild,
Count
};
class AssetBrowserFilterModel;
//! EntryDelegate draws a single item in AssetBrowser
//! EntryDelegate draws a single item in AssetBrowser.
class EntryDelegate
: public QStyledItemDelegate
{
@@ -52,5 +62,23 @@ namespace AzToolsFramework
//! Draw a thumbnail and return its width
int DrawThumbnail(QPainter* painter, const QPoint& point, const QSize& size, Thumbnailer::SharedThumbnailKey thumbnailKey) const;
};
//! SearchEntryDelegate draws a single item in AssetBrowserTableView.
class SearchEntryDelegate
: public EntryDelegate
{
Q_OBJECT
public:
explicit SearchEntryDelegate(QWidget* parent = nullptr);
void paint(QPainter* painter, const QStyleOptionViewItem& option, const QModelIndex& index) const override;
private:
void LoadBranchPixMaps();
void DrawBranchPixMap(EntryBranchType branchType, QPainter* painter, const QPoint& point, const QSize& size) const;
private:
QMap<EntryBranchType, QPixmap> m_branchIcons;
};
} // namespace AssetBrowser
} // namespace AzToolsFramework
@@ -33,7 +33,7 @@ namespace AzToolsFramework
virtual AZ::EntityId GetFocusRoot() = 0;
//! Returns whether the entity id provided is part of the focused sub-tree.
virtual bool IsInFocusSubTree(AZ::EntityId entityId) = 0;
virtual bool IsInFocusSubTree(AZ::EntityId entityId) const = 0;
};
} // namespace AzToolsFramework
@@ -86,7 +86,7 @@ namespace AzToolsFramework
return m_focusRoot;
}
bool FocusModeSystemComponent::IsInFocusSubTree(AZ::EntityId entityId)
bool FocusModeSystemComponent::IsInFocusSubTree(AZ::EntityId entityId) const
{
if (m_focusRoot == AZ::EntityId())
{
@@ -42,7 +42,7 @@ namespace AzToolsFramework
void SetFocusRoot(AZ::EntityId entityId) override;
void ClearFocusRoot() override;
AZ::EntityId GetFocusRoot() override;
bool IsInFocusSubTree(AZ::EntityId entityId) override;
bool IsInFocusSubTree(AZ::EntityId entityId) const override;
private:
AZ::EntityId m_focusRoot;
@@ -39,7 +39,7 @@ namespace AzToolsFramework::Prefab
{
InstanceOptionalReference focusedInstance;
if (entityId == AZ::EntityId())
if (!entityId.IsValid())
{
PrefabEditorEntityOwnershipInterface* prefabEditorEntityOwnershipInterface =
AZ::Interface<PrefabEditorEntityOwnershipInterface>::Get();
@@ -121,7 +121,7 @@ namespace AzToolsFramework::Prefab
return false;
}
if (entityId == AZ::EntityId())
if (!entityId.IsValid())
{
return false;
}
@@ -18,18 +18,19 @@ AzToolsFramework--EntityOutlinerWidget QTreeView
selection-background-color: transparent;
}
/*
* Entity Outliner handles hover and selected state of items via code,
* so we need to override the AzQtComponents::Treeview style.
*/
AzToolsFramework--EntityOutlinerWidget QTreeView::branch:hover
, AzToolsFramework--EntityOutlinerWidget QTreeView::item:hover
{
background: rgba(255, 255, 255, 30);
}
AzToolsFramework--EntityOutlinerWidget QTreeView::branch:selected
, AzToolsFramework--EntityOutlinerWidget QTreeView::branch:selected
, AzToolsFramework--EntityOutlinerWidget QTreeView::item:selected
, AzToolsFramework--EntityOutlinerWidget QTreeView::branch:selected:active
, AzToolsFramework--EntityOutlinerWidget QTreeView::item:selected:active
{
background: rgba(255, 255, 255, 45);
background: transparent;
}
@@ -44,6 +44,7 @@
#include <AzToolsFramework/Entity/EditorEntityContextBus.h>
#include <AzToolsFramework/Entity/EditorEntityHelpers.h>
#include <AzToolsFramework/Entity/EditorEntityInfoBus.h>
#include <AzToolsFramework/FocusMode/FocusModeInterface.h>
#include <AzToolsFramework/ToolsComponents/ComponentAssetMimeDataContainer.h>
#include <AzToolsFramework/ToolsComponents/ComponentMimeData.h>
#include <AzToolsFramework/ToolsComponents/EditorEntityIdContainer.h>
@@ -102,10 +103,14 @@ namespace AzToolsFramework
EntityCompositionNotificationBus::Handler::BusConnect();
AZ::EntitySystemBus::Handler::BusConnect();
m_editorEntityFrameworkInterface = AZ::Interface<AzToolsFramework::EditorEntityUiInterface>::Get();
m_editorEntityUiInterface = AZ::Interface<AzToolsFramework::EditorEntityUiInterface>::Get();
AZ_Assert(m_editorEntityUiInterface != nullptr,
"EntityOutlinerListModel requires a EditorEntityUiInterface instance on Initialize.");
AZ_Assert(m_editorEntityFrameworkInterface != nullptr,
"EntityOutlinerListModel requires a EditorEntityFrameworkInterface instance on Initialize.");
m_focusModeInterface = AZ::Interface<FocusModeInterface>::Get();
AZ_Assert(
m_focusModeInterface != nullptr,
"EntityOutlinerListModel requires a FocusModeInterface instance on Initialize.");
}
int EntityOutlinerListModel::rowCount(const QModelIndex& parent) const
@@ -279,7 +284,7 @@ namespace AzToolsFramework
QVariant EntityOutlinerListModel::GetEntityIcon(const AZ::EntityId& id) const
{
auto entityUiHandler = m_editorEntityFrameworkInterface->GetHandler(id);
auto entityUiHandler = m_editorEntityUiInterface->GetHandler(id);
QIcon icon;
// Retrieve the icon from the handler
@@ -316,7 +321,7 @@ namespace AzToolsFramework
QVariant EntityOutlinerListModel::GetEntityTooltip(const AZ::EntityId& id) const
{
auto entityUiHandler = m_editorEntityFrameworkInterface->GetHandler(id);
auto entityUiHandler = m_editorEntityUiInterface->GetHandler(id);
QString tooltip;
// Retrieve the tooltip from the handler
@@ -349,7 +354,7 @@ namespace AzToolsFramework
QVariant EntityOutlinerListModel::dataForVisibility(const QModelIndex& index, int role) const
{
auto entityId = GetEntityFromIndex(index);
auto entityUiHandler = m_editorEntityFrameworkInterface->GetHandler(entityId);
auto entityUiHandler = m_editorEntityUiInterface->GetHandler(entityId);
if (!entityUiHandler || entityUiHandler->CanToggleLockVisibility(entityId))
{
@@ -377,7 +382,7 @@ namespace AzToolsFramework
QVariant EntityOutlinerListModel::dataForLock(const QModelIndex& index, int role) const
{
auto entityId = GetEntityFromIndex(index);
auto entityUiHandler = m_editorEntityFrameworkInterface->GetHandler(entityId);
auto entityUiHandler = m_editorEntityUiInterface->GetHandler(entityId);
if (!entityUiHandler || entityUiHandler->CanToggleLockVisibility(entityId))
{
@@ -436,7 +441,7 @@ namespace AzToolsFramework
if (value.canConvert<Qt::CheckState>())
{
const auto entityId = GetEntityFromIndex(index);
auto entityUiHandler = m_editorEntityFrameworkInterface->GetHandler(entityId);
auto entityUiHandler = m_editorEntityUiInterface->GetHandler(entityId);
if (!entityUiHandler || entityUiHandler->CanToggleLockVisibility(entityId))
{
@@ -532,6 +537,11 @@ namespace AzToolsFramework
break;
}
if (AZ::EntityId entityId = GetEntityFromIndex(index); !m_focusModeInterface->IsInFocusSubTree(entityId))
{
itemFlags &= !Qt::ItemIsEnabled;
}
return itemFlags;
}
@@ -36,6 +36,7 @@
namespace AzToolsFramework
{
class EditorEntityUiInterface;
class FocusModeInterface;
namespace EntityOutliner
{
@@ -273,7 +274,8 @@ namespace AzToolsFramework
QVariant GetEntityIcon(const AZ::EntityId& id) const;
QVariant GetEntityTooltip(const AZ::EntityId& id) const;
EditorEntityUiInterface* m_editorEntityFrameworkInterface = nullptr;
EditorEntityUiInterface* m_editorEntityUiInterface = nullptr;
FocusModeInterface* m_focusModeInterface = nullptr;
};
class EntityOutlinerCheckBox
@@ -40,6 +40,8 @@ namespace AzToolsFramework
"EntityOutlinerTreeView requires a EditorEntityFrameworkInterface instance on Construction.");
FocusModeNotificationBus::Handler::BusConnect();
viewport()->setMouseTracking(true);
}
EntityOutlinerTreeView::~EntityOutlinerTreeView()
@@ -63,6 +65,11 @@ namespace AzToolsFramework
}
}
void EntityOutlinerTreeView::leaveEvent([[maybe_unused]] QEvent* event)
{
m_mousePosition = QPoint();
}
void EntityOutlinerTreeView::mousePressEvent(QMouseEvent* event)
{
//postponing normal mouse pressed logic until mouse is released or dragged
@@ -116,6 +123,8 @@ namespace AzToolsFramework
setSelectionMode(selectionModeBefore);
}
m_mousePosition = event->pos();
//process mouse movement as normal, potentially triggering drag and drop
QTreeView::mouseMoveEvent(event);
}
@@ -176,12 +185,45 @@ namespace AzToolsFramework
void EntityOutlinerTreeView::drawBranches(QPainter* painter, const QRect& rect, const QModelIndex& index) const
{
const bool isEnabled = (this->model()->flags(index) & Qt::ItemIsEnabled);
const bool isSelected = selectionModel()->isSelected(index);
const bool isHovered = (index == indexAt(m_mousePosition)) && isEnabled;
// Paint the branch Selection/Hover Rect
PaintBranchSelectionHoverRect(painter, rect, isSelected, isHovered);
// Paint the branch background as defined by the entity's handler, or its closes ancestor's.
PaintBranchBackground(painter, rect, index);
QTreeView::drawBranches(painter, rect, index);
}
void EntityOutlinerTreeView::PaintBranchSelectionHoverRect(
QPainter* painter, const QRect& rect, bool isSelected, bool isHovered) const
{
painter->save();
painter->setRenderHint(QPainter::Antialiasing, false);
if (isSelected || isHovered)
{
QPainterPath backgroundPath;
QRect backgroundRect(rect);
backgroundPath.addRect(backgroundRect);
QColor backgroundColor = m_hoverColor;
if (isSelected)
{
backgroundColor = m_selectedColor;
}
painter->fillPath(backgroundPath, backgroundColor);
}
painter->restore();
}
void EntityOutlinerTreeView::PaintBranchBackground(QPainter* painter, const QRect& rect, const QModelIndex& index) const
{
// Go through ancestors and add them to the stack
@@ -61,6 +61,7 @@ namespace AzToolsFramework
void startDrag(Qt::DropActions supportedActions) override;
void dragMoveEvent(QDragMoveEvent* event) override;
void dropEvent(QDropEvent* event) override;
void leaveEvent(QEvent* event) override;
// FocusModeNotificationBus overrides ...
void OnEditorFocusChanged(AZ::EntityId entityId) override;
@@ -77,8 +78,10 @@ namespace AzToolsFramework
void StartCustomDrag(const QModelIndexList& indexList, Qt::DropActions supportedActions) override;
void PaintBranchBackground(QPainter* painter, const QRect& rect, const QModelIndex& index) const;
void PaintBranchSelectionHoverRect(QPainter* painter, const QRect& rect, bool isSelected, bool isHovered) const;
QMouseEvent* m_queuedMouseEvent;
QPoint m_mousePosition;
bool m_draggingUnselectedItem; // This is set when an item is dragged outside its bounding box.
int m_expandOnlyDelay = -1;
@@ -13,6 +13,7 @@
#include <AzFramework/Viewport/CameraState.h>
#include <AzFramework/Viewport/ViewportScreen.h>
#include <AzFramework/Visibility/BoundsBus.h>
#include <AzToolsFramework/FocusMode/FocusModeInterface.h>
#include <AzToolsFramework/API/EditorViewportIconDisplayInterface.h>
#include <AzToolsFramework/ToolsComponents/EditorEntityIconComponentBus.h>
#include <AzToolsFramework/Viewport/ViewportMessages.h>
@@ -112,6 +113,17 @@ namespace AzToolsFramework
}
}
EditorHelpers::EditorHelpers(const EditorVisibleEntityDataCache* entityDataCache)
: m_entityDataCache(entityDataCache)
{
m_focusModeInterface = AZ::Interface<FocusModeInterface>::Get();
AZ_Assert(
m_focusModeInterface,
"EditorHelpers - "
"Focus Mode Interface could not be found. "
"Check that it is being correctly initialized.");
}
AZ::EntityId EditorHelpers::HandleMouseInteraction(
const AzFramework::CameraState& cameraState, const ViewportInteraction::MouseInteractionEvent& mouseInteraction)
{
@@ -173,6 +185,12 @@ namespace AzToolsFramework
}
}
// Verify if the entity Id corresponds to an entity that is focused; if not, halt selection.
if (!m_focusModeInterface->IsInFocusSubTree(entityIdUnderCursor))
{
return AZ::EntityId();
}
return entityIdUnderCursor;
}
@@ -22,6 +22,7 @@ namespace AzFramework
namespace AzToolsFramework
{
class EditorVisibleEntityDataCache;
class FocusModeInterface;
namespace ViewportInteraction
{
@@ -38,10 +39,7 @@ namespace AzToolsFramework
//! An EditorVisibleEntityDataCache must be passed to EditorHelpers to allow it to
//! efficiently read entity data without resorting to EBus calls.
explicit EditorHelpers(const EditorVisibleEntityDataCache* entityDataCache)
: m_entityDataCache(entityDataCache)
{
}
explicit EditorHelpers(const EditorVisibleEntityDataCache* entityDataCache);
EditorHelpers(const EditorHelpers&) = delete;
EditorHelpers& operator=(const EditorHelpers&) = delete;
~EditorHelpers() = default;
@@ -62,5 +60,6 @@ namespace AzToolsFramework
private:
const EditorVisibleEntityDataCache* m_entityDataCache = nullptr; //!< Entity Data queried by the EditorHelpers.
const FocusModeInterface* m_focusModeInterface = nullptr;
};
} // namespace AzToolsFramework
@@ -0,0 +1,128 @@
/*
* 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 <AzTest/AzTest.h>
#include <AzCore/UserSettings/UserSettingsComponent.h>
#include <AzCore/Component/TransformBus.h>
#include <AzToolsFramework/FocusMode/FocusModeInterface.h>
#include <AzToolsFramework/UnitTest/AzToolsFrameworkTestHelpers.h>
namespace AzToolsFramework
{
class EditorFocusModeTests
: public ::testing::Test
{
protected:
void SetUp() override
{
m_app.Start(m_descriptor);
// Without this, the user settings component would attempt to save on finalize/shutdown. Since the file is
// shared across the whole engine, if multiple tests are run in parallel, the saving could cause a crash
// in the unit tests.
AZ::UserSettingsComponentRequestBus::Broadcast(&AZ::UserSettingsComponentRequests::DisableSaveOnFinalize);
GenerateTestHierarchy();
}
void GenerateTestHierarchy()
{
/*
* City
* |_ Street
* |_ Car
* | |_ Passenger
* |_ SportsCar
* |_ Passenger
*/
m_entityMap["cityId"] = CreateEditorEntity("City", AZ::EntityId());
m_entityMap["streetId"] = CreateEditorEntity("Street", m_entityMap["cityId"]);
m_entityMap["carId"] = CreateEditorEntity("Car", m_entityMap["streetId"]);
m_entityMap["passengerId1"] = CreateEditorEntity("Passenger", m_entityMap["carId"]);
m_entityMap["sportsCarId"] = CreateEditorEntity("SportsCar", m_entityMap["streetId"]);
m_entityMap["passengerId2"] = CreateEditorEntity("Passenger", m_entityMap["sportsCarId"]);
}
AZ::EntityId CreateEditorEntity(const char* name, AZ::EntityId parentId)
{
AZ::Entity* entity = nullptr;
UnitTest::CreateDefaultEditorEntity(name, &entity);
// Parent
AZ::TransformBus::Event(entity->GetId(), &AZ::TransformInterface::SetParent, parentId);
return entity->GetId();
}
void TearDown() override
{
m_app.Stop();
}
UnitTest::ToolsTestApplication m_app{ "EditorFocusModeTests" };
AZ::ComponentApplication::Descriptor m_descriptor;
AZStd::unordered_map<AZStd::string, AZ::EntityId> m_entityMap;
};
TEST_F(EditorFocusModeTests, EditorFocusModeTests_SetFocus)
{
FocusModeInterface* focusModeInterface = AZ::Interface<FocusModeInterface>::Get();
EXPECT_TRUE(focusModeInterface != nullptr);
focusModeInterface->SetFocusRoot(m_entityMap["carId"]);
EXPECT_EQ(focusModeInterface->GetFocusRoot(), m_entityMap["carId"]);
focusModeInterface->ClearFocusRoot();
EXPECT_EQ(focusModeInterface->GetFocusRoot(), AZ::EntityId());
}
TEST_F(EditorFocusModeTests, EditorFocusModeTests_IsInFocusSubTree)
{
FocusModeInterface* focusModeInterface = AZ::Interface<FocusModeInterface>::Get();
EXPECT_TRUE(focusModeInterface != nullptr);
focusModeInterface->ClearFocusRoot();
EXPECT_EQ(focusModeInterface->IsInFocusSubTree(m_entityMap["cityId"]), true);
EXPECT_EQ(focusModeInterface->IsInFocusSubTree(m_entityMap["streetId"]), true);
EXPECT_EQ(focusModeInterface->IsInFocusSubTree(m_entityMap["carId"]), true);
EXPECT_EQ(focusModeInterface->IsInFocusSubTree(m_entityMap["passengerId1"]), true);
EXPECT_EQ(focusModeInterface->IsInFocusSubTree(m_entityMap["sportsCarId"]), true);
EXPECT_EQ(focusModeInterface->IsInFocusSubTree(m_entityMap["passengerId2"]), true);
focusModeInterface->SetFocusRoot(m_entityMap["streetId"]);
EXPECT_EQ(focusModeInterface->IsInFocusSubTree(m_entityMap["cityId"]), false);
EXPECT_EQ(focusModeInterface->IsInFocusSubTree(m_entityMap["streetId"]), true);
EXPECT_EQ(focusModeInterface->IsInFocusSubTree(m_entityMap["carId"]), true);
EXPECT_EQ(focusModeInterface->IsInFocusSubTree(m_entityMap["passengerId1"]), true);
EXPECT_EQ(focusModeInterface->IsInFocusSubTree(m_entityMap["sportsCarId"]), true);
EXPECT_EQ(focusModeInterface->IsInFocusSubTree(m_entityMap["passengerId2"]), true);
focusModeInterface->SetFocusRoot(m_entityMap["carId"]);
EXPECT_EQ(focusModeInterface->IsInFocusSubTree(m_entityMap["cityId"]), false);
EXPECT_EQ(focusModeInterface->IsInFocusSubTree(m_entityMap["streetId"]), false);
EXPECT_EQ(focusModeInterface->IsInFocusSubTree(m_entityMap["carId"]), true);
EXPECT_EQ(focusModeInterface->IsInFocusSubTree(m_entityMap["passengerId1"]), true);
EXPECT_EQ(focusModeInterface->IsInFocusSubTree(m_entityMap["sportsCarId"]), false);
EXPECT_EQ(focusModeInterface->IsInFocusSubTree(m_entityMap["passengerId2"]), false);
focusModeInterface->SetFocusRoot(m_entityMap["passengerId2"]);
EXPECT_EQ(focusModeInterface->IsInFocusSubTree(m_entityMap["cityId"]), false);
EXPECT_EQ(focusModeInterface->IsInFocusSubTree(m_entityMap["streetId"]), false);
EXPECT_EQ(focusModeInterface->IsInFocusSubTree(m_entityMap["carId"]), false);
EXPECT_EQ(focusModeInterface->IsInFocusSubTree(m_entityMap["passengerId1"]), false);
EXPECT_EQ(focusModeInterface->IsInFocusSubTree(m_entityMap["sportsCarId"]), false);
EXPECT_EQ(focusModeInterface->IsInFocusSubTree(m_entityMap["passengerId2"]), true);
focusModeInterface->ClearFocusRoot();
}
}
@@ -0,0 +1,173 @@
/*
* 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 <AzCore/Component/EntityId.h>
#include <AzToolsFramework/Entity/EditorEntityHelpers.h>
#include <AzToolsFramework/Entity/PrefabEditorEntityOwnershipInterface.h>
#include <AzToolsFramework/Prefab/PrefabFocusInterface.h>
#include <Prefab/PrefabTestFixture.h>
namespace UnitTest
{
class PrefabFocusTests
: public PrefabTestFixture
{
protected:
void GenerateTestHierarchy()
{
/*
* City (Prefab Container)
* |_ City
* |_ Street (Prefab Container)
* |_ Car (Prefab Container)
* | |_ Passenger
* |_ SportsCar (Prefab Container)
* |_ Passenger
*/
m_entityMap["passenger1"] = CreateEntity("Passenger1");
m_entityMap["passenger2"] = CreateEntity("Passenger2");
m_entityMap["city"] = CreateEntity("City");
AzToolsFramework::EditorEntityContextRequestBus::Broadcast(
&AzToolsFramework::EditorEntityContextRequests::HandleEntitiesAdded,
AzToolsFramework::EntityList{ m_entityMap["passenger1"], m_entityMap["passenger2"], m_entityMap["city"] });
AZStd::unique_ptr<AzToolsFramework::Prefab::Instance> carInstance =
m_prefabSystemComponent->CreatePrefab({ m_entityMap["passenger1"] }, {}, "test/car");
ASSERT_TRUE(carInstance);
m_instanceMap["car"] = carInstance.get();
AZStd::unique_ptr<AzToolsFramework::Prefab::Instance> sportsCarInstance =
m_prefabSystemComponent->CreatePrefab({ m_entityMap["passenger2"] }, {}, "test/sportsCar");
ASSERT_TRUE(sportsCarInstance);
m_instanceMap["sportsCar"] = sportsCarInstance.get();
AZStd::unique_ptr<AzToolsFramework::Prefab::Instance> streetInstance =
m_prefabSystemComponent->CreatePrefab({}, MakeInstanceList( AZStd::move(carInstance), AZStd::move(sportsCarInstance) ), "test/street");
ASSERT_TRUE(streetInstance);
m_instanceMap["street"] = streetInstance.get();
m_rootInstance =
m_prefabSystemComponent->CreatePrefab({ m_entityMap["city"] }, MakeInstanceList(AZStd::move(streetInstance)), "test/city");
ASSERT_TRUE(m_rootInstance);
m_instanceMap["city"] = m_rootInstance.get();
}
AZStd::unordered_map<AZStd::string, AZ::Entity*> m_entityMap;
AZStd::unordered_map<AZStd::string, Instance*> m_instanceMap;
AZStd::unique_ptr<AzToolsFramework::Prefab::Instance> m_rootInstance;
};
TEST_F(PrefabFocusTests, PrefabFocus_FocusOnOwningPrefab)
{
GenerateTestHierarchy();
PrefabFocusInterface* prefabFocusInterface = AZ::Interface<PrefabFocusInterface>::Get();
EXPECT_TRUE(prefabFocusInterface != nullptr);
// Verify FocusOnOwningPrefab works when passing the container entity of the root prefab.
{
prefabFocusInterface->FocusOnOwningPrefab(m_instanceMap["city"]->GetContainerEntityId());
EXPECT_EQ(prefabFocusInterface->GetFocusedPrefabTemplateId(), m_instanceMap["city"]->GetTemplateId());
auto instance = prefabFocusInterface->GetFocusedPrefabInstance();
EXPECT_TRUE(instance.has_value());
EXPECT_EQ(&instance->get(), m_instanceMap["city"]);
}
// Verify FocusOnOwningPrefab works when passing a nested entity of the root prefab.
{
prefabFocusInterface->FocusOnOwningPrefab(m_entityMap["city"]->GetId());
EXPECT_EQ(prefabFocusInterface->GetFocusedPrefabTemplateId(), m_instanceMap["city"]->GetTemplateId());
auto instance = prefabFocusInterface->GetFocusedPrefabInstance();
EXPECT_TRUE(instance.has_value());
EXPECT_EQ(&instance->get(), m_instanceMap["city"]);
}
// Verify FocusOnOwningPrefab works when passing the container entity of a nested prefab.
{
prefabFocusInterface->FocusOnOwningPrefab(m_instanceMap["car"]->GetContainerEntityId());
EXPECT_EQ(prefabFocusInterface->GetFocusedPrefabTemplateId(), m_instanceMap["car"]->GetTemplateId());
auto instance = prefabFocusInterface->GetFocusedPrefabInstance();
EXPECT_TRUE(instance.has_value());
EXPECT_EQ(&instance->get(), m_instanceMap["car"]);
}
// Verify FocusOnOwningPrefab works when passing a nested entity of the a nested prefab.
{
prefabFocusInterface->FocusOnOwningPrefab(m_entityMap["passenger1"]->GetId());
EXPECT_EQ(prefabFocusInterface->GetFocusedPrefabTemplateId(), m_instanceMap["car"]->GetTemplateId());
auto instance = prefabFocusInterface->GetFocusedPrefabInstance();
EXPECT_TRUE(instance.has_value());
EXPECT_EQ(&instance->get(), m_instanceMap["car"]);
}
// Verify FocusOnOwningPrefab points to the root prefab when the focus is cleared.
{
AzToolsFramework::PrefabEditorEntityOwnershipInterface* prefabEditorEntityOwnershipInterface =
AZ::Interface<AzToolsFramework::PrefabEditorEntityOwnershipInterface>::Get();
AzToolsFramework::Prefab::InstanceOptionalReference rootPrefabInstance =
prefabEditorEntityOwnershipInterface->GetRootPrefabInstance();
EXPECT_TRUE(rootPrefabInstance.has_value());
prefabFocusInterface->FocusOnOwningPrefab(AZ::EntityId());
EXPECT_EQ(prefabFocusInterface->GetFocusedPrefabTemplateId(), rootPrefabInstance->get().GetTemplateId());
auto instance = prefabFocusInterface->GetFocusedPrefabInstance();
EXPECT_TRUE(instance.has_value());
EXPECT_EQ(&instance->get(), &rootPrefabInstance->get());
}
m_rootInstance.release();
}
TEST_F(PrefabFocusTests, PrefabFocus_IsOwningPrefabBeingFocused)
{
GenerateTestHierarchy();
PrefabFocusInterface* prefabFocusInterface = AZ::Interface<PrefabFocusInterface>::Get();
EXPECT_TRUE(prefabFocusInterface != nullptr);
// Verify IsOwningPrefabBeingFocused returns true for all entities in a focused prefab (container/nested)
{
prefabFocusInterface->FocusOnOwningPrefab(m_instanceMap["city"]->GetContainerEntityId());
EXPECT_TRUE(prefabFocusInterface->IsOwningPrefabBeingFocused(m_instanceMap["city"]->GetContainerEntityId()));
EXPECT_TRUE(prefabFocusInterface->IsOwningPrefabBeingFocused(m_entityMap["city"]->GetId()));
}
// Verify IsOwningPrefabBeingFocused returns false for all entities not in a focused prefab (ancestors/descendants)
{
prefabFocusInterface->FocusOnOwningPrefab(m_instanceMap["street"]->GetContainerEntityId());
EXPECT_TRUE(prefabFocusInterface->IsOwningPrefabBeingFocused(m_instanceMap["street"]->GetContainerEntityId()));
EXPECT_FALSE(prefabFocusInterface->IsOwningPrefabBeingFocused(m_instanceMap["city"]->GetContainerEntityId()));
EXPECT_FALSE(prefabFocusInterface->IsOwningPrefabBeingFocused(m_entityMap["city"]->GetId()));
EXPECT_FALSE(prefabFocusInterface->IsOwningPrefabBeingFocused(m_instanceMap["car"]->GetContainerEntityId()));
EXPECT_FALSE(prefabFocusInterface->IsOwningPrefabBeingFocused(m_entityMap["passenger1"]->GetId()));
}
// Verify IsOwningPrefabBeingFocused returns false for all entities not in a focused prefab (siblings)
{
prefabFocusInterface->FocusOnOwningPrefab(m_instanceMap["sportsCar"]->GetContainerEntityId());
EXPECT_TRUE(prefabFocusInterface->IsOwningPrefabBeingFocused(m_instanceMap["sportsCar"]->GetContainerEntityId()));
EXPECT_TRUE(prefabFocusInterface->IsOwningPrefabBeingFocused(m_entityMap["passenger2"]->GetId()));
EXPECT_FALSE(prefabFocusInterface->IsOwningPrefabBeingFocused(m_instanceMap["car"]->GetContainerEntityId()));
EXPECT_FALSE(prefabFocusInterface->IsOwningPrefabBeingFocused(m_entityMap["passenger1"]->GetId()));
}
m_rootInstance.release();
}
}
@@ -34,6 +34,7 @@ set(FILES
EntityTestbed.h
FileFunc.cpp
FingerprintingTests.cpp
FocusMode/EditorFocusModeTests.cpp
GenericComponentWrapperTest.cpp
InstanceDataHierarchy.cpp
IntegerPrimtitiveTestConfig.h
@@ -50,6 +51,7 @@ set(FILES
Prefab/Benchmark/PrefabLoadBenchmarks.cpp
Prefab/Benchmark/PrefabUpdateInstancesBenchmarks.cpp
Prefab/Benchmark/SpawnableCreateBenchmarks.cpp
Prefab/PrefabFocus/PrefabFocusTests.cpp
Prefab/MockPrefabFileIOActionValidator.cpp
Prefab/MockPrefabFileIOActionValidator.h
Prefab/PrefabDuplicateTests.cpp