diff --git a/samples/BehaviorsTestApplication/Behaviors/BaseTreeViewDropHandler.cs b/samples/BehaviorsTestApplication/Behaviors/BaseTreeViewDropHandler.cs new file mode 100644 index 000000000..deb22be32 --- /dev/null +++ b/samples/BehaviorsTestApplication/Behaviors/BaseTreeViewDropHandler.cs @@ -0,0 +1,133 @@ +using System.Linq; +using Avalonia; +using Avalonia.Controls; +using Avalonia.Input; +using Avalonia.Interactivity; +using Avalonia.LogicalTree; +using Avalonia.VisualTree; +using Avalonia.Xaml.Interactions.DragAndDrop; + +namespace BehaviorsTestApplication.Behaviors; + +public abstract class BaseTreeViewDropHandler : DropHandlerBase +{ + private const string rowDraggingUpStyleClass = "DraggingUp"; + private const string rowDraggingDownStyleClass = "DraggingDown"; + private const string targetHighlightStyleClass = "TargetHighlight"; + + protected abstract (bool Valid, bool WillSourceItemBeMovedToDifferentParent) Validate(TreeView tv, DragEventArgs e, object? sourceContext, object? targetContext, bool bExecute); + + public override bool Validate(object? sender, DragEventArgs e, object? sourceContext, object? targetContext, object? state) + { + if (e.Source is Control && sender is TreeView tv) + { + var (valid, willSourceItemChangeParent) = Validate(tv, e, sourceContext, targetContext, false); + var targetVisual = tv.GetVisualAt(e.GetPosition(tv)); + if (valid) + { + var targetItem = FindTreeViewItemFromChildView(targetVisual); + + // Node dragged to non-sibling node: becomes sibling + // Node dragged to sibling node: reordering + + // Reordering effect: adorner layer, on top or bottom. + // Change of parent: highlight parent. + + var itemToApplyStyle = (willSourceItemChangeParent && targetItem?.Parent is TreeViewItem tviParent) ? + tviParent : targetItem; + string direction = e.Data.Contains("direction") ? (string)e.Data.Get("direction")! : "down"; + ApplyDraggingStyleToItem(itemToApplyStyle!, direction, willSourceItemChangeParent); + ClearDraggingStyleFromAllItems(sender, exceptThis: itemToApplyStyle); + } + return valid; + } + ClearDraggingStyleFromAllItems(sender); + return false; + } + + public override bool Execute(object? sender, DragEventArgs e, object? sourceContext, object? targetContext, object? state) + { + ClearDraggingStyleFromAllItems(sender); + if (e.Source is Control && sender is TreeView tv) + { + var (valid, _) = Validate(tv, e, sourceContext, targetContext, true); + return valid; + } + return false; + } + + public override void Cancel(object? sender, RoutedEventArgs e) + { + base.Cancel(sender, e); + // This is necessary to clear styles + // when mouse exists TreeView, else, + // they would remain even after changing screens. + ClearDraggingStyleFromAllItems(sender); + } + + private static TreeViewItem? FindTreeViewItemFromChildView(StyledElement? sourceChild) + { + if (sourceChild is null) + return null; + + int maxDepth = 16; + StyledElement? current = sourceChild; + while (maxDepth --> 0) + { + if (current is TreeViewItem tvi) + return tvi; + else + current = current?.Parent; + } + return null; + } + + private static void ClearDraggingStyleFromAllItems(object? sender, TreeViewItem? exceptThis = null) + { + if (sender is not Visual rootVisual) + return; + + foreach (var item in rootVisual.GetLogicalChildren().OfType()) + { + if (item == exceptThis) + continue; + + if (item.Classes is not null) + { + item.Classes.Remove(rowDraggingUpStyleClass); + item.Classes.Remove(rowDraggingDownStyleClass); + item.Classes.Remove(targetHighlightStyleClass); + } + ClearDraggingStyleFromAllItems(item, exceptThis); + } + } + + private static void ApplyDraggingStyleToItem(TreeViewItem? item, string direction, bool willSourceItemBeMovedToDifferentParent) + { + if (item is null) + return; + + // Avalonia's Classes.Add() verifies + // if a class has already been added + // (avoiding duplications); no need to + // verify .Contains() here. + if (willSourceItemBeMovedToDifferentParent) + { + item.Classes.Remove(rowDraggingDownStyleClass); + item.Classes.Remove(rowDraggingUpStyleClass); + item.Classes.Add(targetHighlightStyleClass); + } + else if (direction == "up") + { + item.Classes.Remove(rowDraggingDownStyleClass); + item.Classes.Remove(targetHighlightStyleClass); + item.Classes.Add(rowDraggingUpStyleClass); + } + else if (direction == "down") + { + item.Classes.Remove(rowDraggingUpStyleClass); + item.Classes.Remove(targetHighlightStyleClass); + item.Classes.Add(rowDraggingDownStyleClass); + } + } +} diff --git a/samples/BehaviorsTestApplication/Behaviors/NodesTreeViewDropHandler.cs b/samples/BehaviorsTestApplication/Behaviors/NodesTreeViewDropHandler.cs index fce61b996..5fcb41b88 100644 --- a/samples/BehaviorsTestApplication/Behaviors/NodesTreeViewDropHandler.cs +++ b/samples/BehaviorsTestApplication/Behaviors/NodesTreeViewDropHandler.cs @@ -1,27 +1,34 @@ using Avalonia.Controls; using Avalonia.Input; using Avalonia.VisualTree; -using Avalonia.Xaml.Interactions.DragAndDrop; using BehaviorsTestApplication.ViewModels; namespace BehaviorsTestApplication.Behaviors; -public class NodesTreeViewDropHandler : DropHandlerBase +public class NodesTreeViewDropHandler : BaseTreeViewDropHandler { - private bool Validate(TreeView treeView, DragEventArgs e, object? sourceContext, object? targetContext, bool bExecute) where T : DragNodeViewModel + protected override (bool Valid, bool WillSourceItemBeMovedToDifferentParent) Validate(TreeView tv, DragEventArgs e, object? sourceContext, object? targetContext, bool bExecute) { - if (sourceContext is not T sourceNode + if (sourceContext is not DragNodeViewModel sourceNode || targetContext is not DragAndDropSampleViewModel vm - || treeView.GetVisualAt(e.GetPosition(treeView)) is not Control targetControl - || targetControl.DataContext is not T targetNode) + || tv.GetVisualAt(e.GetPosition(tv)) is not Control targetControl + || targetControl.DataContext is not DragNodeViewModel targetNode + || sourceNode == targetNode + || targetNode.IsDescendantOf(sourceNode) // block moving parent to inside child + || vm.HasMultipleTreeNodesSelected) { - return false; + // moving multiple items is disabled because + // when an item is clicked to be dragged (whilst pressing Ctrl), + // it becomes unselected and won't be considered for movement. + // TODO: find how to fix that. + return (false, false); } var sourceParent = sourceNode.Parent; var targetParent = targetNode.Parent; var sourceNodes = sourceParent is not null ? sourceParent.Nodes : vm.Nodes; var targetNodes = targetParent is not null ? targetParent.Nodes : vm.Nodes; + bool areSourceNodesDifferentThanTargetNodes = sourceNodes != targetNodes; if (sourceNodes is not null && targetNodes is not null) { @@ -30,79 +37,74 @@ private bool Validate(TreeView treeView, DragEventArgs e, object? sourceConte if (sourceIndex < 0 || targetIndex < 0) { - return false; + return (false, false); } switch (e.DragEffects) { case DragDropEffects.Copy: - { - if (bExecute) { - var clone = new DragNodeViewModel() { Title = sourceNode.Title + "_copy" }; - InsertItem(targetNodes, clone, targetIndex + 1); - } + if (bExecute) + { + var clone = new DragNodeViewModel() { Title = sourceNode.Title + "_copy" }; + InsertItem(targetNodes, clone, targetIndex + 1); + } - return true; - } + return (true, areSourceNodesDifferentThanTargetNodes); + } case DragDropEffects.Move: - { - if (bExecute) { - if (sourceNodes == targetNodes) + if (bExecute) { - MoveItem(sourceNodes, sourceIndex, targetIndex); + if (sourceNodes == targetNodes) + { + if (sourceIndex < targetIndex) + { + sourceNodes.RemoveAt(sourceIndex); + sourceNodes.Insert(targetIndex, sourceNode); + } + else + { + int removeIndex = sourceIndex + 1; + if (sourceNodes.Count + 1 > removeIndex) + { + sourceNodes.RemoveAt(removeIndex - 1); + sourceNodes.Insert(targetIndex, sourceNode); + } + } + } + else + { + sourceNode.Parent = targetParent; + sourceNodes.RemoveAt(sourceIndex); + targetNodes.Add(sourceNode); // always adding to the end + } } - else - { - sourceNode.Parent = targetParent; - MoveItem(sourceNodes, targetNodes, sourceIndex, targetIndex); - } + return (true, areSourceNodesDifferentThanTargetNodes); } - - return true; - } case DragDropEffects.Link: - { - if (bExecute) { - if (sourceNodes == targetNodes) + if (bExecute) { - SwapItem(sourceNodes, sourceIndex, targetIndex); - } - else - { - sourceNode.Parent = targetParent; - targetNode.Parent = sourceParent; + if (sourceNodes == targetNodes) + { + SwapItem(sourceNodes, sourceIndex, targetIndex); + } + else + { + sourceNode.Parent = targetParent; + targetNode.Parent = sourceParent; - SwapItem(sourceNodes, targetNodes, sourceIndex, targetIndex); + SwapItem(sourceNodes, targetNodes, sourceIndex, targetIndex); + } } - } - return true; - } + return (true, areSourceNodesDifferentThanTargetNodes); + } } } - return false; - } - - public override bool Validate(object? sender, DragEventArgs e, object? sourceContext, object? targetContext, object? state) - { - if (e.Source is Control && sender is TreeView treeView) - { - return Validate(treeView, e, sourceContext, targetContext, false); - } - return false; - } - - public override bool Execute(object? sender, DragEventArgs e, object? sourceContext, object? targetContext, object? state) - { - if (e.Source is Control && sender is TreeView treeView) - { - return Validate(treeView, e, sourceContext, targetContext, true); - } - return false; + return (false, false); } } diff --git a/samples/BehaviorsTestApplication/ViewModels/DragAndDropSampleViewModel.cs b/samples/BehaviorsTestApplication/ViewModels/DragAndDropSampleViewModel.cs index 68b8dbfe3..35bf17d08 100644 --- a/samples/BehaviorsTestApplication/ViewModels/DragAndDropSampleViewModel.cs +++ b/samples/BehaviorsTestApplication/ViewModels/DragAndDropSampleViewModel.cs @@ -1,4 +1,5 @@ using System.Collections.ObjectModel; +using System.Collections.Specialized; using ReactiveUI; namespace BehaviorsTestApplication.ViewModels; @@ -20,6 +21,15 @@ public ObservableCollection Nodes set => this.RaiseAndSetIfChanged(ref _nodes, value); } + public ObservableCollection SelectedTreeNodes { get; } + + private bool _hasMultipleTreeNodesSelected; + public bool HasMultipleTreeNodesSelected + { + get => _hasMultipleTreeNodesSelected; + set => this.RaiseAndSetIfChanged(ref _hasMultipleTreeNodesSelected, value); + } + public DragAndDropSampleViewModel() { _items = @@ -31,6 +41,9 @@ public DragAndDropSampleViewModel() new() { Title = "Item4" } ]; + SelectedTreeNodes = new(); + SelectedTreeNodes.CollectionChanged += OnSelectedTreeNodesChanged; + var node0 = new DragNodeViewModel() { Title = "Node0" @@ -71,4 +84,8 @@ public DragAndDropSampleViewModel() node2 ]; } + + private void OnSelectedTreeNodesChanged(object? sender, NotifyCollectionChangedEventArgs e) => + HasMultipleTreeNodesSelected = SelectedTreeNodes.Count > 1; + } diff --git a/samples/BehaviorsTestApplication/ViewModels/DragNodeViewModel.cs b/samples/BehaviorsTestApplication/ViewModels/DragNodeViewModel.cs index d2a31688b..4fc853f63 100644 --- a/samples/BehaviorsTestApplication/ViewModels/DragNodeViewModel.cs +++ b/samples/BehaviorsTestApplication/ViewModels/DragNodeViewModel.cs @@ -27,5 +27,18 @@ public ObservableCollection? Nodes set => this.RaiseAndSetIfChanged(ref _nodes, value); } + public bool IsDescendantOf(DragNodeViewModel possibleAncestor) + { + var current = Parent; + while (current is not null) + { + if (current == possibleAncestor) + return true; + else + current = current.Parent; + } + return false; + } + public override string? ToString() => _title; } diff --git a/samples/BehaviorsTestApplication/Views/Pages/DragAndDropView.axaml b/samples/BehaviorsTestApplication/Views/Pages/DragAndDropView.axaml index 8b6ef942c..7f88c0791 100644 --- a/samples/BehaviorsTestApplication/Views/Pages/DragAndDropView.axaml +++ b/samples/BehaviorsTestApplication/Views/Pages/DragAndDropView.axaml @@ -86,12 +86,34 @@ - + + + + + + +