using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using SheetMe.Designer.ViewModels;
using SheetMe.Designer.ViewModels.Controls;
namespace SheetMe.Designer.Behaviors;
///
/// 레이어 목록 드래그 재정렬 — 행을 끌어다 놓아 z-order(겹침 순서)를 바꾼다.
///
/// 목록은 z-order 최상위가 맨 위이므로 위로 끌면 앞으로 나오고 아래로 끌면 뒤로 물러난다.
/// 대상은 같은 부모 안의 최상위 컨트롤끼리로 제한한다 — 컨테이너 안팎으로 옮기는 것은
/// 좌표계가 바뀌는 별개의 연산이고, 그룹 폴더·페이지 행은 순서를 갖지 않는다.
///
/// z-order 는 저장되는 값이라(XML Object 순서) 오작동이 실제 서식의 겹침을 망가뜨린다.
/// 그래서 드롭 위치 판정은 행 높이의 절반 기준으로 단순하게 두고, 삽입선을 눈으로 보여준다.
///
public static class LayerDragBehavior
{
#region Attached Properties
/// 드래그 재정렬을 붙일 디자이너 — 문서 탭이 바뀌면 갈아탄다
public static readonly DependencyProperty DesignerProperty = DependencyProperty.RegisterAttached(
"Designer", typeof(DesignerViewModel), typeof(LayerDragBehavior),
new PropertyMetadata(null, OnDesignerChanged));
public static void SetDesigner(DependencyObject element, DesignerViewModel? value)
=> element.SetValue(DesignerProperty, value);
public static DesignerViewModel? GetDesigner(DependencyObject element)
=> (DesignerViewModel?)element.GetValue(DesignerProperty);
#endregion
#region Member Fields
private const double DragThreshold = 5;
private static ListBox? activeList;
private static ControlViewModel? dragSource;
private static Point pressPoint;
private static bool pressed;
private static InsertionAdorner? adorner;
#endregion
#region Methods
private static void OnDesignerChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
{
if (sender is not ListBox list)
{
return;
}
list.PreviewMouseLeftButtonDown -= OnPress;
list.PreviewMouseMove -= OnMove;
list.PreviewMouseLeftButtonUp -= OnRelease;
list.QueryContinueDrag -= OnQueryContinue;
list.DragOver -= OnDragOver;
list.Drop -= OnDrop;
list.DragLeave -= OnDragLeave;
if (e.NewValue is not DesignerViewModel)
{
return;
}
list.AllowDrop = true;
list.PreviewMouseLeftButtonDown += OnPress;
list.PreviewMouseMove += OnMove;
list.PreviewMouseLeftButtonUp += OnRelease;
list.QueryContinueDrag += OnQueryContinue;
list.DragOver += OnDragOver;
list.Drop += OnDrop;
list.DragLeave += OnDragLeave;
}
/// 누른 지점 기억 — 실제 드래그는 임계 거리를 넘어야 시작한다(클릭 선택을 방해하지 않도록)
private static void OnPress(object sender, MouseButtonEventArgs e)
{
if (sender is not ListBox list)
{
return;
}
// 눈·자물쇠·셰브론 같은 행 안의 버튼에서 시작한 누름은 드래그가 아니다
if (e.OriginalSource is DependencyObject origin
&& FindAncestor(origin) is not null)
{
return;
}
var row = RowDataOf(e.OriginalSource);
// 최상위 컨트롤만 — 컨테이너 자식은 부모 안에서만 의미가 있어 목록 드래그로 다루지 않는다
dragSource = row is ControlViewModel control && control.Parent is null ? control : null;
activeList = list;
pressPoint = e.GetPosition(list);
pressed = dragSource is not null;
}
private static void OnMove(object sender, MouseEventArgs e)
{
if (!pressed || dragSource is null || activeList is null || e.LeftButton != MouseButtonState.Pressed)
{
return;
}
var delta = e.GetPosition(activeList) - pressPoint;
if (Math.Abs(delta.X) < DragThreshold && Math.Abs(delta.Y) < DragThreshold)
{
return;
}
pressed = false;
DragDrop.DoDragDrop(activeList, new DataObject(typeof(ControlViewModel), dragSource), DragDropEffects.Move);
ClearAdorner();
dragSource = null;
}
private static void OnRelease(object sender, MouseButtonEventArgs e) => pressed = false;
/// Esc 로 드래그 취소 — 삽입선도 함께 걷는다
private static void OnQueryContinue(object sender, QueryContinueDragEventArgs e)
{
if (e.EscapePressed)
{
ClearAdorner();
}
}
private static void OnDragOver(object sender, DragEventArgs e)
{
if (sender is not ListBox list || !e.Data.GetDataPresent(typeof(ControlViewModel)))
{
return;
}
var (target, above) = ResolveTarget(list, e);
if (target is null)
{
ClearAdorner();
e.Effects = DragDropEffects.None;
e.Handled = true;
return;
}
e.Effects = DragDropEffects.Move;
e.Handled = true;
ShowAdorner(list, target, above);
}
private static void OnDragLeave(object sender, DragEventArgs e) => ClearAdorner();
private static void OnDrop(object sender, DragEventArgs e)
{
ClearAdorner();
if (sender is not ListBox list
|| GetDesigner(list) is not { } designer
|| e.Data.GetData(typeof(ControlViewModel)) is not ControlViewModel source)
{
return;
}
var (targetItem, above) = ResolveTarget(list, e);
if (targetItem?.DataContext is not ControlViewModel target
|| ReferenceEquals(target, source)
|| target.Parent is not null)
{
return;
}
designer.MoveControlNextTo(source, target, above);
e.Handled = true;
}
/// 커서 아래의 드롭 대상 행과 위/아래 판정
private static (ListBoxItem? Item, bool Above) ResolveTarget(ListBox list, DragEventArgs e)
{
var hit = list.InputHitTest(e.GetPosition(list)) as DependencyObject;
var item = hit is null ? null : FindAncestor(hit);
if (item?.DataContext is not ControlViewModel control || control.Parent is not null)
{
return (null, false);
}
var local = e.GetPosition(item);
return (item, local.Y < item.ActualHeight / 2);
}
private static void ShowAdorner(ListBox list, ListBoxItem item, bool above)
{
var layer = AdornerLayer.GetAdornerLayer(list);
if (layer is null)
{
return;
}
if (adorner is not null && ReferenceEquals(adorner.AdornedElement, item) && adorner.Above == above)
{
return;
}
ClearAdorner();
adorner = new InsertionAdorner(item, above);
layer.Add(adorner);
}
private static void ClearAdorner()
{
if (adorner is null)
{
return;
}
AdornerLayer.GetAdornerLayer(adorner.AdornedElement)?.Remove(adorner);
adorner = null;
}
private static object? RowDataOf(object? source)
=> source is DependencyObject node ? FindAncestor(node)?.DataContext : null;
private static T? FindAncestor(DependencyObject? node) where T : DependencyObject
{
while (node is not null)
{
if (node is T match)
{
return match;
}
node = node is Visual or System.Windows.Media.Media3D.Visual3D
? VisualTreeHelper.GetParent(node)
: LogicalTreeHelper.GetParent(node);
}
return null;
}
#endregion
#region Types
///
/// 드롭 위치 삽입선 — 대상 행의 위 또는 아래 모서리에 2px 선을 그린다.
///
/// [200] 은 대상 행의 BorderThickness 를 0→2 로 올려 표시하는데, 그러면 행 내용이 2px 밀려
/// 드래그 중 목록이 들썩인다. 어도너는 레이아웃 바깥에 그려져 그런 흔들림이 없다.
///
private sealed class InsertionAdorner : Adorner
{
private static readonly Pen Line = CreatePen();
public bool Above { get; }
public InsertionAdorner(UIElement adorned, bool above) : base(adorned)
{
Above = above;
IsHitTestVisible = false;
}
protected override void OnRender(DrawingContext drawingContext)
{
var width = ((FrameworkElement)AdornedElement).ActualWidth;
var height = ((FrameworkElement)AdornedElement).ActualHeight;
var y = Above ? 1 : height - 1;
drawingContext.DrawLine(Line, new Point(0, y), new Point(width, y));
}
private static Pen CreatePen()
{
// 어도너는 리소스 조회 컨텍스트 밖이라 DynamicResource 가 닿지 않는다.
// 브랜드색은 양 테마 공용값(B.Accent)이라 여기서만 상수로 둔다.
var brush = new SolidColorBrush(Color.FromRgb(0x00, 0x99, 0xFF));
brush.Freeze();
var pen = new Pen(brush, 2);
pen.Freeze();
return pen;
}
}
#endregion
}