Skip to content

Commit

Permalink
feat(dragdrop): Add re-order support in ListView (managed only for no…
Browse files Browse the repository at this point in the history
…w, i.e. wasm + skia)
  • Loading branch information
dr1rrb committed Dec 1, 2020
1 parent c2003d4 commit b79f6d8
Show file tree
Hide file tree
Showing 5 changed files with 308 additions and 27 deletions.
173 changes: 167 additions & 6 deletions src/Uno.UI/UI/Xaml/Controls/ListViewBase/ListViewBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
using Uno.Extensions.Specialized;
using System.Collections;
using System.Linq;
using System.Reflection;
using Windows.UI.Xaml.Controls.Primitives;
using Uno.Logging;
using Uno.Disposables;
Expand Down Expand Up @@ -656,7 +657,10 @@ protected override void PrepareContainerForItemOverride(DependencyObject element
}
}

private const string DragItemsFormatId = DataPackage.UnoPrivateDataPrefix + "__dragged__items__";
private const string ReorderOwnerFormatId = DataPackage.UnoPrivateDataPrefix + "__list__view__base__source__";
private const string ReorderItemFormatId = DataPackage.UnoPrivateDataPrefix + "__list__view__base__source__item__";
private const string ReorderContainerFormatId = DataPackage.UnoPrivateDataPrefix + "__list__view__base__source__container__";
private const string DragItemsFormatId = DataPackage.UnoPrivateDataPrefix + "__list__view__base__items__";

