JJ_MKVMaker/lib/ui/explorer_keys.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);