Skip to content
This repository has been archived by the owner on Nov 1, 2024. It is now read-only.

Commit

Permalink
samples: add highlight effects to TreeView drag-and-drop
Browse files Browse the repository at this point in the history
  • Loading branch information
alexandrehtrb committed Aug 8, 2024
1 parent d62d2aa commit 38a0a9e
Show file tree
Hide file tree
Showing 5 changed files with 226 additions and 36 deletions.
135 changes: 135 additions & 0 deletions samples/DragAndDropSample/Behaviors/BaseTreeViewDropHandler.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
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 DragAndDropSample.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);
// If its a movement within the same tree level,
// then an adorner layer will be applied.

// But, if the source item will move to a different level,
// the level's owner will receive a background highlight.

// In the case of being moved to a root target item,
// (with targetItem.Parent not being another TreeViewItem),
// then this root target item will receive this style.
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);
}
}
}
69 changes: 36 additions & 33 deletions samples/DragAndDropSample/Behaviors/NodesTreeViewDropHandler.cs
Original file line number Diff line number Diff line change
@@ -1,27 +1,35 @@
using Avalonia.Controls;
using Avalonia.Input;
using Avalonia.VisualTree;
using Avalonia.Xaml.Interactions.DragAndDrop;
using DragAndDropSample.ViewModels;

namespace DragAndDropSample.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 : NodeViewModel
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 NodeViewModel sourceNode
|| targetContext is not MainWindowViewModel 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 NodeViewModel targetNode
|| sourceNode == targetNode
|| sourceNode.Parent == 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,7 +38,7 @@ private bool Validate<T>(TreeView treeView, DragEventArgs e, object? sourceConte

if (sourceIndex < 0 || targetIndex < 0)
{
return false;
return (false, false);
}

switch (e.DragEffects)
Expand All @@ -43,25 +51,38 @@ private bool Validate<T>(TreeView treeView, DragEventArgs e, object? sourceConte
InsertItem(targetNodes, clone, targetIndex + 1);
}

return true;
return (true, areSourceNodesDifferentThanTargetNodes);
}
case DragDropEffects.Move:
{
if (bExecute)
{
if (sourceNodes == targetNodes)
{
MoveItem(sourceNodes, sourceIndex, targetIndex);
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;

MoveItem(sourceNodes, targetNodes, sourceIndex, targetIndex);
sourceNodes.RemoveAt(sourceIndex);
targetNodes.Add(sourceNode); // always adding to the end
}
}

return true;
return (true, areSourceNodesDifferentThanTargetNodes);
}
case DragDropEffects.Link:
{
Expand All @@ -80,29 +101,11 @@ private bool Validate<T>(TreeView treeView, DragEventArgs e, object? sourceConte
}
}

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<NodeViewModel>(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<NodeViewModel>(treeView, e, sourceContext, targetContext, true);
}
return false;
return (false, false);
}
}
18 changes: 17 additions & 1 deletion samples/DragAndDropSample/ViewModels/MainWindowViewModel.cs
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 DragAndDropSample.ViewModels;
Expand All @@ -20,6 +21,15 @@ public ObservableCollection<NodeViewModel> Nodes
set => this.RaiseAndSetIfChanged(ref _nodes, value);
}

public ObservableCollection<NodeViewModel> SelectedTreeNodes { get; }

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

public MainWindowViewModel()
{
_items = new ObservableCollection<ItemViewModel>()
Expand All @@ -31,6 +41,9 @@ public MainWindowViewModel()
new() { Title = "Item4" }
};

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

var node0 = new NodeViewModel()
{
Title = "Node0"
Expand Down Expand Up @@ -71,4 +84,7 @@ public MainWindowViewModel()
node2
};
}
}

private void OnSelectedTreeNodesChanged(object? sender, NotifyCollectionChangedEventArgs e) =>
HasMultipleTreeNodesSelected = SelectedTreeNodes.Count > 1;
}
15 changes: 14 additions & 1 deletion samples/DragAndDropSample/ViewModels/NodeViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,4 +28,17 @@ public ObservableCollection<NodeViewModel>? Nodes
}

public override string? ToString() => _title;
}

public bool IsDescendantOf(NodeViewModel possibleAncestor)
{
var current = Parent;
while (current is not null)
{
if (current == possibleAncestor)
return true;
else
current = current.Parent;
}
return false;
}
}
25 changes: 24 additions & 1 deletion samples/DragAndDropSample/Views/MainWindow.axaml
Original file line number Diff line number Diff line change
Expand Up @@ -90,12 +90,34 @@
<Setter Property="(i:Interaction.Behaviors)">
<i:BehaviorCollectionTemplate>
<i:BehaviorCollection>
<idd:ContextDragBehavior />
<b:ContextDragWithDirectionBehavior HorizontalDragThreshold="3" VerticalDragThreshold="3" />
</i:BehaviorCollection>
</i:BehaviorCollectionTemplate>
</Setter>
</Style>

<Style Selector="TreeView.NodesDragAndDrop TreeViewItem.DraggingUp">
<Setter Property="AdornerLayer.Adorner">
<Template>
<Border BorderThickness="0 2 0 0" BorderBrush="{DynamicResource SystemAccentColor}"/>
</Template>
</Setter>
</Style>

<Style Selector="TreeView.NodesDragAndDrop TreeViewItem.DraggingDown">
<Setter Property="AdornerLayer.Adorner">
<Template>
<Border BorderThickness="0 0 0 2" BorderBrush="{DynamicResource SystemAccentColor}"/>
</Template>
</Setter>
</Style>

<Style Selector="TreeViewItem.TargetHighlight">
<Setter
Property="Background"
Value="{DynamicResource TreeViewItemBackgroundPointerOver}"/>
</Style>

<Style Selector="DataGrid.DragAndDrop">
<Style.Resources>
<b:ItemsDataGridDropHandler x:Key="ItemsDataGridDropHandler" />
Expand Down Expand Up @@ -175,6 +197,7 @@
<TabItem Header="TreeView">
<Grid ColumnDefinitions="*,8,*">
<TreeView ItemsSource="{Binding Nodes}"
SelectedItems="{Binding SelectedTreeNodes, Mode=TwoWay}"
Classes="NodesDragAndDrop"
Grid.Column="0">
<TreeView.ItemTemplate>
Expand Down

0 comments on commit 38a0a9e

Please sign in to comment.