using System.Windows; using System.Windows.Controls; using System.Windows.Input; using MergeHub.ViewModels; namespace MergeHub.Views { /// /// 폴더 비교 탭. 트리 자체는 XAML 이 다 하고, 여기서는 TreeView 가 /// 바인딩으로 못 넘겨 주는 것(선택 항목·더블클릭·Enter)만 이어 준다. /// public partial class FolderView : UserControl { public FolderView() { InitializeComponent(); // 12차 D-1: 찾기 줄은 늘 한 줄을 먹었다. 이제 Ctrl+F 로 부를 때만 편다(파일 비교의 찾기와 같은 방식). PreviewKeyDown += OnPreviewKeyDown; } /// /// Ctrl+F 로 찾기 줄을 펴고 칸에 손을 옮긴다. Esc 로 접는다(12차 D-1). /// /// 손을 옮기는 것은 배치가 끝난 뒤여야 한다 - 접혀 있던 칸은 아직 화면에 없어서 /// 그 자리에서 Focus() 를 불러도 받지 않는다. /// private void OnPreviewKeyDown(object sender, KeyEventArgs e) { if (DataContext is not FolderViewModel vm) return; if (e.Key == Key.F && Keyboard.Modifiers == ModifierKeys.Control) { vm.OpenFindCommand.Execute(null); Dispatcher.BeginInvoke(new System.Action(() => { SearchBox.Focus(); SearchBox.SelectAll(); }), System.Windows.Threading.DispatcherPriority.Loaded); e.Handled = true; return; } // 찾기 칸이 접혀 있으면 Esc 는 손대지 않는다 - 다른 곳에서 쓸 자리를 뺏지 않는다. if (e.Key == Key.Escape && vm.IsFindOpen) { vm.CloseFindCommand.Execute(null); e.Handled = true; } } /// [옵션 ▾] 를 누르면 서랍을 여닫는다(12차 D-1). private void Options_Click(object sender, RoutedEventArgs e) { if (DataContext is FolderViewModel vm) vm.IsOptionsOpen = !vm.IsOptionsOpen; } /// TreeView.SelectedItem 은 읽기 전용이라 바인딩할 수 없다. 여기서 옮겨 준다. private void Tree_SelectedItemChanged(object sender, RoutedPropertyChangedEventArgs e) { if (DataContext is FolderViewModel vm) vm.SelectedNode = e.NewValue as FolderNode; } /// /// 항목을 더블클릭하면 그 파일을 "파일 비교" 탭에서 연다. /// /// 클릭한 자리에서 항목을 직접 집는다 - TreeView 의 선택은 첫 클릭에 따라오지만, /// 펼침 화살표처럼 선택을 바꾸지 않는 자리를 두 번 누르면 엉뚱한 항목이 열린다. /// private void Tree_MouseDoubleClick(object sender, MouseButtonEventArgs e) { if (DataContext is not FolderViewModel vm) return; if (NodeUnder(e.OriginalSource as DependencyObject) is not { } node) return; // 폴더는 펼치고 접는 것이 기본 동작이다. 그대로 둔다. if (node.IsDirectory) return; vm.SelectedNode = node; vm.OpenInDiffCore(node); e.Handled = true; } /// Enter 로도 연다. 목록을 키보드로 훑는 사람에게는 이쪽이 빠르다. private void Tree_KeyDown(object sender, KeyEventArgs e) { if (e.Key != Key.Enter) return; if (DataContext is not FolderViewModel vm || vm.SelectedNode is not { } node) return; if (node.IsDirectory) return; vm.OpenInDiffCore(node); e.Handled = true; } /// /// 최근 목록에서 하나를 고르면 그 짝으로 곧바로 견준다. /// 고른 뒤 바로 선택을 지운다 - 같은 짝을 다시 고를 수 있어야 한다. /// private void Recent_SelectionChanged(object sender, SelectionChangedEventArgs e) { if (sender is not ComboBox box || box.SelectedItem is not Services.RecentPair pair) return; box.SelectedIndex = -1; if (DataContext is FolderViewModel vm) vm.OpenRecent(pair); } /// 찾기 칸에서 Enter 를 누르면 바로 찾는다. 단추까지 가는 걸음을 줄인다. private void SearchBox_KeyDown(object sender, KeyEventArgs e) { if (e.Key != Key.Enter) return; if (DataContext is not FolderViewModel vm) return; if (vm.SearchCommand.CanExecute(null)) vm.SearchCommand.Execute(null); e.Handled = true; } /// 찾은 줄을 더블클릭하면 그 자리에서 파일 비교를 연다. private void HitList_MouseDoubleClick(object sender, MouseButtonEventArgs e) { if (DataContext is not FolderViewModel vm) return; if (HitUnder(e.OriginalSource as DependencyObject) is not { } hit) return; vm.SelectedHit = hit; vm.OpenHitCore(hit); e.Handled = true; } /// Enter 로도 연다. 결과를 키보드로 훑는 쪽이 빠르다. private void HitList_KeyDown(object sender, KeyEventArgs e) { if (e.Key != Key.Enter) return; if (DataContext is not FolderViewModel vm || vm.SelectedHit is not { } hit) return; vm.OpenHitCore(hit); e.Handled = true; } /// 클릭이 떨어진 자리에서 위로 올라가며 그 줄의 결과를 찾는다. private static Services.FolderSearchHit? HitUnder(DependencyObject? source) { while (source is not null) { if (source is ListBoxItem item) return item.DataContext as Services.FolderSearchHit; source = System.Windows.Media.VisualTreeHelper.GetParent(source); } return null; } /// 클릭이 떨어진 시각 요소에서 위로 올라가며 그 줄의 항목을 찾는다. private static FolderNode? NodeUnder(DependencyObject? source) { while (source is not null) { if (source is TreeViewItem item) return item.DataContext as FolderNode; if (source is FrameworkElement { DataContext: FolderNode node }) return node; source = System.Windows.Media.VisualTreeHelper.GetParent(source); } return null; } // ================= 끌어다 놓기 ================= /// /// 탐색기에서 끌어 온 것을 받을 수 있다고 알린다. /// /// 이 표시가 없으면 커서에 금지 표시가 뜨고 놓아도 아무 일이 없다. /// 파일·폴더가 아닌 것(글자·그림)은 받지 않는다. /// private static void AllowFileDrop(DragEventArgs e) { e.Effects = e.Data.GetDataPresent(DataFormats.FileDrop) ? DragDropEffects.Copy : DragDropEffects.None; e.Handled = true; } private void View_DragOver(object sender, DragEventArgs e) => AllowFileDrop(e); /// 놓는 차례대로 첫 칸 · 둘째 칸에 넣는다(7차 E-2). private readonly Services.DropSlotOrder _dropOrder = new Services.DropSlotOrder(); /// /// 화면 아무 데나 놓았을 때. 두 개를 놓으면 양쪽에 하나씩 들어간다. /// /// 폴더 하나를 놓으면 차례대로 첫 칸 → 둘째 칸에 들어간다. 경로 칸 위에 정확히 놓지 않아도 된다(7차 E-2). /// 예전에는 늘 첫 칸에 들어가 두 번째 폴더가 첫 폴더를 덮어썼다. 양쪽이 차면 바로 견준다. /// /// 그 밖(두 개 · 파일)은 명령줄로 들어오는 길과 같은 판단을 쓴다 - 폴더면 폴더 비교, 파일이면 파일 비교로 /// 탭까지 옮긴다. 폴더 화면에 파일을 놓았다고 아무 일도 안 하는 것은 불친절하다. /// private void View_Drop(object sender, DragEventArgs e) { if (DataContext is not FolderViewModel vm) return; if (DroppedPaths(e) is not { Length: > 0 } paths) return; if (paths.Length == 1 && System.IO.Directory.Exists(paths[0])) { vm.DropFolder(Services.PathUtil.FullFolderPath(paths[0]), _dropOrder.Take()); e.Handled = true; return; } vm.Workspace.RouteDroppedPaths(paths); e.Handled = true; } /// /// 왼쪽·오른쪽 칸에 바로 놓으면 그 자리에만 들어간다. /// /// XAML 에서 PreviewDrop 으로 받는 까닭 - 경로 칸이 TextBox 라서 /// TextBoxBase 의 클래스 처리기가 DragOver·Drop 을 먼저 먹는다. /// 그쪽은 글자 끌어다 놓기만 알아서 파일 목록은 버리고 Handled 로 막아 버린다. /// 실제로 화면에서 칸 위에 놓아 봤더니 아무 일도 일어나지 않았다. /// 터널링(Preview)은 클래스 처리기보다 먼저 도므로 이쪽이 먼저 받는다. /// private void LeftFolder_Drop(object sender, DragEventArgs e) => DropIntoSlot(e, left: true); private void RightFolder_Drop(object sender, DragEventArgs e) => DropIntoSlot(e, left: false); private void DropIntoSlot(DragEventArgs e, bool left) { if (DataContext is not FolderViewModel vm) return; if (DroppedPaths(e) is not { Length: > 0 } paths) return; // 파일을 놓았으면 그 파일이 든 폴더로 받는다 - 폴더 칸이니 그렇게 읽는 편이 자연스럽다. var folder = Services.PathUtil.FullFolderPath(paths[0]); if (folder.Length == 0) folder = Services.PathUtil.FolderOfFile(paths[0]); if (folder.Length == 0) return; // 두 개를 한꺼번에 놓았으면 나머지 하나는 반대쪽에 넣는다. if (paths.Length > 1) { var other = Services.PathUtil.FullFolderPath(paths[1]); if (other.Length == 0) other = Services.PathUtil.FolderOfFile(paths[1]); if (other.Length > 0) { if (left) vm.RightFolder = other; else vm.LeftFolder = other; } } // 넣고 나서 양쪽이 다 찼으면 그 자리에서 견준다. vm.DropFolder(folder, left); // 칸에 바로 놓은 것도 차례에 친다 - 다음에 화면에 놓는 폴더는 반대 칸으로 간다. // 두 개를 한꺼번에 놓아 양쪽을 다 채웠으면 다음은 첫 칸부터다. _dropOrder.Placed(paths.Length > 1 ? false : left); e.Handled = true; } private static string[]? DroppedPaths(DragEventArgs e) => e.Data.GetDataPresent(DataFormats.FileDrop) ? e.Data.GetData(DataFormats.FileDrop) as string[] : null; } }