diff --git a/README.md b/README.md index 5bfd7a12..d6304c92 100644 --- a/README.md +++ b/README.md @@ -96,3 +96,5 @@ Does your company use `GongSolutions.WPF.DragDrop`? Ask your manager or marketi ![screenshot04](./screenshots/2016-09-03_00h53_21.png) ![gif02](./screenshots/DragDropSample01.gif) + +![gif03](./screenshots/DragHint-Demo.gif) diff --git a/screenshots/DragHint-Demo.gif b/screenshots/DragHint-Demo.gif new file mode 100644 index 00000000..2e5ac0bd Binary files /dev/null and b/screenshots/DragHint-Demo.gif differ diff --git a/src/GongSolutions.WPF.DragDrop/DefaultDropHandler.cs b/src/GongSolutions.WPF.DragDrop/DefaultDropHandler.cs index 7d025b79..ab0a9ccc 100644 --- a/src/GongSolutions.WPF.DragDrop/DefaultDropHandler.cs +++ b/src/GongSolutions.WPF.DragDrop/DefaultDropHandler.cs @@ -61,6 +61,11 @@ public static bool CanAcceptData(IDropInfo dropInfo) public static IEnumerable ExtractData(object data) { + if (data == null) + { + return Enumerable.Empty(); + } + if (data is IEnumerable enumerable and not string) { return enumerable; @@ -200,6 +205,11 @@ protected static bool IsChildOf(UIElement targetItem, UIElement sourceItem) protected static bool TestCompatibleTypes(IEnumerable target, object data) { + if (data == null) + { + return false; + } + bool InterfaceFilter(Type t, object o) => (t.IsGenericType && t.GetGenericTypeDefinition() == typeof(IEnumerable<>)); var enumerableInterfaces = target.GetType().FindInterfaces(InterfaceFilter, null); @@ -217,6 +227,11 @@ protected static bool TestCompatibleTypes(IEnumerable target, object data) } } + public virtual void DropHint(IDropHintInfo dropHintInfo) + { + dropHintInfo.DropTargetHintAdorner = DropTargetAdorners.Hint; + } + #if !NETCOREAPP3_1_OR_GREATER /// public void DragEnter(IDropInfo dropInfo) @@ -234,6 +249,15 @@ public virtual void DragOver(IDropInfo dropInfo) dropInfo.Effects = copyData ? DragDropEffects.Copy : DragDropEffects.Move; var isTreeViewItem = dropInfo.InsertPosition.HasFlag(RelativeInsertPosition.TargetItemCenter) && dropInfo.VisualTargetItem is TreeViewItem; dropInfo.DropTargetAdorner = isTreeViewItem ? DropTargetAdorners.Highlight : DropTargetAdorners.Insert; + + dropInfo.DropTargetHintState = DropHintState.Active; + dropInfo.DropTargetHintAdorner = DropTargetAdorners.Hint; + } + else + { + dropInfo.Effects = DragDropEffects.None; + dropInfo.DropTargetHintAdorner = DropTargetAdorners.Hint; + dropInfo.DropTargetHintState = DropHintState.Error; } } diff --git a/src/GongSolutions.WPF.DragDrop/DragDrop.Properties.cs b/src/GongSolutions.WPF.DragDrop/DragDrop.Properties.cs index 04ade227..a2efde8e 100644 --- a/src/GongSolutions.WPF.DragDrop/DragDrop.Properties.cs +++ b/src/GongSolutions.WPF.DragDrop/DragDrop.Properties.cs @@ -571,6 +571,31 @@ public static void SetDropTargetAdornerBrush(DependencyObject element, Brush val element.SetValue(DropTargetAdornerBrushProperty, value); } + /// + /// Gets or sets the brush to use for the . + /// + public static readonly DependencyProperty DropTargetHighlightBrushProperty = DependencyProperty.RegisterAttached( + "DropTargetHighlightBrush", typeof(Brush), typeof(DragDrop), new PropertyMetadata(default(Brush))); + + /// Helper for setting on . + /// to set on. + /// The brush to use for the background of . + /// Sets the brush for the DropTargetAdorner. + [AttachedPropertyBrowsableForType(typeof(UIElement))] + public static void SetDropTargetHighlightBrush(DependencyObject element, Brush value) + { + element.SetValue(DropTargetHighlightBrushProperty, value); + } + + /// Helper for setting on . + /// to set on. + /// Sets the brush for the DropTargetAdorner. + [AttachedPropertyBrowsableForType(typeof(UIElement))] + public static Brush GetDropTargetHighlightBrush(DependencyObject element) + { + return (Brush)element.GetValue(DropTargetHighlightBrushProperty); + } + /// /// Gets or sets the pen for the DropTargetAdorner. /// @@ -720,6 +745,84 @@ public static void SetDragDropCopyKeyState(DependencyObject element, DragDropKey element.SetValue(DragDropCopyKeyStateProperty, value); } + /// + /// Data template for displaying drop hint. + /// + public static readonly DependencyProperty DropHintDataTemplateProperty = DependencyProperty.RegisterAttached( + "DropHintDataTemplate", typeof(DataTemplate), typeof(DragDrop)); + + /// + /// Helper method for setting the on the given . + /// This property is used when is set to true to display hint overlay + /// + /// The element to set the drop hint template for + /// The to display + [AttachedPropertyBrowsableForType(typeof(UIElement))] + public static void SetDropHintDataTemplate(DependencyObject element, DataTemplate value) + { + element.SetValue(DropHintDataTemplateProperty, value); + } + + [AttachedPropertyBrowsableForType(typeof(UIElement))] + public static DataTemplate GetDropHintDataTemplate(DependencyObject element) + { + return (DataTemplate)element.GetValue(DropHintDataTemplateProperty); + } + + /// + /// Get or set whether drop target hint is used to indicate where the user can drop. + /// + public static readonly DependencyProperty UseDropTargetHintProperty + = DependencyProperty.RegisterAttached("UseDropTargetHint", + typeof(bool), + typeof(DragDrop), + new PropertyMetadata(default(bool), OnUseDropTargetHintChanged)); + + /// Helper for setting on . + /// to set on. + /// UseDropTargetHintProperty property value. + /// Sets whether the hint adorner should be displayed. + [AttachedPropertyBrowsableForType(typeof(UIElement))] + public static void SetUseDropTargetHint(DependencyObject element, bool value) + { + element.SetValue(UseDropTargetHintProperty, value); + } + + /// Helper for getting from . + /// to read from. + /// Gets whether if the default DragAdorner is used. + /// UseDropTargetHintProperty property value. + [AttachedPropertyBrowsableForType(typeof(UIElement))] + public static bool GetUseDropTargetHint(DependencyObject element) + { + return (bool)element.GetValue(UseDropTargetHintProperty); + } + + /// + /// Implements side effects for when the UseDropTargetHintProperty changes. + /// + /// + /// + private static void OnUseDropTargetHintChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + var dropTarget = d as UIElement; + if (dropTarget == null) + { + return; + } + + // Add or remove drop target from hint cache. + bool useDropTargetHint = (bool)e.NewValue; + if (useDropTargetHint) + { + DropHintHelpers.AddDropHintTarget(dropTarget); + } + else + { + DropHintHelpers.RemoveDropHintTarget(dropTarget); + } + } + /// /// Gets or sets whether if the default DragAdorner should be use. /// diff --git a/src/GongSolutions.WPF.DragDrop/DragDrop.cs b/src/GongSolutions.WPF.DragDrop/DragDrop.cs index d1b5d8a5..19261e88 100644 --- a/src/GongSolutions.WPF.DragDrop/DragDrop.cs +++ b/src/GongSolutions.WPF.DragDrop/DragDrop.cs @@ -14,6 +14,21 @@ namespace GongSolutions.Wpf.DragDrop { public static partial class DragDrop { + /// + /// Get the for the drop hint, or return the default template if not set. + /// + /// + /// + internal static DataTemplate TryGetDropHintDataTemplate(UIElement sender) + { + if (sender == null) + { + return null; + } + + return GetDropHintDataTemplate(sender) ?? DropHintHelpers.GetDefaultDropHintTemplate(); + } + /// /// Gets the drag handler from the drag info or from the sender, if the drag info is null /// @@ -33,7 +48,7 @@ private static IDragSource TryGetDragHandler(IDragInfo dragInfo, UIElement sende /// the drop info object /// the sender from an event, e.g. drag over /// - private static IDropTarget TryGetDropHandler(IDropInfo dropInfo, UIElement sender) + internal static IDropTarget TryGetDropHandler(IDropInfo dropInfo, UIElement sender) { var dropHandler = (dropInfo?.VisualTarget != null ? GetDropHandler(dropInfo.VisualTarget) : null) ?? (sender != null ? GetDropHandler(sender) : null); @@ -55,7 +70,7 @@ private static IDragInfoBuilder TryGetDragInfoBuilder(DependencyObject sender) /// /// the sender from an event, e.g. drag over /// - private static IDropInfoBuilder TryGetDropInfoBuilder(DependencyObject sender) + internal static IDropInfoBuilder TryGetDropInfoBuilder(DependencyObject sender) { return sender != null ? GetDropInfoBuilder(sender) : null; } @@ -650,6 +665,7 @@ private static void DoDragSourceMove(object sender, Func g } }); + DropHintHelpers.OnDragStart(dragInfo); var dragDropHandler = dragInfo.DragDropHandler ?? System.Windows.DragDrop.DoDragDrop; var dragDropEffects = dragDropHandler(dragInfo.VisualSource, dataObject, dragInfo.Effects); if (dragDropEffects == DragDropEffects.None) @@ -657,6 +673,7 @@ private static void DoDragSourceMove(object sender, Func g dragHandler.DragCancelled(); } + DropHintHelpers.OnDropFinished(); dragHandler.DragDropOperationFinished(dragDropEffects, dragInfo); } catch (Exception ex) @@ -685,6 +702,7 @@ private static void DragSourceOnQueryContinueDrag(object sender, QueryContinueDr DragDropPreview = null; DragDropEffectPreview = null; DropTargetAdorner = null; + DropHintHelpers.OnDropFinished(); Mouse.OverrideCursor = null; } } @@ -719,7 +737,14 @@ private static void OnRealTargetDragLeave(object sender, DragEventArgs e) var dropInfo = dropInfoBuilder?.CreateDropInfo(sender, e, dragInfo, eventType) ?? new DropInfo(sender, e, dragInfo, eventType); var dropHandler = TryGetDropHandler(dropInfo, sender as UIElement); - dropHandler?.DragLeave(dropInfo); + if(dropHandler != null) + { + dropHandler.DragLeave(dropInfo); + if(_dragInProgress) + { + DropHintHelpers.OnDragLeave(sender, dropHandler, dragInfo); + } + } DragDropEffectPreview = null; DropTargetAdorner = null; @@ -764,6 +789,7 @@ private static void DropTargetOnDragOver(object sender, DragEventArgs e, EventTy } dropHandler.DragOver(dropInfo); + DropHintHelpers.DragOver(sender, dropInfo); if (dragInfo is not null) { @@ -835,6 +861,15 @@ private static void DropTargetOnDragOver(object sender, DragEventArgs e, EventTy } } + if(adorner is DropTargetHighlightAdorner highlightAdorner) + { + var highlightBrush = GetDropTargetHighlightBrush(dropInfo.VisualTarget); + if (highlightBrush != null) + { + highlightAdorner.Background = highlightBrush; + } + } + adorner.DropInfo = dropInfo; adorner.InvalidateVisual(); } @@ -898,7 +933,7 @@ private static void DropTargetOnDrop(object sender, DragEventArgs e, EventType e DragDropPreview = null; DragDropEffectPreview = null; DropTargetAdorner = null; - + DropHintHelpers.OnDropFinished(); dropHandler.DragOver(dropInfo); if (itemsSorter != null && dropInfo.Data is IEnumerable enumerable and not string) @@ -908,7 +943,6 @@ private static void DropTargetOnDrop(object sender, DragEventArgs e, EventType e dropHandler.Drop(dropInfo); dragHandler.Dropped(dropInfo); - e.Effects = dropInfo.Effects; e.Handled = !dropInfo.NotHandled; diff --git a/src/GongSolutions.WPF.DragDrop/DropHintData.cs b/src/GongSolutions.WPF.DragDrop/DropHintData.cs new file mode 100644 index 00000000..5a743bc0 --- /dev/null +++ b/src/GongSolutions.WPF.DragDrop/DropHintData.cs @@ -0,0 +1,58 @@ +namespace GongSolutions.Wpf.DragDrop; + +using System.ComponentModel; +using System.Runtime.CompilerServices; +using JetBrains.Annotations; + +/// +/// Data presented in drop hint adorner. +/// +public class DropHintData : INotifyPropertyChanged +{ + private DropHintState hintState; + private string hintText; + + public DropHintData(DropHintState hintState, string hintText) + { + this.HintState = hintState; + this.HintText = hintText; + } + + /// + /// The hint text to display to the user. See + /// and . + /// + public string HintText + { + get => this.hintText; + set + { + if (value == this.hintText) return; + this.hintText = value; + this.OnPropertyChanged(); + } + } + + /// + /// The hint state to display different colors for hints. See + /// and . + /// + public DropHintState HintState + { + get => this.hintState; + set + { + if (value == this.hintState) return; + this.hintState = value; + this.OnPropertyChanged(); + } + } + + public event PropertyChangedEventHandler PropertyChanged; + + [NotifyPropertyChangedInvocator] + protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null) + { + this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); + } +} \ No newline at end of file diff --git a/src/GongSolutions.WPF.DragDrop/DropHintHelpers.cs b/src/GongSolutions.WPF.DragDrop/DropHintHelpers.cs new file mode 100644 index 00000000..514d8929 --- /dev/null +++ b/src/GongSolutions.WPF.DragDrop/DropHintHelpers.cs @@ -0,0 +1,204 @@ +namespace GongSolutions.Wpf.DragDrop; + +using System; +using System.Collections.Generic; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Data; +using System.Windows.Media; +using JetBrains.Annotations; + +/// +/// Helper methods to assist with drop hints, used through . +/// +internal static class DropHintHelpers +{ + private static readonly List _dropTargetHintReferences = new(); + + /// + /// Add reference to drop target so we can show hint when drag operation start. + /// + /// + public static void AddDropHintTarget(UIElement dropTarget) + { + _dropTargetHintReferences.Add(new DropTargetHintWeakReference(dropTarget)); + CleanDeadwood(); + } + + /// + /// Remove reference to drop target. + /// + /// + public static void RemoveDropHintTarget(UIElement dropTarget) + { + _dropTargetHintReferences.RemoveAll(m => m.Target == dropTarget); + CleanDeadwood(); + } + + /// + /// Show all available drop hints. + /// + /// + public static void OnDragStart(IDragInfo dragInfo) + { + CleanDeadwood(); + var visibleTargets = GetVisibleTargets(); + foreach (var weakReference in visibleTargets) + { + var sender = weakReference.Target; + + var handler = DragDrop.TryGetDropHandler(null, sender); + if (handler != null) + { + var dropHintInfo = new DropHintInfo(dragInfo); + handler.DropHint(dropHintInfo); + UpdateHintAdorner(weakReference, + dropHintInfo.DropTargetHintAdorner, + new DropHintData(dropHintInfo.DropTargetHintState, dropHintInfo.DropHintText)); + } + } + } + + /// + /// Clears all hint adorner from all drop targets when drag operation is finished. + /// + public static void OnDropFinished() + { + CleanDeadwood(); + foreach (var target in _dropTargetHintReferences) + { + target.DropTargetHintAdorner = null; + } + } + + /// + /// Update drop hint for the current element when drag leaves a drop target. + /// + /// The for the operation + /// The initiating the drag + /// The target element of the drag + public static void OnDragLeave(object sender, IDropTarget dropHandler, IDragInfo dragInfo) + { + var wrapper = _dropTargetHintReferences.Find(m => m.Target == sender); + if (wrapper != null) + { + var dropHintInfo = new DropHintInfo(dragInfo); + dropHandler.DropHint(dropHintInfo); + UpdateHintAdorner(wrapper, dropHintInfo.DropTargetHintAdorner, new DropHintData(dropHintInfo.DropTargetHintState, dropHintInfo.DropHintText)); + } + } + + /// + /// Update drop hint for the current element. + /// + /// + /// + public static void DragOver(object sender, IDropInfo dropInfo) + { + var wrapper = _dropTargetHintReferences.Find(m => m.Target == sender); + if (wrapper != null) + { + UpdateHintAdorner(wrapper, dropInfo.DropTargetHintAdorner, new DropHintData(dropInfo.DropTargetHintState, dropInfo.DropHintText)); + } + } + + private static void UpdateHintAdorner(DropTargetHintWeakReference weakReference, [CanBeNull] Type adornerType, DropHintData hintData) + { + if (adornerType == null) + { + // Discard existing adorner as new parameter is not set + weakReference.DropTargetHintAdorner = null; + return; + } + + + if (weakReference.DropTargetHintAdorner != null && weakReference.DropTargetHintAdorner.GetType() != adornerType) + { + // Type has changed, so we need to remove the old adorner. + weakReference.DropTargetHintAdorner = null; + } + + if (weakReference.DropTargetHintAdorner == null) + { + // Create new adorner if it does not exist. + var dataTemplate = DragDrop.TryGetDropHintDataTemplate(weakReference.Target); + weakReference.DropTargetHintAdorner = DropTargetHintAdorner.CreateHintAdorner(adornerType, weakReference.Target, dataTemplate, hintData); + } + + weakReference.DropTargetHintAdorner?.Update(hintData); + } + + /// + /// Helper method for getting available hint drop targets. + /// + /// + private static List GetVisibleTargets() + { + return _dropTargetHintReferences.FindAll(m => m.Target?.IsVisible == true && DragDrop.GetIsDropTarget(m.Target)); + } + + /// + /// Clean deadwood in case we are holding on to references to dead objects. + /// + private static void CleanDeadwood() + { + _dropTargetHintReferences.RemoveAll((m => !m.IsAlive)); + } + + /// + /// Get the default drop hint template if none other has been provided. + /// + /// + public static DataTemplate GetDefaultDropHintTemplate() + { + var rootBorderName = "RootBorder"; + var backgroundBrush = new SolidColorBrush(SystemColors.HighlightColor) { Opacity = 0.3 }; + backgroundBrush.Freeze(); + var activeBackgroundBrush = new SolidColorBrush(SystemColors.HighlightColor) { Opacity = 0.5 }; + activeBackgroundBrush.Freeze(); + var errorBackgroundBrush = new SolidColorBrush(Colors.DarkRed) { Opacity = 0.3 }; + errorBackgroundBrush.Freeze(); + + var template = new DataTemplate(); + + var hintStateBinding = new Binding(nameof(DropHintData.HintState)); + var activeSetter = new Setter + { + Property = Border.BackgroundProperty, + TargetName = rootBorderName, + Value = activeBackgroundBrush + }; + var errorSetter = new Setter + { + Property = Border.BackgroundProperty, + TargetName = rootBorderName, + Value = errorBackgroundBrush + }; + + template.Triggers.Add(new DataTrigger { Binding = hintStateBinding, Value = DropHintState.Active, Setters = { activeSetter } }); + template.Triggers.Add(new DataTrigger { Binding = hintStateBinding, Value = DropHintState.Error, Setters = { errorSetter } }); + + var textBlockFactory = new FrameworkElementFactory(typeof(TextBlock)); + textBlockFactory.SetValue(TextBlock.TextProperty, new Binding(nameof(DropHintData.HintText))); + textBlockFactory.SetValue(TextBlock.TextWrappingProperty, TextWrapping.Wrap); + textBlockFactory.SetValue(FrameworkElement.HorizontalAlignmentProperty, HorizontalAlignment.Center); + textBlockFactory.SetValue(FrameworkElement.VerticalAlignmentProperty, VerticalAlignment.Center); + + // Create a Border factory + var borderFactory = new FrameworkElementFactory(typeof(Border)) + { + Name = rootBorderName + }; + borderFactory.SetValue(Border.BorderBrushProperty, Brushes.CornflowerBlue); + borderFactory.SetValue(Border.BorderThicknessProperty, new Thickness(2)); + borderFactory.SetValue(Border.BackgroundProperty, backgroundBrush); + + // Set the TextBlock as the child of the Border + borderFactory.AppendChild(textBlockFactory); + + // Set the Border as the root of the visual tree + template.VisualTree = borderFactory; + + return template; + } +} \ No newline at end of file diff --git a/src/GongSolutions.WPF.DragDrop/DropHintInfo.cs b/src/GongSolutions.WPF.DragDrop/DropHintInfo.cs new file mode 100644 index 00000000..cb8d7394 --- /dev/null +++ b/src/GongSolutions.WPF.DragDrop/DropHintInfo.cs @@ -0,0 +1,26 @@ +namespace GongSolutions.Wpf.DragDrop; + +using System; + +/// +/// Implementation of the interface to hold DropHint information. +/// +public class DropHintInfo : IDropHintInfo +{ + /// + public IDragInfo DragInfo { get; } + + /// + public Type DropTargetHintAdorner { get; set; } + + /// + public string DropHintText { get; set; } + + /// + public DropHintState DropTargetHintState { get; set; } + + public DropHintInfo(IDragInfo dragInfo) + { + this.DragInfo = dragInfo; + } +} \ No newline at end of file diff --git a/src/GongSolutions.WPF.DragDrop/DropHintState.cs b/src/GongSolutions.WPF.DragDrop/DropHintState.cs new file mode 100644 index 00000000..da96a67f --- /dev/null +++ b/src/GongSolutions.WPF.DragDrop/DropHintState.cs @@ -0,0 +1,20 @@ +namespace GongSolutions.Wpf.DragDrop; + +/// +/// Represents the mode of the drop hint to display different adorner based on the state of the hint. +/// +public enum DropHintState +{ + /// + /// Default hint state, indicating that a drop target is available for drop. + /// + None, + /// + /// Highlights the target, such as on drag over. + /// + Active, + /// + /// Warning state, indicating that the drop target is not available for drop. + /// + Error +} \ No newline at end of file diff --git a/src/GongSolutions.WPF.DragDrop/DropInfo.cs b/src/GongSolutions.WPF.DragDrop/DropInfo.cs index 740afed0..67ddc7b0 100644 --- a/src/GongSolutions.WPF.DragDrop/DropInfo.cs +++ b/src/GongSolutions.WPF.DragDrop/DropInfo.cs @@ -35,6 +35,15 @@ public class DropInfo : IDropInfo /// public Type DropTargetAdorner { get; set; } + /// + public Type DropTargetHintAdorner { get; set; } + + /// + public DropHintState DropTargetHintState { get; set; } + + /// + public string DropHintText { get; set; } + /// public DragDropEffects Effects { get; set; } diff --git a/src/GongSolutions.WPF.DragDrop/DropTargetAdorner.cs b/src/GongSolutions.WPF.DragDrop/DropTargetAdorner.cs index 5b9aa963..bc49bfcd 100644 --- a/src/GongSolutions.WPF.DragDrop/DropTargetAdorner.cs +++ b/src/GongSolutions.WPF.DragDrop/DropTargetAdorner.cs @@ -1,13 +1,26 @@ using System; using System.Windows; -using System.Windows.Documents; -using System.Windows.Media; namespace GongSolutions.Wpf.DragDrop { + using System.Windows.Documents; + using System.Windows.Media; + + /// + /// Base class for drop target adorner. + /// public abstract class DropTargetAdorner : Adorner { - public DropTargetAdorner(UIElement adornedElement, IDropInfo dropInfo) + private readonly AdornerLayer m_AdornerLayer; + + /// + /// Gets or Sets the pen which can be used for the render process. + /// + public Pen Pen { get; set; } = new Pen(Brushes.Gray, 2); + + public IDropInfo DropInfo { get; set; } + + protected DropTargetAdorner(UIElement adornedElement, IDropInfo dropInfo) : base(adornedElement) { this.DropInfo = dropInfo; @@ -15,19 +28,12 @@ public DropTargetAdorner(UIElement adornedElement, IDropInfo dropInfo) this.AllowDrop = false; this.SnapsToDevicePixels = true; this.m_AdornerLayer = AdornerLayer.GetAdornerLayer(adornedElement); - this.m_AdornerLayer.Add(this); + this.m_AdornerLayer?.Add(this); } - public IDropInfo DropInfo { get; set; } - - /// - /// Gets or Sets the pen which can be used for the render process. - /// - public Pen Pen { get; set; } = new Pen(Brushes.Gray, 2); - public void Detatch() { - this.m_AdornerLayer.Remove(this); + this.m_AdornerLayer?.Remove(this); } internal static DropTargetAdorner Create(Type type, UIElement adornedElement, IDropInfo dropInfo) @@ -38,7 +44,5 @@ internal static DropTargetAdorner Create(Type type, UIElement adornedElement, ID } return type.GetConstructor(new[] { typeof(UIElement), typeof(DropInfo) })?.Invoke(new object[] { adornedElement, dropInfo }) as DropTargetAdorner; } - - private readonly AdornerLayer m_AdornerLayer; } } \ No newline at end of file diff --git a/src/GongSolutions.WPF.DragDrop/DropTargetAdorners.cs b/src/GongSolutions.WPF.DragDrop/DropTargetAdorners.cs index 5b938a12..92886554 100644 --- a/src/GongSolutions.WPF.DragDrop/DropTargetAdorners.cs +++ b/src/GongSolutions.WPF.DragDrop/DropTargetAdorners.cs @@ -13,5 +13,10 @@ public class DropTargetAdorners /// Gets the type of the default insert target adorner. /// public static Type Insert { get; } = typeof(DropTargetInsertionAdorner); + + /// + /// Get the type for the default hint target adorner. + /// + public static Type Hint { get; } = typeof(DropTargetHintAdorner); } } \ No newline at end of file diff --git a/src/GongSolutions.WPF.DragDrop/DropTargetHighlightAdorner.cs b/src/GongSolutions.WPF.DragDrop/DropTargetHighlightAdorner.cs index 56bb14d0..6012185d 100644 --- a/src/GongSolutions.WPF.DragDrop/DropTargetHighlightAdorner.cs +++ b/src/GongSolutions.WPF.DragDrop/DropTargetHighlightAdorner.cs @@ -7,11 +7,17 @@ namespace GongSolutions.Wpf.DragDrop { public class DropTargetHighlightAdorner : DropTargetAdorner { - public DropTargetHighlightAdorner(UIElement adornedElement, DropInfo dropInfo) + public DropTargetHighlightAdorner(UIElement adornedElement, IDropInfo dropInfo) : base(adornedElement, dropInfo) { } + /// + /// The background brush for the highlight rectangle for TreeViewItem. This can be overridden through + /// . The default value is . + /// + public Brush Background { get; set; } = Brushes.Transparent; + /// /// When overridden in a derived class, participates in rendering operations that are directed by the layout system. /// The rendering instructions for this element are not used directly when this method is invoked, and are instead preserved for @@ -44,7 +50,7 @@ protected override void OnRender(DrawingContext drawingContext) rect = new Rect(location, bounds.Size); } - drawingContext.DrawRoundedRectangle(null, this.Pen, rect, 2, 2); + drawingContext.DrawRoundedRectangle(this.Background, this.Pen, rect, 2, 2); } } } diff --git a/src/GongSolutions.WPF.DragDrop/DropTargetHintAdorner.cs b/src/GongSolutions.WPF.DragDrop/DropTargetHintAdorner.cs new file mode 100644 index 00000000..e055fd80 --- /dev/null +++ b/src/GongSolutions.WPF.DragDrop/DropTargetHintAdorner.cs @@ -0,0 +1,135 @@ +namespace GongSolutions.Wpf.DragDrop; + +using System; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Data; +using System.Windows.Documents; +using System.Windows.Media; + +/// +/// This adorner is used to display hints for where items can be dropped. +/// +public class DropTargetHintAdorner : Adorner +{ + private readonly ContentPresenter m_Presenter; + private readonly AdornerLayer m_AdornerLayer; + + public static readonly DependencyProperty DropHintDataProperty = DependencyProperty.Register( + nameof(DropHintData), typeof(DropHintData), typeof(DropTargetHintAdorner), new PropertyMetadata(default(DropHintData))); + + public DropHintData DropHintData + { + get => (DropHintData)GetValue(DropHintDataProperty); + set => SetValue(DropHintDataProperty, value); + } + + public DropTargetHintAdorner(UIElement adornedElement, + DataTemplate dataTemplate, + DropHintData dropHintData) + : base(adornedElement) + { + SetCurrentValue(DropHintDataProperty, dropHintData); + this.IsHitTestVisible = false; + this.AllowDrop = false; + this.SnapsToDevicePixels = true; + this.m_AdornerLayer = AdornerLayer.GetAdornerLayer(adornedElement); + this.m_AdornerLayer?.Add(this); + + this.m_Presenter = new ContentPresenter() + { + IsHitTestVisible = false, + ContentTemplate = dataTemplate + }; + var binding = new Binding(nameof(DropHintData)) + { + Source = this, + Mode = BindingMode.OneWay + }; + this.m_Presenter.SetBinding(ContentPresenter.ContentProperty, binding); + } + + /// + /// Detach the adorner from it's adorner layer. + /// + public void Detatch() + { + this.m_AdornerLayer?.Remove(this); + } + + /// + /// Construct a new drop hint target adorner. + /// + /// + /// + /// + /// + /// + /// + internal static DropTargetHintAdorner CreateHintAdorner(Type type, UIElement adornedElement, DataTemplate dataTemplate, DropHintData hintData) + { + if (!typeof(DropTargetHintAdorner).IsAssignableFrom(type)) + { + throw new InvalidOperationException("The requested adorner class does not derive from DropTargetHintAdorner."); + } + + return type.GetConstructor(new[] + { + typeof(UIElement), + typeof(DataTemplate), + typeof(DropHintData) + }) + ?.Invoke(new object[] + { + adornedElement, + dataTemplate, + hintData + }) + as DropTargetHintAdorner; + } + + private static Rect GetBounds(FrameworkElement element, UIElement visual) + { + return new Rect( + element.TranslatePoint(new Point(0, 0), visual), + element.TranslatePoint(new Point(element.ActualWidth, element.ActualHeight), visual)); + } + + protected override Size MeasureOverride(Size constraint) + { + this.m_Presenter.Measure(constraint); + return this.m_Presenter.DesiredSize; + } + + protected override Size ArrangeOverride(Size finalSize) + { + var bounds = GetBounds(AdornedElement as FrameworkElement, AdornedElement); + this.m_Presenter.Arrange(bounds); + return bounds.Size; + } + + protected override Visual GetVisualChild(int index) + { + return this.m_Presenter; + } + + protected override int VisualChildrenCount + { + get { return 1; } + } + + /// + /// Update hint text and state for the adorner. + /// + /// + public void Update(DropHintData hintData) + { + var currentData = DropHintData; + bool requiresUpdate = (hintData?.HintState != currentData?.HintState || hintData?.HintText != currentData?.HintText); + SetCurrentValue(DropHintDataProperty, hintData); + if(requiresUpdate) + { + this.m_AdornerLayer.Update(); + } + } +} \ No newline at end of file diff --git a/src/GongSolutions.WPF.DragDrop/DropTargetHintWeakReference.cs b/src/GongSolutions.WPF.DragDrop/DropTargetHintWeakReference.cs new file mode 100644 index 00000000..76e3de1a --- /dev/null +++ b/src/GongSolutions.WPF.DragDrop/DropTargetHintWeakReference.cs @@ -0,0 +1,44 @@ +namespace GongSolutions.Wpf.DragDrop; + +using System; +using System.Windows; + +/// +/// Wrapper of the so we only have weak references to the drop targets +/// to avoid memory leaks. +/// +internal sealed class DropTargetHintWeakReference : IDisposable +{ + private readonly WeakReference _dropTarget; + private DropTargetHintAdorner dropTargetHintAdorner; + + public DropTargetHintWeakReference(UIElement dropTarget) + { + this._dropTarget = new WeakReference(dropTarget); + } + + public UIElement Target => this._dropTarget.TryGetTarget(out var target) ? target : null; + + /// + /// Property indicating if the weak reference is still alive, or should be disposed of. + /// + public bool IsAlive => this._dropTarget.TryGetTarget(out _); + + /// + /// The current adorner for the drop target. + /// + public DropTargetHintAdorner DropTargetHintAdorner + { + get => this.dropTargetHintAdorner; + set + { + this.dropTargetHintAdorner?.Detatch(); + this.dropTargetHintAdorner = value; + } + } + + public void Dispose() + { + this.DropTargetHintAdorner = null; + } +} \ No newline at end of file diff --git a/src/GongSolutions.WPF.DragDrop/IDropHintInfo.cs b/src/GongSolutions.WPF.DragDrop/IDropHintInfo.cs new file mode 100644 index 00000000..21814c17 --- /dev/null +++ b/src/GongSolutions.WPF.DragDrop/IDropHintInfo.cs @@ -0,0 +1,38 @@ +namespace GongSolutions.Wpf.DragDrop; + +using System; + +/// +/// This interface is used with the for +/// hint to the user about potential drop targets. +/// +public interface IDropHintInfo +{ + /// + /// Gets a object holding information about the source of the drag, + /// if the drag came from within the framework. + /// + IDragInfo DragInfo { get; } + + /// + /// Gets or sets the class of drop target hint to display. + /// + /// + /// The standard drop target Adorner classes are held in the + /// class. + /// + Type DropTargetHintAdorner { get; set; } + + /// + /// Get or set the text that is displayed when initial drop hint is displayed. + /// + /// + /// This corresponds to in + /// and . + /// + string DropHintText { get; set; } + /// + /// The hint state to display different colors for hints. + /// + DropHintState DropTargetHintState { get; set; } +} \ No newline at end of file diff --git a/src/GongSolutions.WPF.DragDrop/IDropInfo.cs b/src/GongSolutions.WPF.DragDrop/IDropInfo.cs index 37d5b66b..0f673021 100644 --- a/src/GongSolutions.WPF.DragDrop/IDropInfo.cs +++ b/src/GongSolutions.WPF.DragDrop/IDropInfo.cs @@ -39,6 +39,25 @@ public interface IDropInfo /// Type DropTargetAdorner { get; set; } + /// + /// Gets or sets the class of drop target to display for hint. + /// + /// + /// The standard drop target Adorner classes are held in the + /// class. + /// + Type DropTargetHintAdorner { get; set; } + + /// + /// The hint state to display different colors for hints. + /// + DropHintState DropTargetHintState { get; set; } + + /// + /// Get or set the text that is displayed when the drop hint is displayed. + /// + string DropHintText { get; set; } + /// /// Gets or sets the allowed effects for the drop. /// @@ -121,7 +140,7 @@ public interface IDropInfo FlowDirection VisualTargetFlowDirection { get; } /// - /// Gets and sets the text displayed in the DropDropEffects Adorner. + /// Gets and sets the text displayed in the DropDropEffects Adorner and DropTargetHintAdorner. /// string DestinationText { get; set; } diff --git a/src/GongSolutions.WPF.DragDrop/IDropTarget.cs b/src/GongSolutions.WPF.DragDrop/IDropTarget.cs index 1a6970d5..8994d324 100644 --- a/src/GongSolutions.WPF.DragDrop/IDropTarget.cs +++ b/src/GongSolutions.WPF.DragDrop/IDropTarget.cs @@ -7,6 +7,19 @@ namespace GongSolutions.Wpf.DragDrop /// public interface IDropTarget { + /// + /// Notifies the drop handler when a drag is initiated to display hint about potential drop targets. + /// + /// Object which contains several drop information. +#if NETCOREAPP3_1_OR_GREATER + void DropHint(IDropHintInfo dropHintInfo) + { + // nothing here + } +#else + void DropHint(IDropHintInfo dropHintInfo); +#endif + /// /// Notifies the drop handler when dragging operation enters a potential drop target. /// diff --git a/src/Showcase/Models/CustomDropHintHandler.cs b/src/Showcase/Models/CustomDropHintHandler.cs new file mode 100644 index 00000000..bcf8d784 --- /dev/null +++ b/src/Showcase/Models/CustomDropHintHandler.cs @@ -0,0 +1,93 @@ +namespace Showcase.WPF.DragDrop.Models; + +using System.Linq; +using System.Windows; +using GongSolutions.Wpf.DragDrop; + +public class CustomDropHintHandler : DefaultDropHandler +{ + /// + /// When false, will set red border and hint text to "Drop not allowed for this element" + /// + public bool IsDropAllowed { get; set; } = true; + public bool BlockOdd { get; set; } + + /// + /// Do not display active hint on mouse over. + /// + public bool IsActiveHintDisabled { get; set; } + + public override void DropHint(IDropHintInfo dropHintInfo) + { + if (!this.CanAccept(dropHintInfo.DragInfo)) + { + return; + } + + if (!this.IsDropAllowed) + { + dropHintInfo.DropTargetHintAdorner = DropTargetAdorners.Hint; + dropHintInfo.DropTargetHintState = DropHintState.Error; + dropHintInfo.DropHintText = "Drop not allowed for this element"; + } + else + { + dropHintInfo.DropHintText = "Drop data here"; + dropHintInfo.DropTargetHintAdorner = typeof(DropTargetHintAdorner); + } + } + + public override void DragOver(IDropInfo dropInfo) + { + if (!this.CanAccept(dropInfo.DragInfo)) + { + return; + } + + if (!IsDropAllowed) + { + dropInfo.DropTargetHintAdorner = DropTargetAdorners.Hint; + dropInfo.DropTargetHintState = DropHintState.Error; + dropInfo.DropHintText = "Drop not allowed for this element"; + return; + } + + if (BlockOdd && dropInfo.DragInfo.SourceItem is ItemModel item && item.Index % 2 != 0) + { + dropInfo.DropTargetHintAdorner = DropTargetAdorners.Hint; + dropInfo.DropTargetHintState = DropHintState.Error; + dropInfo.DropHintText = "Only items with even index is allowed"; + dropInfo.Effects = DragDropEffects.None; + return; + } + + var copyData = ShouldCopyData(dropInfo); + + dropInfo.Effects = copyData ? DragDropEffects.Copy : DragDropEffects.Move; + dropInfo.DropTargetAdorner = DropTargetAdorners.Highlight; + dropInfo.EffectText = "Send"; + if(IsActiveHintDisabled) + { + // No drag over hint + return; + } + + dropInfo.DropTargetHintAdorner = DropTargetAdorners.Hint; + dropInfo.DropHintText = $"Dropping {(dropInfo.DragInfo.SourceItem as ItemModel)?.Caption} on {(dropInfo.TargetItem as ItemModel)?.Caption}"; + dropInfo.DropTargetHintState = DropHintState.Active; + } + + + private bool CanAccept(IDragInfo dragInfo) + { + if (dragInfo == null) + { + return false; + } + + var items = ExtractData(dragInfo.Data) + .OfType() + .ToList(); + return items.Count > 0; + } +} \ No newline at end of file diff --git a/src/Showcase/Models/GroupedDropHandler.cs b/src/Showcase/Models/GroupedDropHandler.cs index 4402a8df..fe2ab461 100644 --- a/src/Showcase/Models/GroupedDropHandler.cs +++ b/src/Showcase/Models/GroupedDropHandler.cs @@ -15,6 +15,12 @@ public void DragEnter(IDropInfo dropInfo) { // nothing here } + + /// + public void DropHint(IDropHintInfo dropHintInfo) + { + // nothing here + } #endif /// diff --git a/src/Showcase/Models/NestedDropHandler.cs b/src/Showcase/Models/NestedDropHandler.cs index 437b453d..338787f7 100644 --- a/src/Showcase/Models/NestedDropHandler.cs +++ b/src/Showcase/Models/NestedDropHandler.cs @@ -12,6 +12,12 @@ public void DragEnter(IDropInfo dropInfo) { // nothing here } + + /// + public void DropHint(IDropHintInfo dropHintInfo) + { + // nothing here + } #endif /// diff --git a/src/Showcase/Models/SampleData.cs b/src/Showcase/Models/SampleData.cs index 4b1a12f4..36dfddaa 100644 --- a/src/Showcase/Models/SampleData.cs +++ b/src/Showcase/Models/SampleData.cs @@ -104,5 +104,7 @@ public SampleData() public ListBoxCustomDropHandler ListBoxCustomDropHandler { get; set; } = new ListBoxCustomDropHandler(); public IDropTarget NestedDropHandler { get; set; } = new NestedDropHandler(); + + public CustomDropHintHandler CustomDropHintHandler { get; set; } = new CustomDropHintHandler(); } } \ No newline at end of file diff --git a/src/Showcase/Models/SerializableDropHandler.cs b/src/Showcase/Models/SerializableDropHandler.cs index 49605163..2eedf9ed 100644 --- a/src/Showcase/Models/SerializableDropHandler.cs +++ b/src/Showcase/Models/SerializableDropHandler.cs @@ -17,6 +17,12 @@ public void DragEnter(IDropInfo dropInfo) { // nothing here } + + /// + public void DropHint(IDropHintInfo dropHintInfo) + { + // nothing here + } #endif /// diff --git a/src/Showcase/Models/TextBoxCustomDropHandler.cs b/src/Showcase/Models/TextBoxCustomDropHandler.cs index b4fff1ea..f7a51a90 100644 --- a/src/Showcase/Models/TextBoxCustomDropHandler.cs +++ b/src/Showcase/Models/TextBoxCustomDropHandler.cs @@ -15,6 +15,12 @@ public void DragEnter(IDropInfo dropInfo) { // nothing here } + + /// + public void DropHint(IDropHintInfo dropHintInfo) + { + // nothing here + } #endif /// diff --git a/src/Showcase/Views/MixedSamples.xaml b/src/Showcase/Views/MixedSamples.xaml index 0910aa36..973a6c2e 100644 --- a/src/Showcase/Views/MixedSamples.xaml +++ b/src/Showcase/Views/MixedSamples.xaml @@ -98,6 +98,7 @@ dd:DragDrop.IsDragSource="True" dd:DragDrop.IsDropTarget="True" dd:DragDrop.UseDefaultEffectDataTemplate="True" + dd:DragDrop.UseDropTargetHint="True" ItemsSource="{Binding Data.Collection1}" /> @@ -110,6 +111,7 @@ dd:DragDrop.IsDragSource="True" dd:DragDrop.IsDropTarget="True" dd:DragDrop.UseDefaultEffectDataTemplate="True" + dd:DragDrop.UseDropTargetHint="True" ItemsSource="{Binding Data.Collection2}" /> @@ -122,6 +124,7 @@ dd:DragDrop.IsDragSource="True" dd:DragDrop.IsDropTarget="True" dd:DragDrop.UseDefaultEffectDataTemplate="True" + dd:DragDrop.UseDropTargetHint="True" ItemsSource="{Binding Data.Collection2}" /> @@ -133,6 +136,7 @@ dd:DragDrop.IsDragSource="True" dd:DragDrop.IsDropTarget="True" dd:DragDrop.UseDefaultEffectDataTemplate="True" + dd:DragDrop.UseDropTargetHint="True" ItemsSource="{Binding Data.Collection4}" /> @@ -595,6 +599,115 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Showcase/Views/SettingsView.xaml b/src/Showcase/Views/SettingsView.xaml index 81a6aa94..44c47b73 100644 --- a/src/Showcase/Views/SettingsView.xaml +++ b/src/Showcase/Views/SettingsView.xaml @@ -66,6 +66,11 @@ Content="UseDefaultEffectDataTemplate" IsChecked="{Binding Path=(dd:DragDrop.UseDefaultEffectDataTemplate), Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" ToolTip="Sets whether if the default DataTemplate for the effects should be use." /> + + diff --git a/src/Showcase/Views/TreeViewSamples.xaml b/src/Showcase/Views/TreeViewSamples.xaml index 9d3dfe50..c41a8660 100644 --- a/src/Showcase/Views/TreeViewSamples.xaml +++ b/src/Showcase/Views/TreeViewSamples.xaml @@ -32,6 +32,9 @@ + + + @@ -49,6 +52,7 @@ dd:DragDrop.SelectDroppedItems="True" dd:DragDrop.UseDefaultDragAdorner="True" dd:DragDrop.UseDefaultEffectDataTemplate="True" + dd:DragDrop.DropTargetHighlightBrush="{StaticResource DropTargetHighlightBrush}" ItemContainerStyle="{StaticResource BoundTreeViewItemStyle}" ItemsSource="{Binding Data.TreeCollection1}" Loaded="LeftBoundTreeView_Loaded"> @@ -64,6 +68,7 @@ dd:DragDrop.IsDragSource="True" dd:DragDrop.IsDropTarget="True" dd:DragDrop.UseDefaultDragAdorner="True" + dd:DragDrop.DropTargetHighlightBrush="{StaticResource DropTargetHighlightBrush}" ItemContainerStyle="{StaticResource BoundTreeViewItemStyle}" ItemsSource="{Binding Data.TreeCollection2}">