Shift+휠 가로 · Ctrl+↓ 와 아래 "지금 줄" 칸 · Ctrl+W 로 폴더 비교 · 다크 모드

- Shift+휠을 가로 스크롤로(파일 비교 · 메모). 파일 비교는 가로 휠(틸트)과 같은
  길로 보내 마우스가 놓인 칸이 움직이고 나머지가 따라온다. 휠 아래 = 오른쪽.
- Ctrl+↓ / Ctrl+↑ 를 다음 · 이전 차이의 대체 키로. 칸과 상태 줄 사이에 캐럿 행의
  BASE(변경 전) · LOCAL(변경 후)을 위아래로 보이는 칸 - 칸과 같은 CodeTextBlock
  (문법 색 · 바뀐 글자 칠하기 · 줄 색), 긴 줄은 접는다. HomeViewModel.CaretLine.
- 폴더 비교에서 연 문서 탭을 닫으면(Ctrl+W) 폴더 비교 탭으로 간다.
  RequestPlainCompare(fromFolder:) 로 표시를 보내 DocTab.FromFolder 에 붙이고,
  ◀ 이전 파일 · 다음 파일 ▶ 로 짝을 바꿔도 이어 받는다. 다른 길로 연 탭은 그대로.
- 어두운 테마에서 폴더 비교 목록만 흰 바탕 · 검은 글자였다 - 플랫폼 테마가
  TreeView 는 칠하지 않는다. 바탕 · 글자색(항목 포함)을 테마에서 받는다.
  색 구분의 0 항목 흐리기 0.35 -> 0.55(어두운 바탕에서 묻혔다).

시험: 빌드 경고 0 · 오류 0. 화면 g27-five(새, 어두운 테마) - Shift+휠 편집기 가로
0 -> 20% · 세로 그대로 · BASE 칸 위에서도 · 메모, Ctrl+↓ 로 차이 이동 · 아래 칸
표시, 폴더 비교에서 연 탭만 Ctrl+W 뒤 폴더 비교로, 사진으로 폴더 목록 · 아래 칸
확인. 실패 0. 기존 회귀는 돌리지 않았다.

Agent: main (claude-opus-5-5)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
김승준 2026-10-01 19:02:25 +09:00
parent 18b05d6b38
commit df06947afc
14 changed files with 177 additions and 6 deletions

View File

@ -2,6 +2,23 @@
지시서: [../작업지시/12차.md](../작업지시/12차.md) 지시서: [../작업지시/12차.md](../작업지시/12차.md)
## Shift+휠 가로 · Ctrl+↓ 와 아래 "지금 줄" 칸 · Ctrl+W 로 폴더 비교 · 다크 모드 (2026-10-01, 사용자 요청 · 결정 · 완료)
1. **Shift+휠 = 가로 스크롤**(파일 비교 · 메모, 사용자 결정). 예전에는 Ctrl+휠(글자 크기)만 따로 받고 Shift+휠은 세로로 굴렀다.
파일 비교는 가로 휠(틸트)과 같은 길(`ScrollSourcePanes` · `ScrollEditor`)로 보낸다 — 마우스가 놓인 칸이 움직이고 나머지는 지금처럼 따라온다. 휠 아래 = 오른쪽.
2. **Ctrl+↓ / Ctrl+↑ = 다음 · 이전 차이**(대체 키, 단축키 표에 더함 — 설명 창에도 나온다). 그리고 **아래 "지금 줄" 칸**(사용자 결정: 캐럿 줄 두 줄) —
칸과 상태 줄 사이에 캐럿 행의 BASE(변경 전) · LOCAL(변경 후)을 위아래로. 칸과 같은 `CodeTextBlock`(문법 색 · 바뀐 글자 칠하기 · 줄 색), 긴 줄은 접어 끝까지,
한쪽에 없는 줄이면 `(이 줄 없음 …)`. `HomeViewModel.CaretLine` — 캐럿이 옮겨지거나 행이 다시 만들어지면(`UpdateSummaries`) 다시 알린다.
3. **Ctrl+W 로 닫으면 폴더 비교로**(사용자 결정: 폴더 비교에서 연 파일일 때만). `RequestPlainCompare(fromFolder:)` 로 표시를 실어 보내고
(폴더 비교 두 번 누르기 · 폴더 검색 결과), 파일 비교가 문서 탭(`DocTab.FromFolder`)에 붙인다. ◀ 이전 파일 · 다음 파일 ▶ 로 짝을 바꿔도(탭을 새로 만드는 `Replace`) 이어 받는다.
그 탭을 닫으면 `TabManager.Activate(ViewIds.Folder)`. 남은 탭은 그대로 다음 탭을 띄워 둔다.
"폴더 비교에서 파일을 열면 하단 탭도 파일 비교로" 는 플랫폼 `8158bbf`(배포본 `2026-10-01 13:37:46`)에서 이미 고쳤다 — 그 전 플랫폼인 PC 에서 보인 것.
4. **다크 모드 가독성** — 어두운 테마로 켜서 찍어 봤다. 가장 큰 것은 **폴더 비교 목록만 흰 바탕**: 플랫폼 테마가 ListBox · ListView · DataGrid 는 칠하지만
TreeView 는 칠하지 않아 WPF 기본(흰 바탕 · 검은 글자)으로 남았다 → 바탕 · 글자색을 테마에서 받고 항목(TreeViewItem) 글자색도 준다(줄 상태 색은 반투명이라 그대로 맞는다).
색 구분의 0 항목 흐리기 0.35 → 0.55(어두운 바탕에서 거의 묻혔다). 차이 줄 바탕색 대비는 두 테마가 비슷해(1.2~1.5) 그대로. 꺼진 단추 글자가 흐린 것은 플랫폼 테마 몫이라 손대지 않았다.
시험(이것만): `g27-five`(새, 어두운 테마) — Shift+휠 편집기 가로 0 → 20%(세로 그대로) · BASE 칸 위에서도 · 메모 0 → 5.7%, Ctrl+↓ 로 차이 2/2 → 1/2 · 아래 칸 BASE · LOCAL 이름표,
기본 짝은 Ctrl+W 뒤 파일 비교 그대로 · 폴더 비교에서 연 alpha 는 하단 탭 파일 비교(1) → Ctrl+W 뒤 폴더 비교(0), 사진으로 폴더 목록 어두운 바탕 · 아래 칸 확인. 실패 0. 기존 회귀는 돌리지 않았다.
## 전체 반영 · 자동 병합의 주석이 너무 많다 (2026-10-01, 사용자 요청 · 결정 · 완료) ## 전체 반영 · 자동 병합의 주석이 너무 많다 (2026-10-01, 사용자 요청 · 결정 · 완료)
[전체 반영] 은 반영할 줄마다 `Apply` 를 따로 불러 **바뀐 줄마다 머지 주석 한 줄**, 되돌리기도 줄마다 한 걸음이었다. [전체 반영] 은 반영할 줄마다 `Apply` 를 따로 불러 **바뀐 줄마다 머지 주석 한 줄**, 되돌리기도 줄마다 한 걸음이었다.

