재현: 경로 · 옵션 · 찾기 · 색 범례가 네 줄로 305px(배율 150% 기준 203 DIP)를 먹었고, 비교하기 전에도 그대로였다. 자주 쓰는 [비교] 는 옵션 줄 끝에 묻혀 있었다. 위 한 줄 [...] 왼쪽 경로 <-> [...] 오른쪽 경로 [최근 v] 켠 옵션 [비교] [취소] [옵션 v] - 폴더 고르기는 글자 대신 ... 로. 경로 칸 바로 앞에 두어 어느 쪽 것인지 헷갈리지 않는다 - 좌우 바꾸기를 새로 넣었다(사용자 결정) - 반대로 견주려고 두 번 고르지 않아도 된다 - 최근 목록은 화살표만 남겼다 옵션 서랍 - 빠른 비교 · 하위 폴더 포함 · 보기 · 포함 · 제외를 [옵션 v] 안으로. [폴더 안 찾기] 도 여기에 - 서랍에 넣으면 "내가 뭘 켜 뒀더라" 를 알 수 없게 된다 - 특히 제외 규칙은 켠 줄 모르고 "왜 이 파일이 안 보이지" 로 이어진다. 기본과 다른 것만 한 줄에 짧게 적는다(FolderOptionText). 기본 그대로면 아무것도 적지 않는다 - 늘 떠 있는 글은 눈에 띄지 않는다 찾기 · 색 범례 - 찾기는 늘 한 줄을 먹던 것을 Ctrl+F 로 부를 때만 편다. Esc · [찾기 접기] 로 접는다 - 색 범례는 아래 상태 줄로 내렸다 - 색은 목록을 보는 동안 필요한 것이라 결과 요약 옆이 제자리다 화면으로 확인 - 위쪽 높이 305px -> 77px (203 -> 51 DIP, -75%) - 좌우 바꾸기: 누르면 맞바뀌고 다시 누르면 되돌아온다 - 옵션 서랍에 넣은 것이 다 있고, 빠른 비교를 켜면 줄에 "빠른 비교", 제외 규칙이 있어 "규칙 1" - Ctrl+F 로 77 -> 158px, Esc 로 다시 77px - 비교 그대로 동작(전체 2개 · 다름 2 · 왼쪽만 0 · 오른쪽만 0 · 같음 0) - 되던 것이 하나도 없어지지 않았다: 폴더 고르기 · 최근 · 빠른 비교 · 하위 폴더 · 보기 거르기 · 포함/제외 · 폴더 안 찾기 · 결과 내보내기 · 복사/삭제 · 줄 단위 비교 · 색 구분(아래로) - 좁은 창(1920px = 1280 DIP)에서도 여전히 77px 한 줄, [비교] · [옵션] 다 보임 화면을 보고 고친 것: [옵션 v] 를 ToggleButton 으로 뒀더니 플랫폼 기본 스타일이 스위치 모양이라 같은 줄의 다른 단추와 따로 놀았다. 여느 단추로 바꿨다. 최근 목록도 폭을 못 박지 않아 넓게 벌어지던 것을 고쳤다. 하네스 FolderTest 118 -> 131건, 전체 28벌 1,865건 · 실패 0. 빌드 경고 0 · 오류 0. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
256 lines
11 KiB
C#
256 lines
11 KiB
C#
using System.Windows;
|
|
using System.Windows.Controls;
|
|
using System.Windows.Input;
|
|
using MergeHub.ViewModels;
|
|
|
|
namespace MergeHub.Views
|
|
{
|
|
/// <summary>
|
|
/// 폴더 비교 탭. 트리 자체는 XAML 이 다 하고, 여기서는 TreeView 가
|
|
/// 바인딩으로 못 넘겨 주는 것(선택 항목·더블클릭·Enter)만 이어 준다.
|
|
/// </summary>
|
|
public partial class FolderView : UserControl
|
|
{
|
|
public FolderView()
|
|
{
|
|
InitializeComponent();
|
|
|
|
// 12차 D-1: 찾기 줄은 늘 한 줄을 먹었다. 이제 Ctrl+F 로 부를 때만 편다(파일 비교의 찾기와 같은 방식).
|
|
PreviewKeyDown += OnPreviewKeyDown;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Ctrl+F 로 찾기 줄을 펴고 칸에 손을 옮긴다. Esc 로 접는다(12차 D-1).
|
|
///
|
|
/// 손을 옮기는 것은 <b>배치가 끝난 뒤</b>여야 한다 - 접혀 있던 칸은 아직 화면에 없어서
|
|
/// 그 자리에서 Focus() 를 불러도 받지 않는다.
|
|
/// </summary>
|
|
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;
|
|
}
|
|
}
|
|
|
|
/// <summary>[옵션 ▾] 를 누르면 서랍을 여닫는다(12차 D-1).</summary>
|
|
private void Options_Click(object sender, RoutedEventArgs e)
|
|
{
|
|
if (DataContext is FolderViewModel vm) vm.IsOptionsOpen = !vm.IsOptionsOpen;
|
|
}
|
|
|
|
/// <summary>TreeView.SelectedItem 은 읽기 전용이라 바인딩할 수 없다. 여기서 옮겨 준다.</summary>
|
|
private void Tree_SelectedItemChanged(object sender, RoutedPropertyChangedEventArgs<object> e)
|
|
{
|
|
if (DataContext is FolderViewModel vm) vm.SelectedNode = e.NewValue as FolderNode;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 항목을 더블클릭하면 그 파일을 "파일 비교" 탭에서 연다.
|
|
///
|
|
/// 클릭한 자리에서 항목을 직접 집는다 - TreeView 의 선택은 첫 클릭에 따라오지만,
|
|
/// 펼침 화살표처럼 선택을 바꾸지 않는 자리를 두 번 누르면 <b>엉뚱한 항목</b>이 열린다.
|
|
/// </summary>
|
|
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;
|
|
}
|
|
|
|
/// <summary>Enter 로도 연다. 목록을 키보드로 훑는 사람에게는 이쪽이 빠르다.</summary>
|
|
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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 최근 목록에서 하나를 고르면 그 짝으로 곧바로 견준다.
|
|
/// 고른 뒤 바로 선택을 지운다 - 같은 짝을 다시 고를 수 있어야 한다.
|
|
/// </summary>
|
|
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);
|
|
}
|
|
|
|
/// <summary>찾기 칸에서 Enter 를 누르면 바로 찾는다. 단추까지 가는 걸음을 줄인다.</summary>
|
|
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;
|
|
}
|
|
|
|
/// <summary>찾은 줄을 더블클릭하면 그 자리에서 파일 비교를 연다.</summary>
|
|
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;
|
|
}
|
|
|
|
/// <summary>Enter 로도 연다. 결과를 키보드로 훑는 쪽이 빠르다.</summary>
|
|
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;
|
|
}
|
|
|
|
/// <summary>클릭이 떨어진 자리에서 위로 올라가며 그 줄의 결과를 찾는다.</summary>
|
|
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;
|
|
}
|
|
|
|
/// <summary>클릭이 떨어진 시각 요소에서 위로 올라가며 그 줄의 항목을 찾는다.</summary>
|
|
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;
|
|
}
|
|
// ================= 끌어다 놓기 =================
|
|
|
|
/// <summary>
|
|
/// 탐색기에서 끌어 온 것을 받을 수 있다고 알린다.
|
|
///
|
|
/// 이 표시가 없으면 커서에 <b>금지 표시</b>가 뜨고 놓아도 아무 일이 없다.
|
|
/// 파일·폴더가 아닌 것(글자·그림)은 받지 않는다.
|
|
/// </summary>
|
|
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);
|
|
|
|
/// <summary>놓는 차례대로 첫 칸 · 둘째 칸에 넣는다(7차 E-2).</summary>
|
|
private readonly Services.DropSlotOrder _dropOrder = new Services.DropSlotOrder();
|
|
|
|
/// <summary>
|
|
/// 화면 아무 데나 놓았을 때. 두 개를 놓으면 양쪽에 하나씩 들어간다.
|
|
///
|
|
/// <b>폴더 하나</b>를 놓으면 차례대로 첫 칸 → 둘째 칸에 들어간다. 경로 칸 위에 정확히 놓지 않아도 된다(7차 E-2).
|
|
/// 예전에는 늘 첫 칸에 들어가 두 번째 폴더가 첫 폴더를 덮어썼다. 양쪽이 차면 바로 견준다.
|
|
///
|
|
/// 그 밖(두 개 · 파일)은 명령줄로 들어오는 길과 <b>같은 판단</b>을 쓴다 - 폴더면 폴더 비교, 파일이면 파일 비교로
|
|
/// 탭까지 옮긴다. 폴더 화면에 파일을 놓았다고 아무 일도 안 하는 것은 불친절하다.
|
|
/// </summary>
|
|
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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 왼쪽·오른쪽 칸에 바로 놓으면 <b>그 자리에만</b> 들어간다.
|
|
///
|
|
/// XAML 에서 <c>PreviewDrop</c> 으로 받는 까닭 - 경로 칸이 <c>TextBox</c> 라서
|
|
/// <c>TextBoxBase</c> 의 클래스 처리기가 <c>DragOver</c>·<c>Drop</c> 을 먼저 먹는다.
|
|
/// 그쪽은 글자 끌어다 놓기만 알아서 파일 목록은 버리고 <c>Handled</c> 로 막아 버린다.
|
|
/// 실제로 화면에서 칸 위에 놓아 봤더니 아무 일도 일어나지 않았다.
|
|
/// 터널링(Preview)은 클래스 처리기보다 먼저 도므로 이쪽이 먼저 받는다.
|
|
/// </summary>
|
|
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;
|
|
}
|
|
}
|