From 320025c5fa56ff141927ed458fb006e6a9aba486 Mon Sep 17 00:00:00 2001 From: RyuJongJin <266630912+RyuJongJin@users.noreply.github.com> Date: Mon, 12 Oct 2026 08:56:20 +0900 Subject: [PATCH] =?UTF-8?q?WIP=2076=20=EB=B3=B4=EC=B6=A9:=20=EB=AA=A9?= =?UTF-8?q?=EB=A1=9D=20=EC=A4=84=EC=97=90=20=ED=95=AD=EB=AA=A9=20=EA=B2=BD?= =?UTF-8?q?=EB=A1=9C=20=ED=82=A4=20-=20=EC=A4=84=20=EB=AA=A9=EB=A1=9D?= =?UTF-8?q?=EC=9D=B4=20=EB=B0=94=EB=80=94=20=EB=95=8C=20=EB=88=84=EB=A5=B8?= =?UTF-8?q?=20=EC=A4=84=EC=9D=98=20=EB=91=90=20=EB=B2=88=20=EB=88=84?= =?UTF-8?q?=EB=A5=B4=EA=B8=B0=20=EA=B8=B0=EB=8B=A4=EB=A6=BC=EC=9D=B4=20?= =?UTF-8?q?=EB=8B=A4=EB=A5=B8=20=ED=95=AD=EB=AA=A9=EC=9C=BC=EB=A1=9C=20?= =?UTF-8?q?=EB=84=98=EC=96=B4=EA=B0=80=EC=A7=80=20=EC=95=8A=EA=B2=8C=20(se?= =?UTF-8?q?tState=20during=20build)=20(=EA=B2=80=ED=86=A0=20=EB=8C=80?= =?UTF-8?q?=EA=B8=B0)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- lib/ui/explorer_page.dart | 18 ++++++++++++++++-- test/explorer_test.dart | 31 ++++++++++++++++++++++++++++++- 2 files changed, 46 insertions(+), 3 deletions(-) diff --git a/lib/ui/explorer_page.dart b/lib/ui/explorer_page.dart index 22ab9e0..b5f3afb 100644 --- a/lib/ui/explorer_page.dart +++ b/lib/ui/explorer_page.dart @@ -3139,7 +3139,9 @@ class _ExplorerPageState extends State { controller: pane.scroll, itemCount: rows.length, itemExtent: look.rowHeightOf(context), - itemBuilder: (_, k) => _rowView(pane, rows[k], compact: foldersOnly), + // 76: 줄 위젯은 항목을 따라간다 (아래 [_rowKey]) + itemBuilder: (_, k) => KeyedSubtree(key: _rowKey(rows[k]), child: _rowView(pane, rows[k], compact: foldersOnly)), + findChildIndexCallback: _rowIndexOf(rows), ), // 읽는 중 (WebDAV 서버를 기다릴 때 등). 180: 목록 위에 겹쳐 그린다 (끼어들면 목록이 밀려 다음 것을 누르려다 엉뚱한 것을 누른다) if (pane.loading.isNotEmpty) const Positioned(left: 0, right: 0, top: 0, child: LinearProgressIndicator(minHeight: 2)), @@ -3149,6 +3151,17 @@ class _ExplorerPageState extends State { ])); } + /// 76: 목록 줄의 키 = 항목 경로. 키가 없으면 줄 목록이 바뀔 때 (빠른 필터 · 펼치기 · 새로 고침) 같은 자리의 줄 위젯을 + /// 다른 항목에 다시 써서, 방금 누른 파일 줄이 폴더 줄로 바뀌면 두 번 누르기 기다림이 그리는 중에 버려지고 + /// 그 누르기가 다른 항목에 들어갔다 (디버그에서는 "setState() called during build") + static Key _rowKey(_Row r) => ValueKey('${r.isRoot ? 'root:' : r.isUp ? 'up:' : ''}${r.entry.path}'); + + /// 76: 줄이 옮겨 가도 같은 줄 위젯을 찾게 (키 → 지금 자리) + static int? Function(Key) _rowIndexOf(List<_Row> rows) { + Map? index; + return (key) => (index ??= {for (var i = 0; i < rows.length; i++) _rowKey(rows[i]): i})[key]; + } + /// "폴더 + 파일 목록" 배치의 오른쪽: 왼쪽 트리에서 고른 폴더의 내용 (맨 위 ".." = 상위 폴더) Widget _listView(_Pane pane) { final dir = pane.current; @@ -3182,7 +3195,8 @@ class _ExplorerPageState extends State { controller: pane.listScroll, itemCount: rows.length, itemExtent: look.rowHeightOf(context), - itemBuilder: (_, k) => _rowView(pane, rows[k], list: true), + itemBuilder: (_, k) => KeyedSubtree(key: _rowKey(rows[k]), child: _rowView(pane, rows[k], list: true)), + findChildIndexCallback: _rowIndexOf(rows), ), ), if (_selecting) _marksBar(pane, listDir: dir), diff --git a/test/explorer_test.dart b/test/explorer_test.dart index 0059add..9d5dfa3 100644 --- a/test/explorer_test.dart +++ b/test/explorer_test.dart @@ -569,8 +569,11 @@ void main() { expect(find.textContaining('이름에 든 글자로 좁히기'), findsNothing); expect(names(), containsAll(['doc.txt', 'sub', 'ep01.mkv', 'other.txt'])); - // 닫으면 키는 탐색기로 돌아온다 - 목록에서 글자를 바로 치면 그 글자로 열린다 + // 닫으면 키는 탐색기로 돌아온다 - 목록에서 글자를 바로 치면 그 글자로 열린다. + // 사람처럼 줄을 누르고 곧바로 친다 (두 번 누르기를 기다리는 중에 줄 목록이 바뀜) await tester.pump(); + await tester.tap(find.text('doc.txt').first); + await tester.pump(const Duration(milliseconds: 20)); await tester.sendKeyEvent(LogicalKeyboardKey.keyO, character: 'o'); await calm(tester); expect(tester.widget(find.byType(TextField).first).controller!.text, 'o'); @@ -579,6 +582,32 @@ void main() { await tester.enterText(find.byType(TextField).first, 'oth'); await tester.pump(); expect(names(), ['other.txt']); + expect(tester.takeException(), isNull); + }); + + testWidgets('76: 파일 줄을 누르고 곧바로 쳐서 그 줄이 사라지거나 다른 줄이 그 자리에 와도, 누른 것이 다른 항목에 들어가지 않는다', (tester) async { + for (final n in ['a1.mkv', 'b2.mkv']) { + File(p.join(left, n)).writeAsStringSync('x'); + } + Directory(p.join(left, 'a_folder')).createSync(); + await open(tester); + await settle(tester, () => find.text('b2.mkv').evaluate().isNotEmpty); + // 누르기 = 선택 (두 번 = 실행) 모드: 파일 줄은 두 번 누르기를 기다린다 + await tester.tap(find.text('b2.mkv').first); + await tester.pump(const Duration(milliseconds: 20)); + // "a" → b2.mkv 가 사라지고 그 자리에 폴더 · 다른 파일이 온다 + await tester.sendKeyEvent(LogicalKeyboardKey.keyA, character: 'a'); + await tester.pump(); + expect(tester.takeException(), isNull); + await tester.pump(const Duration(milliseconds: 500)); + expect(tester.takeException(), isNull); + expect(find.text('b2.mkv'), findsNothing); + expect(shell.opened, isEmpty, reason: '누른 것이 실행되지 않음'); + // 다른 항목이 고른 것 (강조) 이 되지 않음 - 폴더가 펼쳐지지도 않음 + expect(find.text('a_folder'), findsOneWidget); + await tester.sendKeyEvent(LogicalKeyboardKey.escape); + await calm(tester); + expect(find.text('b2.mkv'), findsOneWidget); }); test('76: 빠른 필터 맞추기 - 빈 칸은 모두 · 글자는 이름 안 어디든 · * ? 는 이름 전체 패턴', () {