페이지 속성 편집 — 레이어의 페이지 행을 고르면 속성 패널이 페이지를 연다

레거시에서 페이지는 MDesignerHost 라는 하나의 Object 이고 자기 속성을 갖는다.
SheetMe 도 그 루트 노드를 FormPage.Root 로 통째로 보존해 왔고(그래서 운영 1,271건 왕복
diff 가 0이다), 값도 일부는 이미 쓰고 있었다 — BackColor 는 용지 배경으로 렌더하고
Font 는 자식이 상속한다. 없던 것은 편집 화면뿐이었다.

그 결과 용지 크기조차 바꿀 수 없는 상태였다. ApplyPaperSize 는 구현돼 있었지만 호출처가
편집 스모크뿐이고 메뉴·XAML 어디에도 배선이 없었다 — 새 서식은 720×856 고정으로만 시작했다.

레이어 아웃라인의 페이지 헤더 행을 누르면 선택을 비우고 속성 패널이 페이지 모드로 바뀐다.
컨트롤을 하나라도 고르면 자동으로 빠져나온다.

  용지        너비·높이(한 줄), 배경색
  기본 글꼴    글꼴·크기 — 여기를 바꾸면 Font 를 명시하지 않은 컨트롤이 전부 따라온다
  전체 속성    나머지 원본 속성(레거시 PropertyGrid 등가)

저장 규약을 지켰다. writer 는 저장할 때 Size 를 Bounds 에서, Name 을 Id 에서 다시 만들어
덮으므로 그 두 키는 raw 편집 대상에서 뺐다 — 열어 두면 고쳐도 저장하면 되돌아간다.
크기는 raw Size 가 아니라 Bounds 를 고치고, 편집 결과가 저장 XML 의 Size 와 같은지
스모크로 못 박았다.

용지 크기가 바뀌면 페이지 스택 오프셋과 월드 크기를 다시 계산한다(여러 장이면 한 장의
높이 변화가 아래 페이지를 전부 밀어낸다). 기본 글꼴이 바뀌면 그 페이지 컨트롤의 상속
컨텍스트를 다시 흘린다.