View File

@ -8,6 +8,7 @@
| 날짜 | 항목 | 커밋 | | 날짜 | 항목 | 커밋 |
|---|---|---| |---|---|---|
| 2026-10-01 | **Shift+휠 가로 · Ctrl+↓ 와 아래 "지금 줄" 칸 · Ctrl+W 로 폴더 비교 · 다크 모드(사용자 요청 · 결정)** — ① Shift+휠 = 가로(파일 비교는 가로 휠과 같은 길, 메모도) ② Ctrl+↓/↑ = 다음 · 이전 차이(대체 키) + 칸과 상태 줄 사이에 캐럿 행의 BASE(변경 전) · LOCAL(변경 후)을 위아래로(같은 `CodeTextBlock`, 긴 줄은 접음, `CaretLine`) ③ 폴더 비교에서 연 탭(`DocTab.FromFolder`, `RequestPlainCompare(fromFolder:)`, ◀ ▶ 로 바꿔도 이어 받음)을 닫으면 폴더 비교로 ④ 폴더 비교 목록이 어두운 테마에서도 흰 바탕 · 검은 글자 — 플랫폼 테마가 TreeView 는 안 칠함 → 테마 바탕 · 글자색, 색 구분 0 항목 흐리기 0.35 → 0.55. 하단 탭이 파일 비교로 안 바뀌던 것은 플랫폼 `8158bbf` 에서 이미 고침. 화면 `g27-five`(새, 어두운 테마) 실패 0. 기존 회귀는 돌리지 않음. 빌드 경고 0 · 오류 0 | 아래 |
| 2026-10-01 | **배포 묶음 스크립트를 런처 기준으로(사용자 요청)** — `scripts\make-bundle.ps1` 은 11차(셸을 켜던 때) 것이라 새 플랫폼의 `UbisamBase.Launcher.exe` · 그 설정 · `platform-version.json` 을 "까닭 미상" 으로 경고했다. 셋에 까닭을 적고, 런처와 런처 설정을 꼭 있어야 하는 파일로, 런처 설정의 바인딩 리디렉트 검사를 더함(없으면 켜자마자 죽는다 - 플랫폼 `publish-platform.ps1` 과 같은 검사). `배포묶음.txt` 는 "런처를 켠다" · 런처가 켤 때 하는 일 · 플랫폼 버전. 확인: `-NoBuild` 로 44개 · 경고 없음, 묶음에서 런처로 켜고 끔. 같은 날 알파판 ZIP(`배포\MergeHub_v0.12_alpha_20261001.zip`, 커밋 `b717f4e` · 플랫폼 `2026-10-01 13:37:46`)은 지난번 알파판 구성(Release 출력 + `UbisamPlatform\bin` + 설치안내)으로 따로 만들었다 | 아래 | | 2026-10-01 | **배포 묶음 스크립트를 런처 기준으로(사용자 요청)** — `scripts\make-bundle.ps1` 은 11차(셸을 켜던 때) 것이라 새 플랫폼의 `UbisamBase.Launcher.exe` · 그 설정 · `platform-version.json` 을 "까닭 미상" 으로 경고했다. 셋에 까닭을 적고, 런처와 런처 설정을 꼭 있어야 하는 파일로, 런처 설정의 바인딩 리디렉트 검사를 더함(없으면 켜자마자 죽는다 - 플랫폼 `publish-platform.ps1` 과 같은 검사). `배포묶음.txt` 는 "런처를 켠다" · 런처가 켤 때 하는 일 · 플랫폼 버전. 확인: `-NoBuild` 로 44개 · 경고 없음, 묶음에서 런처로 켜고 끔. 같은 날 알파판 ZIP(`배포\MergeHub_v0.12_alpha_20261001.zip`, 커밋 `b717f4e` · 플랫폼 `2026-10-01 13:37:46`)은 지난번 알파판 구성(Release 출력 + `UbisamPlatform\bin` + 설치안내)으로 따로 만들었다 | 아래 |
| 2026-10-01 | **전체 반영 · 자동 병합의 주석을 구간마다 한 줄로(사용자 요청 · 결정)** — 줄마다 머지 주석이 붙던 것. ① 두 파일 비교 [전체 반영]: 반영할 수 있는 줄이 이어진 구간마다 [구간 반영] 과 같게(`ApplyGroup`), 지우는 반영은 `삭제 N줄`, 모든 구간을 한 묶음으로 Undo 한 번 ② 3-Way [전체 반영] 끔 — 차이 나는 모든 줄을 공통 조상으로 바꾸던 동작(꺼져도 툴팁으로 까닭) ③ 3-Way [자동 병합]: 저쪽만 고친 줄 구간마다 주석 한 줄 · Undo 한 번(충돌 · 내 줄은 구간을 끊고 그대로). 화면 `g22-accept-all` 주석 7 → 3, `g23-three-way`(`--merge`) 5 → 2, 실패 0. 기존 회귀는 돌리지 않음. 빌드 경고 0 · 오류 0 | 아래 | | 2026-10-01 | **전체 반영 · 자동 병합의 주석을 구간마다 한 줄로(사용자 요청 · 결정)** — 줄마다 머지 주석이 붙던 것. ① 두 파일 비교 [전체 반영]: 반영할 수 있는 줄이 이어진 구간마다 [구간 반영] 과 같게(`ApplyGroup`), 지우는 반영은 `삭제 N줄`, 모든 구간을 한 묶음으로 Undo 한 번 ② 3-Way [전체 반영] 끔 — 차이 나는 모든 줄을 공통 조상으로 바꾸던 동작(꺼져도 툴팁으로 까닭) ③ 3-Way [자동 병합]: 저쪽만 고친 줄 구간마다 주석 한 줄 · Undo 한 번(충돌 · 내 줄은 구간을 끊고 그대로). 화면 `g22-accept-all` 주석 7 → 3, `g23-three-way`(`--merge`) 5 → 2, 실패 0. 기존 회귀는 돌리지 않음. 빌드 경고 0 · 오류 0 | 아래 |
| 2026-10-01 | **스크롤을 내리면 프로그램이 뻗던 것(사용자 보고)** — 어제 칸 맞추기(`f105bab`) 뒤 응답 없음. ① 사람이 BASE 를 굴려도 LOCAL 기준으로 되돌려 초당 32번 밀고 당김 → 손이 닿은 칸(휠 · 누름 · 키)을 기준으로, LOCAL 쪽일 때만 BASE 를 맞춤 ② 따라 굴러가는 도중에 재서 보정값이 휠 한 칸씩 6,619px 까지 쌓임 → 메아리 막기 중이면 큰 맞추기는 앱이 한가할 때 한 번, 못 굴리면 실제로 움직인 만큼만 ③ 칸 목록(그 칸에 있는 줄만)의 항목 번호를 정렬 행 번호로 써 엉뚱한 줄끼리 견줌 → 줄 객체로 오감 ④ 빠르게 굴리면 사람의 스크롤이 메아리로 버려져 따라가는 칸이 멈춤 → 멈춘 뒤 한 번 따라잡기, BASE 를 굴릴 때 보정값을 보이는 줄에서 다시 잼. 화면 `g20-scroll-end` · `g21-blocks-fast`(새) 응답 · 따라옴, `g7` 실패 0, `g17` 통과. 사용자 파일로는 확인 안 함 · 기존 회귀는 돌리지 않음. 빌드 경고 0 · 오류 0 | 아래 | | 2026-10-01 | **스크롤을 내리면 프로그램이 뻗던 것(사용자 보고)** — 어제 칸 맞추기(`f105bab`) 뒤 응답 없음. ① 사람이 BASE 를 굴려도 LOCAL 기준으로 되돌려 초당 32번 밀고 당김 → 손이 닿은 칸(휠 · 누름 · 키)을 기준으로, LOCAL 쪽일 때만 BASE 를 맞춤 ② 따라 굴러가는 도중에 재서 보정값이 휠 한 칸씩 6,619px 까지 쌓임 → 메아리 막기 중이면 큰 맞추기는 앱이 한가할 때 한 번, 못 굴리면 실제로 움직인 만큼만 ③ 칸 목록(그 칸에 있는 줄만)의 항목 번호를 정렬 행 번호로 써 엉뚱한 줄끼리 견줌 → 줄 객체로 오감 ④ 빠르게 굴리면 사람의 스크롤이 메아리로 버려져 따라가는 칸이 멈춤 → 멈춘 뒤 한 번 따라잡기, BASE 를 굴릴 때 보정값을 보이는 줄에서 다시 잼. 화면 `g20-scroll-end` · `g21-blocks-fast`(새) 응답 · 따라옴, `g7` 실패 0, `g17` 통과. 사용자 파일로는 확인 안 함 · 기존 회귀는 돌리지 않음. 빌드 경고 0 · 오류 0 | 아래 |

