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
parent
5411558779
commit
a780139eeb
|
|
@ -3167,8 +3167,9 @@ class _ExplorerPageState extends State<ExplorerPage> {
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildPage(BuildContext context) {
|
Widget _buildPage(BuildContext context) {
|
||||||
return CallbackShortcuts(
|
// P0 (15 지적 10/17): 글자 칸 (빠른 필터 · 경로 등) 에 치는 중이면 파일 단축키를 쓰지 않는다 - 예전에는 CallbackShortcuts 가
|
||||||
bindings: {
|
// 글자 칸보다 먼저 키를 받아, 칸에서 Delete 를 누르면 파일 지우기 창이 뜨고 Ctrl+A · C · V · Space 도 파일에 쓰였다
|
||||||
|
final bindings = <ShortcutActivator, VoidCallback>{
|
||||||
// 83: 스타일에 맞는 단축키 (Total Commander: F5 복사 · F6 이동 · F7 새 폴더 · F8 지우기 · Insert/Space 표시 ·
|
// 83: 스타일에 맞는 단축키 (Total Commander: F5 복사 · F6 이동 · F7 새 폴더 · F8 지우기 · Insert/Space 표시 ·
|
||||||
// Ctrl+R 새로 고침 / Windows 탐색기 · X-plore: Ctrl+C · X · V · A · F5 새로 고침)
|
// Ctrl+R 새로 고침 / Windows 탐색기 · X-plore: Ctrl+C · X · V · A · F5 새로 고침)
|
||||||
..._styleKeys(),
|
..._styleKeys(),
|
||||||
|
|
@ -3195,7 +3196,11 @@ class _ExplorerPageState extends State<ExplorerPage> {
|
||||||
const SingleActivator(LogicalKeyboardKey.arrowRight, alt: true): () => _travel(_panes[_active], forward: true),
|
const SingleActivator(LogicalKeyboardKey.arrowRight, alt: true): () => _travel(_panes[_active], forward: true),
|
||||||
const SingleActivator(LogicalKeyboardKey.tab): () =>
|
const SingleActivator(LogicalKeyboardKey.tab): () =>
|
||||||
setState(() => _active = _dual ? 1 - _active : 0),
|
setState(() => _active = _dual ? 1 - _active : 0),
|
||||||
},
|
};
|
||||||
|
return Focus(
|
||||||
|
canRequestFocus: false,
|
||||||
|
skipTraversal: true,
|
||||||
|
onKeyEvent: (_, ev) => _shortcut(bindings, ev),
|
||||||
child: Focus(
|
child: Focus(
|
||||||
focusNode: _keys,
|
focusNode: _keys,
|
||||||
autofocus: true,
|
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) {
|
KeyEventResult _typeToFilter(KeyEvent ev) {
|
||||||
if (ev is! KeyDownEvent) return KeyEventResult.ignored;
|
if (ev is! KeyDownEvent) return KeyEventResult.ignored;
|
||||||
final ch = ev.character;
|
final ch = ev.character;
|
||||||
|
|
@ -3661,9 +3681,7 @@ class _ExplorerPageState extends State<ExplorerPage> {
|
||||||
if (ch == ' ' && _tcKeys && !_panes[_active].filterOpen) return KeyEventResult.ignored;
|
if (ch == ' ' && _tcKeys && !_panes[_active].filterOpen) return KeyEventResult.ignored;
|
||||||
if (kb.isControlPressed || kb.isAltPressed || kb.isMetaPressed) return KeyEventResult.ignored;
|
if (kb.isControlPressed || kb.isAltPressed || kb.isMetaPressed) return KeyEventResult.ignored;
|
||||||
// 글자 칸 (필터 · 경로 입력 등) 에 치는 중이면 그 칸이 받는다
|
// 글자 칸 (필터 · 경로 입력 등) 에 치는 중이면 그 칸이 받는다
|
||||||
if (FocusManager.instance.primaryFocus?.context?.findAncestorStateOfType<EditableTextState>() != null) {
|
if (_typing) return KeyEventResult.ignored;
|
||||||
return KeyEventResult.ignored;
|
|
||||||
}
|
|
||||||
if (!_ready || _panes.isEmpty) return KeyEventResult.ignored;
|
if (!_ready || _panes.isEmpty) return KeyEventResult.ignored;
|
||||||
final pane = _panes[_active];
|
final pane = _panes[_active];
|
||||||
_openFilter(pane, pane.filterOpen ? pane.filterCtl.text + ch : ch);
|
_openFilter(pane, pane.filterOpen ? pane.filterCtl.text + ch : ch);
|
||||||
|
|
|
||||||
|
|
@ -1013,6 +1013,52 @@ void main() {
|
||||||
expect(find.textContaining('담았습니다'), findsOneWidget);
|
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 {
|
testWidgets('84: 열 머리를 누르면 그 기준으로 · 다시 누르면 거꾸로 (처음: 모든 폴더 같은 정렬)', (tester) async {
|
||||||
File(p.join(left, 'a_big.txt')).writeAsStringSync('x' * 50);
|
File(p.join(left, 'a_big.txt')).writeAsStringSync('x' * 50);
|
||||||
c.settings.explorerStyle = 'windows';
|
c.settings.explorerStyle = 'windows';
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue