diff --git a/lib/ui/explorer_page.dart b/lib/ui/explorer_page.dart index 5c2d0de..9d265e0 100644 --- a/lib/ui/explorer_page.dart +++ b/lib/ui/explorer_page.dart @@ -3167,8 +3167,9 @@ class _ExplorerPageState extends State { } Widget _buildPage(BuildContext context) { - return CallbackShortcuts( - bindings: { + // P0 (15 지적 10/17): 글자 칸 (빠른 필터 · 경로 등) 에 치는 중이면 파일 단축키를 쓰지 않는다 - 예전에는 CallbackShortcuts 가 + // 글자 칸보다 먼저 키를 받아, 칸에서 Delete 를 누르면 파일 지우기 창이 뜨고 Ctrl+A · C · V · Space 도 파일에 쓰였다 + final bindings = { // 83: 스타일에 맞는 단축키 (Total Commander: F5 복사 · F6 이동 · F7 새 폴더 · F8 지우기 · Insert/Space 표시 · // Ctrl+R 새로 고침 / Windows 탐색기 · X-plore: Ctrl+C · X · V · A · F5 새로 고침) ..._styleKeys(), @@ -3195,7 +3196,11 @@ class _ExplorerPageState extends State { const SingleActivator(LogicalKeyboardKey.arrowRight, alt: true): () => _travel(_panes[_active], forward: true), const SingleActivator(LogicalKeyboardKey.tab): () => setState(() => _active = _dual ? 1 - _active : 0), - }, + }; + return Focus( + canRequestFocus: false, + skipTraversal: true, + onKeyEvent: (_, ev) => _shortcut(bindings, ev), child: Focus( focusNode: _keys, autofocus: true, @@ -3652,6 +3657,21 @@ class _ExplorerPageState extends State { } } + /// 글자 칸에 치는 중인지 (그 칸이 키를 받아야 한다) + static bool get _typing => FocusManager.instance.primaryFocus?.context?.findAncestorStateOfType() != null; + + /// 탐색기 단축키: 글자 칸에 치는 중이면 넘기고 (칸의 기본 동작 - 선택 · 복사 · 붙여넣기 · 지우기 · 빈칸), 아니면 맞는 것을 한다 + KeyEventResult _shortcut(Map bindings, KeyEvent ev) { + if (ev is KeyUpEvent || _typing) return KeyEventResult.ignored; + for (final b in bindings.entries) { + if (b.key.accepts(ev, HardwareKeyboard.instance)) { + b.value(); + return KeyEventResult.handled; + } + } + return KeyEventResult.ignored; + } + KeyEventResult _typeToFilter(KeyEvent ev) { if (ev is! KeyDownEvent) return KeyEventResult.ignored; final ch = ev.character; @@ -3661,9 +3681,7 @@ class _ExplorerPageState extends State { if (ch == ' ' && _tcKeys && !_panes[_active].filterOpen) return KeyEventResult.ignored; if (kb.isControlPressed || kb.isAltPressed || kb.isMetaPressed) return KeyEventResult.ignored; // 글자 칸 (필터 · 경로 입력 등) 에 치는 중이면 그 칸이 받는다 - if (FocusManager.instance.primaryFocus?.context?.findAncestorStateOfType() != null) { - return KeyEventResult.ignored; - } + if (_typing) return KeyEventResult.ignored; if (!_ready || _panes.isEmpty) return KeyEventResult.ignored; final pane = _panes[_active]; _openFilter(pane, pane.filterOpen ? pane.filterCtl.text + ch : ch); diff --git a/test/explorer_test.dart b/test/explorer_test.dart index 0cda6dc..023ef43 100644 --- a/test/explorer_test.dart +++ b/test/explorer_test.dart @@ -1013,6 +1013,52 @@ void main() { expect(find.textContaining('담았습니다'), findsOneWidget); }); + // P0 (15 지적 10/17): 글자 칸에 치는 중에는 파일 단축키가 키를 가로채지 않는다 (예전: 필터 칸의 Delete → 파일 지우기 창) + for (final style in const ['windows', 'xplore', 'totalcmd']) { + testWidgets('P0 $style: 빠른 필터 칸에서 Ctrl+A · C · V · Delete · Space · F8 · Backspace 는 파일에 쓰이지 않는다', (tester) async { + c.settings.explorerStyle = style; + await open(tester); + await act(tester, () => tester.tap(find.text(style == 'totalcmd' ? 'doc' : 'doc.txt'))); + await settle(tester, () => false, rounds: 15); + // Ctrl+F 로 필터 칸 열고 글자 + Future ctrl(LogicalKeyboardKey k) => act(tester, () async { + await tester.sendKeyDownEvent(LogicalKeyboardKey.controlLeft); + await tester.sendKeyEvent(k); + await tester.sendKeyUpEvent(LogicalKeyboardKey.controlLeft); + }); + await ctrl(LogicalKeyboardKey.keyF); + await calm(tester); + final field = find.byType(TextField); + expect(field, findsOneWidget); + await tester.enterText(field, 'do'); + await calm(tester); + await ctrl(LogicalKeyboardKey.keyA); + await calm(tester); + final ctl = tester.widget(field).controller!; + expect(ctl.selection, const TextSelection(baseOffset: 0, extentOffset: 2), reason: 'Ctrl+A 는 칸의 글 전체 선택'); + await ctrl(LogicalKeyboardKey.keyC); + await calm(tester); + await ctrl(LogicalKeyboardKey.keyV); + await calm(tester); + for (final k in [LogicalKeyboardKey.delete, LogicalKeyboardKey.space, LogicalKeyboardKey.f8, LogicalKeyboardKey.backspace]) { + await act(tester, () => tester.sendKeyEvent(k)); + await calm(tester); + } + expect(find.textContaining('표시함'), findsNothing, reason: '파일이 표시되지 않음'); + expect(find.textContaining('담았습니다'), findsNothing, reason: '파일을 담지 않음'); + expect(find.textContaining('휴지통'), findsNothing, reason: '지우기 창이 뜨지 않음'); + expect(find.textContaining('지울까요'), findsNothing); + expect(File(p.join(left, 'doc.txt')).existsSync(), isTrue); + expect(field, findsOneWidget, reason: '칸은 그대로 열려 있음 (Backspace 로 위 폴더로 가지 않음)'); + // Esc 는 지금처럼 필터를 닫는다 + await act(tester, () => tester.sendKeyEvent(LogicalKeyboardKey.escape)); + await calm(tester); + expect(find.byType(TextField), findsNothing); + ScaffoldMessenger.of(tester.element(find.byType(ExplorerPage))).clearSnackBars(); + await calm(tester); + }); + } + testWidgets('84: 열 머리를 누르면 그 기준으로 · 다시 누르면 거꾸로 (처음: 모든 폴더 같은 정렬)', (tester) async { File(p.join(left, 'a_big.txt')).writeAsStringSync('x' * 50); c.settings.explorerStyle = 'windows';