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.
o3de/Code/Editor/Plugins/ComponentEntityEditorPlugin/UI/Outliner/OutlinerTreeView.cpp

431 lines
14 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 "ComponentEntityEditorPlugin_precompiled.h"
#include "OutlinerTreeView.hxx"
#include "OutlinerListModel.hxx"
#include <AzCore/Component/EntityId.h>
#include <AzCore/std/sort.h>
#include <AzCore/std/algorithm.h>
#include <AzToolsFramework/Entity/EditorEntityHelpers.h>
#include <AzQtComponents/Components/Style.h>
#include <QDrag>
#include <QPainter>
#include <QHeaderView>
OutlinerTreeView::OutlinerTreeView(QWidget* pParent)
: QTreeView(pParent)
, m_queuedMouseEvent(nullptr)
, m_draggingUnselectedItem(false)
{
setUniformRowHeights(true);
setHeaderHidden(true);
}
OutlinerTreeView::~OutlinerTreeView()
{
ClearQueuedMouseEvent();
}
void OutlinerTreeView::setAutoExpandDelay(int delay)
{
m_expandOnlyDelay = delay;
}
void OutlinerTreeView::ClearQueuedMouseEvent()
{
if (m_queuedMouseEvent)
{
delete m_queuedMouseEvent;
m_queuedMouseEvent = nullptr;
}
}
void OutlinerTreeView::mousePressEvent(QMouseEvent* event)
{
//postponing normal mouse pressed logic until mouse is released or dragged
//this means selection occurs on mouse released now
//this is to support drag/drop of non-selected items
ClearQueuedMouseEvent();
m_queuedMouseEvent = new QMouseEvent(*event);
}
void OutlinerTreeView::mouseReleaseEvent(QMouseEvent* event)
{
if (m_queuedMouseEvent && !m_draggingUnselectedItem)
{
// mouseMoveEvent will set the state to be DraggingState, which will make Qt ignore
// mousePressEvent in QTreeViewPrivate::expandOrCollapseItemAtPos. So we manually
// and temporarily set it to EditingState.
QAbstractItemView::State stateBefore = QAbstractItemView::state();
QAbstractItemView::setState(QAbstractItemView::State::EditingState);
//treat this as a mouse pressed event to process selection etc
processQueuedMousePressedEvent(m_queuedMouseEvent);
QAbstractItemView::setState(stateBefore);
}
ClearQueuedMouseEvent();
m_draggingUnselectedItem = false;
QTreeView::mouseReleaseEvent(event);
}
void OutlinerTreeView::mouseDoubleClickEvent(QMouseEvent* event)
{
//cancel pending mouse press
ClearQueuedMouseEvent();
QTreeView::mouseDoubleClickEvent(event);
}
void OutlinerTreeView::mouseMoveEvent(QMouseEvent* event)
{
// Store selection mode
QAbstractItemView::SelectionMode selectionModeBefore = selectionMode();
// Disable selection for the pending click so selection is maintained for dragging
setSelectionMode(QAbstractItemView::NoSelection);
// If a mouse event is queued, treat this as a mouse pressed event to process everything
// but selection, but use the position data from the mousePress message
if (m_queuedMouseEvent)
{
processQueuedMousePressedEvent(m_queuedMouseEvent);
}
// Process mouse movement as normal, potentially triggering drag and drop
QTreeView::mouseMoveEvent(event);
// Restore selection state
setSelectionMode(selectionModeBefore);
}
void OutlinerTreeView::focusInEvent(QFocusEvent* event)
{
//cancel pending mouse press
ClearQueuedMouseEvent();
QTreeView::focusInEvent(event);
}
void OutlinerTreeView::focusOutEvent(QFocusEvent* event)
{
//cancel pending mouse press
ClearQueuedMouseEvent();
QTreeView::focusOutEvent(event);
}
void OutlinerTreeView::startDrag(Qt::DropActions supportedActions)
{
//if we are attempting to drag an unselected item then we must special case drag and drop logic
//QAbstractItemView::startDrag only supports selected items
if (m_queuedMouseEvent)
{
QModelIndex index = indexAt(m_queuedMouseEvent->pos());
if (!index.isValid() || index.column() != 0)
{
return;
}
if (!selectionModel()->isSelected(index))
{
startCustomDrag({ index }, supportedActions);
return;
}
}
if (!selectionModel()->selectedIndexes().empty())
{
startCustomDrag(selectionModel()->selectedIndexes(), supportedActions);
return;
}
}
void OutlinerTreeView::dragMoveEvent(QDragMoveEvent* event)
{
if (m_expandOnlyDelay >= 0)
{
m_expandTimer.start(m_expandOnlyDelay, this);
}
QTreeView::dragMoveEvent(event);
}
void OutlinerTreeView::dropEvent(QDropEvent* event)
{
emit ItemDropped();
QTreeView::dropEvent(event);
m_draggingUnselectedItem = false;
}
QColor OutlinerTreeView::GetHierarchyLineColor(bool isSliceEntity, bool isSelected) const
{
if (isSliceEntity)
{
if (isSelected)
{
return m_colorConfig.hierarchyLinesSlicesSelected;
}
else
{
return m_colorConfig.hierarchyLinesSlices;
}
}
else
{
if (isSelected)
{
return m_colorConfig.hierarchyLinesNonSliceEntitiesSelected;
}
else
{
return m_colorConfig.hierarchyLinesNonSliceEntities;
}
}
}
void OutlinerTreeView::DrawLayerUI(QPainter* painter, const QRect& rect, const QModelIndex& index) const
{
bool isSelected = selectionModel()->isSelected(index);
QColor layerBranchesBGColor = isSelected ? m_colorConfig.layerChildBGSelectionColor : m_colorConfig.layerChildBackgroundColor;
painter->save();
painter->setRenderHint(QPainter::RenderHint::Antialiasing, false);
bool hasLayerAncestor = false;
for (QModelIndex ancestorIndex = index.parent(); ancestorIndex.isValid(); ancestorIndex = ancestorIndex.parent())
{
auto ancestorType = OutlinerListModel::EntryType(ancestorIndex.data(OutlinerListModel::EntityTypeRole).value<int>());
if (ancestorType == OutlinerListModel::LayerType)
{
hasLayerAncestor = true;
break;
}
}
if (hasLayerAncestor)
{
QPainterPath layerBGPath;
QRect layerBGRect(rect);
layerBGRect.setLeft(layerBGRect.left() + indentation());
layerBGPath.addRect(layerBGRect);
painter->fillPath(layerBGPath, layerBranchesBGColor);
}
OutlinerListModel::EntryType indexEntryType = OutlinerListModel::EntryType(index.data(OutlinerListModel::EntityTypeRole).value<int>());
if (indexEntryType == OutlinerListModel::LayerType)
{
QColor layerColor = index.data(OutlinerListModel::LayerColorRole).value<QColor>();
QPainterPath layerIconPath;
const int layerSquareSize = GetLayerSquareSize();
QPoint layerBoxOffset(1 + OutlinerListModel::GetLayerStripeWidth()*2, (rect.height() - layerSquareSize) / 2);
QRect layerIconRect(rect.topRight() + layerBoxOffset, QSize(layerSquareSize, layerSquareSize));
layerIconPath.addRect(layerIconRect);
painter->fillPath(layerIconPath, layerColor);
}
painter->restore();
}
void OutlinerTreeView::drawBranches(QPainter* painter, const QRect& rect, const QModelIndex& index) const
{
DrawLayerUI(painter, rect, index);
// Make sure the base class is called after the layer rect is drawn,
// so that the foldout arrow draws on top of the layer color.
QTreeView::drawBranches(painter, rect, index);
// No need to draw connecting lines if this has no parent.
if (!index.parent().isValid())
{
return;
}
QPen branchLinePen;
branchLinePen.setWidthF(m_branchLineWidth);
int lineBaseX = rect.right();
QModelIndex previousIndex = index;
for (QModelIndex ancestorIndex = index.parent(); ancestorIndex.isValid(); ancestorIndex = ancestorIndex.parent())
{
auto ancestorType = OutlinerListModel::EntryType(ancestorIndex.data(OutlinerListModel::EntityTypeRole).value<int>());
bool isSliceEntity = ancestorType == OutlinerListModel::SliceEntityType || ancestorType == OutlinerListModel::SliceHandleType;
bool isSelected = selectionModel()->isSelected(index);
branchLinePen.setColor(GetHierarchyLineColor(isSliceEntity, isSelected));
// Layers don't have connecting lines drawn.
if (ancestorType == OutlinerListModel::LayerType)
{
// Layers can only have other layers as parents, so once a layer is found in a hierarchy
// the line drawing can stop.
break;
}
painter->save();
painter->setRenderHint(QPainter::RenderHint::Antialiasing, false);
painter->setPen(branchLinePen);
int rectHalfHeight = rect.height() / 2;
if (previousIndex == index)
{
// draw a horizontal line from the parent branch to the item
// if the item has children offset the drawn line to compensate for drawn expander buttons
bool hasChildren = previousIndex.model()->index(0, 0, previousIndex).isValid();
int horizontalLineY = rect.top() + rectHalfHeight;
int horizontalLineLeft = rect.right() - indentation() * 1.5f;
int horizontalLineRight = hasChildren ? (lineBaseX - indentation()) : (lineBaseX - indentation() * 0.5f);
painter->drawLine(horizontalLineLeft, horizontalLineY, horizontalLineRight, horizontalLineY);
}
// draw a vertical line segment connecting parent to child and child to child
// if this is the last item, only draw half the line to terminate the segment
bool hasNext = previousIndex.sibling(previousIndex.row() + 1, previousIndex.column()).isValid();
if (hasNext || previousIndex == index)
{
int verticalLineX = lineBaseX - indentation() * 1.5f;
int verticalLineTop = rect.top();
int verticalLineBottom = hasNext ? rect.bottom() : rect.bottom() - rectHalfHeight;
painter->drawLine(verticalLineX, verticalLineTop, verticalLineX, verticalLineBottom);
}
painter->restore();
lineBaseX -= indentation();
previousIndex = ancestorIndex;
}
}
void OutlinerTreeView::timerEvent(QTimerEvent* event)
{
if (event->timerId() == m_expandTimer.timerId())
{
//duplicates functionality from QTreeView, but won't collapse an already expanded item
QPoint pos = this->viewport()->mapFromGlobal(QCursor::pos());
if (state() == QAbstractItemView::DraggingState && this->rect().contains(pos))
{
QModelIndex index = indexAt(pos);
if (!isExpanded(index))
{
setExpanded(index, true);
}
}
m_expandTimer.stop();
}
QTreeView::timerEvent(event);
}
void OutlinerTreeView::processQueuedMousePressedEvent(QMouseEvent* event)
{
//interpret the mouse event as a button press
QMouseEvent mousePressedEvent(
QEvent::MouseButtonPress,
event->localPos(),
event->windowPos(),
event->screenPos(),
event->button(),
event->buttons(),
event->modifiers(),
event->source());
QTreeView::mousePressEvent(&mousePressedEvent);
}
void OutlinerTreeView::startCustomDrag(const QModelIndexList& indexList, Qt::DropActions supportedActions)
{
m_draggingUnselectedItem = true;
//sort by container entity depth and order in hierarchy for proper drag image and drop order
QModelIndexList indexListSorted = indexList;
AZStd::unordered_map<AZ::EntityId, AZStd::list<AZ::u64>> locations;
for (auto index : indexListSorted)
{
AZ::EntityId entityId(index.data(OutlinerListModel::EntityIdRole).value<AZ::u64>());
AzToolsFramework::GetEntityLocationInHierarchy(entityId, locations[entityId]);
}
AZStd::sort(indexListSorted.begin(), indexListSorted.end(), [&locations](const QModelIndex& index1, const QModelIndex& index2) {
AZ::EntityId e1(index1.data(OutlinerListModel::EntityIdRole).value<AZ::u64>());
AZ::EntityId e2(index2.data(OutlinerListModel::EntityIdRole).value<AZ::u64>());
const auto& locationsE1 = locations[e1];
const auto& locationsE2 = locations[e2];
return AZStd::lexicographical_compare(locationsE1.begin(), locationsE1.end(), locationsE2.begin(), locationsE2.end());
});
//get the data for the unselected item(s)
QMimeData* mimeData = model()->mimeData(indexListSorted);
if (mimeData)
{
//initiate drag/drop for the item
QDrag* drag = new QDrag(this);
drag->setPixmap(QPixmap::fromImage(createDragImage(indexListSorted)));
drag->setMimeData(mimeData);
Qt::DropAction defDropAction = Qt::IgnoreAction;
if (defaultDropAction() != Qt::IgnoreAction && (supportedActions & defaultDropAction()))
{
defDropAction = defaultDropAction();
}
else if (supportedActions & Qt::CopyAction && dragDropMode() != QAbstractItemView::InternalMove)
{
defDropAction = Qt::CopyAction;
}
drag->exec(supportedActions, defDropAction);
}
}
QImage OutlinerTreeView::createDragImage(const QModelIndexList& indexList)
{
//generate a drag image of the item icon and text, normally done internally, and inaccessible
QRect rect(0, 0, 0, 0);
for (auto index : indexList)
{
if (index.column() != 0)
{
continue;
}
QRect itemRect = visualRect(index);
rect.setHeight(rect.height() + itemRect.height());
rect.setWidth(AZStd::GetMax(rect.width(), itemRect.width()));
}
QImage dragImage(rect.size(), QImage::Format_ARGB32_Premultiplied);
QPainter dragPainter(&dragImage);
dragPainter.setCompositionMode(QPainter::CompositionMode_Source);
dragPainter.fillRect(dragImage.rect(), Qt::transparent);
dragPainter.setCompositionMode(QPainter::CompositionMode_SourceOver);
dragPainter.setOpacity(0.35f);
dragPainter.fillRect(rect, QColor("#222222"));
dragPainter.setOpacity(1.0f);
int imageY = 0;
for (auto index : indexList)
{
if (index.column() != 0)
{
continue;
}
QRect itemRect = visualRect(index);
dragPainter.drawPixmap(QPoint(0, imageY),
model()->data(index, Qt::DecorationRole).value<QIcon>().pixmap(QSize(16, 16)));
dragPainter.setPen(
model()->data(index, Qt::ForegroundRole).value<QBrush>().color());
dragPainter.setFont(
font());
dragPainter.drawText(QRect(20, imageY, rect.width() - 20, rect.height()),
model()->data(index, Qt::DisplayRole).value<QString>());
imageY += itemRect.height();
}
dragPainter.end();
return dragImage;
}
#include <UI/Outliner/moc_OutlinerTreeView.cpp>