4차 B-1: 최근 비교 목록

파일 짝과 폴더 짝을 따로 15개까지 기억한다. 탭으로 나눈 글로 설정 폴더에 둔다.
없는 경로는 흐리게 보여 주고 지우지 않는다. 검사 33건 추가.
This commit is contained in:
김승준 2026-09-10 20:43:14 +09:00
parent 0625236fc3
commit b9456b7f14
11 changed files with 412 additions and 3 deletions

View File

@ -157,3 +157,43 @@ MergeHub 파일 비교 결과
화면의 머지 주석 미리보기가 `// [김승준] 2026-09-10` 이었으니 **두 이름이 같다.** 화면의 머지 주석 미리보기가 `// [김승준] 2026-09-10` 이었으니 **두 이름이 같다.**
검사 8건 추가(`ReportTest` 23 → 31). 전체 **522건 통과** · 빌드 경고 0 · 오류 0. 검사 8건 추가(`ReportTest` 23 → 31). 전체 **522건 통과** · 빌드 경고 0 · 오류 0.
---
## B-1. 최근 비교 목록 — **완료** (2026-09-10)
**왜** - 같은 짝을 반복해서 여는데 그때마다 경로를 다시 골라야 했다. 매일 쓰는 사람에게 가장 잦은 마찰이다.
`Services/RecentPairs.cs` 신설. 화면 없이 검사할 수 있게 **기억하는 일만** 여기서 한다.
**파일 짝과 폴더 짝을 따로 둔다.** 한 목록에 섞으면 파일 비교 화면에서 폴더가 나온다.
상한은 15개 - 목록이 길면 고르는 것이 더 느려진다.
**저장 형식은 탭으로 나눈 글이다.** JSON 을 안 쓴 까닭 - 줄 몇 개가 전부라 라이브러리를 하나 더 매달 값이 없고,
글이면 사람이 열어 보고 손으로 고칠 수도 있다. 윈도우 경로에는 탭이 못 들어가서 나누는 데 문제가 없다.
자리는 `D:\UbisamConfig\Settings\MergeHub.Recent.txt` 다. **설정 화면에는 안 내놓는다** -
사람이 손으로 고칠 값이 아니고 설정 목록만 어지럽힌다.
**켤 때 한 번 읽고 바뀔 때마다 바로 쓴다.** 프로그램이 갑자기 꺼져도 목록이 남게 하려는 것이다.
**없는 경로를 자동으로 지우지 않는다.** 지시서가 그렇게 못박았다 - 네트워크 드라이브나 USB 는
잠깐 안 보일 뿐인데 지워 버리면 다시 찾아 들어가야 한다. 흐리게 보여 주고 열려고 할 때 알린다.
**비교가 끝난 짝만 담는다.** 읽다 실패했거나 취소한 짝을 남기면 다음에 골랐을 때 또 실패한다.
**목록 이름은 양쪽 끝 이름만** 쓴다. 전체 경로는 길어서 목록에서 읽히지 않는다.
두 이름이 같으면 하나만 적고(`Other.cpp`), 다르면 둘 다 적는다(`기준.cs ↔ 내작업.cs`). 전체 경로는 툴팁에 있다.
여는 길은 다른 데와 같은 `RequestPlainCompare` 다. 그래서 **저장하지 않은 편집도 A-1 에서 함께 걸린다.**
**화면에서 확인한 것**
| 한 일 | 결과 |
|---|---|
| 파일 비교 · 폴더 비교를 한 번씩 | `MergeHub.Recent.txt` 에 `F` 줄과 `D` 줄이 하나씩 |
| 프로그램을 껐다 켜기 | 두 목록이 그대로 남았다 |
| 폴더 최근에서 고르기 | 두 칸이 채워지고 비교가 바로 돌았다 (`전체 2개 · 다름 2`) |
| 파일 최근에서 고르기 | `BASE · Weld.cpp` / `LOCAL · Weld.cpp` 로 열렸다 |
| 짝의 두 파일을 지운 뒤 | 목록에 그대로 남고 **흐리게** 보였다 |
| 그 죽은 짝을 고르기 | `두 파일 모두 찾을 수 없습니다` 토스트, 화면은 그대로 |
검사 33건 추가(`RecentTest`). 전체 **555건 통과** · 빌드 경고 0 · 오류 0.