View File

@ -446,6 +446,7 @@
| 상태 | 항목 | | 상태 | 항목 |
|---|---| |---|---|
| **완료** | **Shift+휠 가로 · Ctrl+↓ 와 아래 "지금 줄" 칸 · Ctrl+W 로 폴더 비교 · 다크 모드**(사용자 요청 · 결정) — Shift+휠 가로(파일 비교 · 메모), Ctrl+↓/↑ 차이 이동 대체 키 + 캐럿 행의 BASE · LOCAL 을 아래에 위아래로, 폴더 비교에서 연 탭을 닫으면 폴더 비교로, 폴더 비교 목록이 어두운 테마에서도 흰 바탕이던 것(TreeView 는 플랫폼 테마가 안 칠함) · 0 항목 흐리기 완화. 화면 `g27-five` 실패 0 |
| **완료** | **전체 반영 · 자동 병합의 주석이 너무 많다**(사용자 요청 · 결정) — 두 파일 비교 [전체 반영] 을 구간 반영처럼(구간마다 주석 한 줄 · Undo 한 번), 3-Way [전체 반영] 은 끔(공통 조상으로 되돌리는 동작이었다), 3-Way [자동 병합] 도 구간마다 주석 한 줄 · Undo 한 번. 화면 `g22` 주석 7 → 3, `g23` 5 → 2, 실패 0 | | **완료** | **전체 반영 · 자동 병합의 주석이 너무 많다**(사용자 요청 · 결정) — 두 파일 비교 [전체 반영] 을 구간 반영처럼(구간마다 주석 한 줄 · Undo 한 번), 3-Way [전체 반영] 은 끔(공통 조상으로 되돌리는 동작이었다), 3-Way [자동 병합] 도 구간마다 주석 한 줄 · Undo 한 번. 화면 `g22` 주석 7 → 3, `g23` 5 → 2, 실패 0 |
| **완료** | **스크롤을 내리면 프로그램이 뻗는다**(사용자 보고, 어제 `f105bab` 뒤) — 응답 없음(AppHang). 원인 넷: BASE 를 굴려도 LOCAL 쪽으로 되돌려 밀고 당김 · 따라 굴러가는 도중에 재서 보정값이 6,619px 까지 쌓임 · 칸 목록 항목 번호를 정렬 행 번호로 씀(한쪽에만 있는 줄만큼 엉뚱한 줄끼리 견줌) · 빠르게 굴리면 사람의 스크롤이 버려짐. 손이 닿은 칸을 기준으로, 큰 맞추기는 한가할 때 한 번, 줄 객체로 오가기, 버려지면 멈춘 뒤 따라잡기. 화면 `g20` · `g21`(새) 응답, `g7` 실패 0, `g17` 통과 | | **완료** | **스크롤을 내리면 프로그램이 뻗는다**(사용자 보고, 어제 `f105bab` 뒤) — 응답 없음(AppHang). 원인 넷: BASE 를 굴려도 LOCAL 쪽으로 되돌려 밀고 당김 · 따라 굴러가는 도중에 재서 보정값이 6,619px 까지 쌓임 · 칸 목록 항목 번호를 정렬 행 번호로 씀(한쪽에만 있는 줄만큼 엉뚱한 줄끼리 견줌) · 빠르게 굴리면 사람의 스크롤이 버려짐. 손이 닿은 칸을 기준으로, 큰 맞추기는 한가할 때 한 번, 줄 객체로 오가기, 버려지면 멈춘 뒤 따라잡기. 화면 `g20` · `g21`(새) 응답, `g7` 실패 0, `g17` 통과 |
| **완료** | **끌 때 저장하지 않은 편집을 묻는다**(사용자 요청 · 결정) — 파일 비교의 모든 탭(Git 모드 · 뒤 탭 · 닫은 탭 포함, 메모 제외)에 편집이 있으면 [저장하고 종료] · [저장하지 않고 종료] · [취소]. 버리면 보관본도 비워 다음에 안 살아남. 플랫폼 `ICloseGuard` 를 그대로 씀(플랫폼 수정 없음). 화면 `g19-exit-confirm` 실패 0. Git 모드는 화면 시험 안 함 · 기존 회귀는 사용자 지시로 안 돌림 | | **완료** | **끌 때 저장하지 않은 편집을 묻는다**(사용자 요청 · 결정) — 파일 비교의 모든 탭(Git 모드 · 뒤 탭 · 닫은 탭 포함, 메모 제외)에 편집이 있으면 [저장하고 종료] · [저장하지 않고 종료] · [취소]. 버리면 보관본도 비워 다음에 안 살아남. 플랫폼 `ICloseGuard` 를 그대로 씀(플랫폼 수정 없음). 화면 `g19-exit-confirm` 실패 0. Git 모드는 화면 시험 안 함 · 기존 회귀는 사용자 지시로 안 돌림 |

