Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
133 changes: 133 additions & 0 deletions samples/BehaviorsTestApplication/Behaviors/BaseTreeViewDropHandler.cs
Original file line number Diff line number Diff line change
@@ -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<TreeViewItem>())
{
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);
}
}
}
120 changes: 61 additions & 59 deletions samples/BehaviorsTestApplication/Behaviors/NodesTreeViewDropHandler.cs
Original file line number Diff line number Diff line change
@@ -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<T>(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)
{
Expand All @@ -30,79 +37,74 @@ private bool Validate<T>(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<DragNodeViewModel>(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<DragNodeViewModel>(treeView, e, sourceContext, targetContext, true);
}
return false;
return (false, false);
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using ReactiveUI;

namespace BehaviorsTestApplication.ViewModels;
Expand All @@ -20,6 +21,15 @@ public ObservableCollection<DragNodeViewModel> Nodes
set => this.RaiseAndSetIfChanged(ref _nodes, value);
}

public ObservableCollection<DragNodeViewModel> SelectedTreeNodes { get; }

private bool _hasMultipleTreeNodesSelected;
public bool HasMultipleTreeNodesSelected
{
get => _hasMultipleTreeNodesSelected;
set => this.RaiseAndSetIfChanged(ref _hasMultipleTreeNodesSelected, value);
}

public DragAndDropSampleViewModel()
{
_items =
Expand All @@ -31,6 +41,9 @@ public DragAndDropSampleViewModel()
new() { Title = "Item4" }
];

SelectedTreeNodes = new();
SelectedTreeNodes.CollectionChanged += OnSelectedTreeNodesChanged;

var node0 = new DragNodeViewModel()
{
Title = "Node0"
Expand Down Expand Up @@ -71,4 +84,8 @@ public DragAndDropSampleViewModel()
node2
];
}

private void OnSelectedTreeNodesChanged(object? sender, NotifyCollectionChangedEventArgs e) =>
HasMultipleTreeNodesSelected = SelectedTreeNodes.Count > 1;

}
13 changes: 13 additions & 0 deletions samples/BehaviorsTestApplication/ViewModels/DragNodeViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,5 +27,18 @@ public ObservableCollection<DragNodeViewModel>? 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;
}
Loading