View File

@ -8,6 +8,7 @@
| 날짜 | 항목 | 커밋 | | 날짜 | 항목 | 커밋 |
|---|---|---| |---|---|---|
| 2026-09-10 | **4차 B-1** — 최근에 견준 파일 짝·폴더 짝을 기억해 위 줄에서 바로 연다. 없는 경로는 흐리게만 하고 지우지 않는다 | 아래 |
| 2026-09-10 | **4차 A-2** — 내보낸 보고서 머리글에 만든 사람을 넣음. 이름 판단을 머지 주석과 한 함수로 모았다 | 아래 | | 2026-09-10 | **4차 A-2** — 내보낸 보고서 머리글에 만든 사람을 넣음. 이름 판단을 머지 주석과 한 함수로 모았다 | 아래 |
| 2026-09-10 | **4차 A-1** — 저장하지 않은 편집이 말없이 사라지던 길 여덟 군데를 막음. 플랫폼에 세 갈래 확인 창을 새로 넣었다 | 아래 | | 2026-09-10 | **4차 A-1** — 저장하지 않은 편집이 말없이 사라지던 길 여덟 군데를 막음. 플랫폼에 세 갈래 확인 창을 새로 넣었다 | 아래 |
| 2026-09-10 | **4차 D-2** — 항목을 끝내면 상세·상태·한 줄을 각각 어디에 적을지 공통 규칙으로 못박음 | 아래 | | 2026-09-10 | **4차 D-2** — 항목을 끝내면 상세·상태·한 줄을 각각 어디에 적을지 공통 규칙으로 못박음 | 아래 |
@ -29,6 +30,7 @@
| 날짜 | 문서 | 내용 | | 날짜 | 문서 | 내용 |
|---|---|---| |---|---|---|
| 2026-09-10 | **4차 B-1** — 최근에 견준 파일 짝·폴더 짝을 기억해 위 줄에서 바로 연다. 없는 경로는 흐리게만 하고 지우지 않는다 | 아래 |
| 2026-09-10 | **4차 A-2** — 내보낸 보고서 머리글에 만든 사람을 넣음. 이름 판단을 머지 주석과 한 함수로 모았다 | 아래 | | 2026-09-10 | **4차 A-2** — 내보낸 보고서 머리글에 만든 사람을 넣음. 이름 판단을 머지 주석과 한 함수로 모았다 | 아래 |
| 2026-09-10 | **4차 A-1** — 저장하지 않은 편집이 말없이 사라지던 길 여덟 군데를 막음. 플랫폼에 세 갈래 확인 창을 새로 넣었다 | 아래 | | 2026-09-10 | **4차 A-1** — 저장하지 않은 편집이 말없이 사라지던 길 여덟 군데를 막음. 플랫폼에 세 갈래 확인 창을 새로 넣었다 | 아래 |
| 2026-09-10 | **4차 D-2** — 항목을 끝내면 상세·상태·한 줄을 각각 어디에 적을지 공통 규칙으로 못박음 | 아래 | | 2026-09-10 | **4차 D-2** — 항목을 끝내면 상세·상태·한 줄을 각각 어디에 적을지 공통 규칙으로 못박음 | 아래 |

View File

@ -29,7 +29,7 @@
| 상태 | 항목 | | 상태 | 항목 |
|---|---| |---|---|
| 대기 | B-1 최근 비교 목록 | | **완료** | B-1 최근 비교 목록 |
| 대기 | B-2 같은 줄 접기 | | 대기 | B-2 같은 줄 접기 |
| 대기 | B-3 폴더 비교에서 다음 다른 파일로 | | 대기 | B-3 폴더 비교에서 다음 다른 파일로 |
| 대기 | B-4 외부 편집기로 열기 | | 대기 | B-4 외부 편집기로 열기 |

View File

