Files
SheetMe/src/SheetMe.Designer/Views/LayerPanelView.xaml
T
MsystechandClaude Opus 5 bd0d055e0d 페이지 행의 복제·삭제도 호버할 때만 표시
컨트롤 행의 눈·자물쇠는 호버 규칙으로 바꿔 놓고 페이지 행 버튼만 상시 노출이라
목록 우측이 들쭉날쭉했다. 같은 규칙으로 맞춘다.

버튼 각각이 아니라 묶음(StackPanel)에 거는 이유가 있다 — 삭제 버튼은 CanDelete 에
Visibility 를 물려 있는데 인스턴스의 지역 값이 스타일 트리거를 이기므로, 버튼마다 걸면
삭제만 상시 노출된다. 바깥을 접으면 둘 다 숨고, 펴지면 삭제는 원래대로 CanDelete 를 따른다.

되돌리기가 번거로운 '페이지 삭제'가 항상 노출돼 있지 않게 되는 부수 효과도 있다.

회귀: 테스트 124/124, 편집 스모크 실패 0, DB 왕복 1,271건 diff 0/예외 0,
종이 렌더 P062 바이트 동일.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-13 09:08:13 +09:00

315 lines
25 KiB
XML

<UserControl x:Class="SheetMe.Designer.Views.LayerPanelView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:vm="clr-namespace:SheetMe.Designer.ViewModels"
xmlns:bh="clr-namespace:SheetMe.Designer.Behaviors"
xmlns:ctl="clr-namespace:SheetMe.Designer.Controls"
mc:Ignorable="d"
d:DataContext="{d:DesignInstance vm:MainViewModel}"
Background="{DynamicResource B.Panel}">
<!--
레이어 아웃라인 — [200]SheetMe 이식.
페이지를 별도 섹션으로 두지 않고 트리의 최상위 노드로 삼는다. 부수 효과가 하나 있는데
의도한 것이다: 페이지 행은 항상 무강조라 연속 선택 블록이 페이지 경계에서 저절로 끊긴다.
MainView 에서 떼어 낸 이유는 둘이다 — MainView.xaml 이 1,000줄을 넘겼고,
진단 모드(dialog-shots)가 이 패널만 따로 렌더해 라이트/다크·선택 상태를 검사할 수 있어야 한다.
DataContext 는 창에서 상속받는다(MainViewModel).
-->
<UserControl.Resources>
<ctl:IconNameToVisualConverter x:Key="IconName"/>
<ctl:IconSpecConverter x:Key="IconSpec"/>
<BooleanToVisibilityConverter x:Key="BoolToVisibility"/>
</UserControl.Resources>
<DockPanel>
<!-- 검색줄 — 검색칩(가변폭) + 우측 타입 필터 버튼 2개 -->
<DockPanel DockPanel.Dock="Top">
<StackPanel DockPanel.Dock="Right" Orientation="Horizontal"
VerticalAlignment="Center" Margin="0,8,8,6">
<!-- 타입 필터 — 이름을 몰라도 "체크박스만" 처럼 종류로 좁힐 수 있어야 한다 -->
<Button Width="26" Height="22" Padding="0"
Margin="0,0,2,0" Click="OnLayerFilterClick"
ToolTip="{Binding CurrentDesigner.LayerTypeFilterTip}">
<Button.Style>
<Style TargetType="Button" BasedOn="{StaticResource Subtle}">
<Setter Property="Content"
Value="{Binding Source=filter, Converter={StaticResource IconName}, ConverterParameter=13|B.Muted}"/>
<Style.Triggers>
<DataTrigger Binding="{Binding CurrentDesigner.HasLayerTypeFilter}" Value="True">
<Setter Property="Content"
Value="{Binding Source=filter, Converter={StaticResource IconName}, ConverterParameter=13|B.Accent}"/>
</DataTrigger>
</Style.Triggers>
</Style>
</Button.Style>
</Button>
<!-- 초기화는 숨기지 않고 흐리게 둔다 — 버튼이 사라지면 옆 버튼 위치가 흔들린다 -->
<Button Style="{StaticResource Subtle}" Width="26" Height="22" Padding="0"
Click="OnLayerFilterClearClick" ToolTip="타입 필터 해제"
IsEnabled="{Binding CurrentDesigner.HasLayerTypeFilter}"
Content="{Binding Source=filter-x, Converter={StaticResource IconName}, ConverterParameter=13|B.Muted}"/>
</StackPanel>
<!--
레이어 검색칩 — 컨트롤이 수백 개인 서식에서 이름으로 좁혀 찾는다.
내부 TextBox 에 MinHeight=0 필수: 전역 TextBox MinHeight 30 때문에 32px 칩에서 글자가 잘린다.
BorderBrush 도 필수: 라이트에서 B.Input 과 B.Panel 이 둘 다 흰색이라
테두리가 없으면 칩 경계 대비가 정확히 1.00:1 — 입력란이 거기 있다는 것 자체가 안 보인다.
-->
<Border Height="32" CornerRadius="7" Margin="8,8,0,6"
Background="{DynamicResource B.Input}" BorderThickness="1">
<Border.Style>
<Style TargetType="Border">
<Setter Property="BorderBrush" Value="{DynamicResource B.InputBorder}"/>
<Style.Triggers>
<!-- 포커스 표시는 칩이 준다 — 안쪽 입력이 자기 테두리를 그리면 상자 속 상자가 된다 -->
<Trigger Property="IsKeyboardFocusWithin" Value="True">
<Setter Property="BorderBrush" Value="{DynamicResource B.Accent}"/>
</Trigger>
</Style.Triggers>
</Style>
</Border.Style>
<DockPanel>
<ContentControl DockPanel.Dock="Left" Margin="9,0,7,0" VerticalAlignment="Center" IsTabStop="False"
Content="{Binding Source=search, Converter={StaticResource IconName}, ConverterParameter=13|B.Muted}"/>
<Grid>
<TextBox x:Name="LayerSearchBox" Style="{StaticResource ChipTextBox}"
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>
</DockPanel>
<!--
아웃라인 — 페이지 헤더 / 그룹 폴더 / 컨트롤 / '페이지 추가' 네 종류가 한 시퀀스로 섞인다.
ItemTemplate 하나로는 안 되고(암시 DataType 매칭은 LabelViewModel 같은 구체 형에만 걸린다)
선택기로 가른다. 계층은 LayerIndent 여백으로만 표현한다 — TreeView 로 바꾸면
1,067행 서식에서 가상화를 잃는다.
행 배경·모서리는 LayerRowItem 스타일이 Highlight 3상태로 그린다(IsSelected 아님).
그래서 DataTemplate 루트에는 세로 Margin 을 주면 안 된다 — 1px 만 있어도
선택 블록이 이어지지 않는다.
-->
<Grid Margin="6,0,6,6">
<ListBox x:Name="LayerListBox" ItemsSource="{Binding CurrentDesigner.LayerRows}"
bh:LayerListBehavior.Designer="{Binding CurrentDesigner}"
bh:LayerDragBehavior.Designer="{Binding CurrentDesigner}"
ItemContainerStyle="{StaticResource LayerRowItem}"
SelectionMode="Extended" Padding="0,6,0,0"
MouseDoubleClick="OnLayerDoubleClick"
PreviewMouseLeftButtonDown="OnLayerRowPressed"
KeyDown="OnLayerListKeyDown"
ScrollViewer.HorizontalScrollBarVisibility="Disabled">
<ListBox.ItemTemplateSelector>
<ctl:LayerRowTemplateSelector>
<!-- 페이지 헤더 — 셰브론 / 문서 아이콘 / "페이지 N" / (컨트롤수) / 복제·삭제 -->
<ctl:LayerRowTemplateSelector.PageTemplate>
<DataTemplate>
<DockPanel Margin="4,4,4,4">
<DockPanel.Style>
<Style TargetType="DockPanel">
<Style.Triggers>
<DataTrigger Binding="{Binding IsFirst}" Value="False">
<Setter Property="Margin" Value="4,10,4,4"/>
</DataTrigger>
</Style.Triggers>
</Style>
</DockPanel.Style>
<StackPanel DockPanel.Dock="Right" Orientation="Horizontal"
Style="{StaticResource LayerRowActions}">
<Button Style="{StaticResource Subtle}" Width="24" Height="22" Padding="0"
Margin="1,0,0,0" Click="OnPageDuplicateClick" ToolTip="페이지 복제"
Content="{Binding Source=copy, Converter={StaticResource IconName}, ConverterParameter=13|B.Muted}"/>
<Button Style="{StaticResource Subtle}" Width="24" Height="22" Padding="0"
Margin="1,0,0,0" Click="OnPageDeleteClick" ToolTip="페이지 삭제"
Visibility="{Binding CanDelete, Converter={StaticResource BoolToVisibility}}"
Content="{Binding Source=trash-2, Converter={StaticResource IconName}, ConverterParameter=13|B.Muted}"/>
</StackPanel>
<StackPanel>
<StackPanel Orientation="Horizontal">
<ToggleButton Style="{StaticResource MiniChevron}" Width="18" Height="20"
IsChecked="{Binding IsExpanded}" ToolTip="페이지 접기/펼치기"
Content="{Binding ChevronSpec, Converter={StaticResource IconSpec}}"/>
<ContentControl Margin="0,0,6,0" VerticalAlignment="Center" IsTabStop="False"
Content="{Binding PageIconSpec, Converter={StaticResource IconSpec}}"/>
<TextBlock Text="{Binding Title}" FontWeight="SemiBold" VerticalAlignment="Center"/>
<TextBlock Text="{Binding CountText}" FontSize="11" Margin="6,0,0,0"
VerticalAlignment="Center" Foreground="{DynamicResource B.Muted}"/>
</StackPanel>
<TextBlock Text="(비어 있음)" FontSize="11" Margin="20,1,0,0"
Foreground="{DynamicResource B.Muted}"
Visibility="{Binding IsEmpty, Converter={StaticResource BoolToVisibility}}"/>
</StackPanel>
</DockPanel>
</DataTemplate>
</ctl:LayerRowTemplateSelector.PageTemplate>
<!-- 그룹 폴더 — 셰브론 / 폴더 / 이름(더블클릭 편집) / (n) / 일괄 눈·자물쇠 -->
<ctl:LayerRowTemplateSelector.GroupTemplate>
<DataTemplate>
<DockPanel Margin="6,4,6,4">
<ToggleButton DockPanel.Dock="Right" Style="{StaticResource LayerRowToggle}"
IsChecked="{Binding IsLockedFlag}" ToolTip="그룹 전체 잠금"
Content="{Binding LockIconSpec, Converter={StaticResource IconSpec}}"/>
<ToggleButton DockPanel.Dock="Right" Style="{StaticResource LayerRowToggle}"
IsChecked="{Binding IsHiddenFlag}" ToolTip="그룹 전체 숨김"
Content="{Binding HiddenIconSpec, Converter={StaticResource IconSpec}}"/>
<TextBlock DockPanel.Dock="Right" Text="{Binding MemberCountText}" FontSize="11"
VerticalAlignment="Center" Margin="4,0,4,0">
<TextBlock.Style>
<Style TargetType="TextBlock" BasedOn="{StaticResource {x:Type TextBlock}}">
<Setter Property="Foreground" Value="{DynamicResource B.Muted}"/>
<Style.Triggers>
<DataTrigger Binding="{Binding Highlight}" Value="Blue">
<Setter Property="Foreground" Value="{DynamicResource B.OnSelMuted}"/>
</DataTrigger>
</Style.Triggers>
</Style>
</TextBlock.Style>
</TextBlock>
<Border DockPanel.Dock="Left" Width="12"/>
<ToggleButton DockPanel.Dock="Left" Style="{StaticResource MiniChevron}" Width="18" Height="20"
IsChecked="{Binding IsExpanded}" ToolTip="접기/펼치기"
Content="{Binding ChevronSpec, Converter={StaticResource IconSpec}}"/>
<ContentControl DockPanel.Dock="Left" Margin="0,0,6,0" VerticalAlignment="Center" IsTabStop="False"
Content="{Binding FolderIconSpec, Converter={StaticResource IconSpec}}"/>
<Grid VerticalAlignment="Center">
<TextBlock Text="{Binding Name}" 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="8,4,6,4">
<!-- 눈·자물쇠는 아이콘이 상태에 따라 바뀐다 — 사각 체크로는 잠김/숨김이 안 읽힌다 -->
<ToggleButton DockPanel.Dock="Right" Style="{StaticResource LayerRowToggle}"
IsChecked="{Binding IsLockedFlag}" ToolTip="잠금"
Content="{Binding LockIconSpec, Converter={StaticResource IconSpec}}"/>
<ToggleButton DockPanel.Dock="Right" Style="{StaticResource LayerRowToggle}"
IsChecked="{Binding IsHiddenFlag}" ToolTip="숨김"
Content="{Binding HiddenIconSpec, Converter={StaticResource IconSpec}}"/>
<!-- 계층 들여쓰기: 페이지 직속 12, 그룹 멤버 32, 컨테이너 자식 +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 LayerChevronSpec, Converter={StaticResource IconSpec}}"/>
<ContentControl DockPanel.Dock="Left" Margin="0,0,7,0" VerticalAlignment="Center" IsTabStop="False"
Content="{Binding LayerIconSpec, Converter={StaticResource IconSpec}}"/>
<!-- 데이터 배선 배지 — 값이 조회/데이터소스에서 오는 컨트롤 -->
<ContentControl DockPanel.Dock="Right" Margin="0,0,3,0" VerticalAlignment="Center" IsTabStop="False"
ToolTip="데이터소스·조회와 연결된 컨트롤"
Visibility="{Binding HasDataBinding, Converter={StaticResource BoolToVisibility}}"
Content="{Binding Source=link, Converter={StaticResource IconName}, ConverterParameter=12|B.Muted}"/>
<!--
문구와 이름을 두 줄로. 컨트롤 이름(Id)은 외부 참조 키라
(액션 대상·입력 파라미터가 이름으로 서로를 가리키고 CalcBox 수식도 이름으로 합산한다)
배선하려면 목록에서 보여야 하는데, 248px 폭에 나란히 두면 양쪽 다 잘려
'txtKinPhone…' 처럼 구분이 안 되는 꼬리가 남는다. 한 줄 더 쓰는 편이 낫다.
표시 문구가 없어 이름이 곧 문구인 행에서는 둘째 줄이 뜨지 않는다.
-->
<StackPanel VerticalAlignment="Center">
<!-- 숨김 흐리기는 이름 줄에만 — 행 전체를 깎으면 눈·자물쇠 아이콘 대비까지 무너진다 -->
<TextBlock Text="{Binding LayerName}"
TextTrimming="CharacterEllipsis" ToolTip="{Binding LayerName}">
<TextBlock.Style>
<Style TargetType="TextBlock" BasedOn="{StaticResource {x:Type TextBlock}}">
<Style.Triggers>
<DataTrigger Binding="{Binding IsHiddenFlag}" Value="True">
<Setter Property="Opacity" Value="0.55"/>
</DataTrigger>
<DataTrigger Binding="{Binding Highlight}" Value="Blue">
<Setter Property="FontWeight" Value="SemiBold"/>
</DataTrigger>
</Style.Triggers>
</Style>
</TextBlock.Style>
</TextBlock>
<TextBlock Text="{Binding LayerId}" FontSize="10.5" Margin="0,1,0,0"
TextTrimming="CharacterEllipsis"
ToolTip="컨트롤 이름 — 다른 속성이 이 이름으로 참조합니다"
Visibility="{Binding HasLayerId, Converter={StaticResource BoolToVisibility}}">
<TextBlock.Style>
<Style TargetType="TextBlock" BasedOn="{StaticResource {x:Type TextBlock}}">
<Setter Property="Foreground" Value="{DynamicResource B.Muted}"/>
<Style.Triggers>
<!-- 선택 밴드 위에서 B.Muted 는 1.03:1 — 둘째 줄이 통째로 사라진다 -->
<DataTrigger Binding="{Binding Highlight}" Value="Blue">
<Setter Property="Foreground" Value="{DynamicResource B.OnSelMuted}"/>
</DataTrigger>
<DataTrigger Binding="{Binding Highlight}" Value="Navy">
<Setter Property="Foreground" Value="{DynamicResource B.Ink}"/>
</DataTrigger>
</Style.Triggers>
</Style>
</TextBlock.Style>
</TextBlock>
</StackPanel>
</DockPanel>
</DataTemplate>
</ctl:LayerRowTemplateSelector.ControlTemplate>
<!-- 맨 아래 '페이지 추가' — 페이지 섹션을 없앤 자리의 진입점 -->
<ctl:LayerRowTemplateSelector.AddPageTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal" Margin="5,6,4,6"
Background="Transparent" ToolTip="맨 뒤에 빈 페이지를 추가합니다">
<ContentControl Margin="2,0,7,0" VerticalAlignment="Center" IsTabStop="False"
Content="{Binding Source=plus, Converter={StaticResource IconName}, ConverterParameter=13|B.Muted}"/>
<TextBlock Text="페이지 추가" FontSize="12" VerticalAlignment="Center"
Foreground="{DynamicResource B.Muted}"/>
</StackPanel>
</DataTemplate>
</ctl:LayerRowTemplateSelector.AddPageTemplate>
</ctl:LayerRowTemplateSelector>
</ListBox.ItemTemplateSelector>
</ListBox>
<!-- 빈 목록 안내 — 검색·필터가 다 걸러냈는지, 애초에 컨트롤이 없는지 구분해 준다 -->
<TextBlock Text="{Binding CurrentDesigner.LayerEmptyText}" FontSize="12"
Foreground="{DynamicResource B.Muted}" TextWrapping="Wrap" TextAlignment="Center"
HorizontalAlignment="Center" VerticalAlignment="Center" Margin="12,0"
IsHitTestVisible="False"
Visibility="{Binding CurrentDesigner.IsLayerListEmpty, Converter={StaticResource BoolToVisibility}}"/>
</Grid>
</DockPanel>
</UserControl>