using System.Windows;
using SheetMe.Designer.ViewModels;
namespace SheetMe.Designer.Views;
///
/// 레이어 아웃라인 패널 — [200]SheetMe 이식본.
///
/// 여기 있는 코드비하인드는 전부 ListBox 가 커맨드로 표현할 수 없는 입력이다:
/// 더블클릭(InputBinding 미지원), 선택 대상이 아닌 행의 클릭, 인라인 편집의 포커스 인계.
/// 문서를 바꾸는 판단은 전부 에 위임한다.
///
public partial class LayerPanelView : System.Windows.Controls.UserControl
{
#region Constructors
public LayerPanelView() => InitializeComponent();
#endregion
#region Properties
/// 현재 문서의 디자이너 — DataContext 는 창에서 상속받는다
private DesignerViewModel? Designer => (DataContext as MainViewModel)?.CurrentDesigner;
#endregion
#region Methods - 행 조작
///
/// 레이어 더블클릭 → 컨트롤은 캔버스 이동, 그룹 폴더는 이름 편집 시작.
///
/// 컨트롤이 수백 개인 서식에서는 목록에서 이름을 찾아도 종이 어디에 있는지 알 수 없다.
/// [200] 은 더블클릭을 컨트롤 이름 편집에 쓰지만 그 경로는 이식하지 않았다 —
/// [200] 이 고치는 값은 표시 전용 별칭이고, SheetMe 의 컨트롤 이름은 외부 참조 키라
/// 목록에서 무심코 고치면 액션 대상·CalcBox 수식 배선이 끊긴다.
///
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;
}
}
///
/// 레이어 행 눌림 — 페이지 헤더·'페이지 추가'처럼 선택 대상이 아닌 행의 클릭을 여기서 받는다.
///
/// 이 두 행은 Focusable=False 라 ListBox 선택 경로를 타지 않는다.
/// 행 안의 버튼(복제·삭제·셰브론)은 자기 Click 을 따로 처리하므로 먼저 걸러 낸다.
///
private void OnLayerRowPressed(object sender, System.Windows.Input.MouseButtonEventArgs e)
{
if (Designer is not { } designer)
{
return;
}
if (e.OriginalSource is DependencyObject source
&& FindAncestor(source) is not null)
{
return;
}
switch (RowDataOf(e.OriginalSource))
{
case LayerPageViewModel page:
designer.ActivatePage(page.Page);
break;
}
}
///
/// 레이어 목록 단축키 — 그룹/삭제/복사/되돌리기.
///
/// 이 키들은 원래 가 처리하는데, 그 behavior 는
/// 캔버스에 붙어 있어서 포커스가 목록에 있으면 이벤트가 거기까지 가지 않는다.
/// 목록에서 여러 행을 골라 놓고 Ctrl+G 를 눌렀을 때 아무 일도 안 일어나던 이유가 이것이다.
///
/// 방향키·Home/End·문자 타이핑은 넘기지 않는다 — 목록 자체의 이동·형식 검색이 우선이다.
/// 그룹 이름을 편집 중일 때도 통과시킨다(입력 보호).
///
private void OnLayerListKeyDown(object sender, System.Windows.Input.KeyEventArgs e)
{
if (Designer is not { } designer
|| System.Windows.Input.Keyboard.FocusedElement
is System.Windows.Controls.Primitives.TextBoxBase or System.Windows.Controls.ComboBox)
{
return;
}
var ctrl = System.Windows.Input.Keyboard.Modifiers.HasFlag(System.Windows.Input.ModifierKeys.Control);
var shift = System.Windows.Input.Keyboard.Modifiers.HasFlag(System.Windows.Input.ModifierKeys.Shift);
var handled = true;
switch (e.Key)
{
case System.Windows.Input.Key.G when ctrl && shift: designer.UngroupSelection(); break;
case System.Windows.Input.Key.G when ctrl: designer.GroupSelection(); break;
case System.Windows.Input.Key.C when ctrl: designer.CopySelection(); break;
case System.Windows.Input.Key.X when ctrl: designer.CutSelection(); break;
case System.Windows.Input.Key.V when ctrl: designer.Paste(); break;
case System.Windows.Input.Key.D when ctrl: designer.CopySelection(); designer.Paste(); break;
// 캔버스와 같은 짝 — 레이어 목록에 포커스가 있을 때도 Ctrl+Shift+Z 는 다시 실행이다
case System.Windows.Input.Key.Z when ctrl && shift: designer.Undo.Redo(); break;
case System.Windows.Input.Key.Z when ctrl: designer.Undo.Undo(); break;
case System.Windows.Input.Key.Y when ctrl: designer.Undo.Redo(); break;
case System.Windows.Input.Key.Delete: designer.DeleteSelection(); break;
case System.Windows.Input.Key.Escape: designer.Selection.Clear(); break;
default: handled = false; break;
}
e.Handled = handled;
}
/// 돋보기를 켰다 — 상자가 실제로 보이게 된 뒤에 포커스를 준다(한 틱 뒤)
private void OnLayerSearchOpened(object sender, RoutedEventArgs e)
=> Dispatcher.BeginInvoke(new Action(() =>
{
LayerSearchBox.Focus();
System.Windows.Input.Keyboard.Focus(LayerSearchBox);
}), System.Windows.Threading.DispatcherPriority.Input);
///
/// 돋보기를 껐다 — 검색어를 지운다.
/// 상자가 접힌 채 필터만 살아 있으면 목록이 통째로 비어 보이고 원인을 찾을 단서가 화면에 없다.
///
private void OnLayerSearchClosed(object sender, RoutedEventArgs e)
{
if (Designer is { } designer)
{
designer.LayerFilter = string.Empty;
}
}
/// 페이지 구획의 + — 트리 안에 있던 '페이지 추가' 행을 대신한다
private void OnPageAddClick(object sender, RoutedEventArgs e) => Designer?.AddPage();
///
/// 페이지 행 누름 — 그 페이지를 활성화한다(캔버스가 거기로 스크롤하고 인스펙터가 용지 속성을 연다).
///
/// ListBox 의 선택으로 처리하지 않는 이유: 복제·삭제 버튼을 눌러도 행 선택이 먼저 일어나
/// 엉뚱한 페이지가 활성화된다. 눌린 지점이 버튼 위면 넘긴다.
///
private void OnPageRowPressed(object sender, System.Windows.Input.MouseButtonEventArgs e)
{
if (e.OriginalSource is DependencyObject source
&& FindAncestor(source) is not null)
{
return;
}
if (RowDataOf(e.OriginalSource) is LayerPageViewModel row)
{
Designer?.ActivatePage(row.Page);
}
}
/// 페이지 헤더의 복제 버튼
private void OnPageDuplicateClick(object sender, RoutedEventArgs e)
{
if (Designer is { } designer && (sender as FrameworkElement)?.DataContext is LayerPageViewModel row)
{
designer.DuplicatePage(row.Page);
}
}
/// 페이지 헤더의 삭제 버튼
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 - 그룹 이름 인라인 편집
///
/// 이름 편집 상자가 보이게 된 순간 포커스+전체선택 — 더블클릭 직후 바로 덮어쓸 수 있게.
/// Loaded 로는 안 된다: 행이 만들어질 때 상자는 아직 접혀 있고, 나중에 펼쳐질 때 Loaded 는 다시 오지 않는다.
/// 표시가 실제로 반영된 뒤에 포커스를 줘야 하므로 한 틱 미룬다.
///
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);
}
/// Enter=확정 / Esc=취소 — 둘 다 편집 상태를 끈다
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;
}
}
/// 포커스가 빠지면 확정(LostFocus 바인딩이 이미 값을 밀어 넣는다)
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 - 타입 필터
/// 레이어 타입 필터 — 문서에 실제로 있는 타입만 후보로 띄운다
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);
}
}
/// 타입 필터 해제
private void OnLayerFilterClearClick(object sender, RoutedEventArgs e)
=> Designer?.SetLayerTypeFilter(Array.Empty());
#endregion
#region Methods - 트리 탐색
/// 눌린 지점이 속한 레이어 행의 데이터
private static object? RowDataOf(object? source)
=> source is DependencyObject node
? FindAncestor(node)?.DataContext
: null;
///
/// 조상 찾기 — 시각 트리를 따라 오르되, 시각 요소가 아닌 노드(FrameworkContentElement 등)에서는
/// 논리 트리로 갈아탄다. VisualTreeHelper 만 쓰면 그런 노드에서 예외가 난다.
///
private static T? FindAncestor(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
}