257 lines
9.9 KiB
C#
257 lines
9.9 KiB
C#
using System.Windows;
|
|
using System.Windows.Controls;
|
|
using System.Windows.Input;
|
|
using System.Windows.Media;
|
|
using System.Xml.Linq;
|
|
|
|
namespace CopaData.FileInspector.GUI.TilingPanels.Controls.DragAndDrop;
|
|
|
|
/// <summary>
|
|
/// Used to handle drag and drop callbacks via the abstract methods <see cref="OnDragStart(object, UIElement)"/> and <see cref="OnDrop(object, UIElement)"/>.
|
|
/// Can be used as a parent to any amount of <see cref="UIElement"/>s that refer to the their handler via <see cref="HandlerProperty"/>.
|
|
/// If no Handler is provided, the element itself will be considered its own handler, as long as it is a <see cref="DragAndDropControl"/>.
|
|
/// In order to make an element draggable, set <see cref="IsDraggableProperty"/> to true within the element's XAML attributes.
|
|
/// To mark a FrameworkElement as a drop zone, set <see cref="IsDropZoneProperty"/> to true.
|
|
/// If a draggable element is dropped on a drop zone, the handler's <see cref="DragAndDropControl.OnDrop(object, UIElement)" /> is called.
|
|
/// </summary>
|
|
public abstract class DragAndDropControl : Control
|
|
{
|
|
/// <summary>
|
|
/// Called when a DragDrop operation starts.
|
|
/// </summary>
|
|
protected virtual void OnElementDragStart(UIElement draggedElement) { }
|
|
|
|
/// <summary>
|
|
/// Called during a DragDrop operation in order to update the cursor, if needed.
|
|
/// </summary>
|
|
protected virtual void OnElementDragGiveCursorFeedback(object sender, GiveFeedbackEventArgs args) { }
|
|
|
|
/// <summary>
|
|
/// Called during a DragDrop operation.
|
|
/// </summary>
|
|
protected virtual void OnElementDrag(UIElement draggedElement, MouseEventArgs args) { }
|
|
|
|
/// <summary>
|
|
/// Called when an element was dropped without being on an element marked as a DropZone.
|
|
/// </summary>
|
|
/// <param name="draggedElement"></param>
|
|
protected virtual void OnElementDragStop(UIElement draggedElement) { }
|
|
|
|
/// <summary>
|
|
/// Called when this element is the potential target of a DragDrop operation.
|
|
/// </summary>
|
|
protected virtual void OnElementDragOver(DragAndDropData data, UIElement targetElement) { }
|
|
|
|
/// <summary>
|
|
/// Called when this element stops being a potential target of a DragDrop operation.
|
|
/// </summary>
|
|
protected virtual void OnElementDragLeave(UIElement targetElement) { }
|
|
|
|
/// <summary>
|
|
/// Required callback to handle <paramref name="targetElement"/>,
|
|
/// provided with <see cref="DragAndDropData"/> containing both the source element being dragged and it's original data context.
|
|
/// </summary>
|
|
protected abstract void OnElementDropped(DragAndDropData data, UIElement targetElement);
|
|
|
|
#region Attached Properties
|
|
|
|
/* === IsDraggable === */
|
|
|
|
public static readonly DependencyProperty IsDraggableProperty = DependencyProperty
|
|
.RegisterAttached(
|
|
"IsDraggable",
|
|
typeof(bool),
|
|
typeof(DragAndDropControl),
|
|
new PropertyMetadata(false, OnIsDraggableChanged));
|
|
|
|
public static bool GetIsDraggable(DependencyObject obj) => (bool)obj.GetValue(IsDraggableProperty);
|
|
public static void SetIsDraggable(DependencyObject obj, bool value) => obj.SetValue(IsDraggableProperty, value);
|
|
|
|
/// <summary>
|
|
/// Adds event listeners to the element's GiveFeedback, MouseLeftButtonUp and MouseMove events.
|
|
/// </summary>
|
|
private static void OnIsDraggableChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
|
|
{
|
|
if (sender is not FrameworkElement element)
|
|
{
|
|
throw new InvalidOperationException($"IsDraggable may only be attached to FrameworkElements, got {sender.GetType()} instead.");
|
|
}
|
|
|
|
if ((bool)e.NewValue)
|
|
{
|
|
element.GiveFeedback += HandleGiveFeedback;
|
|
element.MouseLeftButtonUp += HandleLeftMouseButtonUp;
|
|
element.MouseMove += HandleMouseMove;
|
|
}
|
|
else
|
|
{
|
|
element.GiveFeedback -= HandleGiveFeedback;
|
|
element.MouseLeftButtonUp -= HandleLeftMouseButtonUp;
|
|
element.MouseMove -= HandleMouseMove;
|
|
}
|
|
}
|
|
|
|
/* === IsDropZone === */
|
|
|
|
public static readonly DependencyProperty IsDropZoneProperty = DependencyProperty
|
|
.RegisterAttached(
|
|
"IsDropZone",
|
|
typeof(bool),
|
|
typeof(DragAndDropControl),
|
|
new PropertyMetadata(false, OnIsDropZoneChanged));
|
|
|
|
public static bool GetIsDropZone(DependencyObject obj) => (bool)obj.GetValue(IsDropZoneProperty);
|
|
public static void SetIsDropZone(DependencyObject obj, bool value) => obj.SetValue(IsDropZoneProperty, value);
|
|
|
|
/// <summary>
|
|
/// Adds an event listener to this element's <see cref="UIElement.Drop"/> event.
|
|
/// </summary>
|
|
private static void OnIsDropZoneChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
|
|
{
|
|
if (sender is not FrameworkElement element)
|
|
{
|
|
throw new InvalidOperationException($"IsDraggable may only be attached to FrameworkElements, got {sender.GetType()} instead.");
|
|
}
|
|
|
|
if ((bool)e.NewValue)
|
|
{
|
|
element.AllowDrop = true;
|
|
element.DragOver += HandleDragOver;
|
|
element.DragLeave += HandleDragLeave;
|
|
element.Drop += HandleDrop;
|
|
}
|
|
else
|
|
{
|
|
element.DragOver -= HandleDragOver;
|
|
element.DragLeave -= HandleDragLeave;
|
|
element.Drop -= HandleDrop;
|
|
}
|
|
}
|
|
|
|
/* === Handler === */
|
|
|
|
public static readonly DependencyProperty HandlerProperty = DependencyProperty
|
|
.RegisterAttached(
|
|
"Handler",
|
|
typeof(DependencyObject),
|
|
typeof(DragAndDropControl),
|
|
new PropertyMetadata(null, OnHandlerChanged));
|
|
|
|
public static DependencyObject GetHandler(DependencyObject obj) => (DependencyObject)obj.GetValue(HandlerProperty);
|
|
public static void SetHandler(DependencyObject obj, bool value) => obj.SetValue(HandlerProperty, value);
|
|
|
|
private static void OnHandlerChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
|
|
{
|
|
// Simple validation
|
|
if (sender is not UIElement element || e.NewValue is not DragAndDropControl dndControl)
|
|
{
|
|
throw new InvalidOperationException($"Can not set {e.NewValue} as handler. Handler needs to be of type DragAndDropControl.");
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
|
|
/// <summary>
|
|
/// Attempts to retrieve the correct <see cref="DragAndDropControl"/> handler for the provided element.<br/>
|
|
/// This may either be via the defined Handler property, or the element itself if no handler property was attached.
|
|
/// </summary>
|
|
private static DragAndDropControl RetrieveHandler(DependencyObject obj)
|
|
{
|
|
if (obj.GetValue(HandlerProperty) is not DragAndDropControl handler)
|
|
{
|
|
if (obj is DragAndDropControl)
|
|
{
|
|
handler = (DragAndDropControl)obj;
|
|
}
|
|
else // Todo: last resort if ((handler = FindVisualParent<DragAndDropControl>(obj)!) == null)
|
|
{
|
|
throw new InvalidOperationException($"Could not find a DragAndDropControl for element {obj}. You may explicitly provide one using the DragAndDropControl.Handler attached property.");
|
|
}
|
|
}
|
|
|
|
return handler;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Dynamic handler for the <see cref="UIElement.MouseMove"/> event.
|
|
/// Called when the mouse cursor moves while on an element.
|
|
/// This also calls the <see cref="OnElementDrag(UIElement, MouseEventArgs)"/> method in order to handle any specific behaviour <b>during</b> the operation.
|
|
/// </summary>
|
|
private static void HandleMouseMove(object sender, MouseEventArgs args)
|
|
{
|
|
if (args.LeftButton == MouseButtonState.Pressed)
|
|
{
|
|
FrameworkElement element = (FrameworkElement)sender;
|
|
DragAndDropControl handler = RetrieveHandler(element);
|
|
DragAndDropData data = new(element, element.DataContext);
|
|
DragDrop.DoDragDrop(element, data, DragDropEffects.All);
|
|
handler.OnElementDrag(element, args);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Dynamic handler for the <see cref="UIElement.GiveFeedback"/> event.
|
|
/// </summary>
|
|
private static void HandleGiveFeedback(object sender, GiveFeedbackEventArgs args)
|
|
{
|
|
FrameworkElement element = (FrameworkElement)sender;
|
|
DragAndDropControl handler = RetrieveHandler(element);
|
|
handler.OnElementDragGiveCursorFeedback(element, args);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Dynamic handler for the <see cref="UIElement.MouseLeftButtonUp"/> event.
|
|
/// </summary>
|
|
private static void HandleLeftMouseButtonUp(object sender, MouseButtonEventArgs args)
|
|
{
|
|
FrameworkElement element = (FrameworkElement)sender;
|
|
DragAndDropControl handler = RetrieveHandler(element);
|
|
handler.OnElementDragStop(element);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Dynamic handler for the <see cref="UIElement.DragOver"/> event.
|
|
/// </summary>
|
|
private static void HandleDragOver(object sender, DragEventArgs args)
|
|
{
|
|
FrameworkElement element = (FrameworkElement)sender;
|
|
DragAndDropControl handler = RetrieveHandler(element);
|
|
handler.OnElementDragOver((DragAndDropData)args.Data.GetData(typeof(DragAndDropData)), element);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Dynamic handler for the <see cref="UIElement.DragLeave"/> event.
|
|
/// </summary>
|
|
private static void HandleDragLeave(object sender, DragEventArgs args)
|
|
{
|
|
FrameworkElement element = (FrameworkElement)sender;
|
|
DragAndDropControl handler = RetrieveHandler(element);
|
|
handler.OnElementDragLeave(element);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Dynamic handler for the <see cref="UIElement.Drop"/> event.
|
|
/// </summary>
|
|
private static void HandleDrop(object sender, DragEventArgs args)
|
|
{
|
|
FrameworkElement element = (FrameworkElement)sender;
|
|
DragAndDropControl handler = RetrieveHandler(element);
|
|
handler.OnElementDropped((DragAndDropData)args.Data.GetData(typeof(DragAndDropData)), element);
|
|
}
|
|
|
|
private static T? FindVisualParent<T>(DependencyObject child) where T : DependencyObject
|
|
{
|
|
// Todo: find a better solution to this, if there even is one.
|
|
// There are, in fact, many controls even in the WPF standard that do similar lookups,
|
|
// so I assume it can't be _that_ bad, but every clean solution kind of seems to need some ugly spot in it. :(
|
|
|
|
DependencyObject parent = VisualTreeHelper.GetParent(child);
|
|
while (parent != null)
|
|
{
|
|
if (parent is T expected) return expected;
|
|
parent = VisualTreeHelper.GetParent(parent);
|
|
}
|
|
return null;
|
|
}
|
|
}
|