diff --git a/lib/ui/explorer_page.dart b/lib/ui/explorer_page.dart index 57dc5fc..dda06bb 100644 --- a/lib/ui/explorer_page.dart +++ b/lib/ui/explorer_page.dart @@ -738,26 +738,34 @@ class _ExplorerPageState extends State { /// 75: 창마다 뒤로 · 앞으로 (가 본 폴더로) Future _travel(_Pane pane, {required bool forward}) async { + // 앞의 이동이 끝나기 전에 또 불리면 (빨리 두 번 · 같은 누름이 두 번 들어옴) 무시 - 내역에서 두 칸이 빠지지 않게 + if (pane.traveling) return; final from = forward ? pane.forward : pane.back; if (from.isEmpty) return; - final to = from.removeLast(); - setState(() => _active = _panes.indexOf(pane)); - // 지워졌거나 열 수 없는 폴더: 그 항목은 버리고 지금 폴더는 그대로 (이유를 알림) - bool ok; - try { - ok = await vIsDir(to); - } catch (_) { - ok = false; - } - if (!mounted) return; - if (!ok) { - setState(() {}); - _snack(trf('{0} 폴더가 없거나 열 수 없어 내역에서 뺐습니다', [_displayPath(to)])); - return; - } - (forward ? pane.back : pane.forward).add(pane.current); pane.traveling = true; try { + final to = from.removeLast(); + setState(() => _active = _panes.indexOf(pane)); + // 지워졌거나 열 수 없는 폴더: 그 항목은 버리고 지금 폴더는 그대로 (이유를 알림) + bool ok; + try { + ok = await vIsDir(to); + } catch (_) { + ok = false; + } + if (!mounted) return; + if (!ok) { + // 트리에도 남지 않게 (밖에서 지운 폴더) - 그 상위 폴더를 두 창 모두 다시 읽는다 + for (final x in _panes) { + x.expanded.removeWhere((d) => samePath(d, to) || d.toLowerCase().startsWith('${to.toLowerCase()}${isDav(to) ? '/' : p.separator}')); + } + await _refreshAll([vDirname(to)]); + if (!mounted) return; + setState(() {}); + _snack(trf('{0} 폴더가 없거나 열 수 없어 내역에서 뺐습니다', [_displayPath(to)])); + return; + } + (forward ? pane.back : pane.forward).add(pane.current); await _goTo(pane, to); } finally { pane.traveling = false; @@ -990,7 +998,16 @@ class _ExplorerPageState extends State { return; } if (e.isDir) { - await _toggle(pane, e); + // 189: 이름을 누르면 그 폴더로 (닫혀 있으면 펼친다). 이름으로는 접지 않는다 - 다시 누르거나 두 번 누르면 접혀 + // 아래 줄이 움직여 다른 것을 누르게 된다. 접기 · 펼치기는 › 화살표로 (탐색기 · Total Commander 처럼) + if (!pane.expanded.contains(e.path)) { + await _toggle(pane, e); + } else if (!samePath(pane.current, e.path)) { + pane + ..current = e.path + ..changed(); + _remember(pane, e.path); + } return; } if (!list) pane.current = vDirname(e.path); @@ -2706,10 +2723,7 @@ class _ExplorerPageState extends State { autofocus: true, child: Listener( // 75: 마우스 4 · 5번 (뒤로 · 앞으로) 버튼 - 창 밖 (위 막대 등) 이면 지금 창 (창 위면 그 창이 먼저 받음, [_frame]) - onPointerDown: (ev) { - if (identical(ev, _navEvent)) return; - _mouseTravel(_active, ev); - }, + onPointerDown: (ev) => _mouseTravel(_active, ev), child: Scaffold( body: SwipeNav( current: widget.rsync ? 'rsync' : 'explorer', @@ -2833,17 +2847,18 @@ class _ExplorerPageState extends State { /// 180: 왼쪽 창 · 오른쪽 창 · 파일 목록 (배치가 바뀌어도 같은 위젯 - [_body]) final _viewKeys = [GlobalKey(), GlobalKey(), GlobalKey()]; - /// 75: 마우스 4 · 5번 - 그 창의 뒤로 · 앞으로 (그 창을 활성으로) - PointerDownEvent? _navEvent; + /// 75: 마우스 4 · 5번 - 그 창의 뒤로 · 앞으로 (그 창을 활성으로). + /// 창 위의 누름은 창 ([_frame]) 과 화면 Listener 가 둘 다 받는다. 받는 곳마다 좌표를 바꾼 새 이벤트라 객체로는 같은지 알 수 없어 + /// 누른 손가락 (pointer) · 시각으로 한 번만 쓴다 (창이 먼저 받는다) + (int, Duration)? _navPress; void _mouseTravel(int i, PointerDownEvent ev) { if (i >= _panes.length) return; - if (ev.buttons & kBackMouseButton != 0) { - _navEvent = ev; - _travel(_panes[i], forward: false); - } else if (ev.buttons & kForwardMouseButton != 0) { - _navEvent = ev; - _travel(_panes[i], forward: true); - } + final back = ev.buttons & kBackMouseButton != 0; + if (!back && ev.buttons & kForwardMouseButton == 0) return; + final press = (ev.pointer, ev.timeStamp); + if (press == _navPress) return; + _navPress = press; + _travel(_panes[i], forward: !back); } Widget _frame(int i, List children) { diff --git a/test/explorer_test.dart b/test/explorer_test.dart index 3088b2f..cc64173 100644 --- a/test/explorer_test.dart +++ b/test/explorer_test.dart @@ -80,14 +80,21 @@ void main() { expect(find.text('모니터링'), findsNothing); expect(find.text('좌 → 우'), findsNothing); - // 폴더를 한 번 누르면 바로 펼치고, 다시 누르면 접는다 + // 폴더를 한 번 누르면 바로 펼친다. 189: 다시 눌러도 (두 번 누름 포함) 접지 않는다 - 접기 · 펼치기는 화살표로 await act(tester, () => tester.tap(find.text('sub'))); await settle(tester, () => find.text('inner.txt').evaluate().isNotEmpty); expect(find.text('inner.txt'), findsOneWidget); await act(tester, () => tester.tap(find.text('sub'))); + await settle(tester, () => false, rounds: 15); + expect(find.text('inner.txt'), findsOneWidget, reason: '이름을 다시 눌러도 그대로'); + await doubleTap(tester, find.text('sub')); + await settle(tester, () => false, rounds: 15); + expect(find.text('inner.txt'), findsOneWidget, reason: '두 번 눌러도 그대로'); + await act(tester, () => tester.tap(find.descendant( + of: find.ancestor(of: find.text('sub'), matching: find.byType(InkWell)).first, matching: find.byIcon(Icons.expand_more)))); await settle(tester, () => find.text('inner.txt').evaluate().isEmpty); - expect(find.text('inner.txt'), findsNothing); - // › 화살표로도 펼친다 + expect(find.text('inner.txt'), findsNothing, reason: '화살표로 접는다'); + // › 화살표로 펼친다 await act(tester, () => tester.tap(chevronOf('sub'))); await settle(tester, () => find.text('inner.txt').evaluate().isNotEmpty); expect(find.text('inner.txt'), findsOneWidget); @@ -476,10 +483,13 @@ void main() { await act(tester, () => tester.tap(find.byTooltip('뒤로 (Alt+←)').first)); await settle(tester, () => now() == left, rounds: 50); await tester.runAsync(() => Directory(sub).delete(recursive: true)); + expect(find.text('sub'), findsOneWidget, reason: '밖에서 지운 것은 아직 모름'); await act(tester, () => tester.tap(find.byTooltip('앞으로 (Alt+→)').first)); await settle(tester, () => find.textContaining('내역에서 뺐습니다').evaluate().isNotEmpty, rounds: 50); expect(find.textContaining('내역에서 뺐습니다'), findsOneWidget); expect(now(), left); + await settle(tester, () => find.text('sub').evaluate().isEmpty, rounds: 50); + expect(find.text('sub'), findsNothing, reason: '트리에도 남지 않음 (상위 폴더를 다시 읽음)'); // 버린 항목은 다시 가지 않는다 (앞으로 버튼이 꺼짐) final fwd = tester.widget(find.ancestor(of: find.byTooltip('앞으로 (Alt+→)').first, matching: find.byType(IconButton)).first); expect(fwd.onPressed, isNull);