WIP 76 보충: 목록 줄에 항목 경로 키 - 줄 목록이 바뀔 때 누른 줄의 두 번 누르기 기다림이 다른 항목으로 넘어가지 않게 (setState during build) (검토 대기)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
묶음6
RyuJongJin 2026-10-12 08:56:20 +09:00
parent 1eef4ff65f
commit 320025c5fa
2 changed files with 46 additions and 3 deletions

View File

@ -3139,7 +3139,9 @@ class _ExplorerPageState extends State<ExplorerPage> {
controller: pane.scroll, controller: pane.scroll,
itemCount: rows.length, itemCount: rows.length,
itemExtent: look.rowHeightOf(context), 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: 목록 위에 겹쳐 그린다 (끼어들면 목록이 밀려 다음 것을 누르려다 엉뚱한 것을 누른다) // 읽는 중 (WebDAV 서버를 기다릴 때 등). 180: 목록 위에 겹쳐 그린다 (끼어들면 목록이 밀려 다음 것을 누르려다 엉뚱한 것을 누른다)
if (pane.loading.isNotEmpty) const Positioned(left: 0, right: 0, top: 0, child: LinearProgressIndicator(minHeight: 2)), if (pane.loading.isNotEmpty) const Positioned(left: 0, right: 0, top: 0, child: LinearProgressIndicator(minHeight: 2)),
@ -3149,6 +3151,17 @@ class _ExplorerPageState extends State<ExplorerPage> {
])); ]));
} }
/// 76: 목록 줄의 키 = 항목 경로. 키가 없으면 줄 목록이 바뀔 때 (빠른 필터 · 펼치기 · 새로 고침) 같은 자리의 줄 위젯을
/// 다른 항목에 다시 써서, 방금 누른 파일 줄이 폴더 줄로 바뀌면 두 번 누르기 기다림이 그리는 중에 버려지고
/// 그 누르기가 다른 항목에 들어갔다 (디버그에서는 "setState() called during build")
static Key _rowKey(_Row r) => ValueKey<String>('${r.isRoot ? 'root:' : r.isUp ? 'up:' : ''}${r.entry.path}');
/// 76: 줄이 옮겨 가도 같은 줄 위젯을 찾게 (키 → 지금 자리)
static int? Function(Key) _rowIndexOf(List<_Row> rows) {
Map<Key, int>? index;
return (key) => (index ??= {for (var i = 0; i < rows.length; i++) _rowKey(rows[i]): i})[key];
}
/// "폴더 + 파일 목록" 배치의 오른쪽: 왼쪽 트리에서 고른 폴더의 내용 (맨 위 ".." = 상위 폴더) /// "폴더 + 파일 목록" 배치의 오른쪽: 왼쪽 트리에서 고른 폴더의 내용 (맨 위 ".." = 상위 폴더)
Widget _listView(_Pane pane) { Widget _listView(_Pane pane) {
final dir = pane.current; final dir = pane.current;
@ -3182,7 +3195,8 @@ class _ExplorerPageState extends State<ExplorerPage> {
controller: pane.listScroll, controller: pane.listScroll,
itemCount: rows.length, itemCount: rows.length,
itemExtent: look.rowHeightOf(context), 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), if (_selecting) _marksBar(pane, listDir: dir),

View File

@ -569,8 +569,11 @@ void main() {
expect(find.textContaining('이름에 든 글자로 좁히기'), findsNothing); expect(find.textContaining('이름에 든 글자로 좁히기'), findsNothing);
expect(names(), containsAll(['doc.txt', 'sub', 'ep01.mkv', 'other.txt'])); expect(names(), containsAll(['doc.txt', 'sub', 'ep01.mkv', 'other.txt']));
// 닫으면 키는 탐색기로 돌아온다 - 목록에서 글자를 바로 치면 그 글자로 열린다 // 닫으면 키는 탐색기로 돌아온다 - 목록에서 글자를 바로 치면 그 글자로 열린다.
// 사람처럼 줄을 누르고 곧바로 친다 (두 번 누르기를 기다리는 중에 줄 목록이 바뀜)
await tester.pump(); 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 tester.sendKeyEvent(LogicalKeyboardKey.keyO, character: 'o');
await calm(tester); await calm(tester);
expect(tester.widget<TextField>(find.byType(TextField).first).controller!.text, 'o'); expect(tester.widget<TextField>(find.byType(TextField).first).controller!.text, 'o');
@ -579,6 +582,32 @@ void main() {
await tester.enterText(find.byType(TextField).first, 'oth'); await tester.enterText(find.byType(TextField).first, 'oth');
await tester.pump(); await tester.pump();
expect(names(), ['other.txt']); 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: 빠른 필터 맞추기 - 빈 칸은 모두 · 글자는 이름 안 어디든 · * ? 는 이름 전체 패턴', () { test('76: 빠른 필터 맞추기 - 빈 칸은 모두 · 글자는 이름 안 어디든 · * ? 는 이름 전체 패턴', () {