diff --git a/assets/l10n/en.json b/assets/l10n/en.json index 76f12e3..4c2571d 100644 --- a/assets/l10n/en.json +++ b/assets/l10n/en.json @@ -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)", diff --git a/assets/l10n/ja.json b/assets/l10n/ja.json index f6cf56a..53b3cb4 100644 --- a/assets/l10n/ja.json +++ b/assets/l10n/ja.json @@ -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 更新", "단축키 해제": "ショートカット解除", "닫기": "閉じる", "닫힘 (실행 안 함)": "閉じた (実行しない)", diff --git a/assets/l10n/zh-Hans.json b/assets/l10n/zh-Hans.json index 83ce441..427d937 100644 --- a/assets/l10n/zh-Hans.json +++ b/assets/l10n/zh-Hans.json @@ -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 刷新", "단축키 해제": "注销快捷键", "닫기": "关闭", "닫힘 (실행 안 함)": "已关闭 (未执行)", diff --git a/lib/ui/explorer_page.dart b/lib/ui/explorer_page.dart index 1029c87..5c2d0de 100644 --- a/lib/ui/explorer_page.dart +++ b/lib/ui/explorer_page.dart @@ -3169,7 +3169,9 @@ class _ExplorerPageState extends State { 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 { _keys.requestFocus(); } + /// 83: 지금 스타일이 Total Commander 인지 (Rsync 화면은 스타일 단축키를 쓰지 않는다 - 늘 같은 버튼) + bool get _tcKeys => !widget.rsync && _look == ExplorerStyle.totalcmd; + + /// 83: 스타일에 맞는 단축키 + Map _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 _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() != null) { diff --git a/lib/ui/settings_page.dart b/lib/ui/settings_page.dart index 20ae353..ec88a9f 100644 --- a/lib/ui/settings_page.dart +++ b/lib/ui/settings_page.dart @@ -793,8 +793,13 @@ class _SettingsPageState extends State { ), 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( value: s.explorerStyle, items: [ diff --git a/test/explorer_test.dart b/test/explorer_test.dart index 804bee0..0cda6dc 100644 --- a/test/explorer_test.dart +++ b/test/explorer_test.dart @@ -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';