using System.Windows;
using System.Windows.Input;
using Microsoft.Xaml.Behaviors;
using SheetMe.Designer.Services;
using SheetMe.Designer.ViewModels;
namespace SheetMe.Designer.Behaviors;
///
/// 캔버스 마우스 입력 → 월드 좌표 → InteractionController 위임.
/// World 요소(LayoutTransform 줌 하위)에 부착 — GetPosition(World)가 줌 불변 논리좌표를 반환.
/// 로직 없음: 좌표 변환·캡처·커서 매핑만 담당.
///
public sealed class CanvasMouseBehavior : Behavior
{
#region Methods
protected override void OnAttached()
{
base.OnAttached();
AssociatedObject.MouseLeftButtonDown += OnMouseDown;
AssociatedObject.MouseMove += OnMouseMove;
AssociatedObject.MouseLeftButtonUp += OnMouseUp;
AssociatedObject.LostMouseCapture += OnLostCapture;
AssociatedObject.PreviewMouseRightButtonDown += OnRightButtonDown;
}
protected override void OnDetaching()
{
AssociatedObject.MouseLeftButtonDown -= OnMouseDown;
AssociatedObject.MouseMove -= OnMouseMove;
AssociatedObject.MouseLeftButtonUp -= OnMouseUp;
AssociatedObject.LostMouseCapture -= OnLostCapture;
AssociatedObject.PreviewMouseRightButtonDown -= OnRightButtonDown;
base.OnDetaching();
}
private DesignerViewModel? Designer => AssociatedObject.DataContext as DesignerViewModel;
private static PointerContext ContextOf(int clickCount = 1) => new(
Keyboard.Modifiers.HasFlag(ModifierKeys.Control),
Keyboard.Modifiers.HasFlag(ModifierKeys.Shift),
Keyboard.Modifiers.HasFlag(ModifierKeys.Alt),
clickCount);
private void OnMouseDown(object sender, MouseButtonEventArgs e)
{
if (Designer is not { } designer)
{
return;
}
// 키보드 포커스를 캔버스 컨테이너(Focusable 상위)로 — 단축키 수신
FocusCanvasHost();
designer.Interaction.PointerDown(e.GetPosition(AssociatedObject), ContextOf(e.ClickCount));
AssociatedObject.CaptureMouse();
e.Handled = true;
}
///
/// 우클릭 — 메뉴를 열기 전에 커서 아래 컨트롤을 선택한다.
///
/// 전에는 우클릭이 선택을 전혀 건드리지 않았다. 라벨 A 를 골라 둔 채 떨어진 체크박스 B 를
/// 우클릭하면 메뉴는 B 위에 뜨는데 [삭제]는 A 를 지웠다. 선택이 비어 있으면
/// 메뉴 항목이 전부 살아 있는 채로 아무 일도 하지 않았다.
/// 윈도우의 거의 모든 프로그램이 지키는 규약이라, 사용자는 메뉴가 가리키는 대상을
/// 확인할 생각조차 하지 않는다 — 20항목짜리 메뉴 전체의 신뢰가 이 한 걸음에 달려 있다.
///
/// 이미 선택 안에 있으면 선택을 유지한다 — 다중 선택을 우클릭해서 정렬하는 흐름을 깨면 안 된다.
/// e.Handled 는 두지 않는다. 그대로 흘려보내야 기존 ContextMenu 가 평소처럼 열린다.
///
private void OnRightButtonDown(object sender, MouseButtonEventArgs e)
{
if (Designer is not { } designer || designer.IsTabOrderMode)
{
return; // 탭순서 편집 중에는 클릭이 순번 지정이라 선택을 건드리지 않는다
}
FocusCanvasHost();
var world = e.GetPosition(AssociatedObject);
if (designer.HitTestControl(world) is { } hit)
{
if (!hit.IsSelected)
{
designer.Selection.Set(designer.GroupMatesOf(hit), hit);
}
return;
}
// 빈 곳이면 선택을 푼다 — 다만 잠긴/숨긴 것 위는 빈 곳이 아니다.
// HitTestControl 이 그것들을 후보에서 아예 빼기 때문에(DesignerViewModel) 여기까지 오는데,
// 잠긴 배경 괘선 위에서 우클릭할 때마다 선택이 사라지면 "왜 자꾸 풀리지"가 된다.
if (!designer.AnyControlAt(world))
{
designer.Selection.Clear();
}
}
private void OnMouseMove(object sender, MouseEventArgs e)
{
if (Designer is not { } designer)
{
return;
}
var world = e.GetPosition(AssociatedObject);
if (e.LeftButton == MouseButtonState.Pressed && AssociatedObject.IsMouseCaptured)
{
designer.Interaction.PointerMove(world, ContextOf());
}
AssociatedObject.Cursor = MapCursor(designer.Interaction.CursorAt(world));
}
private void OnMouseUp(object sender, MouseButtonEventArgs e)
{
if (Designer is not { } designer)
{
return;
}
designer.Interaction.PointerUp(e.GetPosition(AssociatedObject), ContextOf());
AssociatedObject.ReleaseMouseCapture();
e.Handled = true;
}
private void OnLostCapture(object sender, MouseEventArgs e)
=> Designer?.Interaction.CancelDrag(); // 정상 업 이후에는 no-op — 선택 유지
private void FocusCanvasHost()
{
// 상위 DesignerCanvasView(Focusable)로 포커스 — 키보드 단축키 수신
DependencyObject? current = AssociatedObject;
while (current is not null)
{
if (current is FrameworkElement { Focusable: true } focusable)
{
focusable.Focus();
return;
}
current = System.Windows.Media.VisualTreeHelper.GetParent(current);
}
}
private static Cursor MapCursor(CursorKind kind) => kind switch
{
CursorKind.SizeAll => Cursors.SizeAll,
CursorKind.SizeNWSE => Cursors.SizeNWSE,
CursorKind.SizeNESW => Cursors.SizeNESW,
CursorKind.SizeNS => Cursors.SizeNS,
CursorKind.SizeWE => Cursors.SizeWE,
_ => Cursors.Arrow,
};
#endregion
}