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 }