스크롤을 내리면 프로그램이 뻗던 것 (사용자 보고)
f105bab(칸 어긋남 고침) 뒤부터 스크롤을 내리면 응답 없음(AppHang)으로 닫혔다. 같은 재료로 BASE 칸을 휠로 끝까지 내려 재현했고, 임시 기록으로 원인 넷을 찾았다 (기록 코드는 뺐다). 1. 사람이 BASE 를 굴려도 BASE 맞추기가 LOCAL 기준으로 되돌렸다 - 굴린 만큼(48px)을 끌어당기고 보정값이 오르내려 초당 32번 밀고 당겼다. 손이 닿은 칸(휠 · 누름 · 키)을 기준(_scrollDriver)으로 삼고 LOCAL 쪽일 때만 BASE 를 맞춘다. 스크롤 이벤트로 정하면 글자 크기를 바꿀 때 목록이 저절로 내는 스크롤까지 BASE 로 읽었다. 확대 · 다음 차이 · 줄 이동은 LOCAL 기준. 2. 따라 굴러가는 도중에 재서 휠 한 칸(48px)씩 보정값이 6,619px 까지 쌓였다. 메아리 막기 중이면 큰 맞추기는 앱이 한가할 때(ApplicationIdle) 한 번 하고, 못 굴리는 끝에서는 실제로 움직인 만큼만 보정한다. 3. 칸 목록(그 칸에 있는 줄만 거른 PaneView)의 항목 번호를 정렬 행 번호로 써서 한쪽에만 있는 줄만큼 엉뚱한 줄끼리 견줬다(10줄 -> 157px, 마지막 차이에서 칸이 228px 밀림). 줄 객체로 오간다(RowOf · ContainerOfRow). 4. 빠르게 굴리면 사람의 스크롤까지 메아리로 버려져 따라가는 칸이 멈췄다(BASE 784 · LOCAL 555). 버려지면 표시만 남기고 멈춘 뒤 한 번 따라잡는다. BASE 를 굴릴 때는 보정값을 보이는 줄에서 다시 잰다(MeasureBias). 시험: 빌드 경고 0 · 오류 0. 화면 g20-scroll-end(새, 보통 · 빠르게) 모든 단계 응답 · BASE · LOCAL 가운데 줄 한 줄 안, g21-blocks-fast(새, 한쪽에만 있는 묶음이 많은 재료) 응답 · 따라옴(가운데가 묶음에 걸리면 10줄 남짓 벗어날 때 있음), g7-verify(Alt+↓) 실패 0, g17(엔터 · 확대 · 내리기) 통과. 사용자 파일로는 확인하지 않았다. 기존 회귀는 사용자 지시로 돌리지 않았다. Agent: main (claude-opus-5-5) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
4d66abb7fa
commit
04b76df807
@ -2,6 +2,28 @@
|
||||
|
||||
지시서: [../작업지시/12차.md](../작업지시/12차.md)
|
||||
|
||||
## 스크롤을 내리면 프로그램이 뻗는다 (2026-10-01, 사용자 보고 · 완료)
|
||||
|
||||
어제 커밋(`f105bab` 칸 어긋남 고침) 뒤부터 생겼다. Windows 이벤트에 `UbisamBase.Launcher.exe` **응답 없음(AppHang)** 두 번(13:44 · 13:45) — 죽은 것이 아니라
|
||||
화면 실이 되풀이에 갇혔다. 같은 재료로 BASE 칸을 휠로 끝까지 내리자 재현(9초 만에 꺼짐). 임시 기록으로 찾은 원인 넷(기록 코드는 뺐다):
|
||||
|
||||
1. **사람이 BASE 를 굴려도 BASE 를 LOCAL 쪽으로 되돌렸다.** BASE 맞추기는 늘 LOCAL 을 기준으로 봐서, BASE 를 휠로 굴린 만큼(48px)을 매번 끌어당겼다.
|
||||
그 사이 보정값이 오르내리며(694 → 22) 다른 칸을 다시 움직여 초당 32번 밀고 당겼다. → **누가 굴렸는지**를 손이 닿은 칸(휠 · 누름 · 키)으로 정하고,
|
||||
LOCAL 쪽일 때만 BASE 를 맞춘다. 스크롤 이벤트로 정하면 글자 크기를 바꿀 때 목록이 저절로 내는 스크롤까지 "BASE 를 굴렸다" 로 읽어 확대 뒤 BASE 를 안 맞췄다(14초 동안 0번).
|
||||
확대 · 다음 차이 · 줄 이동(프로그램이 편집기를 옮김)은 LOCAL 기준.
|
||||
2. **따라 굴러가는 도중에 쟀다.** 다른 칸에 굴리라고 한 자리는 다음 배치에야 들어가는데, 배치마다 도는 맞추기가 그 사이에 재서 휠 한 칸(48px)을 늘 어긋남으로 보고
|
||||
보정값에 쌓았다(6,619px). 그 엉터리 보정값으로 BASE 를 기준 삼으면 편집기 · LOCAL 이 수십~백 줄 엉뚱한 곳으로 갔다. → 메아리 막기 중이면 크게 굴리는 맞추기를 하지 않고
|
||||
**앱이 한가할 때(ApplicationIdle) 한 번** 맞춘다 — 한가할 때 도는 일은 입력을 밀어내지 않는다. 못 굴리는 끝에서는 보정값 · 다시 그리기를 건드리지 않고 **실제로 움직인 만큼만** 보정한다.
|
||||
3. **목록 항목 번호를 정렬 행 번호로 썼다.** 칸 목록은 그 칸에 있는 줄만 거른 목록(`PaneView`, 11차 F-10)이라, 한쪽에만 있는 줄이 앞에 있으면 그만큼 밀린다.
|
||||
어제 넣은 코드가 `ContainerFromIndex(row)` · `IndexFromContainer` 로 둘을 같게 써서 엉뚱한 줄끼리 견줬다(한쪽에만 있는 줄 10개 → 157px, 마지막 차이에서 칸이 통째로 228px 밀림).
|
||||
→ 줄 객체로 오간다(`RowOf` · `ContainerOfRow`). 실제 파일처럼 한쪽에만 있는 줄이 많으면 이 어긋남이 커져 되풀이가 길어졌을 것이다.
|
||||
4. **사람이 빠르게 굴리면 스크롤이 버려졌다.** 메아리 막기 중에 온 스크롤은 버리는데, 빠르게 굴리면 사람의 다음 휠까지 버려져 따라가는 칸이 중간에 멈췄다
|
||||
(BASE 784 · LOCAL 555, 편집기를 굴리면 BASE 37 · LOCAL 473). → 사람이 굴리는 칸의 스크롤이 버려지면 표시만 남기고 멈춘 뒤 한 번 따라잡는다.
|
||||
따라잡기로 다른 칸이 움직여 생기는 스크롤은 사람이 굴린 칸이 아니라 표시하지 않는다 — 되풀이가 없다. BASE 를 굴릴 때는 보정값을 **지금 보이는 줄에서 다시 잰다**(`MeasureBias`).
|
||||
|
||||
시험(사용자 지시로 이것만): `g20-scroll-end`(새, 보통 · 빠르게 — 편집기 · BASE 휠로 끝까지 · 처음까지) 모든 단계 **응답**, BASE · LOCAL 가운데 줄 한 줄 안.
|
||||
`g21-blocks-fast`(새, 60줄마다 오른쪽에서 5줄 빼고 7줄 넣은 재료) 응답 · 따라옴 — 화면 가운데가 한쪽에만 있는 묶음에 걸리면 그 자리는 일부러 맞추지 않아 10줄 남짓 벗어날 때가 있다.
|
||||
`g7-verify`(Alt+↓) 실패 0, `g17`(엔터 · 확대 · 내리기) 통과(남은 ✗ 는 대본 한계). **사용자 파일로는 확인하지 않았다** — 실제 저장소를 읽지 않는다. 기존 회귀는 돌리지 않았다.
|
||||
## 끌 때 저장하지 않은 편집을 묻는다 (2026-09-30 밤, 사용자 요청 · 결정 · 완료)
|
||||
|
||||
사용자 보고: 편집이 있는데 하단 [종료] 를 누르면 그냥 꺼진다. 편집은 보관본(E-1)이 다음에 되살렸지만(g16 로 확인), 사용자 판단은
|
||||
|
||||
@ -8,6 +8,7 @@
|
||||
|
||||
| 날짜 | 항목 | 커밋 |
|
||||
|---|---|---|
|
||||
| 2026-10-01 | **스크롤을 내리면 프로그램이 뻗던 것(사용자 보고)** — 어제 칸 맞추기(`f105bab`) 뒤 응답 없음. ① 사람이 BASE 를 굴려도 LOCAL 기준으로 되돌려 초당 32번 밀고 당김 → 손이 닿은 칸(휠 · 누름 · 키)을 기준으로, LOCAL 쪽일 때만 BASE 를 맞춤 ② 따라 굴러가는 도중에 재서 보정값이 휠 한 칸씩 6,619px 까지 쌓임 → 메아리 막기 중이면 큰 맞추기는 앱이 한가할 때 한 번, 못 굴리면 실제로 움직인 만큼만 ③ 칸 목록(그 칸에 있는 줄만)의 항목 번호를 정렬 행 번호로 써 엉뚱한 줄끼리 견줌 → 줄 객체로 오감 ④ 빠르게 굴리면 사람의 스크롤이 메아리로 버려져 따라가는 칸이 멈춤 → 멈춘 뒤 한 번 따라잡기, BASE 를 굴릴 때 보정값을 보이는 줄에서 다시 잼. 화면 `g20-scroll-end` · `g21-blocks-fast`(새) 응답 · 따라옴, `g7` 실패 0, `g17` 통과. 사용자 파일로는 확인 안 함 · 기존 회귀는 돌리지 않음. 빌드 경고 0 · 오류 0 | 아래 |
|
||||
| 2026-09-30 | **끌 때 저장하지 않은 편집을 묻는다(사용자 요청 · 결정)** — 파일 비교의 모든 탭(지금 화면 · Git 모드 · 뒤 탭 · 닫은 탭, 메모 제외)에 편집이 있으면 [저장하고 종료] · [저장하지 않고 종료] · [취소]. 저장은 기존 길(`SaveWorkingCopy` · `WritePlainFile`), 하나라도 실패하면 끄지 않음. 버리면 걸려 있던 보관 쓰기를 기다린 뒤 보관본을 비워 다음에 안 살아남. 플랫폼 `ICloseGuard` 사용(수정 없음) — 곧바로 답하면 셸의 `Close()` 가 Closing 안에서 불려 창이 안 꺼져 `await Task.Yield()` 로 비켜섬. 화면 `g19-exit-confirm` 다섯 경우 실패 0(Git 모드는 화면 시험 안 함). **기존 회귀는 사용자 지시로 돌리지 않음.** 빌드 경고 0 · 오류 0 | 아래 |
|
||||
| 2026-09-30 | **엔터 꾹 · 확대 · 쭉 내리기에서 칸 어긋남(사용자 보고)** — ① 엔터를 꾹 누르면 캐럿 줄과 LOCAL 칸 줄이 벌어졌다 → 캐럿은 글에서 센 줄 자리로, 메아리 막기 중에도 LOCAL 칸은 편집기를 따라감. ② Ctrl+휠 확대 뒤 칸 위가 비고 내리면 BASE 가 다른 줄: 가상화 목록 높이 어림 차이(편집기 28,896 · 목록 27,940)를 네 칸 공통 이동값으로 메우다 467px 까지 커졌다 → 두 줄 넘으면 칸을 실제로 굴리고 칸별 보정으로 기억, BASE · REMOTE 는 같은 줄끼리 맞춤(한쪽에만 있는 묶음 안은 제외 — 짝을 억지로 불러 초당 600번 되풀이하던 것을 기록으로 잡음). ③ 편집기 `GetFirstVisibleLineIndex`(886 → 1033) · 확대 직후 `GetRectFromCharacterIndex`(1,892px 틀림)가 틀려 → 줄 높이 고정이면 계산. 화면 `g17-hold-enter` 통과(남은 ✗ 는 대본 한계 · 꾹 누름 흉내에서 Windows 가 반복 키를 합치는 것 — 고치기 전과 같음). **기존 회귀는 사용자 지시로 돌리지 않음.** 빌드 경고 0 · 오류 0 | 아래 |
|
||||
| 2026-09-30 | **머지 주석 · 타이핑 속도 · 탭 알림(사용자 요청 · 보고)** — ① Accept 뒤 캐럿이 새 주석 줄에 서서 Alt+↓ 를 두 번 눌러야 했다 → 주석을 끼우기 직전 캐럿 행을 기억해 글을 갈아 끼운 뒤 그 행으로(글을 막 갈아 끼운 TextBox 의 줄 정보가 옛 글 기준이라는 것을 기록으로 확인 → 줄 자리는 글에서 직접 셈). ② 줄을 지우는 반영에도 주석 — 한 줄 `삭제`, 여러 줄 `삭제 N줄`(사용자 선택, 왼쪽 · 3-Way · 구간 · 메서드 모두). ③ 색 구분 "줄 안에서 바뀐 부분" 에 줄 수. ④ 타이핑: 1,806줄에서 재 보니 엔터마다 재정렬 0.4초의 대부분이 목록에 1,800줄을 한 줄씩 넣는 알림(파일 비교 자체는 0.012초) → `MergeLineCollection.ReplaceAll`(알림 한 번) + `Patch`(번호 말고 같은 줄은 기존 객체를 두고 번호만, 가운데만 빼고 넣기) → 엔터 한 번 약 0.55초 → 약 0.05초(뒤 다시 칠한 줄 290 → 0~8). ⑤ 탭이 20개를 넘어 조용히 닫히면 알림. 하네스 `NoteTest` 106 · `PatchTest`(새) 16, 화면 `g12-note` · `g14-inline` · `g15-typing-tabs` 통과. **기존 회귀는 사용자 지시로 돌리지 않음.** 빌드 경고 0 · 오류 0 | 아래 |
|
||||
|
||||
@ -446,6 +446,7 @@
|
||||
|
||||
| 상태 | 항목 |
|
||||
|---|---|
|
||||
| **완료** | **스크롤을 내리면 프로그램이 뻗는다**(사용자 보고, 어제 `f105bab` 뒤) — 응답 없음(AppHang). 원인 넷: BASE 를 굴려도 LOCAL 쪽으로 되돌려 밀고 당김 · 따라 굴러가는 도중에 재서 보정값이 6,619px 까지 쌓임 · 칸 목록 항목 번호를 정렬 행 번호로 씀(한쪽에만 있는 줄만큼 엉뚱한 줄끼리 견줌) · 빠르게 굴리면 사람의 스크롤이 버려짐. 손이 닿은 칸을 기준으로, 큰 맞추기는 한가할 때 한 번, 줄 객체로 오가기, 버려지면 멈춘 뒤 따라잡기. 화면 `g20` · `g21`(새) 응답, `g7` 실패 0, `g17` 통과 |
|
||||
| **완료** | **끌 때 저장하지 않은 편집을 묻는다**(사용자 요청 · 결정) — 파일 비교의 모든 탭(Git 모드 · 뒤 탭 · 닫은 탭 포함, 메모 제외)에 편집이 있으면 [저장하고 종료] · [저장하지 않고 종료] · [취소]. 버리면 보관본도 비워 다음에 안 살아남. 플랫폼 `ICloseGuard` 를 그대로 씀(플랫폼 수정 없음). 화면 `g19-exit-confirm` 실패 0. Git 모드는 화면 시험 안 함 · 기존 회귀는 사용자 지시로 안 돌림 |
|
||||
| **완료** | **엔터 꾹 · Ctrl+휠 확대 · 쭉 내리기에서 칸이 어긋난다**(사용자 보고) — 캐럿 줄 ≠ LOCAL 칸 줄, 확대하면 칸 위가 비고, 내리면 BASE 가 다른 줄을 봤다. 가상화 목록의 높이 어림 차이를 이동값으로 메우지 않고 칸을 실제로 굴려 칸별로 기억, BASE 는 같은 줄끼리 맞춤, 편집기 보이는 줄은 줄 높이로 계산(편집기의 `GetFirstVisibleLineIndex` · 확대 직후 `GetRectFromCharacterIndex` 가 틀렸다). 화면 `g17-hold-enter` — 엔터 뒤 936=936, 확대 · 30칸 내린 뒤 LOCAL = BASE. 기존 회귀는 사용자 지시로 안 돌림 |
|
||||
| **완료** | **파일 비교 타이핑 · 엔터 속도**(사용자 보고) — 재서 보니 엔터마다 도는 재정렬 0.4초의 대부분이 1,800줄을 목록에 한 줄씩 넣는 알림이었다. 한 번에 넣기(`ReplaceAll`) + 번호 말고 같은 줄은 기존 객체를 두고 가운데만 바꾸기(`Patch`)로 엔터 한 번 약 0.55초 → 약 0.05초. 하네스 `PatchTest` 16건, 화면 `g15` ① 새 줄 · 번호 제자리. 기존 회귀는 사용자 지시로 안 돌림 |
|
||||
|
||||
@ -243,6 +243,7 @@ namespace MergeHub.Views
|
||||
{
|
||||
if (Keyboard.Modifiers != ModifierKeys.Control || DataContext is not HomeViewModel vm) return;
|
||||
var command = e.Delta > 0 ? vm.FontBiggerCommand : vm.FontSmallerCommand;
|
||||
_scrollDriver = MergeLine.TextPane.Local; // 확대 뒤에는 BASE 를 LOCAL 에 맞춘다
|
||||
if (command.CanExecute(null)) command.Execute(null);
|
||||
e.Handled = true;
|
||||
};
|
||||
@ -272,6 +273,19 @@ namespace MergeHub.Views
|
||||
// 글꼴·배율·창 크기가 바뀌면 다시 재서 맞춘다.
|
||||
LayoutUpdated += (_, _) => AlignDecorationToEditor();
|
||||
|
||||
// 어느 칸을 사람이 굴렸는지는 <b>손이 닿은 칸</b>으로 정한다(2026-10-01). 스크롤 이벤트로 정하면 글자 크기를 바꿀 때
|
||||
// 목록이 저절로 내는 스크롤까지 "BASE 를 굴렸다" 로 읽어, 확대 뒤 BASE 를 LOCAL 에 맞추지 않았다(기록: 14초 동안 한 번도).
|
||||
void DriveFrom(UIElement pane, MergeLine.TextPane kind)
|
||||
{
|
||||
pane.PreviewMouseWheel += (_, _) => _scrollDriver = kind;
|
||||
pane.PreviewMouseDown += (_, _) => _scrollDriver = kind; // 스크롤 막대 끌기 · 칸 누르기
|
||||
pane.PreviewKeyDown += (_, _) => _scrollDriver = kind;
|
||||
}
|
||||
DriveFrom(LocalEditor, MergeLine.TextPane.Local);
|
||||
DriveFrom(LocalLines, MergeLine.TextPane.Local);
|
||||
DriveFrom(BaseLines, MergeLine.TextPane.Base);
|
||||
DriveFrom(RemoteLines, MergeLine.TextPane.Remote);
|
||||
DriveFrom(OriginalLocalLines, MergeLine.TextPane.OriginalLocal);
|
||||
Loaded += (_, _) =>
|
||||
{
|
||||
HookHorizontalWheel();
|
||||
@ -531,7 +545,13 @@ namespace MergeHub.Views
|
||||
// 3-Way 의 위 세 칸은 폭이 달라 따로 움직이므로 여기서 건드리지 않는다.
|
||||
if (DataContext is not HomeViewModel { IsThreeWay: true }) SourceShiftX = TextShiftX;
|
||||
|
||||
if (Syncing) return;
|
||||
if (Syncing)
|
||||
{
|
||||
// 메아리 막기 중에 온 스크롤은 버린다. 그런데 사람이 편집기를 빠르게 굴리면 다음 휠까지 버려져
|
||||
// BASE 가 따라가지 못하고 맨 위에 남았다(기록: BASE 37 · LOCAL 473). 사람이 굴리는 쪽이면 멈춘 뒤 따라잡는다.
|
||||
if (e.VerticalChange != 0 && _scrollDriver == MergeLine.TextPane.Local) { _catchUp = true; AlignWhenSettled(); }
|
||||
return;
|
||||
}
|
||||
HoldSync();
|
||||
|
||||
// 세로는 BASE·REMOTE 가 가운데를 맞춰 같이 굴러간다 - 이쪽은 굴리면 되돌아오는 칸이라 메아리를 막는다.
|
||||
@ -776,7 +796,7 @@ namespace MergeHub.Views
|
||||
|
||||
var row = vm.RowOfEditorLine(editorLine);
|
||||
if (row < 0 || row >= vm.Lines.Count) return;
|
||||
if (LocalLines.ItemContainerGenerator.ContainerFromIndex(row) is not ListBoxItem container || !container.IsVisible)
|
||||
if (ContainerOfRow(vm, LocalLines, row) is not { } container || !container.IsVisible)
|
||||
{
|
||||
LocalLines.ScrollIntoView(vm.Lines[row]);
|
||||
return;
|
||||
@ -834,13 +854,25 @@ namespace MergeHub.Views
|
||||
// LOCAL 목록은 굴러도 편집기를 되움직이지 않는다(Lines_ScrollChanged 가 받지 않는다) - 메아리 없음.
|
||||
var rowHeight = Math.Max(1, container.ActualHeight);
|
||||
if (Math.Abs(wanted) > rowHeight * 2 && FindScrollViewer(LocalLines) is { } listViewer)
|
||||
{
|
||||
// 아직 따라 굴러가는 중이면 재지 않는다 - 굴리라고 한 자리가 다음 배치에야 들어가서, 지금 재면 그만큼을
|
||||
// 어긋남으로 보고 보정값에 쌓는다(기록: 휠 한 칸마다 48px 씩 6,619px 까지). 멈춘 뒤 한 번 맞춘다.
|
||||
if (Syncing) { AlignWhenSettled(); return; }
|
||||
|
||||
// <b>실제로 움직인 만큼만</b> 보정한다(2026-10-01 사용자 보고 "스크롤을 내리면 프로그램이 뻗는다").
|
||||
// 목록 끝이라 더 못 굴리는데 보정값을 쌓고 다시 그리기를 부르면, 배치마다 같은 일을 되풀이할 수 있다.
|
||||
// 못 굴리면 아래 이동값으로 넘긴다 - 그러면 다음 배치에서 "맞음" 으로 끝난다.
|
||||
var target = Clamp(listViewer.VerticalOffset - wanted, listViewer.ScrollableHeight);
|
||||
var moved = target - listViewer.VerticalOffset;
|
||||
if (Math.Abs(moved) >= 1)
|
||||
{
|
||||
_decorationShift.Y = 0;
|
||||
_paneBias[LocalLines] = Bias(LocalLines) - wanted; // 다음 편집기 스크롤도 이만큼 더해 굴린다
|
||||
listViewer.ScrollToVerticalOffset(Clamp(listViewer.VerticalOffset - wanted, listViewer.ScrollableHeight));
|
||||
_paneBias[LocalLines] = Bias(LocalLines) + moved; // 다음 편집기 스크롤도 이만큼 더해 굴린다
|
||||
listViewer.ScrollToVerticalOffset(target);
|
||||
LinkOverlay.InvalidateVisual();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// 가로는 건드리지 않는다. 여백(줄번호·[>])까지 밀려나기 때문이다 -
|
||||
// 가로 스크롤은 글자에만 거는 TextShiftX 가 따로 맡는다.
|
||||
@ -853,6 +885,59 @@ namespace MergeHub.Views
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>한가할 때 맞추기가 이미 걸려 있다.</summary>
|
||||
private bool _alignQueued;
|
||||
|
||||
/// <summary>사람이 굴린 칸의 스크롤이 메아리 막기에 버려졌다 - 멈춘 뒤 그 칸 기준으로 한 번 따라잡는다.</summary>
|
||||
private bool _catchUp;
|
||||
|
||||
/// <summary>
|
||||
/// 스크롤이 멈춘 뒤(입력 · 배치가 다 끝나 앱이 한가할 때) 칸 맞추기를 한 번 더 돌린다.
|
||||
/// 배치마다 도는 자리(LayoutUpdated)에서 곧바로 다시 굴리면, 굴린 것이 또 배치를 부르고 그 배치가 또 맞추기를 불러
|
||||
/// 입력 차례가 오지 않을 수 있다(2026-10-01 사용자 보고 "스크롤을 내리면 뻗는다"). 한가할 때 도는 일은 입력을 밀어내지 않는다.
|
||||
/// </summary>
|
||||
private void AlignWhenSettled()
|
||||
{
|
||||
if (_alignQueued) return;
|
||||
_alignQueued = true;
|
||||
Dispatcher.BeginInvoke(new Action(() =>
|
||||
{
|
||||
_alignQueued = false;
|
||||
if (Syncing) { AlignWhenSettled(); return; }
|
||||
if (_catchUp)
|
||||
{
|
||||
// 사람이 굴린 칸의 스크롤이 버려졌다 - 그 칸의 마지막 자리로 나머지를 한 번 맞춘다.
|
||||
// 이걸로 다른 칸이 움직여 생기는 스크롤은 사람이 굴린 칸이 아니라 표시를 남기지 않는다 - 되풀이가 없다.
|
||||
_catchUp = false;
|
||||
if (DriverPane() is { } driver && FindScrollViewer(driver) is { } viewer)
|
||||
{
|
||||
MeasureBias(driver, _scrollDriver);
|
||||
HoldSync();
|
||||
SyncPanesFrom(_scrollDriver, viewer.VerticalOffset - Bias(driver), viewer.ViewportHeight);
|
||||
}
|
||||
else if (FindScrollViewer(LocalEditor) is { } editor)
|
||||
{
|
||||
HoldSync();
|
||||
SyncPanesFrom(MergeLine.TextPane.Local, editor.VerticalOffset, editor.ViewportHeight);
|
||||
}
|
||||
return;
|
||||
}
|
||||
AlignDecorationToEditor();
|
||||
}), DispatcherPriority.ApplicationIdle);
|
||||
}
|
||||
|
||||
/// <summary>사람이 굴린 원본 칸(BASE · REMOTE · 원본 LOCAL). LOCAL 쪽이면 null.</summary>
|
||||
private ListBox? DriverPane() => _scrollDriver switch
|
||||
{
|
||||
MergeLine.TextPane.Base => BaseLines,
|
||||
MergeLine.TextPane.Remote => RemoteLines,
|
||||
MergeLine.TextPane.OriginalLocal => OriginalLocalLines,
|
||||
_ => null
|
||||
};
|
||||
|
||||
/// <summary>마지막으로 사람이 굴린 칸. 편집기 · LOCAL 이면 BASE 를 LOCAL 에 맞추고, BASE · REMOTE 면 그 칸이 기준이라 되돌리지 않는다.</summary>
|
||||
private MergeLine.TextPane _scrollDriver = MergeLine.TextPane.Local;
|
||||
|
||||
/// <summary>칸마다 마지막으로 불러온 짝 행. 같은 행을 배치마다 거듭 부르지 않게 한다.</summary>
|
||||
private readonly System.Collections.Generic.Dictionary<ListBox, int> _pairRequested = new System.Collections.Generic.Dictionary<ListBox, int>();
|
||||
|
||||
@ -868,8 +953,19 @@ namespace MergeHub.Views
|
||||
private void AlignOtherPanesByRow()
|
||||
{
|
||||
if (DataContext is not HomeViewModel vm || vm.Lines.Count == 0) return;
|
||||
|
||||
// 사람이 BASE · REMOTE 를 굴리는 중이면 그 칸이 기준이다 - 되돌리지 않는다(2026-10-01 사용자 보고 "스크롤을 내리면 뻗는다").
|
||||
// 예전에는 늘 LOCAL 을 기준으로 BASE 를 되돌려, BASE 를 휠로 굴리는 동안 굴린 만큼(기록: 48px)을 매번 끌어당겼다.
|
||||
// 그 사이 보정값이 오르내리며(694 → 22) 다른 칸을 다시 움직여 초당 32번 서로 밀고 당겼다 - 큰 파일에서는 응답 없음으로 번졌다.
|
||||
// 그때는 예전처럼 나머지 칸이 가운데 맞추기(SyncPanesFrom)로 그 칸을 따라온다.
|
||||
if (_scrollDriver != MergeLine.TextPane.Local) return;
|
||||
|
||||
// 따라 굴러가는 중이면 재지 않는다 - BASE 는 가운데 맞추기가 굴리라고 한 자리로 다음 배치에야 간다.
|
||||
// 그 사이에 재면 휠 한 칸(48px)을 늘 어긋남으로 보고 보정값에 쌓았다(기록: 6,619px). 멈춘 뒤 한 번 맞춘다.
|
||||
if (Syncing) { AlignWhenSettled(); return; }
|
||||
|
||||
if (ContainerAtMiddle(LocalLines) is not { } localItem) return;
|
||||
var row = LocalLines.ItemContainerGenerator.IndexFromContainer(localItem);
|
||||
var row = RowOf(vm, LocalLines, localItem);
|
||||
if (row < 0) return;
|
||||
var localTop = localItem.TranslatePoint(new Point(0, 0), LocalLines).Y;
|
||||
var rowHeight = Math.Max(1, localItem.ActualHeight);
|
||||
@ -884,7 +980,7 @@ namespace MergeHub.Views
|
||||
if (vm.Lines[row].PaneHeight(kind, false) <= 0) continue;
|
||||
|
||||
if (FindScrollViewer(list) is not { } viewer) continue;
|
||||
if (list.ItemContainerGenerator.ContainerFromIndex(row) is not ListBoxItem item || !item.IsVisible)
|
||||
if (ContainerOfRow(vm, list, row) is not { } item || !item.IsVisible)
|
||||
{
|
||||
// 짝이 아직 안 그려졌다(어림이 크게 틀림) - 먼저 화면 안으로 부르고, 다음 배치 때 윗변을 맞춘다.
|
||||
// 같은 줄은 한 번만 부른다 - 부른 뒤에도 안 보이면 배치마다 다시 부르며 멈추지 않는다.
|
||||
@ -899,13 +995,32 @@ namespace MergeHub.Views
|
||||
var dy = item.TranslatePoint(new Point(0, 0), list).Y - localTop;
|
||||
if (Math.Abs(dy) <= rowHeight * 2) continue;
|
||||
|
||||
// 실제로 움직일 수 있는 만큼만. 칸 끝이라 못 움직이면 보정값도 다시 그리기도 건드리지 않는다 -
|
||||
// 그러지 않으면 배치마다 같은 어긋남을 보고 같은 일을 되풀이한다.
|
||||
var target = Clamp(viewer.VerticalOffset + dy, viewer.ScrollableHeight);
|
||||
var moved = target - viewer.VerticalOffset;
|
||||
if (Math.Abs(moved) < 1) continue;
|
||||
|
||||
HoldSync();
|
||||
_paneBias[list] = Bias(list) + dy; // 가운데 맞추기도 다음부터 이만큼 더해 굴린다 - 되돌려 밀고 당기지 않게
|
||||
viewer.ScrollToVerticalOffset(Clamp(viewer.VerticalOffset + dy, viewer.ScrollableHeight));
|
||||
_paneBias[list] = Bias(list) + moved; // 가운데 맞추기도 다음부터 이만큼 더해 굴린다 - 되돌려 밀고 당기지 않게
|
||||
viewer.ScrollToVerticalOffset(target);
|
||||
LinkOverlay.InvalidateVisual();
|
||||
}
|
||||
}
|
||||
|
||||
// 칸 목록은 정렬 행 전체가 아니라 <b>그 칸에 있는 줄만</b> 거른 목록이다(PaneView, 11차 F-10).
|
||||
// 그래서 목록의 몇 번째 항목과 정렬 행 번호는 다르다 - 한쪽에만 있는 줄이 앞에 있으면 그만큼 밀린다.
|
||||
// 둘을 같은 것으로 쓰면 엉뚱한 줄끼리 견줘 칸을 잘못 굴렸다(2026-10-01 기록: 한쪽에만 있는 줄 10개 → 157px).
|
||||
// 줄 객체로 오간다.
|
||||
|
||||
/// <summary>목록 항목이 가리키는 정렬 행 번호. 못 찾으면 -1.</summary>
|
||||
private static int RowOf(HomeViewModel vm, ListBox list, ListBoxItem item) =>
|
||||
list.ItemContainerGenerator.ItemFromContainer(item) is MergeLine line ? vm.Lines.IndexOf(line) : -1;
|
||||
|
||||
/// <summary>정렬 행의 목록 항목. 그 칸에 없는 줄이거나 아직 안 그려졌으면 null.</summary>
|
||||
private static ListBoxItem? ContainerOfRow(HomeViewModel vm, ListBox list, int row) =>
|
||||
row >= 0 && row < vm.Lines.Count ? list.ItemContainerGenerator.ContainerFromItem(vm.Lines[row]) as ListBoxItem : null;
|
||||
|
||||
/// <summary>목록 칸의 세로 가운데를 덮는 행. 없으면 null.</summary>
|
||||
private static ListBoxItem? ContainerAtMiddle(ListBox list)
|
||||
{
|
||||
@ -1033,8 +1148,14 @@ namespace MergeHub.Views
|
||||
// 글이나 칸 폭이 바뀌면 밀 수 있는 폭도 달라진다. 스크롤 이벤트가 그때마다 온다.
|
||||
if (e.ExtentWidthChange != 0 || e.ViewportWidthChange != 0) UpdateSourceScrollRange();
|
||||
|
||||
if (Syncing) return;
|
||||
if (e.VerticalChange == 0) return;
|
||||
if (Syncing)
|
||||
{
|
||||
// 다른 칸을 따라 굴리는 동안 들어온 스크롤은 메아리로 보고 버린다. 그런데 사람이 이 칸을 빠르게 굴리면
|
||||
// 그 다음 휠까지 버려져 편집기가 중간에 멈췄다(기록: BASE 784 · LOCAL 555). 사람이 굴리는 칸이면 멈춘 뒤 따라잡는다.
|
||||
if (DriverPane() == source) { _catchUp = true; AlignWhenSettled(); }
|
||||
return;
|
||||
}
|
||||
|
||||
HoldSync();
|
||||
|
||||
@ -1043,9 +1164,32 @@ namespace MergeHub.Views
|
||||
: ReferenceEquals(source, RemoteLines) ? MergeLine.TextPane.Remote
|
||||
: MergeLine.TextPane.OriginalLocal;
|
||||
// 이 칸의 픽셀 자리에는 보정값이 들어 있다 - 빼서 "줄 기준" 자리로 바꾼 뒤 다른 칸을 맞춘다.
|
||||
// 보정값은 <b>지금 보이는 줄에서 다시 잰다</b>(2026-10-01). 목록은 안 그린 줄 높이를 어림하므로 멀리 굴러가면
|
||||
// 어림이 바뀌는데, 예전 자리에서 잰 값을 그대로 쓰면 편집기 · LOCAL 이 수십~백 줄 엉뚱한 곳으로 갔다
|
||||
// (기록: BASE 26 · LOCAL 81, BASE 761 · LOCAL 618). 사람이 굴릴 때만 한 번씩 재므로 되풀이를 만들지 않는다.
|
||||
MeasureBias(source, kind);
|
||||
SyncPanesFrom(kind, e.VerticalOffset - Bias(source), e.ViewportHeight);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 칸 목록의 보정값(실제 픽셀 자리 - 줄 높이로 센 자리)을 가운데 보이는 줄에서 잰다.
|
||||
/// 그 줄이 이 칸에 없으면(높이 0) 가운데 행이 잡히지 않아 그대로 둔다.
|
||||
/// </summary>
|
||||
private void MeasureBias(ListBox list, MergeLine.TextPane kind)
|
||||
{
|
||||
if (DataContext is not HomeViewModel vm || vm.Lines.Count == 0) return;
|
||||
if (FindScrollViewer(list) is not { } viewer || ContainerAtMiddle(list) is not { } item) return;
|
||||
var row = RowOf(vm, list, item);
|
||||
if (row < 0 || row >= vm.Lines.Count) return;
|
||||
|
||||
var modelTop = 0.0;
|
||||
var measured = MeasuredRows;
|
||||
for (var i = 0; i < row; i++) modelTop += vm.Lines[i].PaneHeight(kind, measured);
|
||||
|
||||
var actualTop = viewer.VerticalOffset + item.TranslatePoint(new Point(0, 0), list).Y;
|
||||
_paneBias[list] = actualTop - modelTop;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 칸 목록마다 "픽셀 자리 → 같은 줄" 보정값(2026-09-30 사용자 보고: 확대하면 화면이 잘린다 · 밑으로 쭉 내리면 두 칸 속도가 다르다).
|
||||
///
|
||||
@ -1686,6 +1830,7 @@ namespace MergeHub.Views
|
||||
/// <summary>그 글자가 있는 줄의 윗변을 편집기 칸 가운데로 굴린다(차이 이동 · 11차 F-10).</summary>
|
||||
private void CenterCharInView(int charIndex)
|
||||
{
|
||||
_scrollDriver = MergeLine.TextPane.Local; // 프로그램이 편집기를 옮겼다(다음 차이 · 줄 이동) - 나머지 칸은 LOCAL 을 따른다
|
||||
if (FindScrollViewer(LocalEditor) is not { } viewer) return;
|
||||
|
||||
Rect rect;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user