탐색기 뒤로 · 앞으로를 실제 Windows 에서 한 칸씩 (75) · 이름으로 접지 않기 (189)

- 75 (버그 수정, 실제 Windows 빌드에서 재현): 창 위의 마우스 4 · 5 누름을 창과 화면이 둘 다 받아 두 칸씩 움직였다
  (받는 곳마다 좌표를 바꾼 새 이벤트라 identical 로 막히지 않음) → 누른 손가락 · 시각으로 한 번만.
  이동이 끝나기 전에 다시 불리면 무시 (내역에서 두 칸이 빠지지 않게).
- 75: 지운 폴더로 뒤로 · 앞으로 하면 그 상위 폴더를 두 창 모두 다시 읽는다 (트리에 지운 폴더가 남지 않게)
- 189 (관리자 지시): 폴더 이름을 누르면 그 폴더로 (닫혀 있으면 펼침), 이름으로는 접지 않는다.
  다시 누르거나 두 번 눌러도 아래 줄이 움직이지 않게. 접기 · 펼치기는 화살표로 (Windows · Android 같게)
- 시험: 이름을 다시 · 두 번 눌러도 그대로, 화살표로 접기. 지운 폴더가 트리에서도 사라짐

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
fix_mkv
RyuJongJin 2026-10-11 19:43:14 +09:00
parent 3639aa9cbf
commit a7632a9bed
2 changed files with 58 additions and 33 deletions

View File

@ -738,26 +738,34 @@ class _ExplorerPageState extends State<ExplorerPage> {
/// 75: 창마다 뒤로 · 앞으로 (가 본 폴더로)
Future<void> _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<ExplorerPage> {
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<ExplorerPage> {
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<ExplorerPage> {
/// 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<Widget> children) {

View File

@ -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<IconButton>(find.ancestor(of: find.byTooltip('앞으로 (Alt+→)').first, matching: find.byType(IconButton)).first);
expect(fwd.onPressed, isNull);