From 7f8808eb4497c60a610aced130e9435b93991795 Mon Sep 17 00:00:00 2001 From: Msystech Date: Fri, 14 Aug 2026 10:31:24 +0900 Subject: [PATCH] =?UTF-8?q?=EC=A0=95=EB=A0=AC=20=EA=B8=B0=EC=A4=80?= =?UTF-8?q?=EC=9D=84=20=EA=B3=A0=EB=A5=BC=20=EC=88=98=20=EC=9E=88=EA=B2=8C?= =?UTF-8?q?=20+=20=EA=B0=84=EA=B2=A9=20=EA=B7=A0=EB=93=B1=20+=20=EC=95=84?= =?UTF-8?q?=EC=9D=B4=EC=BD=98=20=EB=90=98=EB=8F=8C=EB=A6=AC=EA=B8=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit "전체 선택 후 오브젝트끼리 정렬 / 페이지에서 정렬" — 둘 다 필요한데 하나만 됐다. 직전 커밋의 규칙은 자동이었다: 여럿이면 무조건 선택 영역, 하나면 무조건 부모/페이지. 여럿을 골라 놓고 종이에 맞추고 싶을 때 방법이 없었다. 기준을 명시적으로 고르게 했다. 인스펙터 정렬 행 아래에 알약 둘 — 왼쪽은 선택에 따라 문구가 바뀐다(여럿이면 '선택 영역', 컨테이너 안의 하나면 '부모'), 오른쪽은 '페이지'. 최상위 컨트롤 하나만 골랐을 때는 이 줄을 아예 접는다 — 그 경우 '선택 영역'도 곧 페이지라 알약 둘이 똑같은 말을 하게 된다(실제로 그렇게 렌더돼서 접기로 했다). 컨테이너 안의 것을 페이지 기준으로 맞추면 부모 밖으로 나간다. 막지는 않는다 — 사용자가 고른 것이고 되돌릴 수 있다 — 대신 '페이지' 알약 툴팁이 그 사실을 말한다. 간격 균등 2개(가로·세로)를 붙였다. 3개 이상 골랐을 때만 나타난다 — 사이가 있어야 나눌 것이 있다. 정렬 6개와 같은 줄에 두면 8칸 276px 가 되어 패널 최소 폭 250 에서 잘리고, WrapPanel 로 흘리면 7+1 로 깨진다(둘 다 렌더로 확인했다). 그래서 '간격' 이라는 제 줄을 준다. 아이콘은 [200] 와이어프레임의 '사각형 하나 + 기준선'에서 원래 Lucide 도형 '길이 다른 막대 둘 + 기준선'으로 되돌렸다 — 후보 4종을 그려 고른 결과다. 막대가 둘이면 '무엇을 무엇에 맞추는가'가 도형만으로 읽히고, 저장소의 다른 아이콘과도 결이 맞는다. LucideIcons 의 align-obj-* 6개는 직전 커밋 이전 상태로 정확히 복귀했다. 기준 알약은 RadioButton 이다. ToggleButton 둘로 만들면 둘 다 꺼진 상태가 표현 가능해지는데 그건 뜻이 없는 상태다. 게이트: 테스트 267/267, --edit-smoke 237건 0실패(기준 전환 검사 3건 신규), --query-popup 17/17, --maxrect 10/10, --snap-shots 7종 0실패, --db-smoke 3000 diff 0·예외 0, --db-render P062 md5 8d683835f5d81e7bb41c79071d6bf954 동일, --dialog-shots 57파일. Co-Authored-By: Claude Opus 5 --- src/SheetMe.Designer/Controls/LucideIcons.cs | 12 +-- src/SheetMe.Designer/Diagnostics/EditSmoke.cs | 19 +++++ .../ViewModels/DesignerViewModel.cs | 42 +++++++++- .../Inspector/InspectorViewModel.cs | 70 ++++++++++++++++- src/SheetMe.Designer/Views/InspectorView.xaml | 77 +++++++++++++++++-- 5 files changed, 205 insertions(+), 15 deletions(-) diff --git a/src/SheetMe.Designer/Controls/LucideIcons.cs b/src/SheetMe.Designer/Controls/LucideIcons.cs index 6531287..c8fded4 100644 --- a/src/SheetMe.Designer/Controls/LucideIcons.cs +++ b/src/SheetMe.Designer/Controls/LucideIcons.cs @@ -84,9 +84,9 @@ public static class LucideIcons ["moon"] = """""", ["move-horizontal"] = """ """, ["move-vertical"] = """ """, - ["align-obj-top"] = """ """, - ["align-obj-middle"] = """ """, - ["align-obj-bottom"] = """ """, + ["align-obj-top"] = """ """, + ["align-obj-middle"] = """ """, + ["align-obj-bottom"] = """ """, ["distribute-h"] = """ """, ["distribute-v"] = """ """, ["square"] = """""", @@ -94,9 +94,9 @@ public static class LucideIcons ["unlock"] = """ """, ["group"] = """ """, ["ungroup"] = """ """, - ["align-obj-left"] = """ """, - ["align-obj-center-h"] = """ """, - ["align-obj-right"] = """ """, + ["align-obj-left"] = """ """, + ["align-obj-center-h"] = """ """, + ["align-obj-right"] = """ """, ["droplet"] = """""", ["rotate-cw"] = """ """, ["frame"] = """ """, diff --git a/src/SheetMe.Designer/Diagnostics/EditSmoke.cs b/src/SheetMe.Designer/Diagnostics/EditSmoke.cs index e1eb683..05840ed 100644 --- a/src/SheetMe.Designer/Diagnostics/EditSmoke.cs +++ b/src/SheetMe.Designer/Diagnostics/EditSmoke.cs @@ -1138,6 +1138,25 @@ public static class EditSmoke Check("정렬: 실행취소 1회로 되돌아온다", restored is not null && restored.X == beforeUndoX, $"실제 {restored?.X}"); + // 기준 전환 — 여럿을 골라 놓고 '서로 맞추기'와 '종이에 맞추기'가 갈린다 + var a = Put(300, 500, 60, 20); + var b = Put(340, 540, 60, 20); + alignDoc.Selection.Set(new List { a, b }, a); + + alignDoc.AlignToPage = false; + alignDoc.Align("Left"); + Check("정렬: 여럿을 서로 맞추면 선택 영역 왼쪽", a.X == 300 && b.X == 300, + $"실제 {a.X} / {b.X}"); + + alignDoc.AlignToPage = true; + alignDoc.Align("Left"); + Check("정렬: 기준을 페이지로 바꾸면 종이 왼쪽", a.X == 0 && b.X == 0, $"실제 {a.X} / {b.X}"); + + alignDoc.Align("Right"); + Check("정렬: 페이지 기준 오른쪽", a.X == page0.WidthDip - a.Width && b.X == page0.WidthDip - b.Width, + $"실제 {a.X} / {b.X}"); + alignDoc.AlignToPage = false; + // 컨테이너 자식은 페이지가 아니라 부모가 기준 — 페이지 기준이면 X 가 음수가 된다 var nestDoc = new DesignerViewModel(business.CreateNew()); nestDoc.AddControlAt("Panel", new Point(120, 90)); diff --git a/src/SheetMe.Designer/ViewModels/DesignerViewModel.cs b/src/SheetMe.Designer/ViewModels/DesignerViewModel.cs index cd65b76..e5fdb16 100644 --- a/src/SheetMe.Designer/ViewModels/DesignerViewModel.cs +++ b/src/SheetMe.Designer/ViewModels/DesignerViewModel.cs @@ -74,6 +74,9 @@ public sealed class DesignerViewModel : ViewModelBase private bool isTabOrderMode; private readonly List tabOrderSequence = new(); private readonly Dictionary? spreadDesigns; + + /// 정렬을 페이지에 맞출 것인가 — 인스펙터 토글이 세운다 + private bool alignToPage; // 레이어 목록 상태 — 접힌 그룹과 타입 필터는 문서가 아니라 편집 세션에 속한다(Undo 대상 아님) private readonly HashSet collapsedGroups = new(StringComparer.Ordinal); private readonly HashSet collapsedPages = new(); @@ -1531,9 +1534,28 @@ public sealed class DesignerViewModel : ViewModelBase Selection.NotifyBoundsChanged(); } - /// 정렬 기준 사각형(월드) — 여럿이면 선택 영역, 하나면 그것을 담고 있는 것 + /// + /// 정렬 기준 — 무엇에 맞출 것인가. + /// + /// 여럿을 골라 놓고도 "서로 맞추기"와 "종이에 맞추기"는 다른 작업이다. + /// 예전에는 전자만 가능했다. + /// + public bool AlignToPage + { + get => alignToPage; + set => SetProperty(ref alignToPage, value); + } + + /// 정렬 기준 사각형(월드) private SheetMe.Core.Layout.SnapRect? ReferenceFor(IReadOnlyList targets) { + if (alignToPage) + { + return PageOf(targets[0]) is { } target + ? new SheetMe.Core.Layout.SnapRect(0, target.OffsetY, target.WidthDip, target.HeightDip) + : null; + } + if (targets.Count >= 2) { if (SelectionWorldBounds() is not { } bbox) @@ -1579,6 +1601,10 @@ public sealed class DesignerViewModel : ViewModelBase { return string.Empty; } + if (alignToPage) + { + return "페이지 기준"; + } if (items.Count >= 2) { return items.Select(PageOf).Distinct().Count() > 1 @@ -1591,6 +1617,20 @@ public sealed class DesignerViewModel : ViewModelBase : "페이지 기준"; } + /// '선택 영역' 쪽 세그먼트에 뭐라고 쓸 것인가 — 하나만 골랐으면 그건 부모(또는 페이지)다 + public string AlignSelectionScopeLabel() + { + var items = Selection.Items; + if (items.Count >= 2) + { + return "선택 영역"; + } + return items.Count == 1 && items[0].Parent is not null ? "부모" : "페이지"; + } + + /// 간격 균등이 뜻을 갖는가 — 사이가 있어야 나눌 것이 있다 + public bool CanDistribute() => Selection.Items.Count >= 3; + /// 세로 정렬이 가능한가 — 여러 페이지에 걸친 선택에서는 뜻이 없다 public bool CanAlignVertical() => Selection.Items.Count > 0 && Selection.Items.Select(PageOf).Distinct().Count() <= 1; diff --git a/src/SheetMe.Designer/ViewModels/Inspector/InspectorViewModel.cs b/src/SheetMe.Designer/ViewModels/Inspector/InspectorViewModel.cs index 621e857..1522f3a 100644 --- a/src/SheetMe.Designer/ViewModels/Inspector/InspectorViewModel.cs +++ b/src/SheetMe.Designer/ViewModels/Inspector/InspectorViewModel.cs @@ -65,11 +65,61 @@ public sealed class InspectorViewModel : ViewModelBase ? AlignScopeText : "여러 페이지에 걸친 선택은 세로 정렬을 할 수 없습니다"; + /// '선택 영역' 세그먼트 문구 — 하나만 골랐으면 그건 부모(또는 페이지)다 + public string AlignSelectionScopeLabel => designer.AlignSelectionScopeLabel(); + + /// + /// 기준 선택 줄을 보일 것인가 — 고를 것이 둘일 때만. + /// 최상위 컨트롤 하나를 골랐으면 '선택 영역'도 곧 페이지라 알약 둘이 똑같은 말을 하게 된다. + /// + public bool ShowAlignScope => designer.Selection.Items.Count >= 2 + || (designer.Selection.Items.Count == 1 && designer.Selection.Items[0].Parent is not null); + + /// 컨테이너 안의 것을 페이지에 맞추면 부모 밖으로 나간다 — 막지는 않되 말은 해 준다 + public string PageScopeTip => designer.Selection.Items.Count == 1 + && designer.Selection.Items[0].Parent is not null + ? "종이 기준으로 맞춥니다 — 부모 밖으로 나가면 실행 시 잘릴 수 있습니다" + : "종이 기준으로 맞춥니다"; + + /// 기준 = 선택 영역(또는 부모) + public bool AlignToSelection + { + get => !designer.AlignToPage; + set + { + if (value && designer.AlignToPage) + { + designer.AlignToPage = false; + NotifyAlignScope(); + } + } + } + + /// 기준 = 페이지 + public bool AlignToPage + { + get => designer.AlignToPage; + set + { + if (value && !designer.AlignToPage) + { + designer.AlignToPage = true; + NotifyAlignScope(); + } + } + } + + /// 간격 균등 버튼을 보일 것인가 — 사이가 있어야 나눌 것이 있다(3개 이상) + public bool CanDistribute => designer.CanDistribute(); + /// /// 정렬 실행 — 뷰의 DataContext 가 인스펙터라 디자이너 커맨드에 직접 닿을 수 없어 위임한다. /// public M.Framework.WPF.ICustomCommand AlignCommand { get; } + /// 간격 균등 실행 — 위와 같은 이유로 위임 + public M.Framework.WPF.ICustomCommand DistributeCommand { get; } + /// /// 선택된 탭. 컨트롤을 바꿔도 유지된다 — 데이터 탭에서 여러 컨트롤의 태그를 잇달아 손보는 /// 작업이 흔한데, 선택할 때마다 디자인 탭으로 되돌아가면 매번 다시 눌러야 한다. @@ -173,12 +223,28 @@ public sealed class InspectorViewModel : ViewModelBase this.designer = designer; AlignCommand = new M.Framework.WPF.Command( (object parameter) => designer.Align(parameter as string ?? string.Empty)); + DistributeCommand = new M.Framework.WPF.Command( + (object parameter) => designer.AdjustSpacing(parameter as string ?? string.Empty)); designer.Selection.SetChanged += Rebuild; designer.Selection.Changed += RefreshBoundsRows; // 드래그/리사이즈 중 X/Y/W/H 실시간 갱신 } #endregion #region Methods + /// 정렬 기준에 딸린 표시들 — 선택이 바뀌거나 기준 토글을 누를 때 함께 움직인다 + private void NotifyAlignScope() + { + OnPropertyChanged(nameof(CanAlignVertical)); + OnPropertyChanged(nameof(CanDistribute)); + OnPropertyChanged(nameof(AlignScopeText)); + OnPropertyChanged(nameof(VerticalScopeText)); + OnPropertyChanged(nameof(AlignSelectionScopeLabel)); + OnPropertyChanged(nameof(ShowAlignScope)); + OnPropertyChanged(nameof(PageScopeTip)); + OnPropertyChanged(nameof(AlignToSelection)); + OnPropertyChanged(nameof(AlignToPage)); + } + /// 경계(X/Y/W/H) 행 값만 갱신 — 이동/리사이즈 프레임(재구성 없이 가볍게) private void RefreshBoundsRows() { @@ -203,9 +269,7 @@ public sealed class InspectorViewModel : ViewModelBase OnPropertyChanged(nameof(Summary)); OnPropertyChanged(nameof(HasSelection)); OnPropertyChanged(nameof(ShowAlignBar)); - OnPropertyChanged(nameof(CanAlignVertical)); - OnPropertyChanged(nameof(AlignScopeText)); - OnPropertyChanged(nameof(VerticalScopeText)); + NotifyAlignScope(); var items = designer.Selection.Items; if (items.Count == 0) diff --git a/src/SheetMe.Designer/Views/InspectorView.xaml b/src/SheetMe.Designer/Views/InspectorView.xaml index fc03c07..eb6e5ad 100644 --- a/src/SheetMe.Designer/Views/InspectorView.xaml +++ b/src/SheetMe.Designer/Views/InspectorView.xaml @@ -132,6 +132,42 @@ + + + + + + + + + + + + + + + + + + + +