JJ_MKVMaker/lib/ui/explorer/key_flow.dart

176 lines
8.3 KiB
Dart

import 'dart:io';
import 'package:flutter/gestures.dart' show kBackMouseButton, kForwardMouseButton;
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import '../../core/file_ops.dart' show samePath;
import '../../core/vfs.dart' show vDirname;
import '../explorer_keys.dart' show defaultExplorerKeys, effectiveExplorerKeys;
import 'explorer_buttons.dart';
import 'explorer_controller.dart';
import 'explorer_flows.dart';
import 'explorer_pane.dart';
import 'explorer_select.dart';
/// 약속 (E5d-6): 키보드 · 마우스로 하는 일. [bindings]: 동작마다의 키 → 할 일 (83 · 5-1 ⑥: 처음 키는 스타일대로, 사용자가 바꾼 것은
/// 설정, Rsync 화면은 늘 같은 키). 글자 칸에 치는 중이면 키를 넘긴다 ([shortcut]). 목록에서 글자를 치면 빠른 필터가 그 글자로
/// 열린다 ([typeToFilter], 76). 마우스 4 · 5번은 그 창의 뒤로 · 앞으로 ([mouseTravel], 75). 단축키를 받는 곳은 [focus]
class ExplorerInput {
ExplorerInput(this.ctl, this.sel, this.flows, this.buttons, {required this.dual});
final ExplorerController ctl;
final ExplorerSelect sel;
final ExplorerFlows flows;
final ExplorerButtons buttons;
/// 두 창으로 보이는지 ([창 바꾸기] 키)
final bool Function() dual;
/// 화면 단축키 (F5 · Delete …) 를 받는 곳. 76: 빠른 필터 칸을 닫으면 여기로 돌려준다
final focus = FocusNode(debugLabel: 'explorer');
void dispose() => focus.dispose();
/// 75: 마우스 4 · 5번 - 그 창의 뒤로 · 앞으로 (그 창을 활성으로).
/// 창 위의 누름은 창 ([ExplorerPaneFrame]) 과 화면 Listener 가 둘 다 받는다. 받는 곳마다 좌표를 바꾼 새 이벤트라 객체로는 같은지 알 수 없어
/// 누른 손가락 (pointer) · 시각으로 한 번만 쓴다 (창이 먼저 받는다)
(int, Duration)? _navPress;
void mouseTravel(int i, PointerDownEvent ev) {
if (i >= ctl.panes.length) return;
final back = ev.buttons & kBackMouseButton != 0;
if (!back && ev.buttons & kForwardMouseButton == 0) return;
final press = (ev.pointer, ev.timeStamp);
if (press == _navPress) return;
_navPress = press;
ctl.travel(ctl.panes[i], forward: !back);
}
/// 76: 빠른 필터 열기 ([text] 가 있으면 그 글자로 시작)
void openFilter(ExplorerPane pane, [String? text]) {
ctl.active = ctl.panes.indexOf(pane);
pane.filterOpen = true;
if (text != null) {
pane.filterCtl.value = TextEditingValue(text: text, selection: TextSelection.collapsed(offset: text.length));
}
ctl.update();
WidgetsBinding.instance.addPostFrameCallback((_) => pane.filterFocus.requestFocus());
// 거른 결과 (지금 폴더 아래) 가 보이게
ctl.host.reveal(pane, pane.current);
}
/// 76: 닫으면 글자도 지워 모두 보인다. 줄이 다시 늘어나 맨 위로 가지 않게 고른 것 (없으면 지금 폴더) 으로
void closeFilter(ExplorerPane pane) {
pane
..filterOpen = false
..filterCtl.clear();
ctl.update();
ctl.host.reveal(pane, pane.focused ?? pane.current);
focus.requestFocus();
}
/// 83: 지금 스타일이 Total Commander 인지 (Rsync 화면은 스타일 단축키를 쓰지 않는다 - 늘 같은 버튼)
bool get _tcKeys => !ctl.rsync && Platform.isWindows;
/// 83 · X-plore 비교 5-1 ⑥ (관리자 지시 10/21): 동작마다의 키 → 할 일. 처음 키는 스타일대로 (Total Commander: F5 복사 · F6 이동 ·
/// F7 새 폴더 · F8 지우기 · Insert/Space 표시 · Ctrl+R 새로 고침 / Windows 탐색기 · X-plore: Ctrl+C · X · V · A · F5 새로 고침),
/// 사용자가 바꾼 것은 설정 explorerKeyOverrides. Rsync 화면은 늘 같은 키 (바꾼 것을 쓰지 않음)
Map<ShortcutActivator, VoidCallback> bindings(BuildContext context) {
final cb = <String, VoidCallback>{
'copy': () => buttons.press(ExplorerButton.copy),
'move': () => buttons.press(ExplorerButton.move),
'newFolder': () => buttons.press(ExplorerButton.newFolder),
'delete': () => buttons.press(ExplorerButton.delete),
// 65: Shift+Delete = 휴지통을 거치지 않고 (창에서 한 번 더 확인)
'deletePermanent': () {
flows.deletes.shiftDelete = true;
buttons.press(ExplorerButton.delete);
},
'rename': () => buttons.press(ExplorerButton.rename),
'refresh': () => buttons.press(ExplorerButton.refresh),
// 81: 빠른 보기
'quickLook': () => flows.opener.quickLook(context, ctl.pane),
'clipCopy': () => flows.transfers.toClipboard(sel.targets(ctl.pane), move: false),
'clipCut': () => flows.transfers.toClipboard(sel.targets(ctl.pane), move: true),
'paste': () => flows.transfers.paste(context, ctl.pane, ctl.pane.current),
'selectAll': () {
if (!sel.selecting) sel.setSelecting(true);
sel.markWhere(ctl.pane, ctl.pane.current, (_) => true);
},
// TC: Space 표시 · Insert 표시하고 다음 줄로
'mark': () => _markFocused(next: false),
'markNext': () => _markFocused(next: true),
'filter': () => openFilter(ctl.panes[ctl.active]),
'up': () => buttons.press(ExplorerButton.up),
// 75: 지금 창의 뒤로 · 앞으로
'back': () => ctl.travel(ctl.panes[ctl.active], forward: false),
'forward': () => ctl.travel(ctl.panes[ctl.active], forward: true),
'switchPane': () {
ctl
..active = dual() ? 1 - ctl.active : 0
..update();
},
};
final keys = ctl.rsync
? {
for (final e in defaultExplorerKeys(tc: false).entries)
if (const {'delete', 'deletePermanent', 'rename', 'refresh', 'filter', 'up', 'back', 'forward', 'switchPane'}.contains(e.key))
e.key: e.value,
}
: effectiveExplorerKeys(tc: _tcKeys, overrides: ctl.c.settings.explorer.keyOverrides);
return {
for (final e in keys.entries)
if (cb[e.key] != null)
for (final a in e.value) a: cb[e.key]!,
};
}
/// 83: (TC) 지금 줄을 표시 · 풀기, [next] 면 다음 줄로
Future<void> _markFocused({required bool next}) async {
final pane = ctl.pane;
final f = pane.focused;
if (f == null || samePath(f, pane.root)) return;
final list = sel.shown(pane, vDirname(f));
final i = list.indexWhere((x) => samePath(x.path, f));
if (i < 0) return;
if (!sel.selecting) sel.setSelecting(true);
await sel.toggleMark(pane, list[i]);
if (next && i + 1 < list.length) {
pane.focused = list[i + 1].path;
pane.changed();
ctl.host.reveal(pane, list[i + 1].path);
}
}
/// 글자 칸에 치는 중인지 (그 칸이 키를 받아야 한다)
static bool get _typing => FocusManager.instance.primaryFocus?.context?.findAncestorStateOfType<EditableTextState>() != null;
/// 탐색기 단축키: 글자 칸에 치는 중이면 넘기고 (칸의 기본 동작 - 선택 · 복사 · 붙여넣기 · 지우기 · 빈칸), 아니면 맞는 것을 한다
KeyEventResult shortcut(Map<ShortcutActivator, VoidCallback> bindings, KeyEvent ev) {
if (ev is KeyUpEvent || _typing) return KeyEventResult.ignored;
for (final b in bindings.entries) {
if (b.key.accepts(ev, HardwareKeyboard.instance)) {
b.value();
return KeyEventResult.handled;
}
}
return KeyEventResult.ignored;
}
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 · 81: Space 는 표시 (TC) · 빠른 보기 (그 밖) - 빠른 필터를 열지 않는다 (필터가 열려 있으면 그대로 글자)
if (ch == ' ' && !ctl.rsync && !ctl.panes[ctl.active].filterOpen) return KeyEventResult.ignored;
if (kb.isControlPressed || kb.isAltPressed || kb.isMetaPressed) return KeyEventResult.ignored;
// 글자 칸 (필터 · 경로 입력 등) 에 치는 중이면 그 칸이 받는다
if (_typing) return KeyEventResult.ignored;
if (!ctl.ready || ctl.panes.isEmpty) return KeyEventResult.ignored;
final pane = ctl.panes[ctl.active];
openFilter(pane, pane.filterOpen ? pane.filterCtl.text + ch : ch);
return KeyEventResult.handled;
}
}