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: 빠른 필터 맞추기 - 빈 칸은 모두 · 글자는 이름 안 어디든 · * ? 는 이름 전체 패턴', () {