@ -48,6 +48,11 @@ namespace MergeHub.Services
// 그러면 주석에 등록한 이름 대신 윈도우 계정이 찍힌다. // 그러면 주석에 등록한 이름 대신 윈도우 계정이 찍힌다.
AuthorName = ResolveAuthorName(); AuthorName = ResolveAuthorName();
// 최근 목록은 켤 때 한 번 읽고, 바뀔 때마다 바로 쓴다.
// 프로그램이 갑자기 꺼져도 목록이 남게 하려는 것이다 - 몇 줄짜리 글이라 쓰는 값이 싸다.
Recent.Load(RecentPath);
Recent.Changed += (_, _) => Recent.Save(RecentPath);
// `git mergetool` · 탐색기 우클릭이 넘긴 것을 화면으로 보낸다. // `git mergetool` · 탐색기 우클릭이 넘긴 것을 화면으로 보낸다.
// 여기서 하는 이유 - 이 객체는 첫 화면이 만들어질 때 함께 만들어지므로 // 여기서 하는 이유 - 이 객체는 첫 화면이 만들어질 때 함께 만들어지므로
// 요청을 남겨 둘 자리로 가장 이르다. // 요청을 남겨 둘 자리로 가장 이르다.
@ -71,6 +76,21 @@ namespace MergeHub.Services
public IGitService Git { get; } public IGitService Git { get; }
/// <summary>
/// 최근에 견준 짝. 파일 비교와 폴더 비교가 함께 본다.
///
/// 여기에 두는 까닭 - 두 화면이 서로를 모르는 채로 같은 목록을 봐야 하고,
/// 이 객체가 이미 그 통로 노릇을 하고 있다.
/// </summary>
public RecentPairs Recent { get; } = new RecentPairs();
/// <summary>
/// 최근 목록을 두는 자리. 플랫폼 설정 폴더 아래에 둔다 - 장비마다 한 자리에 모인다.
/// <b>설정 화면에는 내놓지 않는다.</b> 사람이 손으로 고칠 값이 아니고, 설정 목록만 어지럽힌다.
/// </summary>
private static string RecentPath => Path.Combine(
UbisamBase.Core.Bootstrap.UbisamAppBuilder.ConfigRoot, "Settings", "MergeHub.Recent.txt");
public MergeHubSettings Settings { get; } public MergeHubSettings Settings { get; }
/// <summary>저장소 상태가 다시 읽힐 때마다 발생. 화면들이 자기 패널을 갱신한다.</summary> /// <summary>저장소 상태가 다시 읽힐 때마다 발생. 화면들이 자기 패널을 갱신한다.</summary>

View File

