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 }