편집 스모크에 페이지 속성 검사 11건 추가.
회귀: 테스트 124/124, 편집 스모크 실패 0, DB 왕복 1,271건 diff 0/예외 0,
종이 렌더 P062 바이트 동일.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Msystech
2026-08-13 10:23:49 +09:00
co-authored by Claude Opus 5
parent f5cce00019
commit 38093691ad
4 changed files with 285 additions and 6 deletions
@@ -120,12 +120,21 @@ public sealed class InspectorViewModel : ViewModelBase
/// <summary>선택 요약 텍스트</summary>
public string Summary
=> designer.Selection.Items.Count switch
{
get
{
0 => "선택 없음",
1 => $"{designer.Selection.Primary!.Type} — {designer.Selection.Primary!.Id}",
var n => $"{n}개 선택",
};
if (designer.Selection.Items.Count == 0 && designer.InspectedPage is { } page)
{
return $"페이지 {page.Index + 1} — {page.Model.Root.Id}";
}
return designer.Selection.Items.Count switch
{
0 => "선택 없음",
1 => $"{designer.Selection.Primary!.Type} — {designer.Selection.Primary!.Id}",
var n => $"{n}개 선택",
};
}
}
#endregion
#region Constructors
@@ -166,6 +175,11 @@ public sealed class InspectorViewModel : ViewModelBase
if (items.Count == 0)
{
RefreshTabCounts(null);
if (designer.InspectedPage is { } page)
{
BuildPageRows(page);
OnPropertyChanged(nameof(IsTabEmpty));
}
return;
}
@@ -265,6 +279,148 @@ public sealed class InspectorViewModel : ViewModelBase
OnPropertyChanged(nameof(ShowTabStrip));
}
/// <summary>
/// 페이지 속성 — 레거시에서 페이지는 <c>MDesignerHost</c> 라는 하나의 Object 이고 자기 속성을 갖는다.
/// SheetMe 는 그 루트 노드를 <c>FormPage.Root</c> 로 통째로 보존해 왔지만(그래서 왕복 diff 가 0이다)
/// 편집 화면이 없어 용지 크기조차 바꿀 수 없었다.
///
/// 저장 시 <c>Size</c>·<c>Name</c> 은 writer 가 Bounds·Id 에서 다시 만들어 덮으므로
/// 그 두 키는 raw 로 편집하게 두면 안 된다 — 고쳐도 저장하면 되돌아간다.
/// </summary>
private void BuildPageRows(PageViewModel page)
{
var root = page.Model.Root;
AddSection("용지");
// 크기는 raw Size 가 아니라 Bounds 를 고친다 — writer 가 Bounds 로 Size 를 다시 쓰기 때문
AddPagePair(page,
"너비", () => page.Model.Width, w => page.Model.Width = Math.Max(1, Math.Round(w)),
"높이", () => page.Model.Height, h => page.Model.Height = Math.Max(1, Math.Round(h)));
AddPageRow(page, new ColorRowViewModel("배경색"),
() => root.Props.GetText("BackColor") ?? "White",
value => root.Props.SetText("BackColor", value.Length == 0 ? "White" : value),
page.NotifyPaperChanged);
// 페이지 글꼴은 자식이 상속한다 — 여기를 바꾸면 Font 를 명시하지 않은 컨트롤이 전부 따라온다
AddSection("기본 글꼴 (컨트롤이 상속)");
AddPageFontRow(page, "글꼴", f => f.Family, (f, v) => f.Family = v.Length == 0 ? f.Family : v);
AddPageFontRow(page, "크기(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);
// 저장 시 writer 가 다시 쓰는 키는 raw 편집 대상에서 제외한다
var handled = new HashSet<string>(StringComparer.Ordinal) { "Size", "Name", "BackColor", "Font" };
BuildPageAdvancedRows(page, handled);
}
/// <summary>페이지 루트의 나머지 원본 속성 — 레거시 PropertyGrid 등가의 탈출구</summary>
private void BuildPageAdvancedRows(PageViewModel page, HashSet<string> handled)
{
var root = page.Model.Root;
var keys = root.Props.Keys.Where(k => !handled.Contains(k)).ToList();
if (!showAdvanced)
{
currentSection = null;
AddPlain(new ToggleAdvancedRowViewModel($"전체 속성 표시 ▾ ({keys.Count}개)", () =>
{
showAdvanced = true;
Rebuild();
}));
return;
}
AddSection("전체 속성 (고급 — 레거시 원문)");
AddPlain(new ToggleAdvancedRowViewModel("전체 속성 숨기기 ▴", () =>
{
showAdvanced = false;
Rebuild();
}));
foreach (var key in keys)
{
// 페이지 루트에 목록·중첩·바이너리 속성이 오는 사례는 없지만, 왔을 때 편집으로
// 원문을 깨뜨리지 않도록 문자열만 편집을 허용한다
if (root.Props.Get(key) is LegacyPropValue.TextValue or LegacyPropValue.NullValue or null)
{
AddPageRow(page, new TextRowViewModel(key),
() => root.Props.GetText(key) ?? string.Empty,
value => root.Props.SetText(key, value));
continue;
}
AddPlain(new ReadOnlyRowViewModel(key, "(원문 보존됨)"));
}
}
/// <summary>페이지 속성 행 배선 — 대상이 선택 집합이 아니라 페이지 루트 하나다</summary>
private void AddPageRow(PageViewModel page, PropertyRowViewModel row,
Func<string> get, Action<string> set, Action? afterCommit = null)
{
row.Initialize(get(), isMixed: false);
row.Commit = value =>
{
designer.Undo.Snapshot();
set(value);
afterCommit?.Invoke();
};
currentSection?.Adopt(row);
Rows.Add(row);
}
/// <summary>용지 너비·높이 — 한 줄에 둘. 값이 바뀌면 페이지 오프셋까지 다시 계산해야 한다</summary>
private void AddPagePair(PageViewModel page,
string leftLabel, Func<double> getLeft, Action<double> setLeft,
string rightLabel, Func<double> getRight, Action<double> setRight)
{
var pair = new PairRowViewModel(
leftLabel, NewPageNumber(page, leftLabel, getLeft, setLeft),
rightLabel, NewPageNumber(page, rightLabel, getRight, setRight));
currentSection?.Adopt(pair);
Rows.Add(pair);
}
private NumberRowViewModel NewPageNumber(PageViewModel page, string label,
Func<double> get, Action<double> set)
{
var row = new NumberRowViewModel(label) { CoalesceUndo = true };
row.Initialize(get().ToString("0.##", CultureInfo.InvariantCulture), isMixed: false);
row.Commit = value =>
{
if (!double.TryParse(value, NumberStyles.Number, CultureInfo.InvariantCulture, out var parsed))
{
return;
}
designer.Undo.SnapshotForNudge();
set(parsed);
page.Model.Root.HasSize = true;
page.NotifySizeChanged();
designer.NotifyPaperResized();
};
return row;
}
/// <summary>페이지 기본 글꼴 — 자식 상속 컨텍스트가 바뀌므로 문서 전체 시각을 다시 해석한다</summary>
private void AddPageFontRow(PageViewModel page, string label,
Func<LegacyFont, string> get, Action<LegacyFont, string> set)
{
var root = page.Model.Root;
LegacyFont Current()
{
var text = root.Props.GetText("Font");
return text is not null ? LegacyFormat.ParseFont(text) : new LegacyFont();
}
AddPageRow(page, new TextRowViewModel(label), () => get(Current()), value =>
{
var font = Current();
set(font, value);
root.Props.SetText("Font", LegacyFormat.FormatFont(font));
}, () => designer.RefreshPageInheritance(page));
}
/// <summary>고급 섹션 구성 — 접이식, raw 문자열 편집 + 속성 추가</summary>
private void BuildAdvancedRows(ControlViewModel target, HashSet<string> curatedKeys)
{