초기 커밋: SheetMe 서식생성기 (P0~P5 완료 상태)
레거시 서식생성기(VB.NET WinForms) 대체용 C#/.NET 10 WPF 디자이너. 기준선: 실DB 활성 디자인 1,271건 왕복 의미론 diff 0 / 예외 0, 단위 테스트 49/49. 이 커밋에 함께 포함된 자격증명 분리: - appsettings.json 을 __HOST__/__PASSWORD__ 플레이스홀더로 전환 - 실접속 정보는 appsettings.Development.json 으로 분리(.gitignore 제외, csproj Debug 조건부 복사라 Release 산출물에 실리지 않음) - ConfigLoader 를 환경변수 > Development > appsettings 순 레이어링으로 변경, 미치환 플레이스홀더는 '미설정'으로 간주 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,48 @@
|
||||
using System.Windows;
|
||||
using Microsoft.Xaml.Behaviors;
|
||||
using SheetMe.Designer.ViewModels;
|
||||
|
||||
namespace SheetMe.Designer.Behaviors;
|
||||
|
||||
/// <summary>캔버스 드롭 수신 — 팔레트 타입을 월드 좌표에 배치.</summary>
|
||||
public sealed class CanvasDropBehavior : Behavior<FrameworkElement>
|
||||
{
|
||||
#region Methods
|
||||
protected override void OnAttached()
|
||||
{
|
||||
base.OnAttached();
|
||||
AssociatedObject.AllowDrop = true;
|
||||
AssociatedObject.DragOver += OnDragOver;
|
||||
AssociatedObject.Drop += OnDrop;
|
||||
}
|
||||
|
||||
protected override void OnDetaching()
|
||||
{
|
||||
AssociatedObject.DragOver -= OnDragOver;
|
||||
AssociatedObject.Drop -= OnDrop;
|
||||
base.OnDetaching();
|
||||
}
|
||||
|
||||
private void OnDragOver(object sender, DragEventArgs e)
|
||||
{
|
||||
e.Effects = e.Data.GetDataPresent(PaletteDragBehavior.DataFormat)
|
||||
? DragDropEffects.Copy
|
||||
: DragDropEffects.None;
|
||||
e.Handled = true;
|
||||
}
|
||||
|
||||
private void OnDrop(object sender, DragEventArgs e)
|
||||
{
|
||||
if (AssociatedObject.DataContext is not DesignerViewModel designer)
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (e.Data.GetData(PaletteDragBehavior.DataFormat) is not string type)
|
||||
{
|
||||
return;
|
||||
}
|
||||
designer.AddControlAt(type, e.GetPosition(AssociatedObject));
|
||||
e.Handled = true;
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Controls.Primitives;
|
||||
using System.Windows.Input;
|
||||
using Microsoft.Xaml.Behaviors;
|
||||
using SheetMe.Designer.ViewModels;
|
||||
|
||||
namespace SheetMe.Designer.Behaviors;
|
||||
|
||||
/// <summary>
|
||||
/// 캔버스 키보드 단축키 — 텍스트 입력 요소에 포커스가 있으면 통과(입력 보호).
|
||||
/// 화살표 넛지(Shift=그리드), Del, Ctrl+A/C/V/D/Z/Y, Esc.
|
||||
/// </summary>
|
||||
public sealed class CanvasKeyboardBehavior : Behavior<UIElement>
|
||||
{
|
||||
#region Methods
|
||||
protected override void OnAttached()
|
||||
{
|
||||
base.OnAttached();
|
||||
AssociatedObject.PreviewKeyDown += OnPreviewKeyDown;
|
||||
}
|
||||
|
||||
protected override void OnDetaching()
|
||||
{
|
||||
AssociatedObject.PreviewKeyDown -= OnPreviewKeyDown;
|
||||
base.OnDetaching();
|
||||
}
|
||||
|
||||
private void OnPreviewKeyDown(object sender, KeyEventArgs e)
|
||||
{
|
||||
if ((AssociatedObject as FrameworkElement)?.DataContext is not DesignerViewModel designer)
|
||||
{
|
||||
return;
|
||||
}
|
||||
// 텍스트 입력 보호 — 포커스가 편집 컨트롤이면 단축키 라우팅 안 함
|
||||
if (Keyboard.FocusedElement is TextBoxBase or ComboBox or PasswordBox)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var ctrl = Keyboard.Modifiers.HasFlag(ModifierKeys.Control);
|
||||
var shift = Keyboard.Modifiers.HasFlag(ModifierKeys.Shift);
|
||||
var step = shift ? designer.Snap.GridSize * 2 : 1;
|
||||
var handled = true;
|
||||
|
||||
switch (e.Key)
|
||||
{
|
||||
case Key.Left: designer.NudgeSelection(-step, 0); break;
|
||||
case Key.Right: designer.NudgeSelection(step, 0); break;
|
||||
case Key.Up: designer.NudgeSelection(0, -step); break;
|
||||
case Key.Down: designer.NudgeSelection(0, step); break;
|
||||
case Key.Delete: designer.DeleteSelection(); break;
|
||||
case Key.Escape: designer.Interaction.Cancel(); break;
|
||||
case Key.A when ctrl: designer.SelectAllOnActivePage(); break;
|
||||
case Key.C when ctrl: designer.CopySelection(); break;
|
||||
case Key.X when ctrl: designer.CutSelection(); break;
|
||||
case Key.V when ctrl: designer.Paste(); break;
|
||||
case Key.D when ctrl: designer.CopySelection(); designer.Paste(); break;
|
||||
case Key.Z when ctrl: designer.Undo.Undo(); break;
|
||||
case Key.Y when ctrl: designer.Undo.Redo(); break;
|
||||
case Key.G when ctrl && shift: designer.UngroupSelection(); break;
|
||||
case Key.G when ctrl: designer.GroupSelection(); break;
|
||||
default: handled = false; break;
|
||||
}
|
||||
|
||||
e.Handled = handled;
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
using System.Windows;
|
||||
using System.Windows.Input;
|
||||
using Microsoft.Xaml.Behaviors;
|
||||
using SheetMe.Designer.Services;
|
||||
using SheetMe.Designer.ViewModels;
|
||||
|
||||
namespace SheetMe.Designer.Behaviors;
|
||||
|
||||
/// <summary>
|
||||
/// 캔버스 마우스 입력 → 월드 좌표 → InteractionController 위임.
|
||||
/// World 요소(LayoutTransform 줌 하위)에 부착 — GetPosition(World)가 줌 불변 논리좌표를 반환.
|
||||
/// 로직 없음: 좌표 변환·캡처·커서 매핑만 담당.
|
||||
/// </summary>
|
||||
public sealed class CanvasMouseBehavior : Behavior<FrameworkElement>
|
||||
{
|
||||
#region Methods
|
||||
protected override void OnAttached()
|
||||
{
|
||||
base.OnAttached();
|
||||
AssociatedObject.MouseLeftButtonDown += OnMouseDown;
|
||||
AssociatedObject.MouseMove += OnMouseMove;
|
||||
AssociatedObject.MouseLeftButtonUp += OnMouseUp;
|
||||
AssociatedObject.LostMouseCapture += OnLostCapture;
|
||||
}
|
||||
|
||||
protected override void OnDetaching()
|
||||
{
|
||||
AssociatedObject.MouseLeftButtonDown -= OnMouseDown;
|
||||
AssociatedObject.MouseMove -= OnMouseMove;
|
||||
AssociatedObject.MouseLeftButtonUp -= OnMouseUp;
|
||||
AssociatedObject.LostMouseCapture -= OnLostCapture;
|
||||
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;
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Input;
|
||||
using Microsoft.Xaml.Behaviors;
|
||||
using SheetMe.Core.Catalog;
|
||||
|
||||
namespace SheetMe.Designer.Behaviors;
|
||||
|
||||
/// <summary>팔레트 항목 드래그 시작 — 5px 임계 후 DoDragDrop("SheetMe.ControlType").</summary>
|
||||
public sealed class PaletteDragBehavior : Behavior<ListBox>
|
||||
{
|
||||
#region Member Fields
|
||||
/// <summary>드래그 데이터 포맷 키</summary>
|
||||
public const string DataFormat = "SheetMe.ControlType";
|
||||
|
||||
private Point downPoint;
|
||||
private ControlDescriptor? downItem;
|
||||
#endregion
|
||||
|
||||
#region Methods
|
||||
protected override void OnAttached()
|
||||
{
|
||||
base.OnAttached();
|
||||
AssociatedObject.PreviewMouseLeftButtonDown += OnDown;
|
||||
AssociatedObject.PreviewMouseMove += OnMove;
|
||||
AssociatedObject.MouseDoubleClick += OnDoubleClick;
|
||||
}
|
||||
|
||||
protected override void OnDetaching()
|
||||
{
|
||||
AssociatedObject.PreviewMouseLeftButtonDown -= OnDown;
|
||||
AssociatedObject.PreviewMouseMove -= OnMove;
|
||||
AssociatedObject.MouseDoubleClick -= OnDoubleClick;
|
||||
base.OnDetaching();
|
||||
}
|
||||
|
||||
/// <summary>더블클릭 즉시 배치 — 레거시 툴박스 UX(활성 페이지 중앙 부근)</summary>
|
||||
private void OnDoubleClick(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
if ((e.OriginalSource as FrameworkElement)?.DataContext is not ControlDescriptor item)
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (Application.Current.MainWindow?.DataContext is not ViewModels.MainViewModel main
|
||||
|| main.CurrentDesigner is not { } designer)
|
||||
{
|
||||
return;
|
||||
}
|
||||
designer.AddPaletteItemAtCenter(item.Type);
|
||||
e.Handled = true;
|
||||
}
|
||||
|
||||
private void OnDown(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
downPoint = e.GetPosition(AssociatedObject);
|
||||
downItem = (e.OriginalSource as FrameworkElement)?.DataContext as ControlDescriptor;
|
||||
}
|
||||
|
||||
private void OnMove(object sender, MouseEventArgs e)
|
||||
{
|
||||
if (downItem is null || e.LeftButton != MouseButtonState.Pressed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
var position = e.GetPosition(AssociatedObject);
|
||||
if (Math.Abs(position.X - downPoint.X) < 5 && Math.Abs(position.Y - downPoint.Y) < 5)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var item = downItem;
|
||||
downItem = null;
|
||||
DragDrop.DoDragDrop(AssociatedObject, new DataObject(DataFormat, item.Type), DragDropEffects.Copy);
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
Reference in New Issue
Block a user