140 lines
7.1 KiB
Dart
140 lines
7.1 KiB
Dart
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 = <String>[
|
|
'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<String, List<SingleActivator>> 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<String, List<SingleActivator>> effectiveExplorerKeys({required bool tc, required Map<String, String> overrides}) {
|
|
final out = {for (final e in defaultExplorerKeys(tc: tc).entries) e.key: [...e.value]};
|
|
final mine = <String, SingleActivator?>{};
|
|
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<String, String> 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);
|