From abd7eb7d0e7ead319e6e99322beba910a66e1009 Mon Sep 17 00:00:00 2001 From: Msystech Date: Wed, 12 Aug 2026 13:48:10 +0900 Subject: [PATCH] =?UTF-8?q?=EC=86=8D=EC=84=B1=20=ED=8C=A8=EB=84=90=20?= =?UTF-8?q?=EA=B3=A0=EB=8F=84=ED=99=94=20=E2=80=94=20=EC=8C=8D=20=EB=B0=B0?= =?UTF-8?q?=EC=B9=98=C2=B7=EC=8A=A4=ED=83=80=EC=9D=BC=20=EC=84=B8=EA=B7=B8?= =?UTF-8?q?=EB=A8=BC=ED=8A=B8=C2=B7=ED=96=89=20=EC=B6=95=EC=86=8C=EB=A1=9C?= =?UTF-8?q?=20=EC=84=B8=EB=A1=9C=2025%=20=EC=A0=88=EA=B0=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 사용자 지적: "속성패널 디자인 보기 불편한데? 고도화해줘". [실측한 낭비] 단일 선택 시 '공통+글꼴'만으로 354px 를 쓴다. 표준 행 34px 중 실제 필요 높이는 18px(테두리 2 + 글자 16)뿐이라 행마다 12px 가 공기다. 가장 긴 TextBox 는 25행 801px 로 기본 창 뷰포트(약 790px)를 이미 넘겨 스크롤이 강제됐다. [쌍 배치] X|Y, 너비|높이를 한 줄 2칸으로. 라벨은 칩 바깥 왼쪽 자연폭 — [200] 의 XYPair 규약 (Grid star/10/star)과 같다. 네 줄이 두 줄이 된다. 구조가 핵심이다: **자식 행은 평범한 행 그대로**이고 컨테이너만 새로 만들었다. 값·커밋·"여러 값" 판정이 전부 자식 안에서 끝나므로 커밋 규약(Initialize 는 Commit 을 내지 않고, 값이 바뀔 때만 Undo 스냅샷 1회)이 한 글자도 안 바뀌고, boundsRows 가 자식을 들고 있어 드래그 중 실시간 갱신 (RefreshBoundsRows)도 그대로다. X 만 갈리고 Y 는 같은 다중선택도 칸별로 옳게 표시된다 (그래서 셀마다 전용 Grid 를 둔다 — 오버레이의 포커스 트리거가 AncestorType=Grid 다). [스타일 세그먼트] 굵게·기울임·밑줄·취소선을 한 줄 아이콘 토글로. 종전에는 굵게·밑줄만 있었고 각각 한 줄이었다. **기울임·취소선은 기술적 제약이 아니라 단순 누락이었다** — LegacyFont 의 파싱·직렬화·렌더·폰트 일괄 변경 대화상자까지 전부 구현돼 있고 인스펙터만 호출을 빠뜨렸다. CurrentFontOf 가 Italic/Strikeout 을 복사·보존하므로 네 개를 동시에 노출해도 왕복이 안 깨진다. [행 치수] 인스펙터 전용 지역 스타일로 입력 높이 30→26. 전역 TextBox 를 낮추면 검색칩·대화상자 입력까지 따라 줄어 버튼과 정렬이 깨진다. **ComboBox 는 MinHeight 가 아니라 Height 고정**이라 Height 를 함께 내려야 한다 — 안 그러면 선택 행만 4px 어긋난다(설계 검토에서 잡은 실제 결함). [라벨] 폭 86→92 + 줄바꿈. 실측상 가장 긴 라벨이 11자("입력 파라미터 컨트롤")라 86px 에서 "액션 대상 컨…" 처럼 잘렸다. 자르면 툴팁을 띄워야 아는 정보가 되므로 접는 쪽을 택했다. [그 밖에] 태그·마스크 편집기를 여는 "…" 버튼은 26px 칩 옆에서 점 하나로 보여 아이콘 버튼으로. 섹션 헤더 건수를 줄 수가 아니라 **속성 수**로(LeafCount) — 쌍/세그먼트는 한 줄이지만 속성은 여럿이라 그냥 세면 "공통 3"으로 실제보다 적게 나온다. 인스펙터 패널 최소폭 220→250 — 스타일 행이 라벨 92 + 토글 4칸 128 = 220px 를 요구한다. 효과: 공통 5행→3행, 글꼴 4행→3행(속성은 4→6개로 늘었다). TextBox 기준 25행→18행. '전체 속성 표시' 토글이 기본 창에서 스크롤 없이 보인다(종전에는 한참 아래). 검증: 테스트 124/124, edit-smoke 실패 0, 왕복 1,271건 diff 0/예외 0, 종이 렌더 P062 픽셀 차이 0. 인스펙터를 300px 호스트 창에 담아 양 테마로 렌더해 배치 확인 (dialog-shots 진단에 09/10 추가 — 속성 패널 레이아웃 회귀를 눈으로 잡는 자동 경로). Co-Authored-By: Claude Opus 5 --- src/SheetMe.Designer/Controls/LucideIcons.cs | 1 + .../Diagnostics/DialogShots.cs | 22 +++ .../Inspector/InspectorViewModel.cs | 68 +++++++-- .../ViewModels/Inspector/PropertyRows.cs | 88 ++++++++++- src/SheetMe.Designer/Views/InspectorView.xaml | 143 +++++++++++++++--- src/SheetMe.Designer/Views/MainView.xaml | 4 +- 6 files changed, 293 insertions(+), 33 deletions(-) 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 @@ - - 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 @@ - + +