WIP P0 파일 탐색기: 글자 칸에 치는 중에는 파일 단축키가 키를 가로채지 않게 (15 지적 · 관리자 P0) (검토 대기)

- 예전: 화면의 CallbackShortcuts 가 글자 칸 (빠른 필터 등) 보다 먼저 키를 받아, 칸에서 Delete 를 누르면 파일 지우기 창이 뜨고
  Ctrl+A · C · X · V · Space · F8 · Backspace 도 파일 동작으로 쓰였다 (Ctrl+V 는 파일을 지금 폴더에 붙여넣을 수 있었다).
  Delete · F2 · Backspace 는 83 전부터 있던 것.
- 이제 단축키는 Focus.onKeyEvent 로 받아, 글자 칸에 포커스가 있으면 넘긴다 (칸의 기본 동작). 모든 모양 · Rsync 화면 공통.
  Esc 는 지금처럼 필터 칸이 받아 필터를 닫는다.
- 찾기 · 이름 바꾸기 · 경로 입력은 창 (다른 경로) 이라 이 단축키 아래에 있지 않다.
- 시험: explorer_test P0 (windows · xplore · totalcmd 각각 - 필터 칸에서 Ctrl+A 는 글 전체 선택, Ctrl+C · V · Delete · Space · F8 ·
  Backspace 가 파일을 표시 · 담기 · 지우기 하지 않고 칸이 그대로, Esc 로 닫힘).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
묶음6
RyuJongJin 2026-10-17 12:34:15 +09:00
parent 5411558779
commit a780139eeb
2 changed files with 70 additions and 6 deletions

View File

@ -3167,8 +3167,9 @@ class _ExplorerPageState extends State<ExplorerPage> {
}
Widget _buildPage(BuildContext context) {
return CallbackShortcuts(
bindings: {
// P0 (15 지적 10/17): 글자 칸 (빠른 필터 · 경로 등) 에 치는 중이면 파일 단축키를 쓰지 않는다 - 예전에는 CallbackShortcuts 가
// 글자 칸보다 먼저 키를 받아, 칸에서 Delete 를 누르면 파일 지우기 창이 뜨고 Ctrl+A · C · V · Space 도 파일에 쓰였다
final bindings = <ShortcutActivator, VoidCallback>{
// 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<ExplorerPage> {
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<ExplorerPage> {
}
}
/// 글자 칸에 치는 중인지 (그 칸이 키를 받아야 한다)
static bool get _typing => FocusManager.instance.primaryFocus?.context?.findAncestorStateOfType<EditableTextState>() != null;
/// 탐색기 단축키: 글자 칸에 치는 중이면 넘기고 (칸의 기본 동작 - 선택 · 복사 · 붙여넣기 · 지우기 · 빈칸), 아니면 맞는 것을 한다
KeyEventResult _shortcut(Map<ShortcutActivator, VoidCallback> 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<ExplorerPage> {
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<EditableTextState>() != 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);

View File

@ -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<void> 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<TextField>(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';