// Copyright Epic Games, Inc. All Rights Reserved. #pragma once #include "Slate/SObjectWidget.h" #include "Blueprint/IUserObjectListEntry.h" #include "Blueprint/WidgetLayoutLibrary.h" #include "Blueprint/DragDropOperation.h" #include "Types/ReflectionMetadata.h" #include "Widgets/Views/STableRow.h" #include "Widgets/Views/STableViewBase.h" #include "Widgets/Text/STextBlock.h" #include "Widgets/Views/SListView.h" #include "Slate/UMGDragDropOp.h" class IObjectTableRow : public ITableRow { public: virtual UListViewBase* GetOwningListView() const = 0; virtual UUserWidget* GetUserWidget() const = 0; static TSharedPtr ObjectRowFromUserWidget(const UUserWidget* RowUserWidget) { TWeakPtr* ObjectRow = ObjectRowsByUserWidget.Find(RowUserWidget); if (ObjectRow && ObjectRow->IsValid()) { return ObjectRow->Pin(); } return nullptr; } protected: // Intentionally being a bit nontraditional here - we track associations between UserWidget rows and their underlying IObjectTableRow. // This allows us to mirror the ITableRow API very easily on IUserListEntry without requiring rote setting/getting of row states on every UMG subclass. UMG_API static TMap, TWeakPtr> ObjectRowsByUserWidget; }; DECLARE_DELEGATE_OneParam(FOnRowHovered, UUserWidget&); DECLARE_DELEGATE_RetVal_ThreeParams(TOptional, FOnObjectRowCanAcceptDrop, const FDragDropEvent&, EItemDropZone, UUserWidget&); DECLARE_DELEGATE_RetVal_ThreeParams(FReply, FOnObjectRowAcceptDrop, const FDragDropEvent&, EItemDropZone, UUserWidget&); DECLARE_DELEGATE_RetVal_ThreeParams(UDragDropOperation*, FOnObjectRowDragDetected, const FGeometry&, const FPointerEvent&, UUserWidget&); DECLARE_DELEGATE_TwoParams(FOnObjectRowDragEnter, FDragDropEvent const&, UUserWidget&); DECLARE_DELEGATE_TwoParams(FOnObjectRowDragLeave, FDragDropEvent const&, UUserWidget&); DECLARE_DELEGATE_OneParam(FOnObjectRowDragCancelled, const FDragDropEvent&); class UListViewBase; /** * It's an SObjectWidget! It's an ITableRow! It does it all! * * By using UUserWidget::TakeDerivedWidget(), this class allows UMG to fully leverage the robust Slate list view widgets. * The major gain from this is item virtualization, which is an even bigger deal when unnecessary widgets come with a boatload of additional UObject allocations. * * The owning UUserWidget is expected to implement the IUserListEntry UInterface, which allows the row widget to respond to various list-related events. * * Note: Much of the implementation here matches STableRow exactly, so refer there if looking for additional information. */ template class SObjectTableRow : public SObjectWidget, public IObjectTableRow { public: SLATE_BEGIN_ARGS(SObjectTableRow) :_bAllowDragging(true) ,_bAllowDragDrop(false) {} SLATE_ARGUMENT(bool, bAllowDragging) SLATE_ARGUMENT(bool, bAllowDragDrop) SLATE_ARGUMENT(bool, bAllowKeepPreselectedItems) SLATE_DEFAULT_SLOT(FArguments, Content) SLATE_EVENT(FOnRowHovered, OnHovered) SLATE_EVENT(FOnRowHovered, OnUnhovered) // Drag and Drop functionality SLATE_EVENT(FOnObjectRowCanAcceptDrop, OnRowCanAcceptDrop) SLATE_EVENT(FOnObjectRowAcceptDrop, OnRowAcceptDrop) SLATE_EVENT(FOnObjectRowDragDetected, OnRowDragDetected) SLATE_EVENT(FOnObjectRowDragEnter, OnRowDragEnter) SLATE_EVENT(FOnObjectRowDragLeave, OnRowDragLeave) SLATE_EVENT(FOnObjectRowDragCancelled, OnRowDragCancelled) // End Drag and Drop SLATE_END_ARGS() public: void Construct(const FArguments& InArgs, const TSharedRef& InOwnerTableView, UUserWidget& InWidgetObject, UListViewBase* InOwnerListView = nullptr) { TSharedPtr ContentWidget; if (ensureMsgf(InWidgetObject.Implements(), TEXT("Any UserWidget generated as a table row must implement the IUserListEntry interface"))) { ObjectRowsByUserWidget.Add(&InWidgetObject, SharedThis(this)); OwnerListView = InOwnerListView; OwnerTablePtr = StaticCastSharedRef>(InOwnerTableView); bAllowDragging = InArgs._bAllowDragging; bAllowDragDrop = InArgs._bAllowDragDrop; bAllowKeepPreselectedItems = InArgs._bAllowKeepPreselectedItems; OnHovered = InArgs._OnHovered; OnUnhovered = InArgs._OnUnhovered; OnRowCanAcceptDrop = InArgs._OnRowCanAcceptDrop; OnRowAcceptDrop = InArgs._OnRowAcceptDrop; OnRowDragDetected = InArgs._OnRowDragDetected; OnRowDragLeave = InArgs._OnRowDragLeave; OnRowDragEnter = InArgs._OnRowDragEnter; OnRowDragCancelled = InArgs._OnRowDragCancelled; ContentWidget = InArgs._Content.Widget; } else { ContentWidget = SNew(STextBlock) .Text(NSLOCTEXT("SObjectTableRow", "InvalidWidgetClass", "Any UserWidget generated as a table row must implement the IUserListEntry interface")); } SObjectWidget::Construct( SObjectWidget::FArguments() .Content() [ ContentWidget.ToSharedRef() ], &InWidgetObject); // Register an active timer, not an OnTick to determine if item selection changed. // If we use OnTick, it will be potentially stomped by DisableNativeTick, when the // SObjectTableRow is used to wrap the UUserWidget construction. RegisterActiveTimer(0.f, FWidgetActiveTimerDelegate::CreateSP(this, &SObjectTableRow::DetectItemSelectionChanged)); } virtual ~SObjectTableRow() { // Remove the association between this widget and its user widget ObjectRowsByUserWidget.Remove(WidgetObject); } virtual UUserWidget* GetUserWidget() const { return WidgetObject; } virtual UListViewBase* GetOwningListView() const { if (OwnerListView.IsValid()) { return OwnerListView.Get(); } return nullptr; } EActiveTimerReturnType DetectItemSelectionChanged(double InCurrentTime, float InDeltaTime) { DetectItemSelectionChanged(); return EActiveTimerReturnType::Continue; } virtual void NotifyItemExpansionChanged(bool bIsExpanded) { if (WidgetObject) { IUserListEntry::UpdateItemExpansion(*WidgetObject, bIsExpanded); } } //~ ITableRow interface virtual void InitializeRow() override final { // ObjectRows can be generated in the widget designer with dummy data, which we want to ignore if (WidgetObject && !WidgetObject->IsDesignTime()) { InitializeObjectRow(); } } virtual void ResetRow() override final { if (WidgetObject && !WidgetObject->IsDesignTime()) { ResetObjectRow(); } } virtual TSharedRef AsWidget() override { return SharedThis(this); } virtual void SetIndexInList(int32 InIndexInList) override { IndexInList = InIndexInList; } virtual int32 GetIndexInList() override { return IndexInList; } virtual TSharedPtr GetContent() override { return ChildSlot.GetChildAt(0); } virtual int32 GetIndentLevel() const override { TSharedPtr> OwnerTable = OwnerTablePtr.Pin(); return OwnerTable ? OwnerTable->Private_GetNestingDepth(IndexInList) : 0; } virtual int32 DoesItemHaveChildren() const override { TSharedPtr> OwnerTable = OwnerTablePtr.Pin(); return OwnerTable ? OwnerTable->Private_DoesItemHaveChildren(IndexInList) : 0; } virtual void Private_OnExpanderArrowShiftClicked() override { /* Intentionally blank - far too specific to be a valid game UI interaction */ } virtual ESelectionMode::Type GetSelectionMode() const override { TSharedPtr> OwnerTable = OwnerTablePtr.Pin(); return OwnerTable ? OwnerTable->Private_GetSelectionMode() : ESelectionMode::None; } virtual FVector2D GetRowSizeForColumn(const FName& InColumnName) const override { return FVector2D::ZeroVector; } virtual bool IsItemExpanded() const override { TSharedPtr> OwnerTable = OwnerTablePtr.Pin(); const TObjectPtrWrapTypeOf* MyItemPtr = OwnerTable ? GetItemForThis(OwnerTable.ToSharedRef()) : nullptr; if (MyItemPtr) { return OwnerTable->Private_IsItemExpanded(*MyItemPtr); } return false; } virtual void ToggleExpansion() override { TSharedPtr> OwnerTable = OwnerTablePtr.Pin(); if (OwnerTable && OwnerTable->Private_DoesItemHaveChildren(IndexInList)) { if (const TObjectPtrWrapTypeOf* MyItemPtr = GetItemForThis(OwnerTable.ToSharedRef())) { OwnerTable->Private_SetItemExpansion(*MyItemPtr, !OwnerTable->Private_IsItemExpanded(*MyItemPtr)); } } } virtual bool IsItemSelected() const override { TSharedPtr> OwnerTable = OwnerTablePtr.Pin(); const TObjectPtrWrapTypeOf* MyItemPtr = OwnerTable ? GetItemForThis(OwnerTable.ToSharedRef()) : nullptr; if (MyItemPtr) { return OwnerTable->Private_IsItemSelected(*MyItemPtr); } return false; } virtual TBitArray<> GetWiresNeededByDepth() const override { TSharedPtr> OwnerTable = OwnerTablePtr.Pin(); return OwnerTable ? OwnerTable->Private_GetWiresNeededByDepth(IndexInList) : TBitArray<>(); } virtual bool IsLastChild() const override { TSharedPtr> OwnerTable = OwnerTablePtr.Pin(); return OwnerTable ? OwnerTable->Private_IsLastChild(IndexInList) : true; } //~ ITableRow interface //~ SWidget interface virtual bool SupportsKeyboardFocus() const override { return true; } virtual void OnMouseEnter(const FGeometry& MyGeometry, const FPointerEvent& MouseEvent) override { SObjectWidget::OnMouseEnter(MyGeometry, MouseEvent); if (WidgetObject && OnHovered.IsBound()) { OnHovered.ExecuteIfBound(*WidgetObject); } } virtual void OnMouseLeave(const FPointerEvent& MouseEvent) override { SObjectWidget::OnMouseLeave(MouseEvent); if (WidgetObject && OnUnhovered.IsBound()) { OnUnhovered.ExecuteIfBound(*WidgetObject); } } virtual FReply OnMouseButtonDoubleClick(const FGeometry& InMyGeometry, const FPointerEvent& InMouseEvent) override { if (InMouseEvent.GetEffectingButton() == EKeys::LeftMouseButton) { TSharedPtr> OwnerTable = OwnerTablePtr.Pin(); const TObjectPtrWrapTypeOf* MyItemPtr = OwnerTable ? GetItemForThis(OwnerTable.ToSharedRef()) : nullptr; if (MyItemPtr) { OwnerTable->Private_OnItemDoubleClicked(*MyItemPtr); return FReply::Handled(); } } return FReply::Unhandled(); } virtual FReply OnTouchStarted(const FGeometry& MyGeometry, const FPointerEvent& InTouchEvent) override { //TODO: FReply Reply = SObjectWidget::OnTouchStarted(MyGeometry, InTouchEvent); bProcessingSelectionTouch = true; return FReply::Handled() .DetectDrag(SharedThis(this), EKeys::LeftMouseButton); } virtual FReply OnTouchEnded(const FGeometry& MyGeometry, const FPointerEvent& InTouchEvent) override { FReply Reply = SObjectWidget::OnTouchEnded(MyGeometry, InTouchEvent); if (bProcessingSelectionTouch) { bProcessingSelectionTouch = false; TSharedPtr> OwnerTable = OwnerTablePtr.Pin(); const TObjectPtrWrapTypeOf* MyItemPtr = OwnerTable ? GetItemForThis(OwnerTable.ToSharedRef()) : nullptr; if (MyItemPtr) { if (IsItemSelectable()) { ESelectionMode::Type SelectionMode = GetSelectionMode(); if (SelectionMode != ESelectionMode::None) { const bool bIsSelected = OwnerTable->Private_IsItemSelected(*MyItemPtr); if (!bIsSelected) { if (SelectionMode != ESelectionMode::Multi) { OwnerTable->Private_ClearSelection(); } OwnerTable->Private_SetItemSelection(*MyItemPtr, true, true); OwnerTable->Private_SignalSelectionChanged(ESelectInfo::OnMouseClick); Reply = FReply::Handled(); } else if (SelectionMode == ESelectionMode::SingleToggle || SelectionMode == ESelectionMode::Multi) { OwnerTable->Private_SetItemSelection(*MyItemPtr, true, true); OwnerTable->Private_SignalSelectionChanged(ESelectInfo::OnMouseClick); Reply = FReply::Handled(); } } } if (OwnerTable->Private_OnItemClicked(*MyItemPtr)) { Reply = FReply::Handled(); } } } return Reply; } virtual FReply OnDragDetected(const FGeometry& MyGeometry, const FPointerEvent& MouseEvent) override { FReply Reply = FReply::Unhandled(); if (bAllowDragging || bAllowDragDrop) { TSharedPtr> OwnerTable = OwnerTablePtr.Pin(); if (bProcessingSelectionTouch) { bProcessingSelectionTouch = false; return OwnerTable ? FReply::Handled().CaptureMouse(OwnerTable->AsWidget()) : FReply::Unhandled(); } //@todo DanH TableRow: does this potentially trigger twice? If not, why does an unhandled drag detection result in not calling mouse up? else if (HasMouseCapture() && bChangedSelectionOnMouseDown) { // Do not change the selection on mouse up if we are dragging bDragWasDetected = true; if (OwnerTable) { OwnerTable->Private_SignalSelectionChanged(ESelectInfo::OnMouseClick); } } Reply = SObjectWidget::OnDragDetected(MyGeometry, MouseEvent); if (!Reply.IsEventHandled() && bAllowDragDrop) { if (OnRowDragDetected.IsBound() && WidgetObject) { // Allow the view to create and pass back a UMG drag drop operation using config data UDragDropOperation* Operation = OnRowDragDetected.Execute(MyGeometry, MouseEvent, *WidgetObject); if (Operation) { // Only update the entry drag over state if a valid operation was created IUserListEntry::UpdateEntryDragOverState(*WidgetObject, true); FVector2D ScreenCursorPos = MouseEvent.GetScreenSpacePosition(); FVector2D ScreenDragPosition = MyGeometry.GetAbsolutePosition(); float DPIScale = UWidgetLayoutLibrary::GetViewportScale(WidgetObject); uint32 PointerIndex = MouseEvent.GetPointerIndex(); TSharedRef DragDropOp = FUMGDragDropOp::New(Operation, PointerIndex, ScreenCursorPos, ScreenDragPosition, DPIScale, SharedThis(this)); // Set the item value of the row to the drag drop visual. // If the drag visual is not a UUserWidget this will have to be done manually by binding to the list OnDragDetected delegate. UUserWidget* DragVisualWidget = Cast(Operation->DefaultDragVisual.Get()); if (DragVisualWidget && DragVisualWidget->Implements()) { IUserObjectListEntry::SetListItemObject(*DragVisualWidget, Operation->Payload); } return FReply::Handled().BeginDragDrop(DragDropOp); } return FReply::Unhandled(); } } } bProcessingSelectionTouch = false; return Reply; } virtual void OnDragEnter(const FGeometry& MyGeometry, const FDragDropEvent& DragDropEvent) override { SObjectWidget::OnDragEnter(MyGeometry, DragDropEvent); if (WidgetObject && OnRowDragEnter.IsBound()) { IUserListEntry::UpdateEntryDragOverState(*WidgetObject, true); OnRowDragEnter.Execute(DragDropEvent, *WidgetObject); } } virtual void OnDragLeave(FDragDropEvent const& DragDropEvent) override { SObjectWidget::OnDragLeave(DragDropEvent); // Clear out the current drop zone when we leave the drag area ItemDropZone = NullOpt; if (WidgetObject && OnRowDragLeave.IsBound()) { IUserListEntry::UpdateEntryDragOverState(*WidgetObject, false); OnRowDragLeave.Execute(DragDropEvent, *WidgetObject); } } /** @return the zone (above, onto, below) based on where the user is hovering over within the row */ EItemDropZone ZoneFromPointerPosition(UE::Slate::FDeprecateVector2DParameter LocalPointerPos, UE::Slate::FDeprecateVector2DParameter LocalSize, EOrientation Orientation) { const float PointerPos = Orientation == EOrientation::Orient_Horizontal ? LocalPointerPos.X : LocalPointerPos.Y; const float Size = Orientation == EOrientation::Orient_Horizontal ? LocalSize.X : LocalSize.Y; const float ZoneBoundarySu = FMath::Clamp(Size * 0.25f, 3.0f, 10.0f); if (PointerPos < ZoneBoundarySu) { return EItemDropZone::AboveItem; } else if (PointerPos > Size - ZoneBoundarySu) { return EItemDropZone::BelowItem; } else { return EItemDropZone::OntoItem; } } virtual FReply OnDragOver(const FGeometry& MyGeometry, const FDragDropEvent& DragDropEvent) override { FReply Reply = SObjectWidget::OnDragOver(MyGeometry, DragDropEvent); if (OnRowCanAcceptDrop.IsBound() && !Reply.IsEventHandled()) { const TSharedRef< ITypedTableView > OwnerTable = OwnerTablePtr.Pin().ToSharedRef(); const FVector2f LocalPointerPos = MyGeometry.AbsoluteToLocal(DragDropEvent.GetScreenSpacePosition()); const EItemDropZone ItemHoverZone = ZoneFromPointerPosition(LocalPointerPos, MyGeometry.GetLocalSize(), OwnerTable->Private_GetOrientation()); ItemDropZone = [ItemHoverZone, DragDropEvent, this]() { if (WidgetObject) { return OnRowCanAcceptDrop.Execute(DragDropEvent, ItemHoverZone, *WidgetObject); } return TOptional(); }(); if (WidgetObject) { IUserListEntry::UpdateEntryDropIndicator(*WidgetObject, ItemDropZone); } } return Reply; } virtual FReply OnDrop(const FGeometry& MyGeometry, const FDragDropEvent& DragDropEvent) override { if (!bAllowDragDrop) { return FReply::Unhandled(); } FReply Reply = SObjectWidget::OnDrop(MyGeometry, DragDropEvent); if (OnRowAcceptDrop.IsBound() && !Reply.IsEventHandled()) { const TSharedRef< ITypedTableView > OwnerTable = OwnerTablePtr.Pin().ToSharedRef(); // A drop finishes the drag/drop operation, so we are no longer providing any feedback. ItemDropZone = TOptional(); // Find item associated with this widget. if (const TObjectPtrWrapTypeOf* MyItemPtr = GetItemForThis(OwnerTable)) { // Which physical drop zone is the drop about to be performed onto? const FVector2f LocalPointerPos = MyGeometry.AbsoluteToLocal(DragDropEvent.GetScreenSpacePosition()); EItemDropZone HoveredZone = ZoneFromPointerPosition(LocalPointerPos, MyGeometry.GetLocalSize(), OwnerTable->Private_GetOrientation()); // The row gets final say over which zone to drop onto regardless of physical location. TOptional ReportedZone; if (WidgetObject && OnRowCanAcceptDrop.IsBound()) { ReportedZone = OnRowCanAcceptDrop.Execute(DragDropEvent, HoveredZone, *WidgetObject); } else { ReportedZone = HoveredZone; } if (ReportedZone.IsSet()) { Reply = FReply::Unhandled(); if (WidgetObject) { Reply = OnRowAcceptDrop.Execute(DragDropEvent, ReportedZone.GetValue(), *WidgetObject); bool DropReplyHandled = false; if (Reply.IsEventHandled()) { DropReplyHandled = true; } IUserListEntry::EndEntryDropOperation(*WidgetObject, DropReplyHandled); } if (Reply.IsEventHandled() && ReportedZone.GetValue() == EItemDropZone::OntoItem) { // Expand the drop target just in case, so that what we dropped is visible. OwnerTable->Private_SetItemExpansion(*MyItemPtr, true); } return Reply; } else { // If CanAcceptDrop returned a negative result, handle and cancel the drop event. OnDragCancelled(DragDropEvent, nullptr); return FReply::Handled().EndDragDrop(); } } // If there is not an item associated with this widget, the drop is unhandled return FReply::Unhandled(); } // If there is no listener bound|dragging disabled|reply already handled - return the reply return Reply; } void OnDragCancelled(const FDragDropEvent& DragDropEvent, UDragDropOperation* Operation) override { SObjectWidget::OnDragCancelled(DragDropEvent, Operation); if (WidgetObject && OnRowDragCancelled.IsBound()) { IUserListEntry::EndEntryDropOperation(*WidgetObject, false); OnRowDragCancelled.Execute(DragDropEvent); } } virtual void HandleEntryDropped(UDragDropOperation* Operation) { if (WidgetObject) { IUserListEntry::HandleEntryDropped(*WidgetObject, Operation); } } virtual void HandleEntryDragged(UDragDropOperation* Operation) { if (WidgetObject) { IUserListEntry::HandleEntryDragged(*WidgetObject, Operation); } } virtual FReply OnMouseButtonDown(const FGeometry& MyGeometry, const FPointerEvent& MouseEvent) override { bChangedSelectionOnMouseDown = false; bDragWasDetected = false; FReply Reply = SObjectWidget::OnMouseButtonDown(MyGeometry, MouseEvent); if (!Reply.IsEventHandled()) { TSharedPtr> OwnerTable = OwnerTablePtr.Pin(); if (OwnerTable) { const ESelectionMode::Type SelectionMode = GetSelectionMode(); if (MouseEvent.GetEffectingButton() == EKeys::LeftMouseButton && SelectionMode != ESelectionMode::None) { if (IsItemSelectable()) { const TObjectPtrWrapTypeOf* MyItemPtr = GetItemForThis(OwnerTable.ToSharedRef()); // New selections are handled on mouse down, deselection is handled on mouse up if (MyItemPtr) { const ItemType& MyItem = *MyItemPtr; const bool bIsSelected = OwnerTable->Private_IsItemSelected(MyItem); if (SelectionMode == ESelectionMode::Multi) { if (MouseEvent.IsShiftDown()) { OwnerTable->Private_SelectRangeFromCurrentTo(MyItem); bChangedSelectionOnMouseDown = true; } else if (MouseEvent.IsControlDown()) { OwnerTable->Private_SetItemSelection(MyItem, !bIsSelected, true); bChangedSelectionOnMouseDown = true; } } if (!bIsSelected && !bChangedSelectionOnMouseDown) { if (SelectionMode != ESelectionMode::Multi || !bAllowKeepPreselectedItems) { OwnerTable->Private_ClearSelection(); } OwnerTable->Private_SetItemSelection(MyItem, true, true); bChangedSelectionOnMouseDown = true; } } Reply = FReply::Handled() .DetectDrag(SharedThis(this), EKeys::LeftMouseButton) .CaptureMouse(SharedThis(this)); // Set focus back to the owning widget if the item is invalid somehow or its not selectable or can be navigated to if (!MyItemPtr || !OwnerTable->Private_IsItemSelectableOrNavigable(*MyItemPtr)) { Reply.SetUserFocus(OwnerTable->AsWidget(), EFocusCause::Mouse); } } } } } return Reply; } virtual FReply OnMouseButtonUp(const FGeometry& MyGeometry, const FPointerEvent& MouseEvent) override { FReply Reply = SObjectWidget::OnMouseButtonUp(MyGeometry, MouseEvent); if (!Reply.IsEventHandled()) { TSharedPtr> OwnerTable = OwnerTablePtr.Pin(); const TObjectPtrWrapTypeOf* MyItemPtr = OwnerTable ? GetItemForThis(OwnerTable.ToSharedRef()) : nullptr; if (MyItemPtr) { const ESelectionMode::Type SelectionMode = GetSelectionMode(); if (MouseEvent.GetEffectingButton() == EKeys::LeftMouseButton && HasMouseCapture()) { bool bSignalSelectionChanged = bChangedSelectionOnMouseDown; // Don't change selection on mouse up if it already changed on mouse down. Don't change the selection if we are dragging. if (!bChangedSelectionOnMouseDown && IsItemSelectable() && MyGeometry.IsUnderLocation(MouseEvent.GetScreenSpacePosition()) && !bDragWasDetected) { if (SelectionMode == ESelectionMode::SingleToggle) { OwnerTable->Private_ClearSelection(); bSignalSelectionChanged = true; } else if (SelectionMode == ESelectionMode::Multi && OwnerTable->Private_GetNumSelectedItems() > 1 && OwnerTable->Private_IsItemSelected(*MyItemPtr)) { if (!MouseEvent.IsControlDown() && !MouseEvent.IsShiftDown()) { // Releasing mouse over one of the multiple selected items - leave this one as the sole selected item OwnerTable->Private_ClearSelection(); OwnerTable->Private_SetItemSelection(*MyItemPtr, true, true); bSignalSelectionChanged = true; } } } if (bSignalSelectionChanged) { OwnerTable->Private_SignalSelectionChanged(ESelectInfo::OnMouseClick); Reply = FReply::Handled(); } if (OwnerTable->Private_OnItemClicked(*MyItemPtr)) { Reply = FReply::Handled(); } Reply = Reply.ReleaseMouseCapture(); } else if (SelectionMode != ESelectionMode::None && MouseEvent.GetEffectingButton() == EKeys::RightMouseButton) { // Ignore the right click release if it was being used for scrolling TSharedPtr OwnerTableViewBase = StaticCastSharedPtr>(OwnerTable); if (!OwnerTableViewBase->IsRightClickScrolling()) { if (IsItemSelectable() && !OwnerTable->Private_IsItemSelected(*MyItemPtr)) { // If this item isn't selected, it becomes the sole selected item. Otherwise we leave selection untouched. OwnerTable->Private_ClearSelection(); OwnerTable->Private_SetItemSelection(*MyItemPtr, true, true); OwnerTable->Private_SignalSelectionChanged(ESelectInfo::OnMouseClick); } OwnerTable->Private_OnItemRightClicked(*MyItemPtr, MouseEvent); Reply = FReply::Handled(); } } } } return Reply; } // ~SWidget interface bool GetAllowDragDrop() const { return bAllowDragDrop; } protected: virtual void InitializeObjectRow() { TSharedPtr> OwnerTable = OwnerTablePtr.Pin(); const TObjectPtrWrapTypeOf* MyItemPtr = OwnerTable ? GetItemForThis(OwnerTable.ToSharedRef()) : nullptr; if (MyItemPtr) { InitObjectRowInternal(*WidgetObject, *MyItemPtr); // Unselectable items should never be selected if (!ensure(!OwnerTable->Private_IsItemSelected(*MyItemPtr) || IsItemSelectable())) { OwnerTable->Private_SetItemSelection(*MyItemPtr, false, false); } } } virtual void ResetObjectRow() { bIsAppearingSelected = false; if (WidgetObject) { IUserListEntry::ReleaseEntry(*WidgetObject); } } virtual void DetectItemSelectionChanged() { // List views were built assuming the use of attributes on rows to check on selection status, so there is no // clean way to inform individual rows of changes to the selection state of their current items. // Since event-based selection changes are only really needed in a game scenario, we (crudely) monitor it here to generate events. // If desired, per-item selection events could be added as a longer-term todo TSharedPtr> OwnerTable = OwnerTablePtr.Pin(); const TObjectPtrWrapTypeOf* MyItemPtr = OwnerTable ? GetItemForThis(OwnerTable.ToSharedRef()) : nullptr; if (MyItemPtr) { if (bIsAppearingSelected != OwnerTable->Private_IsItemSelected(*MyItemPtr)) { bIsAppearingSelected = !bIsAppearingSelected; OnItemSelectionChanged(bIsAppearingSelected); } } } virtual void OnItemSelectionChanged(bool bIsItemSelected) { if (WidgetObject) { IUserListEntry::UpdateItemSelection(*WidgetObject, bIsItemSelected); } } bool IsItemSelectable() const { IUserListEntry* NativeListEntryImpl = Cast(WidgetObject); return NativeListEntryImpl ? NativeListEntryImpl->IsListItemSelectable() : true; } const TObjectPtrWrapTypeOf* GetItemForThis(const TSharedRef>& OwnerTable) const { const TObjectPtrWrapTypeOf* MyItemPtr = OwnerTable->Private_ItemFromWidget(this); if (MyItemPtr) { return MyItemPtr; } else { checkf(OwnerTable->Private_IsPendingRefresh(), TEXT("We were unable to find the item for this widget. If it was removed from the source collection, the list should be pending a refresh. %s"), *FReflectionMetaData::GetWidgetPath(this, false, false)); } return nullptr; } FOnRowHovered OnHovered; FOnRowHovered OnUnhovered; FOnObjectRowCanAcceptDrop OnRowCanAcceptDrop; FOnObjectRowAcceptDrop OnRowAcceptDrop; FOnObjectRowDragDetected OnRowDragDetected; FOnObjectRowDragEnter OnRowDragEnter; FOnObjectRowDragLeave OnRowDragLeave; FOnObjectRowDragCancelled OnRowDragCancelled; TWeakObjectPtr OwnerListView; TWeakPtr> OwnerTablePtr; private: void InitObjectRowInternal(UUserWidget& ListEntryWidget, ItemType ListItemObject) {} int32 IndexInList = INDEX_NONE; bool bChangedSelectionOnMouseDown = false; bool bIsAppearingSelected = false; bool bProcessingSelectionTouch = false; /** If true, when selecting an item via mouse button, we allow pre-selected items to remain selected */ bool bAllowKeepPreselectedItems = true; /** Whether to allow dragging of this item */ bool bAllowDragging; /** Whether to allow drag drop operations to be performed on the list */ bool bAllowDragDrop; /** Whether or not drag was detected */ bool bDragWasDetected = false; /** Are we currently dragging/dropping over this item? */ TOptional ItemDropZone; }; template <> inline void SObjectTableRow::InitObjectRowInternal(UUserWidget& ListEntryWidget, UObject* ListItemObject) { if (ListEntryWidget.Implements()) { IUserObjectListEntry::SetListItemObject(*WidgetObject, ListItemObject); } }