import 'package:flutter/services.dart'; import 'package:flutter/widgets.dart'; import '../l10n/tr.dart'; /// X-plore 비교 5-1 ⑥ (관리자 지시 10/21): 파일 탐색기 단축키 바꾸기. /// 동작마다 처음 키는 스타일대로 (83 - Total Commander / 그 밖) 이고, 사용자가 바꾼 것만 설정 (explorerKeyOverrides: 동작 → 키) 에 둔다. /// 바꾼 키가 다른 동작의 키와 같으면 바꾼 쪽이 이기고 그 동작은 그 키를 잃는다 (설정 화면이 먼저 알린다). Esc 는 바꾸지 않는다. const explorerKeyActions = [ 'copy', 'move', 'newFolder', 'delete', 'deletePermanent', 'rename', 'refresh', 'quickLook', // 'clipCopy', 'clipCut', 'paste', 'selectAll', 'mark', 'markNext', 'filter', 'up', 'back', 'forward', 'switchPane', ]; /// 동작 이름 (화면 글) String explorerKeyLabel(String id) => switch (id) { 'copy' => tr('다른 창으로 복사'), 'move' => tr('다른 창으로 이동'), 'newFolder' => tr('새 폴더'), 'delete' => tr('삭제'), 'deletePermanent' => tr('영구 삭제 (휴지통을 거치지 않음)'), 'rename' => tr('이름 변경'), 'refresh' => tr('새로 고침'), 'quickLook' => tr('빠른 보기'), 'clipCopy' => tr('복사 (붙여넣기로)'), 'clipCut' => tr('이동 (붙여넣기로)'), 'paste' => tr('붙여넣기'), 'selectAll' => tr('모두 표시'), 'mark' => tr('표시'), 'markNext' => tr('표시하고 다음 줄'), 'filter' => tr('빠른 필터'), 'up' => tr('상위 폴더'), 'back' => tr('뒤로'), 'forward' => tr('앞으로'), 'switchPane' => tr('다른 창으로 옮겨 가기'), _ => id, }; /// 스타일의 처음 키 (83 과 같음). [tc] Total Commander 스타일 Map> defaultExplorerKeys({required bool tc}) => { 'delete': [const SingleActivator(LogicalKeyboardKey.delete), if (tc) const SingleActivator(LogicalKeyboardKey.f8)], 'deletePermanent': [const SingleActivator(LogicalKeyboardKey.delete, shift: true)], 'rename': [const SingleActivator(LogicalKeyboardKey.f2)], 'filter': [const SingleActivator(LogicalKeyboardKey.keyF, control: true)], 'up': [const SingleActivator(LogicalKeyboardKey.backspace)], 'back': [const SingleActivator(LogicalKeyboardKey.arrowLeft, alt: true)], 'forward': [const SingleActivator(LogicalKeyboardKey.arrowRight, alt: true)], 'switchPane': [const SingleActivator(LogicalKeyboardKey.tab)], if (tc) ...{ 'copy': [const SingleActivator(LogicalKeyboardKey.f5)], 'move': [const SingleActivator(LogicalKeyboardKey.f6)], 'newFolder': [const SingleActivator(LogicalKeyboardKey.f7)], 'refresh': [const SingleActivator(LogicalKeyboardKey.keyR, control: true)], 'markNext': [const SingleActivator(LogicalKeyboardKey.insert)], 'mark': [const SingleActivator(LogicalKeyboardKey.space)], 'quickLook': [const SingleActivator(LogicalKeyboardKey.f3)], } else ...{ 'quickLook': [const SingleActivator(LogicalKeyboardKey.space)], 'refresh': [const SingleActivator(LogicalKeyboardKey.f5)], 'clipCopy': [const SingleActivator(LogicalKeyboardKey.keyC, control: true)], 'clipCut': [const SingleActivator(LogicalKeyboardKey.keyX, control: true)], 'paste': [const SingleActivator(LogicalKeyboardKey.keyV, control: true)], 'selectAll': [const SingleActivator(LogicalKeyboardKey.keyA, control: true)], }, }; /// 같은 키 (같은 글쇠 · 같은 Ctrl · Shift · Alt · Meta) bool sameKey(SingleActivator a, SingleActivator b) => a.trigger == b.trigger && a.control == b.control && a.shift == b.shift && a.alt == b.alt && a.meta == b.meta; /// 설정에 두는 꼴: "c,s,a,m|글쇠 번호" (없으면 '' = 그 동작은 단축키 없음). 틀리면 null String keySpec(SingleActivator a) => '${[if (a.control) 'c', if (a.shift) 's', if (a.alt) 'a', if (a.meta) 'm'].join(',')}|${a.trigger.keyId}'; SingleActivator? parseKeySpec(String spec) { final bar = spec.lastIndexOf('|'); if (bar < 0) return null; final id = int.tryParse(spec.substring(bar + 1)); final key = id == null ? null : LogicalKeyboardKey.findKeyByKeyId(id); if (key == null) return null; final mods = spec.substring(0, bar).split(','); return SingleActivator(key, control: mods.contains('c'), shift: mods.contains('s'), alt: mods.contains('a'), meta: mods.contains('m')); } /// 화면에 보일 글: "Ctrl+Shift+K" String keyText(SingleActivator a) { final name = a.trigger.keyLabel.isNotEmpty ? a.trigger.keyLabel : (a.trigger.debugName ?? '?'); return [if (a.control) 'Ctrl', if (a.shift) 'Shift', if (a.alt) 'Alt', if (a.meta) 'Meta', name == ' ' ? 'Space' : name].join('+'); } /// 바꾼 것을 넣은 동작마다의 키. 바꾼 키는 다른 동작에서 뺀다 Map> effectiveExplorerKeys({required bool tc, required Map overrides}) { final out = {for (final e in defaultExplorerKeys(tc: tc).entries) e.key: [...e.value]}; final mine = {}; for (final e in overrides.entries) { if (!explorerKeyActions.contains(e.key)) continue; mine[e.key] = e.value.isEmpty ? null : parseKeySpec(e.value); if (e.value.isNotEmpty && mine[e.key] == null) mine.remove(e.key); // 틀린 값은 처음 키 그대로 } for (final e in mine.entries) { out[e.key] = [?e.value]; } for (final e in mine.entries) { final k = e.value; if (k == null) continue; for (final other in out.keys) { if (other != e.key) out[other]!.removeWhere((x) => sameKey(x, k)); } } return out; } /// [action] 에 [key] 를 주면 키를 잃는 다른 동작 (없으면 null). Esc 는 쓸 수 없음 ('escape') String? keyConflict(String action, SingleActivator key, {required bool tc, required Map overrides}) { if (key.trigger == LogicalKeyboardKey.escape) return 'escape'; final now = effectiveExplorerKeys(tc: tc, overrides: overrides); for (final e in now.entries) { if (e.key != action && e.value.any((x) => sameKey(x, key))) return e.key; } return null; } /// 키를 받는 창에서 모자 키 (Ctrl · Shift · Alt · Meta) 만 누른 것은 아직 키가 아니다 bool isModifierOnly(LogicalKeyboardKey k) => { LogicalKeyboardKey.control, LogicalKeyboardKey.controlLeft, LogicalKeyboardKey.controlRight, // LogicalKeyboardKey.shift, LogicalKeyboardKey.shiftLeft, LogicalKeyboardKey.shiftRight, LogicalKeyboardKey.alt, LogicalKeyboardKey.altLeft, LogicalKeyboardKey.altRight, LogicalKeyboardKey.meta, LogicalKeyboardKey.metaLeft, LogicalKeyboardKey.metaRight, }.contains(k); /// 키 이벤트를 받는 쪽 (설정의 키 받기 창) 에서 쓰는 모자 상태 SingleActivator activatorFromEvent(KeyEvent e) { final hk = HardwareKeyboard.instance; return SingleActivator(e.logicalKey, control: hk.isControlPressed, shift: hk.isShiftPressed, alt: hk.isAltPressed, meta: hk.isMetaPressed); } /// 표시용 위젯이 키 글을 고정 폭 글로 TextStyle get keyTextStyle => const TextStyle(fontFamily: 'monospace', fontSize: 13);