레이어 패널을 [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:
Msystech
2026-08-12 17:48:48 +09:00
co-authored by Claude Opus 5
parent a30421f9b5
commit 76584e5004
17 changed files with 1733 additions and 328 deletions
+8 -178
View File
@@ -46,7 +46,9 @@
<ctl:TypeToIconConverter x:Key="TypeToIcon"/>
<ctl:TypeToCategoryConverter x:Key="TypeToCategory"/>
<ctl:IconNameToVisualConverter x:Key="IconName"/>
<ctl:IconSpecConverter x:Key="IconSpec"/>
<ctl:InverseBoolConverter x:Key="InverseBool"/>
<BooleanToVisibilityConverter x:Key="BoolToVisibility"/>
<vm:SheetClassToGroupConverter x:Key="SheetClassToGroup"/>
@@ -472,189 +474,17 @@
</Grid>
</DockPanel>
</TabItem>
<!-- 레이어 (+ 페이지) -->
<!--
레이어 — [200]SheetMe 아웃라인.
페이지를 별도 섹션으로 두지 않고 트리의 최상위 노드로 삼는다. 부수 효과가 하나 있는데
의도한 것이다: 페이지 행은 항상 무강조라 연속 선택 블록이 페이지 경계에서 저절로 끊긴다.
-->
<TabItem>
<TabItem.Header><TextBlock Text="레이어" Style="{StaticResource TabHeaderText}"/></TabItem.Header>
<DockPanel>
<!-- 페이지 섹션 -->
<DockPanel DockPanel.Dock="Top" Margin="0,4,0,0">
<TextBlock DockPanel.Dock="Top" Text="페이지" FontWeight="Bold" FontSize="11"
Margin="10,4,10,4" Foreground="{DynamicResource B.Muted}"/>
<StackPanel DockPanel.Dock="Bottom" Orientation="Horizontal" Margin="10,2,10,6">
<Button Content="추가" Command="{Binding CurrentDesigner.AddPageCommand}"/>
<Button Content="삭제" Margin="6,0,0,0" Command="{Binding CurrentDesigner.RemovePageCommand}"/>
</StackPanel>
<ListBox ItemsSource="{Binding CurrentDesigner.Pages}"
SelectedItem="{Binding CurrentDesigner.SelectedPage}"
MaxHeight="110" Margin="4,0">
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal" Margin="2,1">
<TextBlock Text="{Binding Index, StringFormat=페이지 {0}}" FontSize="12"/>
<TextBlock Text="{Binding SizeText, StringFormat= ({0})}" FontSize="11" Foreground="{DynamicResource B.Muted}"/>
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</DockPanel>
<Border DockPanel.Dock="Top" Height="1" Background="{DynamicResource B.Line}" Margin="8,2"/>
<!-- 레이어 검색칩 — 컨트롤이 수백 개인 서식에서 이름으로 좁혀 찾는다.
내부 TextBox 에 MinHeight=0 필수: 전역 TextBox MinHeight 30 때문에 32px 칩에서 글자가 잘린다. -->
<Border DockPanel.Dock="Top" Height="32" CornerRadius="7" Margin="8,2,8,4"
Background="{DynamicResource B.Input}">
<DockPanel>
<ContentControl DockPanel.Dock="Left" Margin="9,0,7,0" VerticalAlignment="Center" IsTabStop="False"
Content="{Binding Source=search, Converter={StaticResource IconName}, ConverterParameter=13}"/>
<!-- 타입 필터 — 이름을 몰라도 "체크박스만" 처럼 종류로 좁힐 수 있어야 한다 -->
<Button DockPanel.Dock="Right" Style="{StaticResource Subtle}" Width="26" Height="22" Padding="0"
Margin="0,0,3,0" Click="OnLayerFilterClick"
ToolTip="{Binding CurrentDesigner.LayerTypeFilterTip}"
Content="{Binding Source=filter, Converter={StaticResource IconName}, ConverterParameter=13}"/>
<Button DockPanel.Dock="Right" Style="{StaticResource Subtle}" Width="26" Height="22" Padding="0"
Margin="0,0,1,0" Click="OnLayerFilterClearClick" ToolTip="타입 필터 해제"
Visibility="{Binding CurrentDesigner.HasLayerTypeFilter, Converter={StaticResource BoolToVisibility}}"
Content="{Binding Source=filter-x, Converter={StaticResource IconName}, ConverterParameter=13}"/>
<Grid>
<TextBox x:Name="LayerSearchBox" MinHeight="0" Padding="0" BorderThickness="0"
Background="Transparent" VerticalContentAlignment="Center"
Text="{Binding CurrentDesigner.LayerFilter, UpdateSourceTrigger=PropertyChanged}"/>
<TextBlock Text="레이어 검색…" FontSize="12" Foreground="{DynamicResource B.Muted}"
VerticalAlignment="Center" IsHitTestVisible="False" Margin="1,0,0,0">
<TextBlock.Style>
<Style TargetType="TextBlock" BasedOn="{StaticResource {x:Type TextBlock}}">
<Setter Property="Visibility" Value="Collapsed"/>
<Style.Triggers>
<DataTrigger Binding="{Binding Text, ElementName=LayerSearchBox}" Value="">
<Setter Property="Visibility" Value="Visible"/>
</DataTrigger>
</Style.Triggers>
</Style>
</TextBlock.Style>
</TextBlock>
</Grid>
</DockPanel>
</Border>
<!--
레이어 목록 (활성 페이지, 그리기 순서).
그룹 폴더 행과 컨트롤 행이 한 시퀀스로 섞이는 평탄화 목록이라
ItemTemplate 하나로는 안 되고(암시 DataType 매칭은 LabelViewModel 같은
구체 형에만 걸린다) 선택기로 가른다. 계층은 LayerIndent 여백으로만 표현.
-->
<Grid Margin="4,0,4,6">
<ListBox x:Name="LayerListBox" ItemsSource="{Binding CurrentDesigner.LayerRows}"
bh:LayerListBehavior.Designer="{Binding CurrentDesigner}"
SelectionMode="Extended"
MouseDoubleClick="OnLayerDoubleClick"
ToolTip="더블클릭 — 컨트롤은 캔버스 이동, 그룹은 이름 바꾸기"
ScrollViewer.HorizontalScrollBarVisibility="Disabled">
<ListBox.ItemTemplateSelector>
<ctl:LayerRowTemplateSelector>
<!-- 그룹 폴더 행 — 셰브론 / 폴더 / 이름(더블클릭 편집) / (n) / 일괄 눈·자물쇠 -->
<ctl:LayerRowTemplateSelector.GroupTemplate>
<DataTemplate>
<DockPanel Margin="0,1">
<ToggleButton DockPanel.Dock="Right" Style="{StaticResource MiniToggle}"
IsChecked="{Binding IsLockedFlag}" ToolTip="그룹 전체 잠금"
Content="{Binding LockIcon, Converter={StaticResource IconName}, ConverterParameter=13}"/>
<ToggleButton DockPanel.Dock="Right" Style="{StaticResource MiniToggle}"
IsChecked="{Binding IsHiddenFlag}" ToolTip="그룹 전체 숨김"
Content="{Binding HiddenIcon, Converter={StaticResource IconName}, ConverterParameter=13}"/>
<ToggleButton DockPanel.Dock="Left" Style="{StaticResource MiniChevron}" Width="18"
IsChecked="{Binding IsExpanded}" ToolTip="접기/펼치기"
Content="{Binding ChevronIcon, Converter={StaticResource IconName}, ConverterParameter=12}"/>
<ContentControl DockPanel.Dock="Left" Margin="0,0,6,0" VerticalAlignment="Center" IsTabStop="False"
Content="{Binding Source=folder, Converter={StaticResource IconName}, ConverterParameter=14|B.Accent}"/>
<TextBlock DockPanel.Dock="Right" Text="{Binding MemberCountText}" FontSize="11"
VerticalAlignment="Center" Margin="4,0,4,0"
Foreground="{DynamicResource B.Muted}"/>
<Grid VerticalAlignment="Center">
<TextBlock Text="{Binding Name}" FontSize="12" FontWeight="SemiBold"
TextTrimming="CharacterEllipsis"
ToolTip="더블클릭하면 그룹 이름을 바꿉니다">
<TextBlock.Style>
<Style TargetType="TextBlock" BasedOn="{StaticResource {x:Type TextBlock}}">
<Style.Triggers>
<DataTrigger Binding="{Binding IsEditing}" Value="True">
<Setter Property="Visibility" Value="Collapsed"/>
</DataTrigger>
</Style.Triggers>
</Style>
</TextBlock.Style>
</TextBlock>
<TextBox MinHeight="0" Padding="2,0" FontSize="12"
IsVisibleChanged="OnGroupNameBoxVisibleChanged"
Text="{Binding Name, UpdateSourceTrigger=LostFocus}"
KeyDown="OnGroupNameKeyDown" LostFocus="OnGroupNameLostFocus"
Visibility="{Binding IsEditing, Converter={StaticResource BoolToVisibility}}"/>
</Grid>
</DockPanel>
</DataTemplate>
</ctl:LayerRowTemplateSelector.GroupTemplate>
<ctl:LayerRowTemplateSelector.ControlTemplate>
<DataTemplate>
<DockPanel Margin="0,1">
<!-- 눈·자물쇠는 아이콘이 상태에 따라 바뀐다 — 사각 체크로는 잠김/숨김이 안 읽힌다 -->
<ToggleButton DockPanel.Dock="Right" Style="{StaticResource MiniToggle}"
IsChecked="{Binding IsLockedFlag}" ToolTip="잠금"
Content="{Binding LockIcon, Converter={StaticResource IconName}, ConverterParameter=13}"/>
<ToggleButton DockPanel.Dock="Right" Style="{StaticResource MiniToggle}"
IsChecked="{Binding IsHiddenFlag}" ToolTip="숨김"
Content="{Binding HiddenIcon, Converter={StaticResource IconName}, ConverterParameter=13}"/>
<!-- 계층 들여쓰기: 그룹 멤버 20, 컨테이너 자식 +16 -->
<Border DockPanel.Dock="Left" Width="{Binding LayerIndent}"/>
<!-- 컨테이너(패널/그룹박스)만 자식 접기 셰브론을 갖는다 -->
<ToggleButton DockPanel.Dock="Left" Style="{StaticResource MiniChevron}" Width="16"
IsChecked="{Binding IsLayerExpanded}" ToolTip="자식 접기/펼치기"
Visibility="{Binding HasLayerChildren, Converter={StaticResource BoolToVisibility}}"
Content="{Binding LayerChevronIcon, Converter={StaticResource IconName}, ConverterParameter=12}"/>
<ContentControl DockPanel.Dock="Left" Margin="2,0,6,0" VerticalAlignment="Center" IsTabStop="False"
Content="{Binding Type, Converter={StaticResource TypeToIcon}, ConverterParameter=14|B.Accent}"/>
<!--
문구와 이름을 두 줄로. 컨트롤 이름(Id)은 외부 참조 키라
(액션 대상·입력 파라미터가 이름으로 서로를 가리키고 CalcBox 수식도 이름으로 합산한다)
배선하려면 목록에서 보여야 하는데, 248px 폭에 나란히 두면 양쪽 다 잘려
'txtKinPhone…' 처럼 구분이 안 되는 꼬리가 남는다. 한 줄 더 쓰는 편이 낫다.
표시 문구가 없어 이름이 곧 문구인 행에서는 둘째 줄이 뜨지 않는다.
-->
<StackPanel VerticalAlignment="Center">
<StackPanel.Style>
<Style TargetType="StackPanel">
<Style.Triggers>
<DataTrigger Binding="{Binding IsHiddenFlag}" Value="True">
<Setter Property="Opacity" Value="0.55"/>
</DataTrigger>
</Style.Triggers>
</Style>
</StackPanel.Style>
<TextBlock Text="{Binding LayerName}" FontSize="12"
TextTrimming="CharacterEllipsis" ToolTip="{Binding LayerName}"/>
<TextBlock Text="{Binding LayerId}" FontSize="10.5" Margin="0,1,0,0"
Foreground="{DynamicResource B.Muted}"
TextTrimming="CharacterEllipsis"
ToolTip="컨트롤 이름 — 다른 속성이 이 이름으로 참조합니다"
Visibility="{Binding HasLayerId, Converter={StaticResource BoolToVisibility}}"/>
</StackPanel>
</DockPanel>
</DataTemplate>
</ctl:LayerRowTemplateSelector.ControlTemplate>
</ctl:LayerRowTemplateSelector>
</ListBox.ItemTemplateSelector>
</ListBox>
<!-- 빈 목록 안내 — 검색·필터가 다 걸러냈는지, 애초에 컨트롤이 없는지 구분해 준다 -->
<TextBlock Text="{Binding CurrentDesigner.LayerEmptyText}" FontSize="12"
Foreground="{DynamicResource B.Muted}" TextWrapping="Wrap" TextAlignment="Center"
HorizontalAlignment="Center" VerticalAlignment="Top" Margin="12,18,12,0"
IsHitTestVisible="False"
Visibility="{Binding CurrentDesigner.IsLayerListEmpty, Converter={StaticResource BoolToVisibility}}"/>
</Grid>
<v:LayerPanelView/>
</DockPanel>
</TabItem>
<!-- 도구 상자 ([200] 타일 카드 룩 — 드래그/더블클릭 배치) -->
<TabItem>
<TabItem.Header><TextBlock Text="도구 상자" Style="{StaticResource TabHeaderText}"/></TabItem.Header>