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

samples: add highlight effects to TreeView drag-and-drop #174

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
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
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
Loading