internal override void ContainerPreparedForItem(object item, SelectorItem itemContainer, int itemIndex)
{
Expand Down Expand Up @@ -712,29 +716,186 @@ private static void OnItemContainerDragStarting(UIElement sender, DragStartingEv
if (ItemsControlFromItemContainer(sender) is ListViewBase that && that.CanDragItems)
{
var items = that.SelectedItems.ToList();
if (that.ItemFromContainer(sender) is {} draggedItem && !items.Contains(draggedItem))
var draggedItem = that.ItemFromContainer(sender);
if (draggedItem is { } && !items.Contains(draggedItem))
{
items.Add(draggedItem);
}

var args = new DragItemsStartingEventArgs(innerArgs, items);

that.DragItemsStarting?.Invoke(that, args);

// The application has the ability to add some items in the list, so make sure to freeze it only after event has been raised.
args.Data.SetData(DragItemsFormatId, args.Items.ToList());

// The ListView must have both CanReorderItems and AllowDrop flags set to allow re-ordering (UWP)
// We also do not allow re-ordering if we where not able to find the item (as it has to be hidden in the view) (Uno only)
if (that.CanReorderItems && that.AllowDrop && draggedItem is {})
{
args.Data.SetData(ReorderOwnerFormatId, that);
args.Data.SetData(ReorderItemFormatId, draggedItem);
args.Data.SetData(ReorderContainerFormatId, sender);

that.DragEnter += OnReorderUpdated;
that.DragOver += OnReorderUpdated;
that.DragLeave += OnReorderCompleted;
that.Drop += OnReorderCompleted;
}
}
}

private static void OnReorderUpdated(object sender, DragEventArgs dragEventArgs)
{
var that = sender as ListView;
var src = dragEventArgs.DataView.FindRawData(ReorderOwnerFormatId) as ListView;
var item = dragEventArgs.DataView.FindRawData(ReorderItemFormatId);
var container = dragEventArgs.DataView.FindRawData(ReorderContainerFormatId) as FrameworkElement; // TODO: This might have changed/been recycled if scrolled
if (that is null || src is null || item is null || container is null || src != that)
{
dragEventArgs.Log().Warn("Invalid reorder event.");

return;
}

that.UpdateReordering(dragEventArgs.GetPosition(that), container, item);
}

private static void OnReorderCompleted(object sender, DragEventArgs dragEventArgs)
{
var that = sender as ListView;
var src = dragEventArgs.DataView.FindRawData(ReorderOwnerFormatId) as ListView;
var item = dragEventArgs.DataView.FindRawData(ReorderItemFormatId);
var container = dragEventArgs.DataView.FindRawData(ReorderContainerFormatId) as FrameworkElement; // TODO: This might have changed/been recycled if scrolled
if (that is null || src is null || item is null || container is null || src != that)
{
dragEventArgs.Log().Warn("Invalid reorder event.");

return;
}

var updatedIndex = default(IndexPath?);
that.CompleteReordering(container, item, ref updatedIndex);

if (!that.IsGrouping
&& updatedIndex.HasValue
&& dragEventArgs.DataView.FindRawData(DragItemsFormatId) is List<object> movedItems)
{
if (that.UnwrapItemsSource() is {} unwrappedSource)
{
// The UWP ListView seems to automatically push back changes only if the ItemsSource inherits from ObservableCollection
var srcType = unwrappedSource.GetType();
if (srcType.IsGenericType
&& srcType.GetGenericTypeDefinition() == typeof(ObservableCollection<>)
&& srcType.GetMethod(nameof(ObservableCollection<object>.Move), new[] { typeof(int), typeof(int) }) is {} mv)
{
ProcessMove(
((ICollection)unwrappedSource).Count,
((ICollection)unwrappedSource).IndexOf,
(oldIndex, newIndex) => mv.Invoke(unwrappedSource, new object[] { oldIndex, newIndex }));
}
}
else // The ListView was created with items defined in XAML
{
var items = that.Items;
ProcessMove(
items.Count,
items.IndexOf,
(oldIndex, newIndex) =>
{
var item = items[oldIndex];
items.RemoveAt(oldIndex);
if (newIndex >= items.Count)
{
items.Add(item);
}
else
{
items.Insert(newIndex, item);
}
});
}

void ProcessMove(
int count,
Func<object, int> indexOf,
Action<int, int> mv)
{
var indexOfDraggedItem = indexOf(item);
if (indexOfDraggedItem < 0)
{
return;
}

int newIndex;
if (updatedIndex.Value.Row == int.MaxValue)
{
// I.e. we are at the end, there is no items below
newIndex = count - 1;
}
else
{
newIndex = that.GetIndexFromIndexPath(updatedIndex.Value);
if (indexOfDraggedItem < newIndex)
{
// If we've moved items down, we have to take in consideration that the updatedIndex
// is already assuming that the item has been removed, so it's offsetted by 1.
newIndex--;
}
}

for (var i = 0; i < movedItems.Count; i++)
{
var movedItem = movedItems[i];
var oldIndex = indexOf(movedItem);

if (oldIndex < 0 || oldIndex == newIndex)
{
continue; // Item removed or already at the right place, nothing to do.
}

mv(oldIndex, newIndex);

if (oldIndex > newIndex)
{
newIndex++;
}
}
}
}
}

/// <summary>
/// Update reordering information
/// </summary>
/// <param name="location">The current location of the pointer use to reordering items, in the ListView coordinates space</param>
/// <param name="draggedContainer">The container that has been clicked to initiate the reordering (i.e. drag) operation (cf. remarks)</param>
/// <param name="draggedItem">The item that has been clicked to initiate the reordering (i.e. drag) operation (cf. remarks)</param>
/// <remarks>
/// If the SelectionMode is not None or Single, the draggedItem/Container might not be the single that is being reordered.
/// However, UWP hides in the ListView only the item that is being clicked by the user to initiate the reorder / drag operation.
/// </remarks>
partial void UpdateReordering(Point location, FrameworkElement draggedContainer, object draggedItem);
partial void CompleteReordering(FrameworkElement draggedContainer, object draggedItem, ref IndexPath? updatedIndex);

private static void OnItemContainerDragCompleted(UIElement sender, DropCompletedEventArgs innerArgs)
{
// Note: It's not the responsibility of the ListView to remove item from the source, not matter the AcceptedOperation.

if (ItemsControlFromItemContainer(sender) is ListViewBase that && that.CanDragItems)
if (ItemsControlFromItemContainer(sender) is ListViewBase that)
{
var items = innerArgs.Info.Data.FindRawData(DragItemsFormatId) as IReadOnlyList<object> ?? new List<object>(0);
var args = new DragItemsCompletedEventArgs(innerArgs, items);
if (that.CanDragItems)
{
var items = innerArgs.Info.Data.FindRawData(DragItemsFormatId) as IReadOnlyList<object> ?? new List<object>(0);
var args = new DragItemsCompletedEventArgs(innerArgs, items);

that.DragItemsCompleted?.Invoke(that, args);
}

that.DragItemsCompleted?.Invoke(that, args);
that.DragEnter -= OnReorderUpdated;
that.DragOver -= OnReorderUpdated;
that.DragLeave -= OnReorderCompleted;
that.Drop -= OnReorderCompleted;
}
}

Expand Down
11 changes: 11 additions & 0 deletions src/Uno.UI/UI/Xaml/Controls/ListViewBase/ListViewBase.managed.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
#pragma warning disable 114 // new keyword hiding
using System;
using System.Collections.Generic;
using Windows.Foundation;
using Windows.UI.Xaml.Controls.Primitives;
using Windows.UI.Xaml.Data;

Expand Down Expand Up @@ -73,6 +74,16 @@ private void TryLoadMoreItems()
{
//TODO: ISupportIncrementalLoading
}

partial void UpdateReordering(Point location, FrameworkElement draggedContainer, object draggedItem)
{
VirtualizingPanel?.GetLayouter().UpdateReorderingItem(location, draggedContainer, draggedItem);
}

partial void CompleteReordering(FrameworkElement draggedContainer, object draggedItem, ref Uno.UI.IndexPath? updatedIndex)
{
updatedIndex = VirtualizingPanel?.GetLayouter().CompleteReorderingItem(draggedContainer, draggedItem);
}
}
}
#endif
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,67 @@ abstract partial class VirtualizingPanelLayout : IScrollSnapPointsInfo

