diff --git a/src/SheetMe.Designer/Controls/LucideIcons.cs b/src/SheetMe.Designer/Controls/LucideIcons.cs
index 521ac4d..e3e4e99 100644
--- a/src/SheetMe.Designer/Controls/LucideIcons.cs
+++ b/src/SheetMe.Designer/Controls/LucideIcons.cs
@@ -64,6 +64,7 @@ public static class LucideIcons
["bold"] = """ """,
["italic"] = """ """,
["underline"] = """ """,
+ ["strikethrough"] = """ """,
["zoom-in"] = """ """,
["zoom-out"] = """ """,
["maximize"] = """ """,
diff --git a/src/SheetMe.Designer/Diagnostics/DialogShots.cs b/src/SheetMe.Designer/Diagnostics/DialogShots.cs
index 5a2a62e..64abb4c 100644
--- a/src/SheetMe.Designer/Diagnostics/DialogShots.cs
+++ b/src/SheetMe.Designer/Diagnostics/DialogShots.cs
@@ -119,6 +119,28 @@ public static class DialogShots
("06-font-manager", () => new Views.FontManagerDialogView(designer)),
("07-register-sheet", () => new Views.RegisterSheetDialogView("S999", "표본 서식")),
("08-preview", () => new Views.PreviewWindow(designer)),
+ // 인스펙터는 UserControl 이라 창이 없다 — 실폭 300px 호스트 창에 담아 실제 배치를 찍는다.
+ // 속성 패널 레이아웃 회귀를 눈으로 확인할 수 있는 유일한 자동 경로다.
+ ("09-inspector-label", () => HostInspector(designer, "Label1")),
+ ("10-inspector-textbox", () => HostInspector(designer, "TextBox1")),
+ };
+ }
+
+ /// 인스펙터를 실제 패널 폭(300px)의 창에 담는다 — 컨트롤 하나를 선택한 상태로
+ private static Window HostInspector(DesignerViewModel designer, string controlId)
+ {
+ var page = designer.Pages[0];
+ var target = page.Controls.FirstOrDefault(c => c.Id == controlId) ?? page.Controls[0];
+ designer.Selection.SetSingle(target);
+ designer.Inspector.Rebuild();
+
+ return new Window
+ {
+ Title = $"속성 — {controlId}",
+ Width = 300,
+ Height = 720,
+ Content = new Views.InspectorView { DataContext = designer.Inspector },
+ Background = Application.Current.TryFindResource("B.Panel") as Brush,
};
}
diff --git a/src/SheetMe.Designer/ViewModels/Inspector/InspectorViewModel.cs b/src/SheetMe.Designer/ViewModels/Inspector/InspectorViewModel.cs
index 1b16e73..a2208bb 100644
--- a/src/SheetMe.Designer/ViewModels/Inspector/InspectorViewModel.cs
+++ b/src/SheetMe.Designer/ViewModels/Inspector/InspectorViewModel.cs
@@ -98,18 +98,18 @@ public sealed class InspectorViewModel : ViewModelBase
AffectsVisual = false,
});
}
- AddBoundsRow("X", vm => vm.X, (vm, v) => vm.X = v);
- AddBoundsRow("Y", vm => vm.Y, (vm, v) => vm.Y = v);
- AddBoundsRow("너비", vm => vm.Width, (vm, v) => vm.Width = Math.Max(1, v));
- AddBoundsRow("높이", vm => vm.Height, (vm, v) => vm.Height = Math.Max(1, v));
+ // 위치·크기는 짝지어 한 줄씩 — 네 줄(약 136px)을 두 줄로 줄인다
+ AddBoundsPair("X", vm => vm.X, (vm, v) => vm.X = v,
+ "Y", vm => vm.Y, (vm, v) => vm.Y = v);
+ AddBoundsPair("너비", vm => vm.Width, (vm, v) => vm.Width = Math.Max(1, v),
+ "높이", vm => vm.Height, (vm, v) => vm.Height = Math.Max(1, v));
// 폰트(공통) — Font Property(상속 시 빈 값)
AddSection("글꼴");
AddFontRow("글꼴", f => f.Family, (f, v) => f.Family = v.Length == 0 ? f.Family : v);
AddFontRow("크기(pt)", f => f.SizePt.ToString("0.##", CultureInfo.InvariantCulture),
(f, v) => f.SizePt = double.TryParse(v, NumberStyles.Number, CultureInfo.InvariantCulture, out var size) && size > 0 ? size : f.SizePt);
- AddFontToggleRow("굵게", f => f.Bold, (f, v) => f.Bold = v);
- AddFontToggleRow("밑줄", f => f.Underline, (f, v) => f.Underline = v);
+ AddFontStyleSegment();
// 타입 전용 — 전체 선택이 같은 타입일 때만
var type = items[0].Type;
@@ -270,7 +270,23 @@ public sealed class InspectorViewModel : ViewModelBase
AddRow(row, binding);
}
- private void AddBoundsRow(string label, Func get, Action set)
+ ///
+ /// 경계 값 두 개를 한 줄에 — X|Y, 너비|높이.
+ /// 자식 행은 boundsRows 에 그대로 등록되므로 드래그·리사이즈 중 실시간 갱신
+ /// (RefreshBoundsRows)이 종전과 똑같이 동작한다.
+ ///
+ private void AddBoundsPair(
+ string leftLabel, Func getLeft, Action setLeft,
+ string rightLabel, Func getRight, Action setRight)
+ {
+ var left = CreateBoundsRow(leftLabel, getLeft, setLeft);
+ var right = CreateBoundsRow(rightLabel, getRight, setRight);
+ var pair = new PairRowViewModel(leftLabel, left, rightLabel, right);
+ currentSection?.Adopt(pair);
+ Rows.Add(pair);
+ }
+
+ private NumberRowViewModel CreateBoundsRow(string label, Func get, Action set)
{
var row = new NumberRowViewModel(label);
var binding = new RowBinding
@@ -285,8 +301,9 @@ public sealed class InspectorViewModel : ViewModelBase
},
AffectsVisual = false,
};
- AddRow(row, binding, afterCommit: () => designer.Selection.NotifyBoundsChanged());
+ BindRow(row, binding, afterCommit: () => designer.Selection.NotifyBoundsChanged());
boundsRows.Add((row, binding));
+ return row;
}
private void AddFontRow(string label, Func get, Action set)
@@ -303,9 +320,25 @@ public sealed class InspectorViewModel : ViewModelBase
});
}
- private void AddFontToggleRow(string label, Func get, Action set)
+ ///
+ /// 글꼴 스타일 세그먼트 — 굵게·기울임·밑줄을 한 줄에.
+ /// 체크박스 하나에 한 줄씩이면 세 줄(약 100px)이고, 기울임은 아예 빠져 있었다.
+ ///
+ private void AddFontStyleSegment()
{
- AddRow(new ToggleRowViewModel(label), new RowBinding
+ var segment = new SegmentRowViewModel("스타일");
+ segment.Items.Add(NewFontToggle("bold", "굵게", f => f.Bold, (f, v) => f.Bold = v));
+ segment.Items.Add(NewFontToggle("italic", "기울임", f => f.Italic, (f, v) => f.Italic = v));
+ segment.Items.Add(NewFontToggle("underline", "밑줄", f => f.Underline, (f, v) => f.Underline = v));
+ segment.Items.Add(NewFontToggle("strikethrough", "취소선", f => f.Strikeout, (f, v) => f.Strikeout = v));
+ currentSection?.Adopt(segment);
+ Rows.Add(segment);
+ }
+
+ private SegmentItem NewFontToggle(string icon, string label, Func get, Action set)
+ {
+ var toggle = new ToggleRowViewModel(label);
+ BindRow(toggle, new RowBinding
{
Get = vm => get(vm.EffectiveFont) ? "True" : "False",
Set = (vm, value) =>
@@ -315,6 +348,7 @@ public sealed class InspectorViewModel : ViewModelBase
vm.Model.Props.SetText("Font", LegacyFormat.FormatFont(font));
},
});
+ return new SegmentItem(icon, label, toggle);
}
/// 편집 기준 폰트 — 명시 Font 있으면 그 값, 없으면 유효(상속) 폰트 복사본을 물화
@@ -352,6 +386,18 @@ public sealed class InspectorViewModel : ViewModelBase
}
private void AddRow(PropertyRowViewModel row, RowBinding binding, Action? afterCommit = null)
+ {
+ BindRow(row, binding, afterCommit);
+ currentSection?.Adopt(row);
+ Rows.Add(row);
+ }
+
+ ///
+ /// 행에 값과 커밋을 배선한다 — Rows 에 넣지는 않는다.
+ /// 쌍 행·세그먼트 행의 자식처럼 화면에는 나오지만 목록의 최상위 항목이 아닌 행에 쓴다.
+ /// 커밋 규약은 여기 한 곳에만 있으므로 자식도 동일하게 동작한다.
+ ///
+ private void BindRow(PropertyRowViewModel row, RowBinding binding, Action? afterCommit = null)
{
var items = designer.Selection.Items;
var values = items.Select(binding.Get).Distinct(StringComparer.Ordinal).ToList();
@@ -384,8 +430,6 @@ public sealed class InspectorViewModel : ViewModelBase
}
afterCommit?.Invoke();
};
- currentSection?.Adopt(row);
- Rows.Add(row);
}
private void RenameControl(ControlViewModel vm, string newId)
diff --git a/src/SheetMe.Designer/ViewModels/Inspector/PropertyRows.cs b/src/SheetMe.Designer/ViewModels/Inspector/PropertyRows.cs
index e0a5d42..5605a7c 100644
--- a/src/SheetMe.Designer/ViewModels/Inspector/PropertyRows.cs
+++ b/src/SheetMe.Designer/ViewModels/Inspector/PropertyRows.cs
@@ -21,6 +21,13 @@ public abstract class PropertyRowViewModel : ViewModelBase
/// 선택 대상들의 값이 서로 다른지 — "여러 값" 표시
public bool IsMixed { get; private set; }
+ ///
+ /// 이 행이 품고 있는 '속성' 개수 — 섹션 헤더의 건수 표시용.
+ /// 쌍 행·세그먼트 행은 한 줄이지만 속성은 여럿이라, 줄 수를 세면 헤더가 "공통 3"처럼
+ /// 실제보다 적게 나온다. 사용자가 세는 것은 줄이 아니라 속성이다.
+ ///
+ public virtual int LeafCount => 1;
+
/// 소속 섹션 — null 이면 섹션 밖(항상 표시)
public SectionRowViewModel? Section { get; set; }
@@ -114,7 +121,14 @@ public sealed class SectionRowViewModel : PropertyRowViewModel
public string ChevronIcon => isExpanded ? "chevron-down" : "chevron-right";
/// 접힌 상태에서 몇 개가 숨어 있는지
- public string CountText => Children.Count == 0 ? string.Empty : Children.Count.ToString();
+ public string CountText
+ {
+ get
+ {
+ var total = Children.Sum(c => c.LeafCount);
+ return total == 0 ? string.Empty : total.ToString();
+ }
+ }
/// 헤더 클릭 — 접기/펴기
public M.Framework.WPF.ICustomCommand? ToggleCommand { get; }
@@ -139,6 +153,78 @@ public sealed class TextRowViewModel : PropertyRowViewModel
public TextRowViewModel(string label) : base(label) { }
}
+///
+/// 두 값을 한 줄에 나란히 두는 행 — X|Y, 너비|높이.
+///
+/// 자식 행은 평범한 행 그대로다. 값·커밋·"여러 값" 판정이 전부 자식 안에서 끝나므로
+/// 커밋 규약(Initialize 는 Commit 을 내지 않고, 값이 바뀔 때만 Undo 스냅샷 1회)이 그대로 유지된다.
+/// 한쪽만 값이 갈리는 다중선택도 자연히 옳게 나온다.
+/// 이 행 자신은 컨테이너일 뿐이라 Commit 을 갖지 않는다.
+///
+public sealed class PairRowViewModel : PropertyRowViewModel
+{
+ /// 왼쪽 칸 라벨(짧게 — X, 너비)
+ public string LeftLabel { get; }
+
+ /// 왼쪽 칸 행
+ public PropertyRowViewModel Left { get; }
+
+ /// 오른쪽 칸 라벨
+ public string RightLabel { get; }
+
+ /// 오른쪽 칸 행
+ public PropertyRowViewModel Right { get; }
+
+ /// 한 줄이지만 속성은 둘
+ public override int LeafCount => 2;
+
+ public PairRowViewModel(string leftLabel, PropertyRowViewModel left, string rightLabel, PropertyRowViewModel right)
+ : base(leftLabel + "/" + rightLabel)
+ {
+ LeftLabel = leftLabel;
+ Left = left;
+ RightLabel = rightLabel;
+ Right = right;
+ }
+}
+
+/// 세그먼트 한 칸 — 아이콘 토글 하나
+public sealed class SegmentItem
+{
+ /// Lucide 아이콘명
+ public string Icon { get; }
+
+ /// 툴팁(라벨 대용 — 아이콘만으로는 뜻이 안 서는 사용자를 위해)
+ public string ToolTip { get; }
+
+ /// 실제 값을 들고 있는 토글 행
+ public ToggleRowViewModel Toggle { get; }
+
+ public SegmentItem(string icon, string toolTip, ToggleRowViewModel toggle)
+ {
+ Icon = icon;
+ ToolTip = toolTip;
+ Toggle = toggle;
+ }
+}
+
+///
+/// 토글 묶음을 한 줄 아이콘 세그먼트로 — 굵게·기울임·밑줄.
+/// 체크박스 하나에 한 줄씩 쓰면 세 줄(약 100px)을 먹는데, 서로 배타적이지 않은 같은 축의
+/// 속성이라 한 줄에 모으는 편이 읽기도 쉽다. 과 같은 이유로
+/// 값은 전부 자식 토글이 들고 있다.
+///
+public sealed class SegmentRowViewModel : PropertyRowViewModel
+{
+ /// 세그먼트 칸들
+ public List Items { get; } = new();
+
+ /// 한 줄이지만 속성은 칸 수만큼
+ public override int LeafCount => Items.Count;
+
+ public SegmentRowViewModel(string label) : base(label) { }
+}
+
/// 여러 줄 문자열 행(Text/수식/항목 목록)
public sealed class MultilineTextRowViewModel : PropertyRowViewModel
{
diff --git a/src/SheetMe.Designer/Views/InspectorView.xaml b/src/SheetMe.Designer/Views/InspectorView.xaml
index f9c4c92..2e9328e 100644
--- a/src/SheetMe.Designer/Views/InspectorView.xaml
+++ b/src/SheetMe.Designer/Views/InspectorView.xaml
@@ -11,17 +11,73 @@
-
+
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
@@ -116,7 +218,7 @@
-
+
@@ -136,7 +238,7 @@
-
+
@@ -145,11 +247,13 @@
-
+
+
-
+
@@ -182,7 +286,7 @@
-
-
+ ToolTip="마스크 편집기 — 자주 쓰는 마스크 목록과 미리보기"
+ Content="{Binding Source=list, Converter={StaticResource IconName}, ConverterParameter=13}"/>
-
+ ToolTip="0=필수 숫자, 9=선택 숫자, #=숫자·부호 — 오른쪽 버튼에서 목록·미리보기"/>
@@ -215,7 +320,7 @@
-
-
diff --git a/src/SheetMe.Designer/Views/MainView.xaml b/src/SheetMe.Designer/Views/MainView.xaml
index f3af249..bb81c74 100644
--- a/src/SheetMe.Designer/Views/MainView.xaml
+++ b/src/SheetMe.Designer/Views/MainView.xaml
@@ -307,7 +307,9 @@
-
+
+