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 bindings(BuildContext context) { final cb = { '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.explorerKeyOverrides); 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 _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() != null; /// 탐색기 단축키: 글자 칸에 치는 중이면 넘기고 (칸의 기본 동작 - 선택 · 복사 · 붙여넣기 · 지우기 · 빈칸), 아니면 맞는 것을 한다 KeyEventResult shortcut(Map 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; } }