레이어 패널을 [200]SheetMe 아웃라인으로 교체 — 페이지 노드·선택 밴드·드래그 재정렬
[200] 의 패널은 코드로 Border 를 쌓는 비가상화 렌더고 SheetMe 는 가상화 ListBox 다. 골격은 SheetMe 것을 지키고 겉모습과 조작 표면만 옮겼다 — 통째로 베끼면 1,067 컨트롤 서식에서 검색어 한 글자마다 수천 비주얼을 다시 만들게 되고, [200] 에 대응물이 없는 컨테이너 자식 재귀 표시가 사라진다. 구조 - 페이지를 별도 섹션이 아니라 아웃라인의 최상위 노드로. 접기·복제·삭제·컨트롤 수·현재 강조 - 맨 아래 '+ 페이지 추가' 행 - 목록 순서를 뒤집었다 — 이제 맨 위가 z-order 최상위(캔버스에서 위에 겹쳐 보이는 것이 목록 위) - 들여쓰기 12 / 32 / +16 - 레이어 패널을 LayerPanelView 로 분리(MainView.xaml 이 1,000줄을 넘겼고, 진단 렌더러가 이 패널만 따로 찍어 라이트·다크·선택 상태를 검사할 수 있어야 한다) 선택 표시 - IsSelected 가 아니라 Highlight 3상태(None/Blue/Navy)가 배경을 그린다. 그룹 전원 선택 시 폴더는 파랑, 멤버는 서브트리 배경이 되고 맞닿는 모서리를 각지게 해 하나의 블록으로 이어진다 - 밴드 위에서는 아이콘·보조 텍스트 색도 함께 뒤집는다. 안 그러면 눈·자물쇠가 1.6:1 로 묻혀 선택한 행에서만 잠금 상태가 안 보이는 상태가 된다 - 신설 토큰 3종(B.LayerSel / B.LayerSubtree / B.OnSelMuted) — [200] 원본값은 각각 흰 글자 3.00:1, 다크 밴드 1.03:1, 라이트 밴드 1.14:1 로 이 저장소가 이미 기각한 값이다 기능 - 드래그로 z-order 재정렬(삽입선 어도너). 임의 index 이동 연산 MoveWithinParent 신설 - 페이지 복제 — 컨트롤 이름을 전부 새로 발급하고 그룹도 복제본끼리 다시 묶는다 ([200] 은 GroupId 를 그대로 둬서 복제 그룹이 원본과 gid 를 공유한다) - 페이지 헤더 클릭 → 캔버스가 그 페이지로 이동(ScrollToPage) - 데이터 배선 배지 — 값이 조회·데이터소스에서 오는 컨트롤 표시 - 검색칩에 테두리(라이트에서 B.Input=B.Panel 이라 경계 대비가 1.00:1 이었다) 이식하지 않은 것: 컨트롤 행 인라인 개명. [200] 이 고치는 값은 표시 전용 별칭이지만 SheetMe 의 컨트롤 이름은 외부 참조 키라 목록에서 무심코 고치면 배선이 끊긴다. 더블클릭은 요청대로 캔버스 이동을 유지하고, 그룹 행에서만 이름 편집으로 간다. 동반 수정: 눈·자물쇠 토글에 되돌리기 지점 추가(400ms 코얼레스), 그룹 이름 비우기 경로가 스냅숏 없이 지워지던 것, 숨김 흐리기를 행 전체가 아니라 이름 줄에만. 편집 스모크의 레이어 검사 19 → 42건. 회귀: 테스트 124/124, 편집 스모크 실패 0, DB 왕복 1,271건 diff 0/예외 0, 종이 렌더 P062 바이트 동일. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
a30421f9b5
commit
76584e5004
@@ -128,9 +128,68 @@ public static class DialogShots
|
||||
("12-inspector-behavior", () => HostInspector(designer, "TextBox1", InspectorTab.Behavior)),
|
||||
("13-layer-type-filter", () => new Views.LayerTypeFilterDialog(
|
||||
designer.LayerTypeCandidates(), new[] { "Label" })),
|
||||
// 레이어 아웃라인 — 선택 없음 / 컨트롤 하나 선택 / 그룹 전체 선택 세 상태를 각각 찍는다.
|
||||
// 강조는 IsSelected 가 아니라 Highlight 3상태가 그리므로 눈으로 확인할 경로가 이것뿐이다.
|
||||
("14-layer-plain", () => HostLayerPanel(designer, LayerShotMode.None)),
|
||||
("15-layer-selected", () => HostLayerPanel(designer, LayerShotMode.Single)),
|
||||
("16-layer-group", () => HostLayerPanel(designer, LayerShotMode.Group)),
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>레이어 패널 스냅숏의 선택 상태</summary>
|
||||
private enum LayerShotMode
|
||||
{
|
||||
/// <summary>선택 없음</summary>
|
||||
None,
|
||||
|
||||
/// <summary>컨트롤 하나 선택 — 파란 밴드 하나</summary>
|
||||
Single,
|
||||
|
||||
/// <summary>그룹 전체 선택 — 폴더 파랑 + 멤버 서브트리, 이어진 블록</summary>
|
||||
Group,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 레이어 패널을 실제 패널 폭(255px)의 창에 담는다.
|
||||
/// MainViewModel 을 통째로 만들면 DB 조회가 딸려 오므로, 패널이 읽는 <c>CurrentDesigner</c> 하나만
|
||||
/// 들고 있는 최소 DataContext 를 세워 준다.
|
||||
/// </summary>
|
||||
private static Window HostLayerPanel(DesignerViewModel designer, LayerShotMode mode)
|
||||
{
|
||||
designer.Selection.Clear();
|
||||
designer.SetLayerTypeFilter(Array.Empty<string>());
|
||||
designer.LayerFilter = string.Empty;
|
||||
|
||||
var page = designer.Pages[0];
|
||||
if (mode != LayerShotMode.None && page.Controls.Count >= 3)
|
||||
{
|
||||
designer.Selection.Set(new[] { page.Controls[^1], page.Controls[^2] });
|
||||
designer.GroupSelection();
|
||||
if (mode == LayerShotMode.Single)
|
||||
{
|
||||
designer.Selection.SetSingle(page.Controls[0]);
|
||||
}
|
||||
}
|
||||
designer.RebuildLayerRows();
|
||||
|
||||
return new Window
|
||||
{
|
||||
Title = $"레이어 — {mode}",
|
||||
Width = 255,
|
||||
Height = 620,
|
||||
Content = new Views.LayerPanelView { DataContext = new LayerShotContext(designer) },
|
||||
Background = Application.Current.TryFindResource("B.Panel") as Brush,
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>패널이 바인딩하는 <c>CurrentDesigner</c> 만 노출하는 스냅숏 전용 DataContext</summary>
|
||||
private sealed class LayerShotContext
|
||||
{
|
||||
public LayerShotContext(DesignerViewModel designer) => CurrentDesigner = designer;
|
||||
|
||||
public DesignerViewModel CurrentDesigner { get; }
|
||||
}
|
||||
|
||||
/// <summary>인스펙터를 실제 패널 폭(300px)의 창에 담는다 — 컨트롤 하나를 선택한 상태로</summary>
|
||||
private static Window HostInspector(DesignerViewModel designer, string controlId,
|
||||
InspectorTab tab = InspectorTab.Design)
|
||||
|
||||
@@ -279,12 +279,29 @@ public static class EditSmoke
|
||||
groupRow is { Members.Count: 2 } && groupRow.Name.StartsWith("그룹"),
|
||||
$"실제 {groupRow?.Members.Count} / {groupRow?.Name}");
|
||||
|
||||
// 아웃라인은 페이지 헤더로 시작하고 '페이지 추가' 행으로 끝난다
|
||||
Check("레이어: 첫 행은 페이지 헤더", lay.LayerRows.FirstOrDefault() is LayerPageViewModel);
|
||||
Check("레이어: 마지막 행은 페이지 추가", lay.LayerRows.LastOrDefault() is LayerAddPageViewModel);
|
||||
Check("레이어: 페이지 헤더는 선택 대상 아님",
|
||||
lay.LayerRows.OfType<ILayerRow>().Where(r => r is LayerPageViewModel or LayerAddPageViewModel)
|
||||
.All(r => !r.IsSelectableRow));
|
||||
|
||||
// 폴더 바로 뒤에 멤버가 들여쓰기된 채 이어져야 한다(평탄화 시퀀스)
|
||||
var folderIndex = lay.LayerRows.IndexOf(groupRow!);
|
||||
Check("레이어: 폴더 뒤에 멤버가 들여쓰여 이어짐",
|
||||
folderIndex >= 0
|
||||
&& lay.LayerRows.Skip(folderIndex + 1).Take(2)
|
||||
.OfType<ControlViewModel>().Count(c => c.LayerIndent == 20) == 2);
|
||||
.OfType<ControlViewModel>().Count(c => c.LayerIndent == 32) == 2);
|
||||
Check("레이어: 페이지 직속 컨트롤 들여쓰기 12",
|
||||
lay.LayerRows.OfType<ControlViewModel>()
|
||||
.Where(c => c.Model.GroupId is null && c.Parent is null)
|
||||
.All(c => c.LayerIndent == 12));
|
||||
|
||||
// 목록 맨 위 = z-order 최상위 (모델 리스트 index 0)
|
||||
var topControl = lay.LayerRows.OfType<ControlViewModel>().First();
|
||||
Check("레이어: 목록 맨 위가 z-order 최상위",
|
||||
ReferenceEquals(lay.Document.Pages[0].Controls[0], topControl.Model),
|
||||
$"실제 맨 위 {topControl.Id} / 모델 첫째 {lay.Document.Pages[0].Controls[0].Id}");
|
||||
|
||||
// 접으면 멤버 행이 빠지고, 펴면 되돌아온다
|
||||
var expandedCount = lay.LayerRows.Count;
|
||||
@@ -371,6 +388,87 @@ public static class EditSmoke
|
||||
lay.GroupSelection();
|
||||
Check("레이어: 그룹은 XML 에 나가지 않음",
|
||||
!serializer.Write(lay.Document).Contains("GroupId", StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
// 강조 3상태 — 그룹 전체 선택이면 폴더=Blue, 멤버=Navy 이고 연속 블록으로 이어진다
|
||||
groupRow = lay.LayerRows.OfType<LayerGroupViewModel>().First();
|
||||
lay.Selection.Set(groupRow.Members.ToList(), groupRow.Members[0]);
|
||||
lay.UpdateLayerHighlight();
|
||||
Check("레이어: 그룹 전원 선택 → 폴더 Blue", groupRow.Highlight == LayerHighlight.Blue,
|
||||
$"실제 {groupRow.Highlight}");
|
||||
Check("레이어: 그룹 전원 선택 → 멤버 Navy",
|
||||
groupRow.Members.All(m => m.Highlight == LayerHighlight.Navy));
|
||||
Check("레이어: 폴더와 멤버가 이어진 블록",
|
||||
groupRow.MergeBottom && groupRow.Members[0].MergeTop);
|
||||
|
||||
lay.Selection.SetSingle(groupRow.Members[0]);
|
||||
lay.UpdateLayerHighlight();
|
||||
Check("레이어: 멤버 하나만 선택 → 그 행만 Blue, 폴더 무강조",
|
||||
groupRow.Members[0].Highlight == LayerHighlight.Blue
|
||||
&& groupRow.Highlight == LayerHighlight.None);
|
||||
|
||||
// 강조 계산이 페이지 헤더에서 끊긴다(블록이 페이지를 넘지 않는다)
|
||||
Check("레이어: 페이지 헤더는 항상 무강조",
|
||||
lay.LayerRows.OfType<LayerPageViewModel>().All(p => p.Highlight == LayerHighlight.None));
|
||||
|
||||
// 페이지 복제 — 컨트롤 이름 유일성과 그룹 분리가 지켜져야 한다
|
||||
var beforePages = lay.Pages.Count;
|
||||
var beforeIds = lay.Pages[0].Controls.Select(c => c.Id).ToHashSet(StringComparer.Ordinal);
|
||||
var copied = lay.DuplicatePage(lay.Pages[0]);
|
||||
Check("레이어: 페이지 복제 — 장수 +1", lay.Pages.Count == beforePages + 1 && copied is not null);
|
||||
if (copied is not null)
|
||||
{
|
||||
Check("레이어: 페이지 복제 — 컨트롤 수 동일",
|
||||
copied.Controls.Count == lay.Pages[0].Controls.Count);
|
||||
Check("레이어: 페이지 복제 — 이름이 전부 새로 발급됨",
|
||||
copied.Controls.All(c => !beforeIds.Contains(c.Id)));
|
||||
Check("레이어: 페이지 복제 — 그룹이 원본과 분리됨",
|
||||
copied.Controls.Where(c => c.Model.GroupId is not null)
|
||||
.All(c => lay.Pages[0].Controls.All(o => o.Model.GroupId != c.Model.GroupId)));
|
||||
}
|
||||
Check("레이어: 전 페이지가 아웃라인에 나옴",
|
||||
lay.LayerRows.OfType<LayerPageViewModel>().Count() == lay.Pages.Count);
|
||||
|
||||
// 페이지 접기 — 그 페이지 컨트롤 행만 빠진다
|
||||
var pageHeader = lay.LayerRows.OfType<LayerPageViewModel>().First();
|
||||
var beforeCollapse = lay.LayerRows.Count;
|
||||
pageHeader.IsExpanded = false;
|
||||
lay.RebuildLayerRows();
|
||||
Check("레이어: 페이지 접기 — 행 감소", lay.LayerRows.Count < beforeCollapse);
|
||||
Check("레이어: 페이지 접어도 헤더는 남음",
|
||||
lay.LayerRows.OfType<LayerPageViewModel>().Count() == lay.Pages.Count);
|
||||
lay.LayerRows.OfType<LayerPageViewModel>().First().IsExpanded = true;
|
||||
lay.RebuildLayerRows();
|
||||
Check("레이어: 페이지 펴기 — 행 복귀", lay.LayerRows.Count == beforeCollapse);
|
||||
|
||||
// 드래그 재정렬 — 목록에서 아래 행을 맨 위 행 '위'로 옮기면 z-order 최상위가 된다
|
||||
var order = lay.LayerRows.OfType<ControlViewModel>()
|
||||
.Where(c => c.Parent is null && ReferenceEquals(lay.PageOf(c), lay.Pages[0])).ToList();
|
||||
if (order.Count >= 2)
|
||||
{
|
||||
var mover = order[^1];
|
||||
var anchor = order[0];
|
||||
lay.MoveControlNextTo(mover, anchor, above: true);
|
||||
Check("레이어: 드래그로 맨 위에 놓으면 z-order 최상위",
|
||||
ReferenceEquals(lay.Pages[0].Model.Controls[0], mover.Model),
|
||||
$"실제 {lay.Pages[0].Model.Controls[0].Id} 기대 {mover.Id}");
|
||||
Check("레이어: 재정렬이 되돌리기 스텝", lay.Undo.CanUndo);
|
||||
}
|
||||
|
||||
// 눈·자물쇠도 되돌릴 수 있어야 한다
|
||||
var flagTarget = lay.LayerRows.OfType<ControlViewModel>().First();
|
||||
lay.Undo.Snapshot();
|
||||
var depthBefore = lay.Undo.CanUndo;
|
||||
flagTarget.IsLockedFlag = true;
|
||||
lay.Undo.Undo();
|
||||
Check("레이어: 잠금 토글이 Undo 로 원복",
|
||||
lay.Pages.SelectMany(p => p.Controls).All(c => !c.Model.Locked) && depthBefore);
|
||||
|
||||
// 데이터 배선 배지
|
||||
var badgeTarget = lay.Pages[0].Controls[0];
|
||||
Check("레이어: 배선 없는 컨트롤은 배지 없음", !badgeTarget.HasDataBinding);
|
||||
badgeTarget.Model.Props.SetText("DataTableField", "DT.Item(\"ChtNum\")");
|
||||
Check("레이어: DataTableField 있으면 배지", badgeTarget.HasDataBinding);
|
||||
badgeTarget.Model.Props.SetText("DataTableField", string.Empty);
|
||||
}
|
||||
|
||||
// 20-3) 문서 간 복사/붙여넣기 — A 서식에서 복사 → B 서식에 붙여넣기(클립보드 공유)
|
||||
|
||||
Reference in New Issue
Block a user