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 72708c5c..ac2be346 100644 --- a/src/GongSolutions.WPF.DragDrop/DragDrop.Properties.cs +++ b/src/GongSolutions.WPF.DragDrop/DragDrop.Properties.cs @@ -569,6 +569,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. /// @@ -718,6 +743,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 9a58f344..ccbec639 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) @@ -658,9 +674,11 @@ private static void DoDragSourceMove(object sender, Func g DragDropPreview = null; DragDropEffectPreview = null; DropTargetAdorner = null; + DropHintHelpers.OnDropFinished(); Mouse.OverrideCursor = null; } + DropHintHelpers.OnDropFinished(); dragHandler.DragDropOperationFinished(dragDropEffects, dragInfo); } catch (Exception ex) @@ -712,7 +730,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; @@ -757,6 +782,7 @@ private static void DropTargetOnDragOver(object sender, DragEventArgs e, EventTy } dropHandler.DragOver(dropInfo); + DropHintHelpers.DragOver(sender, dropInfo); if (dragInfo is not null) { @@ -828,6 +854,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(); } @@ -891,7 +926,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) @@ -901,7 +936,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/DragInfo.cs b/src/GongSolutions.WPF.DragDrop/DragInfo.cs index 4ebd442c..99e79a8f 100644 --- a/src/GongSolutions.WPF.DragDrop/DragInfo.cs +++ b/src/GongSolutions.WPF.DragDrop/DragInfo.cs @@ -10,12 +10,11 @@ namespace GongSolutions.Wpf.DragDrop { /// - /// Holds information about a the source of a drag drop operation. + /// Holds information about the source of a drag drop operation. /// - /// /// - /// The class holds all of the framework's information about the source - /// of a drag. It is used by to determine whether a drag + /// The class holds all the framework's information about the source + /// of a drag. It is used by to determine whether a drag /// can start, and what the dragged data should be. /// public class DragInfo : IDragInfo @@ -167,7 +166,7 @@ public DragInfo(object sender, object originalSource, MouseButton mouseButton, F this.SourceItems = selectedItems; // Some controls (I'm looking at you TreeView!) haven't updated their - // SelectedItem by this point. Check to see if there 1 or less item in + // SelectedItem by this point. Check to see if there 1 or less item in // the SourceItems collection, and if so, override the control's SelectedItems with the clicked item. // // The control has still the old selected items at the mouse down event, so we should check this and give only the real selected item to the user. @@ -209,7 +208,7 @@ internal void RefreshSelectedItems(object sender) this.SourceItems = selectedItems; // Some controls (I'm looking at you TreeView!) haven't updated their - // SelectedItem by this point. Check to see if there 1 or less item in + // SelectedItem by this point. Check to see if there 1 or less item in // the SourceItems collection, and if so, override the control's SelectedItems with the clicked item. // // The control has still the old selected items at the mouse down event, so we should check this and give only the real selected item to the user. diff --git a/src/GongSolutions.WPF.DragDrop/DropHintData.cs b/src/GongSolutions.WPF.DragDrop/DropHintData.cs new file mode 100644 index 00000000..3d1cfdca --- /dev/null +++ b/src/GongSolutions.WPF.DragDrop/DropHintData.cs @@ -0,0 +1,59 @@ +using System.ComponentModel; +using System.Runtime.CompilerServices; +using JetBrains.Annotations; + +namespace GongSolutions.Wpf.DragDrop +{ + /// + /// 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..5a3f0774 --- /dev/null +++ b/src/GongSolutions.WPF.DragDrop/DropHintHelpers.cs @@ -0,0 +1,204 @@ +using System; +using System.Collections.Generic; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Data; +using System.Windows.Media; +using JetBrains.Annotations; + +namespace GongSolutions.Wpf.DragDrop +{ + /// + /// 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..15992634 --- /dev/null +++ b/src/GongSolutions.WPF.DragDrop/DropHintInfo.cs @@ -0,0 +1,27 @@ +using System; + +namespace GongSolutions.Wpf.DragDrop +{ + /// + /// 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..8e3f0e41 --- /dev/null +++ b/src/GongSolutions.WPF.DragDrop/DropHintState.cs @@ -0,0 +1,21 @@ +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 d8f3a9fc..909fd273 100644 --- a/src/GongSolutions.WPF.DragDrop/DropInfo.cs +++ b/src/GongSolutions.WPF.DragDrop/DropInfo.cs @@ -11,11 +11,10 @@ namespace GongSolutions.Wpf.DragDrop { /// - /// Holds information about a the target of a drag drop operation. + /// Holds information about the target of a drag drop operation. /// - /// /// - /// The class holds all of the framework's information about the current + /// The class holds all the framework's information about the current /// target of a drag. It is used by method to determine whether /// the current drop target is valid, and by to perform the drop. /// @@ -37,6 +36,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 bff5b9bd..adfa2d29 100644 --- a/src/GongSolutions.WPF.DragDrop/DropTargetAdorner.cs +++ b/src/GongSolutions.WPF.DragDrop/DropTargetAdorner.cs @@ -1,14 +1,26 @@ -using System; +using System; using System.Windows; using System.Windows.Documents; using System.Windows.Media; +using JetBrains.Annotations; namespace GongSolutions.Wpf.DragDrop { - using JetBrains.Annotations; - + /// + /// Base class for drop target Adorner. + /// public abstract class DropTargetAdorner : Adorner { + [CanBeNull] + private readonly AdornerLayer 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; } + public DropTargetAdorner(UIElement adornedElement, IDropInfo dropInfo) : base(adornedElement) { @@ -21,13 +33,9 @@ public DropTargetAdorner(UIElement adornedElement, IDropInfo dropInfo) this.adornerLayer?.Add(this); } - public IDropInfo DropInfo { get; set; } - /// - /// Gets or Sets the pen which can be used for the render process. + /// Detach the adorner from its adorner layer. /// - public Pen Pen { get; set; } = new Pen(Brushes.Gray, 2); - public void Detach() { if (this.adornerLayer is null) @@ -53,8 +61,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; } - - [CanBeNull] - private readonly AdornerLayer 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..f19653f4 --- /dev/null +++ b/src/GongSolutions.WPF.DragDrop/DropTargetHintAdorner.cs @@ -0,0 +1,150 @@ +using JetBrains.Annotations; +using System; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Data; +using System.Windows.Documents; +using System.Windows.Media; + +namespace GongSolutions.Wpf.DragDrop +{ + /// + /// This adorner is used to display hints for where items can be dropped. + /// + public class DropTargetHintAdorner : Adorner + { + private readonly ContentPresenter presenter; + [CanBeNull] + private readonly AdornerLayer adornerLayer; + + public static readonly DependencyProperty DropHintDataProperty + = DependencyProperty.Register(nameof(DropHintData), + typeof(DropHintData), + typeof(DropTargetHintAdorner), + new PropertyMetadata(default(DropHintData))); + + public DropHintData DropHintData + { + get => (DropHintData)this.GetValue(DropHintDataProperty); + set => this.SetValue(DropHintDataProperty, value); + } + + public DropTargetHintAdorner(UIElement adornedElement, DataTemplate dataTemplate, DropHintData dropHintData) + : base(adornedElement) + { + this.SetCurrentValue(DropHintDataProperty, dropHintData); + this.IsHitTestVisible = false; + this.AllowDrop = false; + this.SnapsToDevicePixels = true; + this.adornerLayer = AdornerLayer.GetAdornerLayer(adornedElement); + this.adornerLayer?.Add(this); + + this.presenter = new ContentPresenter() + { + IsHitTestVisible = false, + ContentTemplate = dataTemplate + }; + var binding = new Binding(nameof(this.DropHintData)) + { + Source = this, + Mode = BindingMode.OneWay + }; + this.presenter.SetBinding(ContentPresenter.ContentProperty, binding); + } + + /// + /// Detach the adorner from its adorner layer. + /// + public void Detach() + { + if (this.adornerLayer is null) + { + return; + } + + if (!this.adornerLayer.Dispatcher.CheckAccess()) + { + this.adornerLayer.Dispatcher.Invoke(this.Detach); + return; + } + + this.adornerLayer.Remove(this); + } + + 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.presenter.Measure(constraint); + return this.presenter.DesiredSize; + } + + protected override Size ArrangeOverride(Size finalSize) + { + var bounds = GetBounds(this.AdornedElement as FrameworkElement, this.AdornedElement); + this.presenter.Arrange(bounds); + return bounds.Size; + } + + protected override Visual GetVisualChild(int index) + { + return this.presenter; + } + + protected override int VisualChildrenCount + { + get { return 1; } + } + + /// + /// Update hint text and state for the adorner. + /// + /// + public void Update(DropHintData hintData) + { + var currentData = this.DropHintData; + bool requiresUpdate = (hintData?.HintState != currentData?.HintState || hintData?.HintText != currentData?.HintText); + this.SetCurrentValue(DropHintDataProperty, hintData); + if (requiresUpdate) + { + this.adornerLayer?.Update(); + } + } + + /// + /// 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; + } + } +} \ 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..7f01cf67 --- /dev/null +++ b/src/GongSolutions.WPF.DragDrop/DropTargetHintWeakReference.cs @@ -0,0 +1,45 @@ +using System; +using System.Windows; + +namespace GongSolutions.Wpf.DragDrop +{ + /// + /// 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?.Detach(); + 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..8bd0b9e3 --- /dev/null +++ b/src/GongSolutions.WPF.DragDrop/IDropHintInfo.cs @@ -0,0 +1,40 @@ +using System; + +namespace GongSolutions.Wpf.DragDrop +{ + /// + /// 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 ad8916ae..02b36ce5 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..037bf923 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. /// @@ -25,7 +38,7 @@ void DragEnter(IDropInfo dropInfo) /// /// Object which contains several drop information. /// - /// To allow a drop at the current drag position, the property on + /// To allow a drop at the current drag position, the property on /// should be set to a value other than /// and should be set to a non-null value. /// diff --git a/src/GongSolutions.WPF.DragDrop/IDropTargetItemsSorter.cs b/src/GongSolutions.WPF.DragDrop/IDropTargetItemsSorter.cs index 73180d2f..afe1657b 100644 --- a/src/GongSolutions.WPF.DragDrop/IDropTargetItemsSorter.cs +++ b/src/GongSolutions.WPF.DragDrop/IDropTargetItemsSorter.cs @@ -3,7 +3,7 @@ namespace GongSolutions.Wpf.DragDrop { /// - /// Interface for objects that sort an IEnumerable of drag drop items that are + /// Interface for objects that sort an IEnumerable of drag drop items that are /// going to be dropped on some target /// public interface IDropTargetItemsSorter @@ -15,4 +15,4 @@ public interface IDropTargetItemsSorter /// The sorted list of dragged items IEnumerable SortDropTargetItems(IEnumerable items); } -} +} \ No newline at end of file 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 322c0f66..a56fbd90 100644 --- a/src/Showcase/Models/SampleData.cs +++ b/src/Showcase/Models/SampleData.cs @@ -114,5 +114,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 c4831fe1..ba3242c8 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}" /> @@ -578,6 +582,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 7ec38ecf..ead25b22 100644 --- a/src/Showcase/Views/TreeViewSamples.xaml +++ b/src/Showcase/Views/TreeViewSamples.xaml @@ -33,6 +33,9 @@ + + + @@ -50,6 +53,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"> @@ -65,6 +69,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}">