JJ_MKVMaker/lib/ui/explorer/explorer_select.dart

249 lines
9.2 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

import 'dart:io';
import 'package:flutter/foundation.dart';
import 'package:flutter/gestures.dart' show kDoubleTapTimeout;
import 'package:flutter/services.dart' show HardwareKeyboard;
import '../../app/archive_browse.dart';
import '../../core/explorer/tree_rows.dart';
import '../../core/file_ops.dart' show FileEntry, isSameOrInside, samePath;
import '../../core/selection.dart';
import '../../core/vfs.dart' show isDav, vDirname;
import '../file_error.dart' show explainFileError;
import 'explorer_controller.dart';
import 'explorer_pane.dart';
/// 약속 (E5d-2): 탐색기 줄 누르기 · 고르기 (작은 컨트롤러). 규칙은 [ClickPolicy] (누르기 → 할 일) · [Selection] (표시) 이
/// 가지고, 여기는 화면 상태 (선택 모드 · 지금 창 · 펼치기 · 읽기) 에 이어 줄 뿐이다. 창 · 읽기는 [ExplorerController].
/// - 입력: 줄 누르기 [onTap] · 두 번 [onOpen] · ✔ 단추 [toggleMark] · Rsync [pick] · Shift 범위 · Ctrl 고르기.
/// - 파일을 여는 일은 부르는 쪽 [openFile] (file_opener 흐름).
/// - 지금 시각은 [clock] (폴더 두 번 누르기의 두 번째를 가리려고 - 시험은 정한 시각).
/// - 바뀌면 알림. 목록을 읽지 못하면 표시를 바꾸지 않고 까닭을 알림 (Host).
class ExplorerSelect extends ChangeNotifier {
ExplorerSelect(this.ctl, {required this.clock, required this.openFile});
final ExplorerController ctl;
final DateTime Function() clock;
final Future<void> Function(String path, ExplorerPane pane) openFile;
/// 선택 모드 (가운데 [선택]): 각 줄의 ✔ 가 보이고, 누르면 고르기 · 풀기
bool selecting = false;
/// 한 번 누르면 선택 · 두 번 누르면 열기 (아니면 한 번에 바로 열기)
bool get selectMode => ctl.c.settings.explorerClick != 'open';
/// 마지막으로 누른 폴더 · 시각 (두 번 누르기의 두 번째를 가리려고)
(String, DateTime)? _lastFolderTap;
void setSelecting(bool on) {
selecting = on;
if (!on) {
for (final x in ctl.panes) {
x.sel.clear();
}
}
notifyListeners();
}
void _activate(ExplorerPane pane) {
ctl.active = ctl.panes.indexOf(pane);
ctl.update();
}
/// 줄의 종류 ([ClickPolicy] 에 넘김)
static ItemKind kindOf(TreeRow row) => row.isUp
? ItemKind.up
: row.isRoot
? ItemKind.root
: row.entry.isDir
? ItemKind.folder
: ItemKind.file;
/// 한 번 누르기 - Rsync 는 폴더 하나 고르기, Shift = 범위 · Ctrl = 고르기 (PC), 그 밖은 [ClickPolicy]
Future<void> onTap(ExplorerPane pane, TreeRow row, {bool list = false}) async {
final e = row.entry;
_activate(pane);
if (ctl.rsync && !row.isUp) return pick(pane, e);
final real = !row.isRoot && !row.isUp;
if (real && HardwareKeyboard.instance.isShiftPressed && canRange(pane, e)) {
if (!selecting) setSelecting(true);
return markRange(pane, e);
}
if (real && HardwareKeyboard.instance.isControlPressed) {
if (!selecting) setSelecting(true);
return toggleMark(pane, e);
}
final now = clock();
final last = _lastFolderTap;
_lastFolderTap = (e.path, now);
final repeat = last != null && samePath(last.$1, e.path) && now.difference(last.$2) < kDoubleTapTimeout * 2;
final act = ClickPolicy.decide(Press.tap, kindOf(row),
selectMode: selecting,
expanded: pane.expanded.contains(e.path),
list: list,
repeat: repeat,
tapOpens: !selectMode,
retapCollapses: ctl.c.settings.explorerRetapCollapse);
await _act(pane, row, act, list: list);
}
Future<void> _act(ExplorerPane pane, TreeRow row, ClickAction act, {bool list = false}) async {
final e = row.entry;
switch (act) {
case ClickAction.none:
case ClickAction.menu:
return;
case ClickAction.focus:
pane.focused = e.path;
if (!list) pane.current = vDirname(e.path);
pane.changed();
case ClickAction.open:
await onOpen(pane, row, list: list);
case ClickAction.collapse:
await toggle(pane, e);
case ClickAction.expand:
if (!pane.expanded.contains(e.path)) {
await toggle(pane, e);
} else if (!samePath(pane.current, e.path)) {
// 펼쳐 둔 폴더 (다시 눌러 접기를 끈 설정): 그 폴더를 지금 폴더로
pane
..current = e.path
..focused = e.path
..changed();
ctl.remember(pane, e.path);
}
case ClickAction.toggleMark:
if (!selecting) setSelecting(true);
await toggleMark(pane, e);
}
}
bool isMarked(ExplorerPane pane, String path) => pane.sel.isMarked(path);
Future<List<String>> _childPaths(ExplorerPane pane, String dir) async {
await ctl.load(pane, dir);
return [for (final x in pane.cache[dir] ?? const <FileEntry>[]) x.path];
}
/// Rsync 화면: 창마다 폴더 하나만 (규칙 8). 고르면 펼쳐서 안의 폴더가 보이게
Future<void> pick(ExplorerPane pane, FileEntry e) async {
if (!e.isDir) return;
await pane.sel.toggle(e.path, isDir: true, list: (d) => _childPaths(pane, d));
if (!ctl.host.alive) return;
notifyListeners();
if (!pane.sel.has(e.path)) return;
if (!pane.expanded.contains(e.path)) {
await toggle(pane, e);
} else {
pane
..current = e.path
..focused = e.path
..changed();
}
}
/// 74: 범위 고르기를 할 수 있는지 - 마지막으로 표시한 것이 같은 폴더에 있고 다른 항목
bool canRange(ExplorerPane pane, FileEntry e) {
final a = pane.sel.anchor;
return a != null && !samePath(a, e.path) && samePath(vDirname(a), vDirname(e.path)) && isMarked(pane, a);
}
/// 76: [dir] 에서 지금 보이는 것 (빠른 필터 · 목록에서 숨기기)
List<FileEntry> shown(ExplorerPane pane, String dir) => [
for (final x in pane.cache[dir] ?? const <FileEntry>[])
if (passesFilter(pane, x) && (ctl.showAppHidden || !ctl.appHidden(x.path))) x
];
void markRange(ExplorerPane pane, FileEntry to) {
pane.sel.range([for (final x in shown(pane, vDirname(to.path))) x.path], to.path);
pane.focused = to.path;
notifyListeners();
}
void invertMarks(ExplorerPane pane, String dir) {
pane.sel.invert([for (final x in shown(pane, dir)) x.path]);
notifyListeners();
}
void markWhere(ExplorerPane pane, String dir, bool Function(FileEntry e) test) {
pane.sel.addAll([for (final x in shown(pane, dir)) if (test(x)) x.path]);
notifyListeners();
}
/// [표시 지우기]: 이 창의 표시를 모두 풀기 (선택 모드는 그대로)
void clearMarks(ExplorerPane pane) {
pane.sel.clear();
notifyListeners();
}
/// 선택 모드에서 누르기 (규칙 5 · 6 은 [Selection.toggle]). 고른 폴더는 펼쳐서 안의 것도 고른 모양으로
Future<void> toggleMark(ExplorerPane pane, FileEntry e) async {
pane.focused = e.path;
try {
await pane.sel.toggle(e.path, isDir: e.isDir, list: (d) => _childPaths(pane, d));
} on FileSystemException catch (err) {
ctl.host.snack(explainFileError('$err', dav: isDav(e.path)).$1);
}
if (!ctl.host.alive) return;
notifyListeners();
if (e.isDir && pane.sel.has(e.path) && !pane.expanded.contains(e.path)) {
await ctl.load(pane, e.path);
if (ctl.host.alive) {
pane.expanded.add(e.path);
notifyListeners();
}
}
}
/// 열기: 트리의 폴더는 펼치기 (이름으로는 접지 않음 - 189), 목록의 폴더 · ".." 는 들어가기, 파일은 [openFile]
Future<void> onOpen(ExplorerPane pane, TreeRow row, {bool list = false}) async {
final e = row.entry;
_activate(pane);
pane.focused = e.path;
if (row.isUp || (e.isDir && list)) return ctl.goTo(pane, e.path);
if (e.isDir) {
if (!pane.expanded.contains(e.path)) {
await toggle(pane, e);
} else if (!samePath(pane.current, e.path)) {
pane
..current = e.path
..changed();
ctl.remember(pane, e.path);
}
return;
}
if (!list) pane.current = vDirname(e.path);
pane.changed();
await openFile(e.path, pane);
}
/// 트리에서 펼칠 수 있는 압축 (이 기기의 파일 · 탐색기 화면)
bool canBrowseArchive(FileEntry e) => !ctl.rsync && ArchiveBrowser.canBrowse(e);
/// 트리 폴더 펼치기 · 접기 (› 화살표 · 열기). 압축은 폴더처럼 (읽기 전용)
Future<void> toggle(ExplorerPane pane, FileEntry e) async {
_activate(pane);
if (canBrowseArchive(e) && !pane.expanded.contains(e.path)) {
if (!await ctl.readArchive(e.path)) return;
pane.cache.remove(e.path);
pane.expanded.add(e.path);
await ctl.load(pane, e.path);
return;
}
if (pane.expanded.contains(e.path)) {
pane.expanded.remove(e.path);
// 접은 폴더 안이 지금 폴더였으면 접은 폴더로
if (isSameOrInside(pane.current, e.path)) pane.current = e.path;
pane.changed();
return;
}
pane
..expanded.add(e.path)
..current = e.path
..focused = e.path;
await ctl.load(pane, e.path);
if (pane.listScroll.hasClients) pane.listScroll.jumpTo(0);
ctl.remember(pane, e.path);
}
}