protected enum RelativeHeaderPlacement { Inline, Adjacent }

private (double offset, double breadth, object item, Uno.UI.IndexPath? index)? _pendingReorder;
internal void UpdateReorderingItem(Point location, FrameworkElement element, object item)
{
_pendingReorder = Orientation == Orientation.Horizontal
? (location.X + ScrollOffset, element.ActualWidth, item, default(Uno.UI.IndexPath?))
: (location.Y + ScrollOffset, element.ActualHeight, item, default(Uno.UI.IndexPath?));

LightRefresh();
}

internal Uno.UI.IndexPath? CompleteReorderingItem(FrameworkElement element, object item)
{
var updatedIndex = default(Uno.UI.IndexPath?);
if (_pendingReorder?.index is {} index)
{
var nextItem = _materializedLines
.SelectMany(line => line.Items)
.SkipWhile(i => i.index != index)
.Skip(1)
.FirstOrDefault();

updatedIndex = nextItem.container is null
? Uno.UI.IndexPath.FromRowSection(int.MaxValue, int.MaxValue) // There is no "nextItem", i.e. the item has been moved at the end.
: nextItem.index;
}
_pendingReorder = null;

// We need a full refresh to properly re-arrange all items at their right location,
// ignoring the temp location of the dragged / reordered item.
Refresh();

return updatedIndex;
}

protected bool ShouldInsertReorderingView(double extentOffset)
=> _pendingReorder is { } reorder && reorder.offset > extentOffset && reorder.offset <= extentOffset + reorder.breadth;

protected IndexPath? GetReorderingIndex()
{
if (_pendingReorder is { } reorder)
{
if (reorder.index is null)
{
reorder.index = XamlParent!.GetIndexPathFromItem(reorder.item);
_pendingReorder = reorder; // _pendingReorder is a struct!
}

return reorder.index;
}

return null;
}

private void ResetReorderingIndex()
{
if (_pendingReorder is { } reorder)
{
_pendingReorder = (reorder.offset, reorder.breadth, reorder.item, null);
}
}

/// <summary>
/// The direction of scroll.
/// </summary>
Expand Down Expand Up @@ -225,7 +286,17 @@ public bool ShouldBreadthStretch
/// </summary>
protected Uno.UI.IndexPath? GetNextUnmaterializedItem(GeneratorDirection fillDirection, Uno.UI.IndexPath? currentMaterializedItem)
{
return XamlParent?.GetNextItemIndex(currentMaterializedItem, fillDirection == GeneratorDirection.Forward ? 1 : -1);
var direction = fillDirection == GeneratorDirection.Forward ? 1 : -1;
var index = XamlParent?.GetNextItemIndex(currentMaterializedItem, direction);

// We consider the pending reorder item as non materializable and we ignore it while filling the layout.
// It's then the responsibility of the layout to render it at the appropriate place
if (index is {} && GetReorderingIndex() is {} reorderIndex && index == reorderIndex)
{
index = XamlParent?.GetNextItemIndex(index, direction);
}

return index;
}

// Note that Item1 is used instead of Item to work around an issue
Expand Down
Loading

0 comments on commit b79f6d8

Please sign in to comment.