View File

@ -68,6 +68,9 @@ namespace MergeHub.Services
/// <summary>화면에 없는 동안 맡아 둔 편집. 떠 있는 탭은 화면이 주인이라 비어 있다.</summary> /// <summary>화면에 없는 동안 맡아 둔 편집. 떠 있는 탭은 화면이 주인이라 비어 있다.</summary>
public DocTabState? Parked { get; set; } public DocTabState? Parked { get; set; }
/// <summary>폴더 비교에서 연 탭. 닫으면 폴더 비교로 돌아간다(2026-10-01 사용자 요청).</summary>
public bool FromFolder { get; set; }
private bool _isDirty; private bool _isDirty;
/// <summary>저장하지 않은 편집이 있는가. 탭 이름 앞 · 표시에 쓴다.</summary> /// <summary>저장하지 않은 편집이 있는가. 탭 이름 앞 · 표시에 쓴다.</summary>

View File

@ -653,11 +653,13 @@ namespace MergeHub.Services
/// 찾은 줄을 눈으로 다시 찾아야 한다. /// 찾은 줄을 눈으로 다시 찾아야 한다.
/// </param> /// </param>
/// <param name="lineIsLeft">그 번호가 왼쪽 파일 기준인지. 아니면 오른쪽 기준이다.</param> /// <param name="lineIsLeft">그 번호가 왼쪽 파일 기준인지. 아니면 오른쪽 기준이다.</param>
public void RequestPlainCompare(string leftFile, string rightFile, int line = 0, bool lineIsLeft = true) /// <param name="fromFolder">폴더 비교에서 열었는지. 파일 비교가 그 문서 탭에 적어 두고, 탭을 닫으면 폴더 비교로 돌려보낸다.</param>
public void RequestPlainCompare(string leftFile, string rightFile, int line = 0, bool lineIsLeft = true, bool fromFolder = false)
{ {
_pendingPlainCompare = (leftFile, rightFile); _pendingPlainCompare = (leftFile, rightFile);
RequestedLine = line; RequestedLine = line;
RequestedLineIsLeft = lineIsLeft; RequestedLineIsLeft = lineIsLeft;
RequestedFromFolder = fromFolder;
OnUi(DeliverPendingPlainCompare); OnUi(DeliverPendingPlainCompare);
} }
@ -695,6 +697,17 @@ namespace MergeHub.Services
/// <summary>그 번호가 왼쪽 파일 기준인지.</summary> /// <summary>그 번호가 왼쪽 파일 기준인지.</summary>
public bool RequestedLineIsLeft { get; private set; } public bool RequestedLineIsLeft { get; private set; }
/// <summary>넘긴 짝을 폴더 비교에서 열었는지.</summary>
public bool RequestedFromFolder { get; private set; }
/// <summary>폴더 비교에서 열었는지를 한 번 쓰고 지운다 - 다음에 다른 길로 연 짝까지 끌려가지 않게.</summary>
public bool TakeRequestedFromFolder()
{
var one = RequestedFromFolder;
RequestedFromFolder = false;
return one;
}
/// <summary> /// <summary>
/// 짚어 줄 자리를 한 번 쓰고 지운다. /// 짚어 줄 자리를 한 번 쓰고 지운다.
/// 지우지 않으면 그 다음에 연 파일까지 같은 줄로 끌려간다. /// 지우지 않으면 그 다음에 연 파일까지 같은 줄로 끌려간다.