@ -0,0 +1,195 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
namespace MergeHub.Services
{
/// <summary>최근 목록에 담기는 것이 파일 짝인지 폴더 짝인지.</summary>
public enum RecentKind
{
Files,
Folders
}
/// <summary>최근에 견준 짝 하나.</summary>
public sealed class RecentPair
{
public RecentPair(RecentKind kind, string left, string right, DateTime when)
{
Kind = kind;
Left = left ?? string.Empty;
Right = right ?? string.Empty;
When = when;
}
public RecentKind Kind { get; }
public string Left { get; }
public string Right { get; }
public DateTime When { get; }
/// <summary>
/// 목록에 뿌릴 이름. <b>양쪽 끝 이름만</b> 보여 준다 - 전체 경로는 길어서 목록에서 읽히지 않고,
/// 어느 짝인지 가르는 것은 대개 파일 이름이다. 전체 경로는 툴팁에 있다.
/// </summary>
public string Label
{
get
{
var l = Tail(Left);
var r = Tail(Right);
return string.Equals(l, r, StringComparison.OrdinalIgnoreCase) ? l : $"{l} ↔ {r}";
}
}
public string Tip => $"{Left}{Environment.NewLine}{Right}";
/// <summary>
/// 양쪽이 다 있는지. <b>없다고 목록에서 지우지는 않는다</b> -
/// 네트워크 드라이브나 USB 는 잠깐 안 보일 뿐인데 지워 버리면 다시 찾아 들어가야 한다.
/// 흐리게 보여 주고, 열려고 할 때 알린다.
/// </summary>
public bool Exists => Kind == RecentKind.Folders
? Directory.Exists(Left) && Directory.Exists(Right)
: File.Exists(Left) || File.Exists(Right);
private static string Tail(string path)
{
if (string.IsNullOrWhiteSpace(path)) return "-";
var trimmed = path.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
var name = Path.GetFileName(trimmed);
// 드라이브 뿌리(C:\)는 이름이 비어 나온다. 그때는 경로를 그대로 쓴다.
return name.Length > 0 ? name : trimmed;
}
}
/// <summary>
/// 최근에 견준 짝을 기억한다.
///
/// 같은 짝을 반복해서 여는 일이 잦은데 그때마다 경로를 다시 골라야 했다.
/// 파일 짝과 폴더 짝을 <b>따로</b> 둔다 - 한 목록에 섞으면 파일 비교 화면에서 폴더가 나온다.
///
/// 저장 형식은 탭으로 나눈 글이다. JSON 을 쓰지 않는 까닭 - 이 목록은 줄 몇 개가 전부라
/// 라이브러리를 하나 더 매달 값이 없고, 글이면 사람이 열어 보고 손으로 고칠 수도 있다.
/// 윈도우 경로에는 탭이 들어갈 수 없어 나누는 데 문제가 없다.
/// </summary>
public sealed class RecentPairs
{
/// <summary>이보다 많이 쌓지 않는다. 목록이 길면 고르는 것이 더 느려진다.</summary>
public const int Max = 15;
private readonly List<RecentPair> _files = new List<RecentPair>();
private readonly List<RecentPair> _folders = new List<RecentPair>();
public IReadOnlyList<RecentPair> Files => _files;
public IReadOnlyList<RecentPair> Folders => _folders;
/// <summary>목록이 바뀌었다. 화면이 다시 그리도록.</summary>
public event EventHandler? Changed;
public void Add(RecentKind kind, string left, string right, DateTime when)
{
if (string.IsNullOrWhiteSpace(left) && string.IsNullOrWhiteSpace(right)) return;
var list = kind == RecentKind.Folders ? _folders : _files;
// 같은 짝이 이미 있으면 지우고 맨 위에 다시 넣는다. 최근 것이 위여야 한다.
list.RemoveAll(p => Same(p, left, right));
list.Insert(0, new RecentPair(kind, left ?? string.Empty, right ?? string.Empty, when));
while (list.Count > Max) list.RemoveAt(list.Count - 1);
Changed?.Invoke(this, EventArgs.Empty);
}
public void Add(RecentKind kind, string left, string right) => Add(kind, left, right, DateTime.Now);
private static bool Same(RecentPair pair, string? left, string? right) =>
string.Equals(pair.Left, left ?? string.Empty, StringComparison.OrdinalIgnoreCase) &&
string.Equals(pair.Right, right ?? string.Empty, StringComparison.OrdinalIgnoreCase);
// ================= 글로 옮기기 =================
private const string Header = "# MergeHub 최근 비교 목록";
public string Serialize()
{
var text = new StringBuilder();
text.AppendLine(Header);
foreach (var pair in _files) Write(text, pair);
foreach (var pair in _folders) Write(text, pair);
return text.ToString();
}
private static void Write(StringBuilder text, RecentPair pair) =>
text.AppendLine(string.Join("\t",
pair.Kind == RecentKind.Folders ? "D" : "F",
pair.When.ToString("yyyy-MM-ddTHH:mm:ss"),
pair.Left,
pair.Right));
/// <summary>
/// 글에서 되읽는다. <b>알아볼 수 없는 줄은 조용히 건너뛴다</b> -
/// 최근 목록 하나 때문에 프로그램이 안 켜지면 안 된다.
/// </summary>
public void Parse(string? content)
{
_files.Clear();
_folders.Clear();
if (string.IsNullOrWhiteSpace(content)) { Changed?.Invoke(this, EventArgs.Empty); return; }
foreach (var raw in content!.Replace("\r\n", "\n").Split('\n'))
{
var line = raw.TrimEnd();
if (line.Length == 0 || line.StartsWith("#", StringComparison.Ordinal)) continue;
var parts = line.Split('\t');
if (parts.Length < 4) continue;
var kind = parts[0] == "D" ? RecentKind.Folders : RecentKind.Files;
if (!DateTime.TryParse(parts[1], out var when)) when = DateTime.MinValue;
var list = kind == RecentKind.Folders ? _folders : _files;
if (list.Count >= Max) continue;
list.Add(new RecentPair(kind, parts[2], parts[3], when));
}
Changed?.Invoke(this, EventArgs.Empty);
}
// ================= 파일 =================
public void Load(string path)
{
try
{
Parse(File.Exists(path) ? File.ReadAllText(path, Encoding.UTF8) : null);
}
catch (IOException) { Parse(null); }
catch (UnauthorizedAccessException) { Parse(null); }
}
/// <summary>
/// 저장에 실패해도 조용히 넘어간다. 최근 목록은 편의 기능이라
/// 이것 때문에 하던 일이 막히면 안 된다.
/// </summary>
public void Save(string path)
{
try
{
var dir = Path.GetDirectoryName(path);
if (!string.IsNullOrEmpty(dir) && !Directory.Exists(dir)) Directory.CreateDirectory(dir!);
File.WriteAllText(path, Serialize(), new UTF8Encoding(true));
}
catch (IOException) { }
catch (UnauthorizedAccessException) { }
}
}
}

