레이어 패널을 [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
@@ -41,6 +41,7 @@ public partial class DesignerCanvasView : UserControl
|
||||
{
|
||||
subscribedDesigner.InlineEditRequested -= ShowInlineEditor;
|
||||
subscribedDesigner.ScrollToRequested -= ScrollToControl;
|
||||
subscribedDesigner.ScrollToPageRequested -= ScrollToPage;
|
||||
}
|
||||
CloseInlineEditor(commit: false);
|
||||
subscribedDesigner = e.NewValue as DesignerViewModel;
|
||||
@@ -48,10 +49,29 @@ public partial class DesignerCanvasView : UserControl
|
||||
{
|
||||
subscribedDesigner.InlineEditRequested += ShowInlineEditor;
|
||||
subscribedDesigner.ScrollToRequested += ScrollToControl;
|
||||
subscribedDesigner.ScrollToPageRequested += ScrollToPage;
|
||||
subscribedDesigner.ReportViewport(Scroll.ViewportWidth, Scroll.ViewportHeight);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 페이지 머리가 화면 위쪽에 오도록 스크롤 — 레이어 아웃라인의 페이지 헤더 클릭 경로.
|
||||
/// 캔버스가 전 페이지를 하나의 월드에 세로로 쌓아 그리므로 페이지 오프셋만 알면 된다.
|
||||
/// </summary>
|
||||
private void ScrollToPage(PageViewModel page)
|
||||
{
|
||||
if (subscribedDesigner is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
Dispatcher.BeginInvoke(new Action(() =>
|
||||
{
|
||||
const double CanvasPad = 24; // DesignerCanvasView.xaml 의 Border Padding
|
||||
const double HeadRoom = 8; // 페이지 윗선이 뷰포트 경계에 딱 붙지 않도록
|
||||
Scroll.ScrollToVerticalOffset(page.OffsetY * subscribedDesigner.Zoom + CanvasPad - HeadRoom);
|
||||
}), System.Windows.Threading.DispatcherPriority.Loaded);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 컨트롤이 화면 가운데 오도록 스크롤 — 레이어 목록 더블클릭 경로.
|
||||
///
|
||||
|
||||
@@ -0,0 +1,303 @@
|
||||
<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" BorderBrush="{DynamicResource B.InputBorder}">
|
||||
<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" MinHeight="0" Padding="0,0,6,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>
|
||||
</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"
|
||||
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">
|
||||
<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 MiniToggle}"
|
||||
IsChecked="{Binding IsLockedFlag}" ToolTip="그룹 전체 잠금"
|
||||
Content="{Binding LockIconSpec, Converter={StaticResource IconSpec}}"/>
|
||||
<ToggleButton DockPanel.Dock="Right" Style="{StaticResource MiniToggle}"
|
||||
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 MiniToggle}"
|
||||
IsChecked="{Binding IsLockedFlag}" ToolTip="잠금"
|
||||
Content="{Binding LockIconSpec, Converter={StaticResource IconSpec}}"/>
|
||||
<ToggleButton DockPanel.Dock="Right" Style="{StaticResource MiniToggle}"
|
||||
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>
|
||||
@@ -0,0 +1,205 @@
|
||||
using System.Windows;
|
||||
using SheetMe.Designer.ViewModels;
|
||||
|
||||
namespace SheetMe.Designer.Views;
|
||||
|
||||
/// <summary>
|
||||
/// 레이어 아웃라인 패널 — [200]SheetMe 이식본.
|
||||
///
|
||||
/// 여기 있는 코드비하인드는 전부 <b>ListBox 가 커맨드로 표현할 수 없는 입력</b>이다:
|
||||
/// 더블클릭(InputBinding 미지원), 선택 대상이 아닌 행의 클릭, 인라인 편집의 포커스 인계.
|
||||
/// 문서를 바꾸는 판단은 전부 <see cref="DesignerViewModel"/> 에 위임한다.
|
||||
/// </summary>
|
||||
public partial class LayerPanelView : System.Windows.Controls.UserControl
|
||||
{
|
||||
#region Constructors
|
||||
public LayerPanelView() => InitializeComponent();
|
||||
#endregion
|
||||
|
||||
#region Properties
|
||||
/// <summary>현재 문서의 디자이너 — DataContext 는 창에서 상속받는다</summary>
|
||||
private DesignerViewModel? Designer => (DataContext as MainViewModel)?.CurrentDesigner;
|
||||
#endregion
|
||||
|
||||
#region Methods - 행 조작
|
||||
/// <summary>
|
||||
/// 레이어 더블클릭 → 컨트롤은 캔버스 이동, 그룹 폴더는 이름 편집 시작.
|
||||
///
|
||||
/// 컨트롤이 수백 개인 서식에서는 목록에서 이름을 찾아도 종이 어디에 있는지 알 수 없다.
|
||||
/// [200] 은 더블클릭을 컨트롤 이름 편집에 쓰지만 그 경로는 이식하지 않았다 —
|
||||
/// [200] 이 고치는 값은 표시 전용 별칭이고, SheetMe 의 컨트롤 이름은 <b>외부 참조 키</b>라
|
||||
/// 목록에서 무심코 고치면 액션 대상·CalcBox 수식 배선이 끊긴다.
|
||||
/// </summary>
|
||||
private void OnLayerDoubleClick(object sender, System.Windows.Input.MouseButtonEventArgs e)
|
||||
{
|
||||
if (Designer is not { } designer)
|
||||
{
|
||||
return;
|
||||
}
|
||||
switch (LayerListBox.SelectedItem)
|
||||
{
|
||||
case ViewModels.Controls.ControlViewModel target:
|
||||
designer.RevealControl(target);
|
||||
break;
|
||||
case LayerGroupViewModel group:
|
||||
group.IsEditing = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 레이어 행 눌림 — 페이지 헤더·'페이지 추가'처럼 선택 대상이 아닌 행의 클릭을 여기서 받는다.
|
||||
///
|
||||
/// 이 두 행은 <c>Focusable=False</c> 라 ListBox 선택 경로를 타지 않는다.
|
||||
/// 행 안의 버튼(복제·삭제·셰브론)은 자기 Click 을 따로 처리하므로 먼저 걸러 낸다.
|
||||
/// </summary>
|
||||
private void OnLayerRowPressed(object sender, System.Windows.Input.MouseButtonEventArgs e)
|
||||
{
|
||||
if (Designer is not { } designer)
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (e.OriginalSource is DependencyObject source
|
||||
&& FindAncestor<System.Windows.Controls.Primitives.ButtonBase>(source) is not null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
switch (RowDataOf(e.OriginalSource))
|
||||
{
|
||||
case LayerPageViewModel page:
|
||||
designer.ActivatePage(page.Page);
|
||||
break;
|
||||
case LayerAddPageViewModel:
|
||||
designer.AddPage();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>페이지 헤더의 복제 버튼</summary>
|
||||
private void OnPageDuplicateClick(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (Designer is { } designer && (sender as FrameworkElement)?.DataContext is LayerPageViewModel row)
|
||||
{
|
||||
designer.DuplicatePage(row.Page);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>페이지 헤더의 삭제 버튼</summary>
|
||||
private void OnPageDeleteClick(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (Designer is { } designer && (sender as FrameworkElement)?.DataContext is LayerPageViewModel row)
|
||||
{
|
||||
designer.RemovePage(row.Page);
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Methods - 그룹 이름 인라인 편집
|
||||
/// <summary>
|
||||
/// 이름 편집 상자가 <b>보이게 된 순간</b> 포커스+전체선택 — 더블클릭 직후 바로 덮어쓸 수 있게.
|
||||
/// Loaded 로는 안 된다: 행이 만들어질 때 상자는 아직 접혀 있고, 나중에 펼쳐질 때 Loaded 는 다시 오지 않는다.
|
||||
/// 표시가 실제로 반영된 뒤에 포커스를 줘야 하므로 한 틱 미룬다.
|
||||
/// </summary>
|
||||
private void OnGroupNameBoxVisibleChanged(object sender, DependencyPropertyChangedEventArgs e)
|
||||
{
|
||||
if (sender is not System.Windows.Controls.TextBox box || !box.IsVisible)
|
||||
{
|
||||
return;
|
||||
}
|
||||
Dispatcher.BeginInvoke(new Action(() =>
|
||||
{
|
||||
box.Focus();
|
||||
System.Windows.Input.Keyboard.Focus(box);
|
||||
box.SelectAll();
|
||||
}), System.Windows.Threading.DispatcherPriority.Input);
|
||||
}
|
||||
|
||||
/// <summary>Enter=확정 / Esc=취소 — 둘 다 편집 상태를 끈다</summary>
|
||||
private void OnGroupNameKeyDown(object sender, System.Windows.Input.KeyEventArgs e)
|
||||
{
|
||||
if (sender is not System.Windows.Controls.TextBox box)
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (e.Key == System.Windows.Input.Key.Enter)
|
||||
{
|
||||
box.GetBindingExpression(System.Windows.Controls.TextBox.TextProperty)?.UpdateSource();
|
||||
EndGroupNameEdit(box);
|
||||
e.Handled = true;
|
||||
}
|
||||
else if (e.Key == System.Windows.Input.Key.Escape)
|
||||
{
|
||||
box.GetBindingExpression(System.Windows.Controls.TextBox.TextProperty)?.UpdateTarget();
|
||||
EndGroupNameEdit(box);
|
||||
e.Handled = true;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>포커스가 빠지면 확정(LostFocus 바인딩이 이미 값을 밀어 넣는다)</summary>
|
||||
private void OnGroupNameLostFocus(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (sender is System.Windows.Controls.TextBox box)
|
||||
{
|
||||
EndGroupNameEdit(box);
|
||||
}
|
||||
}
|
||||
|
||||
private static void EndGroupNameEdit(System.Windows.Controls.TextBox box)
|
||||
{
|
||||
if (box.DataContext is LayerGroupViewModel group)
|
||||
{
|
||||
group.IsEditing = false;
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Methods - 타입 필터
|
||||
/// <summary>레이어 타입 필터 — 문서에 실제로 있는 타입만 후보로 띄운다</summary>
|
||||
private void OnLayerFilterClick(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (Designer is not { } designer)
|
||||
{
|
||||
return;
|
||||
}
|
||||
var dialog = new LayerTypeFilterDialog(designer.LayerTypeCandidates(), designer.LayerTypeFilter)
|
||||
{
|
||||
Owner = Window.GetWindow(this),
|
||||
};
|
||||
if (dialog.ShowDialog() == true)
|
||||
{
|
||||
designer.SetLayerTypeFilter(dialog.SelectedTypes);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>타입 필터 해제</summary>
|
||||
private void OnLayerFilterClearClick(object sender, RoutedEventArgs e)
|
||||
=> Designer?.SetLayerTypeFilter(Array.Empty<string>());
|
||||
#endregion
|
||||
|
||||
#region Methods - 트리 탐색
|
||||
/// <summary>눌린 지점이 속한 레이어 행의 데이터</summary>
|
||||
private static object? RowDataOf(object? source)
|
||||
=> source is DependencyObject node
|
||||
? FindAncestor<System.Windows.Controls.ListBoxItem>(node)?.DataContext
|
||||
: null;
|
||||
|
||||
/// <summary>
|
||||
/// 조상 찾기 — 시각 트리를 따라 오르되, 시각 요소가 아닌 노드(FrameworkContentElement 등)에서는
|
||||
/// 논리 트리로 갈아탄다. VisualTreeHelper 만 쓰면 그런 노드에서 예외가 난다.
|
||||
/// </summary>
|
||||
private static T? FindAncestor<T>(DependencyObject? node) where T : DependencyObject
|
||||
{
|
||||
while (node is not null)
|
||||
{
|
||||
if (node is T match)
|
||||
{
|
||||
return match;
|
||||
}
|
||||
node = node is System.Windows.Media.Visual or System.Windows.Media.Media3D.Visual3D
|
||||
? System.Windows.Media.VisualTreeHelper.GetParent(node)
|
||||
: LogicalTreeHelper.GetParent(node);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -38,106 +38,6 @@ public partial class MainView : Window
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 레이어 더블클릭 → 컨트롤은 캔버스 이동, 그룹 폴더는 이름 편집 시작.
|
||||
/// 컨트롤이 수백 개인 서식에서는 목록에서 이름을 찾아도 종이 어디에 있는지 알 수 없었다.
|
||||
/// (ListBox 더블클릭은 InputBinding 을 지원하지 않아 코드비하인드로 위임한다 — 서식 목록과 같은 이유)
|
||||
/// </summary>
|
||||
private void OnLayerDoubleClick(object sender, System.Windows.Input.MouseButtonEventArgs e)
|
||||
{
|
||||
if (DataContext is not MainViewModel { CurrentDesigner: { } designer })
|
||||
{
|
||||
return;
|
||||
}
|
||||
switch (LayerListBox.SelectedItem)
|
||||
{
|
||||
case ViewModels.Controls.ControlViewModel target:
|
||||
designer.RevealControl(target);
|
||||
break;
|
||||
case LayerGroupViewModel group:
|
||||
group.IsEditing = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 그룹 이름 편집 상자가 <b>보이게 된 순간</b> 포커스+전체선택 — 더블클릭 직후 바로 덮어쓸 수 있게.
|
||||
/// Loaded 로는 안 된다: 행이 만들어질 때 상자는 아직 접혀 있고, 나중에 펼쳐질 때 Loaded 는 다시 오지 않는다.
|
||||
/// 표시가 실제로 반영된 뒤에 포커스를 줘야 하므로 한 틱 미룬다.
|
||||
/// </summary>
|
||||
private void OnGroupNameBoxVisibleChanged(object sender, DependencyPropertyChangedEventArgs e)
|
||||
{
|
||||
if (sender is not System.Windows.Controls.TextBox box || !box.IsVisible)
|
||||
{
|
||||
return;
|
||||
}
|
||||
Dispatcher.BeginInvoke(new Action(() =>
|
||||
{
|
||||
box.Focus();
|
||||
System.Windows.Input.Keyboard.Focus(box);
|
||||
box.SelectAll();
|
||||
}), System.Windows.Threading.DispatcherPriority.Input);
|
||||
}
|
||||
|
||||
/// <summary>Enter=확정 / Esc=취소 — 둘 다 편집 상태를 끈다</summary>
|
||||
private void OnGroupNameKeyDown(object sender, System.Windows.Input.KeyEventArgs e)
|
||||
{
|
||||
if (sender is not System.Windows.Controls.TextBox box)
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (e.Key == System.Windows.Input.Key.Enter)
|
||||
{
|
||||
box.GetBindingExpression(System.Windows.Controls.TextBox.TextProperty)?.UpdateSource();
|
||||
EndGroupNameEdit(box);
|
||||
e.Handled = true;
|
||||
}
|
||||
else if (e.Key == System.Windows.Input.Key.Escape)
|
||||
{
|
||||
box.GetBindingExpression(System.Windows.Controls.TextBox.TextProperty)?.UpdateTarget();
|
||||
EndGroupNameEdit(box);
|
||||
e.Handled = true;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>포커스가 빠지면 확정(LostFocus 바인딩이 이미 값을 밀어 넣는다)</summary>
|
||||
private void OnGroupNameLostFocus(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (sender is System.Windows.Controls.TextBox box)
|
||||
{
|
||||
EndGroupNameEdit(box);
|
||||
}
|
||||
}
|
||||
|
||||
private static void EndGroupNameEdit(System.Windows.Controls.TextBox box)
|
||||
{
|
||||
if (box.DataContext is LayerGroupViewModel group)
|
||||
{
|
||||
group.IsEditing = false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>레이어 타입 필터 — 활성 페이지에 실제로 있는 타입만 후보로 띄운다</summary>
|
||||
private void OnLayerFilterClick(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (DataContext is not MainViewModel { CurrentDesigner: { } designer })
|
||||
{
|
||||
return;
|
||||
}
|
||||
var dialog = new LayerTypeFilterDialog(designer.LayerTypeCandidates(), designer.LayerTypeFilter)
|
||||
{
|
||||
Owner = this,
|
||||
};
|
||||
if (dialog.ShowDialog() == true)
|
||||
{
|
||||
designer.SetLayerTypeFilter(dialog.SelectedTypes);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>타입 필터 해제</summary>
|
||||
private void OnLayerFilterClearClick(object sender, RoutedEventArgs e)
|
||||
=> (DataContext as MainViewModel)?.CurrentDesigner?.SetLayerTypeFilter(Array.Empty<string>());
|
||||
|
||||
private void OnThemeToggleClick(object sender, RoutedEventArgs e)
|
||||
{
|
||||
ThemeManager.Toggle();
|
||||
|
||||
Reference in New Issue
Block a user