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

레거시에서 페이지는 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
@@ -77,6 +77,7 @@ public sealed class DesignerViewModel : ViewModelBase
// 레이어 목록 상태 — 접힌 그룹과 타입 필터는 문서가 아니라 편집 세션에 속한다(Undo 대상 아님)
private readonly HashSet<string> collapsedGroups = new(StringComparer.Ordinal);
private readonly HashSet<PageViewModel> collapsedPages = new();
private PageViewModel? inspectedPage;
private readonly HashSet<string> layerTypeFilter = new(StringComparer.Ordinal);
private readonly List<System.Collections.Specialized.INotifyCollectionChanged> watchedControls = new();
#endregion
@@ -275,6 +276,9 @@ public sealed class DesignerViewModel : ViewModelBase
Interaction = new InteractionController(this);
Inspector = new InspectorViewModel(this);
Selection.Changed += RefreshOverlay;
// 순서 주의 — 인스펙터가 Rebuild 하기 전에 페이지 모드를 걷어야 한다
// (Inspector 는 생성자 안에서 SetChanged 를 구독하므로 여기가 먼저다)
Selection.SetChanged += ClearInspectedPageOnSelection;
Selection.SetChanged += LayerSelectionChanged;
UndoCommand = new Command((sender, e) => Undo.Undo());
@@ -502,6 +506,33 @@ public sealed class DesignerViewModel : ViewModelBase
OnPropertyChanged(nameof(WorldHeight));
}
/// <summary>
/// 용지 크기가 바뀐 뒤 — 페이지 스택 오프셋과 월드 크기를 다시 계산한다.
/// 페이지가 여러 장이면 한 장의 높이 변화가 아래 페이지 전부를 밀어낸다.
/// </summary>
public void NotifyPaperResized()
{
RecalcPageOffsets();
OnPropertyChanged(nameof(WorldWidth));
OnPropertyChanged(nameof(WorldHeight));
RefreshOverlay();
}
/// <summary>
/// 페이지 기본 글꼴이 바뀐 뒤 — 그 페이지 컨트롤의 상속 컨텍스트를 다시 흘린다.
/// Font 를 명시하지 않은 컨트롤은 루트 글꼴을 물려받으므로 값 하나가 화면 전체를 바꾼다.
/// </summary>
public void RefreshPageInheritance(PageViewModel page)
{
var rootFontText = page.Model.Root.Props.GetText("Font");
var rootFont = rootFontText is not null ? LegacyFormat.ParseFont(rootFontText) : new LegacyFont();
foreach (var control in page.Controls)
{
control.ResolveVisualContext(rootFont, control.ParentForeground);
}
RefreshOverlay();
}
/// <summary>문서 교체 후 재구성 — Undo 복원 경로</summary>
public void ReplaceDocument(FormDocument newDocument)
{
@@ -1773,13 +1804,20 @@ public sealed class DesignerViewModel : ViewModelBase
}
}
/// <summary>페이지 헤더 행 클릭 — 활성 페이지로 삼고 캔버스를 그 페이지로 옮긴다</summary>
/// <summary>
/// 페이지 헤더 행 클릭 — 활성 페이지로 삼고, 캔버스를 그 페이지로 옮기고, 속성 패널을 페이지로 돌린다.
///
/// 페이지도 레거시에서는 하나의 <c>Object</c>(MDesignerHost)라 자기 속성을 갖는다.
/// 컨트롤을 하나도 고르지 않은 상태에서만 페이지 속성을 보여주므로 선택은 비운다.
/// </summary>
public void ActivatePage(PageViewModel page)
{
if (!ReferenceEquals(SelectedPage, page))
{
SelectedPage = page;
}
Selection.Clear();
InspectedPage = page;
foreach (var header in LayerRows.OfType<LayerPageViewModel>())
{
header.NotifyStateChanged();
@@ -1787,6 +1825,32 @@ public sealed class DesignerViewModel : ViewModelBase
ScrollToPageRequested?.Invoke(page);
}
/// <summary>
/// 속성 패널이 들여다보는 페이지 — 컨트롤 선택이 없을 때만 유효하다.
/// 컨트롤을 고르는 순간 <see cref="SelectionService.SetChanged"/> 경로에서 비워진다.
/// </summary>
public PageViewModel? InspectedPage
{
get => inspectedPage;
private set
{
if (!ReferenceEquals(inspectedPage, value))
{
inspectedPage = value;
Inspector.Rebuild();
}
}
}
/// <summary>선택이 생기면 페이지 속성 모드를 벗어난다</summary>
private void ClearInspectedPageOnSelection()
{
if (inspectedPage is not null && Selection.Items.Count > 0)
{
inspectedPage = null;
}
}
/// <summary>캔버스를 특정 페이지로 스크롤하라는 요청 — 뷰가 구독한다</summary>
public event Action<PageViewModel>? ScrollToPageRequested;