View File

@ -121,6 +121,8 @@ namespace MergeHub.ViewModels
// 탐색기 우클릭으로 켰으면 그 폴더가 왼쪽에 들어온다. // 탐색기 우클릭으로 켰으면 그 폴더가 왼쪽에 들어온다.
_workspace.FolderCompareRequested += OnFolderCompareRequested; _workspace.FolderCompareRequested += OnFolderCompareRequested;
_workspace.Recent.Changed += (_, _) => _workspace.OnUi(RefreshRecent);
RefreshRecent();
_workspace.DeliverPendingFolderCompare(); _workspace.DeliverPendingFolderCompare();
// 저장소를 열어 뒀으면 왼쪽 기본값으로 삼는다 - 대개 그 폴더를 견주게 된다. // 저장소를 열어 뒀으면 왼쪽 기본값으로 삼는다 - 대개 그 폴더를 견주게 된다.
@ -151,6 +153,36 @@ namespace MergeHub.ViewModels
public ObservableCollection<FolderNode> Nodes { get; } = new ObservableCollection<FolderNode>(); public ObservableCollection<FolderNode> Nodes { get; } = new ObservableCollection<FolderNode>();
// ================= 최근 비교 목록 =================
/// <summary>최근에 견준 폴더 짝. 파일 짝과 따로 둔다.</summary>
public ObservableCollection<RecentPair> RecentFolders { get; } = new ObservableCollection<RecentPair>();
private void RefreshRecent()
{
RecentFolders.Clear();
foreach (var pair in _workspace.Recent.Folders) RecentFolders.Add(pair);
}
/// <summary>
/// 최근 목록에서 고른 짝으로 곧바로 견준다.
/// 자리를 옮겼거나 지워진 짝이라도 목록에서 지우지 않는다 - 왜 못 여는지만 알려 준다.
/// </summary>
public void OpenRecent(RecentPair? pair)
{
if (pair is null) return;
if (!Directory.Exists(pair.Left) || !Directory.Exists(pair.Right))
{
MessageUtil.ShowWarningToast("폴더를 찾을 수 없습니다. 자리를 옮겼거나 지워진 것 같습니다.");
return;
}
LeftFolder = pair.Left;
RightFolder = pair.Right;
_ = CompareAsync();
}
[ObservableProperty] private string _leftFolder = string.Empty; [ObservableProperty] private string _leftFolder = string.Empty;
[ObservableProperty] private string _rightFolder = string.Empty; [ObservableProperty] private string _rightFolder = string.Empty;
@ -325,6 +357,9 @@ namespace MergeHub.ViewModels
Rebuild(); Rebuild();
Summary = result.Summary; Summary = result.Summary;
_workspace.AppendLog($"폴더 비교: {left} ↔ {right} · {result.Summary}"); _workspace.AppendLog($"폴더 비교: {left} ↔ {right} · {result.Summary}");
// 비교가 끝난 짝만 담는다. 취소했거나 실패한 짝은 최근에 남길 값이 없다.
_workspace.Recent.Add(RecentKind.Folders, left, right);
} }
catch (OperationCanceledException) catch (OperationCanceledException)
{ {

View File

@ -37,8 +37,11 @@ namespace MergeHub.ViewModels
_workspace.Refreshed += OnWorkspaceRefreshed; _workspace.Refreshed += OnWorkspaceRefreshed;
_workspace.StateChanged += RaiseCanExecuteChanged; _workspace.StateChanged += RaiseCanExecuteChanged;
_workspace.PlainCompareRequested += OnPlainCompareRequested; _workspace.PlainCompareRequested += OnPlainCompareRequested;
_workspace.Recent.Changed += (_, _) => _workspace.OnUi(RefreshRecent);
_reflowTimer.Tick += (_, _) => Reflow(); _reflowTimer.Tick += (_, _) => Reflow();
RefreshRecent();
LoadRules(); LoadRules();
IsGitMode = _workspace.IsRepositoryOpen; IsGitMode = _workspace.IsRepositoryOpen;
RefreshRemoteBranches(); RefreshRemoteBranches();
@ -187,6 +190,37 @@ namespace MergeHub.ViewModels
/// <summary>비교 대상 파일 목록(충돌 중이면 충돌 파일, 아니면 선택 커밋의 파일).</summary> /// <summary>비교 대상 파일 목록(충돌 중이면 충돌 파일, 아니면 선택 커밋의 파일).</summary>
public ObservableCollection<string> Files { get; } = new ObservableCollection<string>(); public ObservableCollection<string> Files { get; } = new ObservableCollection<string>();
// ================= 최근 비교 목록 =================
/// <summary>최근에 견준 파일 짝. 위 줄의 목록에 뿌린다.</summary>
public ObservableCollection<RecentPair> RecentFiles { get; } = new ObservableCollection<RecentPair>();
private void RefreshRecent()
{
RecentFiles.Clear();
foreach (var pair in _workspace.Recent.Files) RecentFiles.Add(pair);
}
/// <summary>
/// 최근 목록에서 고른 짝을 연다.
///
/// 자리를 옮겼거나 지워진 짝이라도 <b>목록에서 지우지 않는다</b> - 네트워크 드라이브나 USB 는
/// 잠깐 안 보일 뿐이다. 대신 왜 못 여는지 알려 준다.
/// 여는 길은 다른 데와 같다(<c>RequestPlainCompare</c>) - 저장하지 않은 편집도 그 길에서 함께 걸린다.
/// </summary>
public void OpenRecent(RecentPair? pair)
{
if (pair is null) return;
if (!File.Exists(pair.Left) && !File.Exists(pair.Right))
{
MessageUtil.ShowWarningToast("두 파일 모두 찾을 수 없습니다. 자리를 옮겼거나 지워진 것 같습니다.");
return;
}
_workspace.RequestPlainCompare(pair.Left, pair.Right);
}
[ObservableProperty] private string? _selectedFile; [ObservableProperty] private string? _selectedFile;
[ObservableProperty] private MergeLine? _selectedLine; [ObservableProperty] private MergeLine? _selectedLine;
[ObservableProperty] private string _baseHeader = "BASE"; [ObservableProperty] private string _baseHeader = "BASE";
@ -1728,6 +1762,10 @@ namespace MergeHub.ViewModels
RefreshDiffPosition(); RefreshDiffPosition();
RaiseCanExecuteChanged(); RaiseCanExecuteChanged();
// 다 읽고 나서 최근 목록에 담는다. 읽다 실패한 짝을 목록에 남기면
// 다음에 골랐을 때 또 실패한다.
_workspace.Recent.Add(RecentKind.Files, LeftFile, RightFile);
// 줄을 고르는 것만으로는 화면이 따라오지 않는다. 캐럿까지 옮겨야 굴러간다. // 줄을 고르는 것만으로는 화면이 따라오지 않는다. 캐럿까지 옮겨야 굴러간다.
if (asked >= 0) GoToRow(asked); if (asked >= 0) GoToRow(asked);
}); });