View File

@ -114,6 +114,8 @@ namespace MergeHub.Services
new ShortcutEntry(ShortcutAction.PrevDiff, Key.Up, ModifierKeys.Alt, "차이 이동", "이전 차이"), new ShortcutEntry(ShortcutAction.PrevDiff, Key.Up, ModifierKeys.Alt, "차이 이동", "이전 차이"),
new ShortcutEntry(ShortcutAction.NextDiff, Key.F4, ModifierKeys.None, "차이 이동", "다음 차이 (대체 키)"), new ShortcutEntry(ShortcutAction.NextDiff, Key.F4, ModifierKeys.None, "차이 이동", "다음 차이 (대체 키)"),
new ShortcutEntry(ShortcutAction.PrevDiff, Key.F4, ModifierKeys.Shift, "차이 이동", "이전 차이 (대체 키)"), new ShortcutEntry(ShortcutAction.PrevDiff, Key.F4, ModifierKeys.Shift, "차이 이동", "이전 차이 (대체 키)"),
new ShortcutEntry(ShortcutAction.NextDiff, Key.Down, ModifierKeys.Control, "차이 이동", "다음 차이 (대체 키)"),
new ShortcutEntry(ShortcutAction.PrevDiff, Key.Up, ModifierKeys.Control, "차이 이동", "이전 차이 (대체 키)"),
new ShortcutEntry(ShortcutAction.FirstDiff, Key.Home, ModifierKeys.Alt, "차이 이동", "파일의 첫 차이"), new ShortcutEntry(ShortcutAction.FirstDiff, Key.Home, ModifierKeys.Alt, "차이 이동", "파일의 첫 차이"),
new ShortcutEntry(ShortcutAction.LastDiff, Key.End, ModifierKeys.Alt, "차이 이동", "파일의 마지막 차이"), new ShortcutEntry(ShortcutAction.LastDiff, Key.End, ModifierKeys.Alt, "차이 이동", "파일의 마지막 차이"),
new ShortcutEntry(ShortcutAction.FirstDiff, Key.Home, ModifierKeys.Control, "차이 이동", "첫 차이 (대체 키)"), new ShortcutEntry(ShortcutAction.FirstDiff, Key.Home, ModifierKeys.Control, "차이 이동", "첫 차이 (대체 키)"),

View File

@ -650,7 +650,7 @@ namespace MergeHub.ViewModels
// 오른쪽에서만 나온 줄이면 오른쪽 파일 기준 번호다. // 오른쪽에서만 나온 줄이면 오른쪽 파일 기준 번호다.
_workspace.RequestPlainCompare(hit.LeftPath, hit.RightPath, hit.LineNumber, _workspace.RequestPlainCompare(hit.LeftPath, hit.RightPath, hit.LineNumber,
lineIsLeft: hit.Side != SearchSide.Right); lineIsLeft: hit.Side != SearchSide.Right, fromFolder: true);
UbisamBase.Core.Modules.TabManager.Activate(ViewIds.Home); UbisamBase.Core.Modules.TabManager.Activate(ViewIds.Home);
} }
@ -762,7 +762,7 @@ namespace MergeHub.ViewModels
// 요청을 먼저 걸고 탭을 연다. 파일 비교 화면이 아직 없으면 그 화면이 만들어지면서 // 요청을 먼저 걸고 탭을 연다. 파일 비교 화면이 아직 없으면 그 화면이 만들어지면서
// 남아 있는 요청을 직접 집어 간다(GitWorkspace.RequestPlainCompare 설명 참고). // 남아 있는 요청을 직접 집어 간다(GitWorkspace.RequestPlainCompare 설명 참고).
_workspace.RequestPlainCompare(node.LeftPath, node.RightPath); _workspace.RequestPlainCompare(node.LeftPath, node.RightPath, fromFolder: true);
UbisamBase.Core.Modules.TabManager.Activate(ViewIds.Home); UbisamBase.Core.Modules.TabManager.Activate(ViewIds.Home);
} }

View File

@ -319,6 +319,10 @@ namespace MergeHub.ViewModels
SyncTabItems(); SyncTabItems();
SaveSession(); // 12차 E-1 - 닫아도 글은 남는다. 그 자리에서 적어 둔다. SaveSession(); // 12차 E-1 - 닫아도 글은 남는다. 그 자리에서 적어 둔다.
// 폴더 비교에서 연 파일을 닫았으면 폴더 비교로 돌아간다(2026-10-01 사용자 요청 - 목록에서 하나씩 열어 보고 닫는 손버릇).
// 남은 탭은 그대로 다음 탭을 띄워 둔다 - 파일 비교 탭으로 돌아오면 거기서 이어진다.
if (tab.FromFolder) UbisamBase.Core.Modules.TabManager.Activate(ViewIds.Folder);
if (next is null || ReferenceEquals(next, tab)) return; if (next is null || ReferenceEquals(next, tab)) return;
_resumeTab = next; _resumeTab = next;

View File

