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 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+