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;
///
/// Used to handle drag and drop callbacks via the abstract methods and .
/// Can be used as a parent to any amount of s that refer to the their handler via .
/// If no Handler is provided, the element itself will be considered its own handler, as long as it is a .
/// In order to make an element draggable, set to true within the element's XAML attributes.
/// To mark a FrameworkElement as a drop zone, set to true.
/// If a draggable element is dropped on a drop zone, the handler's is called.
///
public abstract class DragAndDropControl : Control
{
///
/// Called when a DragDrop operation starts.
///
protected virtual void OnElementDragStart(UIElement draggedElement) { }
///
/// Called during a DragDrop operation in order to update the cursor, if needed.
///
protected virtual void OnElementDragGiveCursorFeedback(object sender, GiveFeedbackEventArgs args) { }
///
/// Called during a DragDrop operation.
///
protected virtual void OnElementDrag(UIElement draggedElement, MouseEventArgs args) { }
///
/// Called when an element was dropped without being on an element marked as a DropZone.
///
///
protected virtual void OnElementDragStop(UIElement draggedElement) { }
///
/// Called when this element is the potential target of a DragDrop operation.
///
protected virtual void OnElementDragOver(DragAndDropData data, UIElement targetElement) { }
///
/// Called when this element stops being a potential target of a DragDrop operation.
///
protected virtual void OnElementDragLeave(UIElement targetElement) { }
///
/// Required callback to handle ,
/// provided with containing both the source element being dragged and it's original data context.
///
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);
///
/// Adds event listeners to the element's GiveFeedback, MouseLeftButtonUp and MouseMove events.
///
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);
///
/// Adds an event listener to this element's event.
///
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
///
/// Attempts to retrieve the correct handler for the provided element.
/// This may either be via the defined Handler property, or the element itself if no handler property was attached.
///
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(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;
}
///
/// Dynamic handler for the event.
/// Called when the mouse cursor moves while on an element.
/// This also calls the method in order to handle any specific behaviour during the operation.
///
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);
}
}
///
/// Dynamic handler for the event.
///
private static void HandleGiveFeedback(object sender, GiveFeedbackEventArgs args)
{
FrameworkElement element = (FrameworkElement)sender;
DragAndDropControl handler = RetrieveHandler(element);
handler.OnElementDragGiveCursorFeedback(element, args);
}
///
/// Dynamic handler for the event.
///
private static void HandleLeftMouseButtonUp(object sender, MouseButtonEventArgs args)
{
FrameworkElement element = (FrameworkElement)sender;
DragAndDropControl handler = RetrieveHandler(element);
handler.OnElementDragStop(element);
}
///
/// Dynamic handler for the event.
///
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);
}
///
/// Dynamic handler for the event.
///
private static void HandleDragLeave(object sender, DragEventArgs args)
{
FrameworkElement element = (FrameworkElement)sender;
DragAndDropControl handler = RetrieveHandler(element);
handler.OnElementDragLeave(element);
}
///
/// Dynamic handler for the event.
///
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(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;
}
}