WIP 83 파일 탐색기 스타일별 단축키: Total Commander F5~F8 · Insert/Space 표시, Windows 탐색기 Ctrl+C · X · V · A (검토 대기)

- 관리자 지시 (F12): 스타일에 맞는 단축키.
- Total Commander 모양: F5 복사 · F6 이동 · F7 새 폴더 · F8 지우기 · Insert (표시하고 다음 줄로) · Space (표시) · Ctrl+R 새로 고침.
  Space 는 이 모양에서 빠른 필터를 열지 않는다 (필터가 열려 있으면 그대로 글자).
- Windows 탐색기 · X-plore 모양: Ctrl+C (담기) · Ctrl+X (잘라내기) · Ctrl+V (지금 폴더에 붙여넣기) · Ctrl+A (지금 폴더 모두 표시) · F5 새로 고침 (예전과 같음).
- 모든 모양 공통 (예전과 같음): Delete · Shift+Delete · F2 · Esc · Ctrl+F · Backspace · Alt+← → · Tab.
- Rsync 화면은 스타일 단축키를 쓰지 않는다 (F5 새로 고침 그대로).
- 내 판단: TC 모양에서는 F5 가 복사라 새로 고침을 Ctrl+R 로 (Total Commander 와 같게).
- 환경 설정 > 파일 탐색기 > 스타일 설명에 단축키를 적었다.
- 시험: explorer_test 83 (TC: Insert · Space 표시 · Space 가 필터를 열지 않음 · F5 복사 창 · F7 새 폴더 / Windows: Ctrl+A · Ctrl+C).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
묶음6
RyuJongJin 2026-10-17 12:17:04 +09:00
parent 5007d30afa
commit 5411558779
6 changed files with 111 additions and 3 deletions

View File

@ -859,6 +859,7 @@
"다음부터 묻지 않기 (환경 설정 > 실행 에서 바꿀 수 있음)": "Don't ask again (can be changed in Settings > Run)",
"단계": "Steps",
"단어 첫 글자만 크게 (Abc)": "Capitalize words (Abc)",
"단축키 - Total Commander: F5 복사 · F6 이동 · F7 새 폴더 · F8 지우기 · Insert/Space 표시 · Ctrl+R 새로 고침. 그 밖: Ctrl+C · X · V (복사 · 잘라내기 · 붙여넣기) · Ctrl+A 모두 표시 · F5 새로 고침": "Shortcuts - Total Commander: F5 copy · F6 move · F7 new folder · F8 delete · Insert/Space mark · Ctrl+R refresh. Others: Ctrl+C · X · V (copy · cut · paste) · Ctrl+A mark all · F5 refresh",
"단축키 해제": "Unregister hotkey",
"닫기": "Close",
"닫힘 (실행 안 함)": "Closed (not run)",

View File

@ -859,6 +859,7 @@
"다음부터 묻지 않기 (환경 설정 > 실행 에서 바꿀 수 있음)": "次回から確認しない(環境設定 > 実行 で変更できます)",
"단계": "ステップ",
"단어 첫 글자만 크게 (Abc)": "単語の先頭だけ大文字 (Abc)",
"단축키 - Total Commander: F5 복사 · F6 이동 · F7 새 폴더 · F8 지우기 · Insert/Space 표시 · Ctrl+R 새로 고침. 그 밖: Ctrl+C · X · V (복사 · 잘라내기 · 붙여넣기) · Ctrl+A 모두 표시 · F5 새로 고침": "ショートカット - Total Commander: F5 コピー · F6 移動 · F7 新しいフォルダー · F8 削除 · Insert/Space マーク · Ctrl+R 更新。その他: Ctrl+C · X · V (コピー · 切り取り · 貼り付け) · Ctrl+A すべてマーク · F5 更新",
"단축키 해제": "ショートカット解除",
"닫기": "閉じる",
"닫힘 (실행 안 함)": "閉じた (実行しない)",

View File

@ -859,6 +859,7 @@
"다음부터 묻지 않기 (환경 설정 > 실행 에서 바꿀 수 있음)": "下次不再询问(可在 设置 > 运行 中更改)",
"단계": "步数",
"단어 첫 글자만 크게 (Abc)": "单词首字母大写 (Abc)",
"단축키 - Total Commander: F5 복사 · F6 이동 · F7 새 폴더 · F8 지우기 · Insert/Space 표시 · Ctrl+R 새로 고침. 그 밖: Ctrl+C · X · V (복사 · 잘라내기 · 붙여넣기) · Ctrl+A 모두 표시 · F5 새로 고침": "快捷键 - Total Commander: F5 复制 · F6 移动 · F7 新建文件夹 · F8 删除 · Insert/Space 标记 · Ctrl+R 刷新。其他: Ctrl+C · X · V (复制 · 剪切 · 粘贴) · Ctrl+A 全部标记 · F5 刷新",
"단축키 해제": "注销快捷键",
"닫기": "关闭",
"닫힘 (실행 안 함)": "已关闭 (未执行)",