@ -133,7 +133,12 @@ namespace MergeHub.ViewModels
// 탭을 먼저 잡아 둔다. 아래 LoadPlainFilesAsync 가 다 읽은 뒤 맡겨 둔 편집을 되살린다. // 탭을 먼저 잡아 둔다. 아래 LoadPlainFilesAsync 가 다 읽은 뒤 맡겨 둔 편집을 되살린다.
var replaceTab = _replaceTab; var replaceTab = _replaceTab;
_replaceTab = false; _replaceTab = false;
// 폴더 비교에서 연 짝이면 탭에 적어 둔다 - 닫으면 폴더 비교로 돌아간다. ◀ 이전 파일 · 다음 파일 ▶ 은 폴더 목록을
// 훑는 것이라 탭을 새로 만들어도(Replace) 그 표시를 이어 받는다.
var fromFolder = _workspace.TakeRequestedFromFolder() || (replaceTab && Tabs.Current?.FromFolder == true);
OpenPairInTab(leftFile, rightFile, replaceTab); OpenPairInTab(leftFile, rightFile, replaceTab);
if (fromFolder && Tabs.Current is { } openedTab) openedTab.FromFolder = true;
// 3-Way 를 끄되 그 때문에 본문을 다시 읽지는 않게 한다 - 그 재로드가 작업 게이트를 // 3-Way 를 끄되 그 때문에 본문을 다시 읽지는 않게 한다 - 그 재로드가 작업 게이트를
// 먼저 잡으면 바로 아래 파일 비교가 "다른 작업 진행 중"으로 건너뛰어진다. // 먼저 잡으면 바로 아래 파일 비교가 "다른 작업 진행 중"으로 건너뛰어진다.
@ -3668,8 +3673,15 @@ namespace MergeHub.ViewModels
AcceptBlockToBaseCommand.NotifyCanExecuteChanged(); // 12차 G-2 - 왼쪽으로 보내는 짝 AcceptBlockToBaseCommand.NotifyCanExecuteChanged(); // 12차 G-2 - 왼쪽으로 보내는 짝
AcceptMemberCommand.NotifyCanExecuteChanged(); AcceptMemberCommand.NotifyCanExecuteChanged();
OnPropertyChanged(nameof(MemberHint)); OnPropertyChanged(nameof(MemberHint));
OnPropertyChanged(nameof(CaretLine));
} }
/// <summary>
/// 캐럿이 있는 행. 화면 아래 "지금 줄" 칸이 이 행의 BASE · LOCAL 을 위아래로 보여 준다(2026-10-01 사용자 요청 -
/// 다음 차이로 옮겨 가면 무엇이 바뀌었는지 아래에서 한 줄씩 견줘 보고 싶다). 행이 다시 만들어지면 <see cref="UpdateSummaries"/> 가 다시 알린다.
/// </summary>
public MergeLine? CaretLine => CaretRow >= 0 && CaretRow < Lines.Count ? Lines[CaretRow] : null;
/// <summary>상태바에 띄우는 "차이 3 / 15".</summary> /// <summary>상태바에 띄우는 "차이 3 / 15".</summary>
[ObservableProperty] private string _diffPosition = "차이 -"; [ObservableProperty] private string _diffPosition = "차이 -";
@ -4233,6 +4245,8 @@ namespace MergeHub.ViewModels
private void UpdateSummaries() private void UpdateSummaries()
{ {
OnPropertyChanged(nameof(CaretLine)); // 행이 다시 만들어지면 아래 "지금 줄" 칸이 옛 행을 붙들지 않게
// 줄을 고르거나 머지 주석이 끼어들면 결과 줄 수가 바뀐다. 그릴 때마다 번호를 다시 매긴다(8차 B-3). // 줄을 고르거나 머지 주석이 끼어들면 결과 줄 수가 바뀐다. 그릴 때마다 번호를 다시 매긴다(8차 B-3).
RenumberMerged(Lines); RenumberMerged(Lines);
RenumberBase(Lines); // 왼쪽도 마찬가지다(12차 B-1 로 왼쪽이 바뀔 수 있게 됐다) RenumberBase(Lines); // 왼쪽도 마찬가지다(12차 B-1 로 왼쪽이 바뀔 수 있게 됐다)

View File

@ -12,8 +12,11 @@ namespace MergeHub.Views.Controls
/// </summary> /// </summary>
public sealed class ZeroToDimConverter : IValueConverter public sealed class ZeroToDimConverter : IValueConverter
{ {
/// <summary>0 일 때의 흐리기. 읽히기는 하되 눈이 먼저 가지는 않는 정도.</summary> /// <summary>
private const double Dim = 0.35; /// 0 일 때의 흐리기. 읽히기는 하되 눈이 먼저 가지는 않는 정도.
/// 0.35 는 어두운 테마에서 바탕에 거의 묻혔다(2026-10-01 사용자 보고) - 두 테마 모두 읽히는 0.55 로.
/// </summary>
private const double Dim = 0.55;
public object Convert(object value, Type targetType, object parameter, CultureInfo culture) => public object Convert(object value, Type targetType, object parameter, CultureInfo culture) =>
value is int count && count == 0 ? Dim : 1.0; value is int count && count == 0 ? Dim : 1.0;

View File

@ -107,6 +107,8 @@
<Setter Property="IsSelected" Value="{Binding IsSelected, Mode=TwoWay}"/> <Setter Property="IsSelected" Value="{Binding IsSelected, Mode=TwoWay}"/>
<!-- 줄 배경이 오른쪽 끝까지 가도록 항목을 가로로 늘린다. --> <!-- 줄 배경이 오른쪽 끝까지 가도록 항목을 가로로 늘린다. -->
<Setter Property="HorizontalContentAlignment" Value="Stretch"/> <Setter Property="HorizontalContentAlignment" Value="Stretch"/>
<!-- WPF 기본 TreeViewItem 은 글자색을 시스템 검정으로 박아 둔다 - 테마 글자색으로(어두운 테마). -->
<Setter Property="Foreground" Value="{DynamicResource Ubisam.Brush.ContentForeground}"/>
</Style> </Style>
</ResourceDictionary> </ResourceDictionary>
</UserControl.Resources> </UserControl.Resources>
@ -366,8 +368,12 @@
<RowDefinition Height="Auto"/> <RowDefinition Height="Auto"/>
</Grid.RowDefinitions> </Grid.RowDefinitions>
<!-- 바탕 · 글자색을 테마에서 받는다(2026-10-01 사용자 보고 "다크 모드에서 잘 안 보인다"). 플랫폼 테마는 ListBox · ListView ·
DataGrid 는 칠하지만 TreeView 는 칠하지 않아, 어두운 테마에서도 이 목록만 WPF 기본(흰 바탕 · 검은 글자)으로 남았다. -->
<TreeView Grid.Row="0" x:Name="Tree" BorderThickness="1" <TreeView Grid.Row="0" x:Name="Tree" BorderThickness="1"
BorderBrush="{DynamicResource Ubisam.Brush.Input.Border}" BorderBrush="{DynamicResource Ubisam.Brush.Input.Border}"
Background="{DynamicResource Ubisam.Brush.ContentBackground}"
Foreground="{DynamicResource Ubisam.Brush.ContentForeground}"
ItemsSource="{Binding Nodes}" ItemsSource="{Binding Nodes}"
ItemTemplate="{StaticResource NodeTemplate}" ItemTemplate="{StaticResource NodeTemplate}"
ItemContainerStyle="{StaticResource NodeItemStyle}" ItemContainerStyle="{StaticResource NodeItemStyle}"

View File

@ -555,6 +555,8 @@
<RowDefinition Height="Auto"/> <RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/> <RowDefinition Height="Auto"/>
<RowDefinition Height="*"/> <RowDefinition Height="*"/>
<!-- 지금 줄 전/후(2026-10-01). 캐럿이 있는 행의 BASE · LOCAL 을 위아래로. -->
<RowDefinition Height="Auto"/>
<!-- 상태바. 패널 안이 아니라 창 아래 한 줄로 둔다(4차 C-2). --> <!-- 상태바. 패널 안이 아니라 창 아래 한 줄로 둔다(4차 C-2). -->
<RowDefinition Height="Auto"/> <RowDefinition Height="Auto"/>
</Grid.RowDefinitions> </Grid.RowDefinitions>
@ -1991,6 +1993,94 @@
</Border> </Border>
</Grid> </Grid>
<!--
③-1 지금 줄 전/후 (2026-10-01 사용자 요청).
다음 차이(Alt+↓ · Ctrl+↓)로 옮겨 가면 그 행의 BASE(변경 전) · LOCAL(변경 후)을 위아래 두 줄로 견줘 보인다 -
나란한 두 칸에서는 긴 줄의 바뀐 곳이 옆으로 멀리 떨어져 눈이 오가야 했다(WinMerge 의 아래 비교 칸과 같은 뜻).
칸과 같은 그리기(CodeTextBlock: 문법 색 · 바뀐 글자 칠하기 · 줄 색)를 쓴다. 긴 줄은 접어 끝까지 보인다.
-->
<Border Grid.Row="3" Padding="8,3" BorderThickness="1,1,1,0" Margin="0,2,0,0"
Background="{DynamicResource Ubisam.Brush.Sidebar.Background}"
BorderBrush="{DynamicResource Ubisam.Brush.Input.Border}"
DataContext="{Binding CaretLine}">
<Border.Style>
<Style TargetType="Border">
<Style.Triggers>
<DataTrigger Binding="{Binding}" Value="{x:Null}">
<Setter Property="Visibility" Value="Collapsed"/>
</DataTrigger>
</Style.Triggers>
</Style>
</Border.Style>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<TextBlock Grid.Row="0" Grid.Column="0" Text="BASE" Style="{StaticResource PanelTitle}" FontSize="11"
VerticalAlignment="Top" Margin="0,1,8,0" ToolTip="변경 전 (왼쪽 칸)"/>
<TextBlock Grid.Row="0" Grid.Column="1" Text="{Binding BaseNumberText}" Style="{StaticResource Mono}"
Foreground="{DynamicResource Ubisam.Brush.Muted}" MinWidth="36" TextAlignment="Right"
VerticalAlignment="Top" Margin="0,0,8,0"/>
<Border Grid.Row="0" Grid.Column="2" Padding="2,0"
Background="{Binding Highlight, Converter={StaticResource HighlightBrush}}">
<Grid>
<ctl:CodeTextBlock Code="{Binding BaseText}" Words="{Binding BaseWords}"
InBlockComment="{Binding BaseInComment}"
CodeLanguage="{Binding DataContext.Language, RelativeSource={RelativeSource AncestorType=UserControl}}"
HighlightSyntax="{Binding DataContext.SyntaxHighlight, RelativeSource={RelativeSource AncestorType=UserControl}}"
TextWrapping="Wrap" FontFamily="Consolas" FontSize="{DynamicResource CodeFontSize}"/>
<TextBlock Text="(이 줄 없음 - LOCAL 에만 있다)" FontSize="11">
<TextBlock.Style>
<Style TargetType="TextBlock" BasedOn="{StaticResource Muted}">
<Setter Property="Visibility" Value="Collapsed"/>
<Style.Triggers>
<DataTrigger Binding="{Binding HasBase}" Value="False">
<Setter Property="Visibility" Value="Visible"/>
</DataTrigger>
</Style.Triggers>
</Style>
</TextBlock.Style>
</TextBlock>
</Grid>
</Border>
<TextBlock Grid.Row="1" Grid.Column="0" Text="LOCAL" Style="{StaticResource PanelTitle}" FontSize="11"
VerticalAlignment="Top" Margin="0,3,8,0" ToolTip="변경 후 (오른쪽 칸 · 편집하는 글)"/>
<TextBlock Grid.Row="1" Grid.Column="1" Text="{Binding LocalNumberText}" Style="{StaticResource Mono}"
Foreground="{DynamicResource Ubisam.Brush.Muted}" MinWidth="36" TextAlignment="Right"
VerticalAlignment="Top" Margin="0,2,8,0"/>
<Border Grid.Row="1" Grid.Column="2" Padding="2,0" Margin="0,2,0,0"
Background="{Binding Highlight, Converter={StaticResource HighlightBrush}}">
<Grid>
<ctl:CodeTextBlock Code="{Binding LocalText}" Words="{Binding LocalWords}"
InBlockComment="{Binding LocalInComment}"
CodeLanguage="{Binding DataContext.Language, RelativeSource={RelativeSource AncestorType=UserControl}}"
HighlightSyntax="{Binding DataContext.SyntaxHighlight, RelativeSource={RelativeSource AncestorType=UserControl}}"
TextWrapping="Wrap" FontFamily="Consolas" FontSize="{DynamicResource CodeFontSize}"/>
<TextBlock Text="(이 줄 없음 - BASE 에만 있다)" FontSize="11">
<TextBlock.Style>
<Style TargetType="TextBlock" BasedOn="{StaticResource Muted}">
<Setter Property="Visibility" Value="Collapsed"/>
<Style.Triggers>
<DataTrigger Binding="{Binding HasLocal}" Value="False">
<Setter Property="Visibility" Value="Visible"/>
</DataTrigger>
</Style.Triggers>
</Style>
</TextBlock.Style>
</TextBlock>
</Grid>
</Border>
</Grid>
</Border>
<!-- <!--
④ 상태바 — 창 아래 한 줄. ④ 상태바 — 창 아래 한 줄.
@ -2002,7 +2092,7 @@
<b>자리 규칙</b> - 왼쪽은 지금 어디에 있는지, 가운데는 얼마나 남았는지, 오른쪽은 이 파일이 무엇인지. <b>자리 규칙</b> - 왼쪽은 지금 어디에 있는지, 가운데는 얼마나 남았는지, 오른쪽은 이 파일이 무엇인지.
패널 밖으로 나오면서 두 패널의 코드 영역 높이가 저절로 같아진다. 패널 밖으로 나오면서 두 패널의 코드 영역 높이가 저절로 같아진다.
--> -->
<Border Grid.Row="3" Padding="8,4" BorderThickness="1,1,1,1" Margin="0,2,0,0" <Border Grid.Row="4" Padding="8,4" BorderThickness="1,1,1,1" Margin="0,2,0,0"
Background="{DynamicResource Ubisam.Brush.Sidebar.Background}" Background="{DynamicResource Ubisam.Brush.Sidebar.Background}"
BorderBrush="{DynamicResource Ubisam.Brush.Input.Border}"> BorderBrush="{DynamicResource Ubisam.Brush.Input.Border}">
<Grid> <Grid>

View File

@ -241,6 +241,15 @@ namespace MergeHub.Views
// Ctrl+휠로 코드 글자 크기(11차 F-5). 휠 한 칸에 1씩. 목록 · 편집기의 스크롤보다 먼저 가로챈다. // Ctrl+휠로 코드 글자 크기(11차 F-5). 휠 한 칸에 1씩. 목록 · 편집기의 스크롤보다 먼저 가로챈다.
PreviewMouseWheel += (_, e) => PreviewMouseWheel += (_, e) =>
{ {
// Shift+휠은 가로로(2026-10-01 사용자 요청 - VS · 브라우저와 같은 손버릇). 가로 휠(틸트)과 같은 길로 보낸다 -
// 마우스가 놓인 칸이 움직이고 나머지는 지금처럼 따라온다. 휠을 아래로 굴리면 오른쪽으로 간다.
if (Keyboard.Modifiers == ModifierKeys.Shift)
{
var step = -e.Delta / 120.0 * 48;
if (ScrollSourcePanes(step) || ScrollEditor(step)) e.Handled = true;
return;
}
if (Keyboard.Modifiers != ModifierKeys.Control || DataContext is not HomeViewModel vm) return; if (Keyboard.Modifiers != ModifierKeys.Control || DataContext is not HomeViewModel vm) return;
var command = e.Delta > 0 ? vm.FontBiggerCommand : vm.FontSmallerCommand; var command = e.Delta > 0 ? vm.FontBiggerCommand : vm.FontSmallerCommand;
_scrollDriver = MergeLine.TextPane.Local; // 확대 뒤에는 BASE 를 LOCAL 에 맞춘다 _scrollDriver = MergeLine.TextPane.Local; // 확대 뒤에는 BASE 를 LOCAL 에 맞춘다

View File

@ -68,6 +68,14 @@ namespace MergeHub.Views
// Ctrl+휠로 글자 크기(파일 비교 F-5 와 같은 손버릇). // Ctrl+휠로 글자 크기(파일 비교 F-5 와 같은 손버릇).
PreviewMouseWheel += (_, e) => PreviewMouseWheel += (_, e) =>
{ {
// Shift+휠은 가로로(2026-10-01 사용자 요청, 파일 비교와 같게). 휠을 아래로 굴리면 오른쪽으로.
if (Keyboard.Modifiers == ModifierKeys.Shift)
{
Editor.ScrollToHorizontalOffset(Math.Max(0, Editor.HorizontalOffset - e.Delta / 120.0 * 48));
e.Handled = true;
return;
}
if (Keyboard.Modifiers != ModifierKeys.Control) return; if (Keyboard.Modifiers != ModifierKeys.Control) return;
var size = Editor.FontSize + (e.Delta > 0 ? 1 : -1); var size = Editor.FontSize + (e.Delta > 0 ? 1 : -1);
Editor.FontSize = Math.Max(8, Math.Min(28, size)); Editor.FontSize = Math.Max(8, Math.Min(28, size));