View File

@ -97,6 +97,8 @@
<ColumnDefinition Width="*"/> <ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/> <ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/> <ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions> </Grid.ColumnDefinitions>
<Button Grid.Column="0" Content="왼쪽 폴더..." Command="{Binding PickLeftFolderCommand}" <Button Grid.Column="0" Content="왼쪽 폴더..." Command="{Binding PickLeftFolderCommand}"
@ -112,7 +114,32 @@
AllowDrop="True" PreviewDragOver="View_DragOver" PreviewDrop="RightFolder_Drop" AllowDrop="True" PreviewDragOver="View_DragOver" PreviewDrop="RightFolder_Drop"
FontFamily="Consolas" FontSize="11" Height="28" VerticalContentAlignment="Center" FontFamily="Consolas" FontSize="11" Height="28" VerticalContentAlignment="Center"
ToolTip="견줄 폴더"/> ToolTip="견줄 폴더"/>
</Grid>
<TextBlock Grid.Column="4" Text="최근" VerticalAlignment="Center" Style="{StaticResource Muted}"
FontSize="11" Margin="14,0,4,0"/>
<ComboBox x:Name="RecentBox" ItemsSource="{Binding RecentFolders}"
SelectionChanged="Recent_SelectionChanged"
Width="200" Height="28" FontSize="11" Grid.Column="5" Margin="0"
VerticalContentAlignment="Center"
ToolTip="최근에 견준 짝. 고르면 바로 엽니다">
<ComboBox.ItemTemplate>
<DataTemplate>
<!-- 자리를 옮겼거나 지워진 짝은 흐리게. 목록에서 지우지는 않는다 -
네트워크 드라이브나 USB 는 잠깐 안 보일 뿐이다. -->
<TextBlock Text="{Binding Label}" ToolTip="{Binding Tip}" FontSize="11">
<TextBlock.Style>
<Style TargetType="TextBlock">
<Style.Triggers>
<DataTrigger Binding="{Binding Exists}" Value="False">
<Setter Property="Opacity" Value="0.45"/>
</DataTrigger>
</Style.Triggers>
</Style>
</TextBlock.Style>
</TextBlock>
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox> </Grid>
</Border> </Border>
<!-- ② 비교 방식 · 필터 --> <!-- ② 비교 방식 · 필터 -->

View File

@ -52,6 +52,18 @@ namespace MergeHub.Views
e.Handled = true; 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> /// <summary>찾기 칸에서 Enter 를 누르면 바로 찾는다. 단추까지 가는 걸음을 줄인다.</summary>
private void SearchBox_KeyDown(object sender, KeyEventArgs e) private void SearchBox_KeyDown(object sender, KeyEventArgs e)
{ {

View File

@ -396,6 +396,32 @@
ToolTip="{Binding RightFile}" ToolTip="{Binding RightFile}"
Background="Transparent" Background="Transparent"
AllowDrop="True" DragOver="View_DragOver" Drop="RightFile_Drop"/> AllowDrop="True" DragOver="View_DragOver" Drop="RightFile_Drop"/>
<TextBlock Text="최근" VerticalAlignment="Center" Style="{StaticResource Muted}"
FontSize="11" Margin="14,0,4,0"/>
<ComboBox x:Name="RecentBox" ItemsSource="{Binding RecentFiles}"
SelectionChanged="Recent_SelectionChanged"
Width="200" Height="26" MinHeight="26" FontSize="11"
VerticalContentAlignment="Center"
ToolTip="최근에 견준 파일 짝. 고르면 바로 엽니다">
<ComboBox.ItemTemplate>
<DataTemplate>
<!-- 자리를 옮겼거나 지워진 짝은 흐리게. 목록에서 지우지는 않는다 -
네트워크 드라이브나 USB 는 잠깐 안 보일 뿐이다. -->
<TextBlock Text="{Binding Label}" ToolTip="{Binding Tip}" FontSize="11">
<TextBlock.Style>
<Style TargetType="TextBlock">
<Style.Triggers>
<DataTrigger Binding="{Binding Exists}" Value="False">
<Setter Property="Opacity" Value="0.45"/>
</DataTrigger>
</Style.Triggers>
</Style>
</TextBlock.Style>
</TextBlock>
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
</StackPanel> </StackPanel>
<!-- Git 일 때: 비교할 파일과 3-Way 상대 브랜치를 여기서 고른다. <!-- Git 일 때: 비교할 파일과 3-Way 상대 브랜치를 여기서 고른다.

View File

@ -915,6 +915,20 @@ namespace MergeHub.Views
return true; return 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 HomeViewModel vm) vm.OpenRecent(pair);
}
// ================= 끌어다 놓기 ================= // ================= 끌어다 놓기 =================
/// <summary> /// <summary>