View File

@ -3169,7 +3169,9 @@ class _ExplorerPageState extends State<ExplorerPage> {
Widget _buildPage(BuildContext context) {
return CallbackShortcuts(
bindings: {
const SingleActivator(LogicalKeyboardKey.f5): () => _button(ExplorerButton.refresh),
// 83: 스타일에 맞는 단축키 (Total Commander: F5 복사 · F6 이동 · F7 새 폴더 · F8 지우기 · Insert/Space 표시 ·
// Ctrl+R 새로 고침 / Windows 탐색기 · X-plore: Ctrl+C · X · V · A · F5 새로 고침)
..._styleKeys(),
const SingleActivator(LogicalKeyboardKey.delete): () => _button(ExplorerButton.delete),
// 65: Shift+Delete = 휴지통을 거치지 않고 (창에서 한 번 더 확인)
const SingleActivator(LogicalKeyboardKey.delete, shift: true): () {
@ -3603,11 +3605,60 @@ class _ExplorerPageState extends State<ExplorerPage> {
_keys.requestFocus();
}
/// 83: 지금 스타일이 Total Commander 인지 (Rsync 화면은 스타일 단축키를 쓰지 않는다 - 늘 같은 버튼)
bool get _tcKeys => !widget.rsync && _look == ExplorerStyle.totalcmd;
/// 83: 스타일에 맞는 단축키
Map<ShortcutActivator, VoidCallback> _styleKeys() {
if (widget.rsync) return {const SingleActivator(LogicalKeyboardKey.f5): () => _button(ExplorerButton.refresh)};
if (_tcKeys) {
return {
const SingleActivator(LogicalKeyboardKey.f5): () => _button(ExplorerButton.copy),
const SingleActivator(LogicalKeyboardKey.f6): () => _button(ExplorerButton.move),
const SingleActivator(LogicalKeyboardKey.f7): () => _button(ExplorerButton.newFolder),
const SingleActivator(LogicalKeyboardKey.f8): () => _button(ExplorerButton.delete),
const SingleActivator(LogicalKeyboardKey.keyR, control: true): () => _button(ExplorerButton.refresh),
// Insert: 표시하고 다음 줄로 · Space: 표시 (TC 처럼)
const SingleActivator(LogicalKeyboardKey.insert): () => _markFocused(next: true),
const SingleActivator(LogicalKeyboardKey.space): () => _markFocused(next: false),
};
}
return {
const SingleActivator(LogicalKeyboardKey.f5): () => _button(ExplorerButton.refresh),
const SingleActivator(LogicalKeyboardKey.keyC, control: true): () => _toClipboard(_targets(_pane), move: false),
const SingleActivator(LogicalKeyboardKey.keyX, control: true): () => _toClipboard(_targets(_pane), move: true),
const SingleActivator(LogicalKeyboardKey.keyV, control: true): () => _paste(_pane, _pane.current),
const SingleActivator(LogicalKeyboardKey.keyA, control: true): () {
if (!_selecting) setState(() => _selecting = true);
_markWhere(_pane, _pane.current, (_) => true);
},
};
}
/// 83: (TC) 지금 줄을 표시 · 풀기, [next] 면 다음 줄로
Future<void> _markFocused({required bool next}) async {
final pane = _pane;
final f = pane.focused;
if (f == null || samePath(f, pane.root)) return;
final list = _shown(pane, vDirname(f));
final i = list.indexWhere((x) => samePath(x.path, f));
if (i < 0) return;
if (!_selecting) setState(() => _selecting = true);
await _toggleMark(pane, list[i]);
if (next && i + 1 < list.length) {
pane.focused = list[i + 1].path;
pane.changed();
_reveal(pane, list[i + 1].path);
}
}
KeyEventResult _typeToFilter(KeyEvent ev) {
if (ev is! KeyDownEvent) return KeyEventResult.ignored;
final ch = ev.character;
final kb = HardwareKeyboard.instance;
if (ch == null || ch.isEmpty || ch.runes.first < 0x20 || ch.runes.first == 0x7f) return KeyEventResult.ignored;
// 83: TC 모양에서 Space 는 표시 (빠른 필터가 아니라)
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) {

View File

@ -793,8 +793,13 @@ class _SettingsPageState extends State<SettingsPage> {
),
SettingTile(
title: Text(tr('스타일')),
subtitle: Text(tr('아이콘 · 목록 모양: X-plore (썸네일 · 두 줄) · Windows 탐색기 (컬러 아이콘 · 열) · '
'Total Commander (촘촘한 목록 · [폴더])')),
// 83: 스타일마다 단축키
subtitle: Text([
tr('아이콘 · 목록 모양: X-plore (썸네일 · 두 줄) · Windows 탐색기 (컬러 아이콘 · 열) · '
'Total Commander (촘촘한 목록 · [폴더])'),
tr('단축키 - Total Commander: F5 복사 · F6 이동 · F7 새 폴더 · F8 지우기 · Insert/Space 표시 · Ctrl+R 새로 고침. '
'그 밖: Ctrl+C · X · V (복사 · 잘라내기 · 붙여넣기) · Ctrl+A 모두 표시 · F5 새로 고침'),
].join('\n')),
trailing: DropdownButton<String>(
value: s.explorerStyle,
items: [

View File

@ -964,6 +964,55 @@ void main() {
});
});
testWidgets('83: Total Commander 모양 단축키 - F5 복사 · F7 새 폴더 · Space/Insert 표시 (Space 는 빠른 필터가 아님)', (tester) async {
File(p.join(left, 'e.txt')).writeAsStringSync('e');
c.settings.explorerStyle = 'totalcmd';
await open(tester);
await act(tester, () => tester.tap(find.text('doc')));
await settle(tester, () => false, rounds: 15);
// Insert: 표시하고 다음 줄 (e.txt) 로, Space: 지금 줄 (e.txt) 표시
await act(tester, () => tester.sendKeyEvent(LogicalKeyboardKey.insert));
await calm(tester);
expect(find.textContaining('1개 표시함'), findsOneWidget);
await act(tester, () => tester.sendKeyEvent(LogicalKeyboardKey.space));
await calm(tester);
expect(find.textContaining('2개 표시함'), findsOneWidget, reason: 'Insert 뒤 다음 줄이 표시됨');
expect(find.byIcon(Icons.filter_alt), findsNothing, reason: 'Space 가 빠른 필터를 열지 않는다');
// F5 = 복사 (확인 창) - 취소
await act(tester, () => tester.sendKeyEvent(LogicalKeyboardKey.f5));
await calm(tester);
expect(find.widgetWithText(FilledButton, '복사'), findsOneWidget);
await tester.tap(find.text('취소').last);
await calm(tester);
// F7 = 새 폴더
await act(tester, () => tester.sendKeyEvent(LogicalKeyboardKey.f7));
await calm(tester);
expect(find.text('새 폴더'), findsWidgets);
await tester.tap(find.text('취소').last);
await calm(tester);
});
testWidgets('83: Windows 탐색기 모양 단축키 - Ctrl+A 모두 표시 · Ctrl+C 담기 · Ctrl+V 붙여넣기', (tester) async {
c.settings.explorerStyle = 'windows';
await open(tester);
await act(tester, () => tester.tap(find.text('doc.txt')));
await settle(tester, () => false, rounds: 15);
await act(tester, () async {
await tester.sendKeyDownEvent(LogicalKeyboardKey.controlLeft);
await tester.sendKeyEvent(LogicalKeyboardKey.keyA);
await tester.sendKeyUpEvent(LogicalKeyboardKey.controlLeft);
});
await calm(tester);
expect(find.textContaining('2개 표시함'), findsOneWidget, reason: 'doc.txt · sub');
await act(tester, () async {
await tester.sendKeyDownEvent(LogicalKeyboardKey.controlLeft);
await tester.sendKeyEvent(LogicalKeyboardKey.keyC);
await tester.sendKeyUpEvent(LogicalKeyboardKey.controlLeft);
});
await calm(tester);
expect(find.textContaining('담았습니다'), findsOneWidget);
});
testWidgets('84: 열 머리를 누르면 그 기준으로 · 다시 누르면 거꾸로 (처음: 모든 폴더 같은 정렬)', (tester) async {
File(p.join(left, 'a_big.txt')).writeAsStringSync('x' * 50);
c.settings.explorerStyle = 'windows';