E6 보충 (관리자 결정 10/23): 지운 뒤 초점 · 좁은 TC 줄의 확장자 · TC 전용 점검 단계
- 지운 뒤 초점: 지운 줄의 다음 줄로, 마지막이었으면 앞 줄로 (TC · X-plore 같음) - focus_after_delete (순수 함수, 약속 시험 4) +
지우기 흐름이 부름. 실제 화면 시험: TC 목록에서 F8 단추로 지우면 다음 줄이 지금 줄
- TC: 확장자 칸이 없으면 (좁은 줄 · 좁아 숨긴 열) 이름에 확장자를 붙인다 ('notes.txt') - 정보를 숨기지 않게.
기존 큰 글자 시험 3곳의 TC 기대를 'a' → 'a.txt' 로 (이 결정에 따른 것, 주석에 적음)
- 탐색기 점검 시험: TC 전용 단계 (Backspace 로 맨 위 · 메뉴 [열기] 로 들어가기 · Backspace 로 나오기 · F5 · F6 · F8 · 지운 뒤 초점,
단계마다 폴더 시각 다시 고정 + Ctrl+R). 두 번 누르기는 이 실제 시간 시험에서 열기로 잡히지 않아 단위 약속 시험이 본다
- 확인: 단위 1461 ×2 · 통합 6/6 ×2 · 캡처 201장 2회 0px 같음 (base9) · X-plore 는 '삭제 확인' 3장만 바뀜 (초점)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
modular
parent
ae3365bff1
commit
afe15620bd
|
|
@ -154,7 +154,11 @@ void main() {
|
|||
: name.contains('.')
|
||||
? name.substring(0, name.lastIndexOf('.'))
|
||||
: '[$name]';
|
||||
Finder txt(String name) => leftOf(find.text(shown(name)));
|
||||
// 좁은 TC 줄 (확장자 칸 없음) 은 이름에 확장자를 붙인다 - 두 꼴 가운데 보이는 것
|
||||
Finder txt(String name) {
|
||||
final a = find.text(shown(name));
|
||||
return leftOf(a.evaluate().isNotEmpty ? a : find.text(name));
|
||||
}
|
||||
Finder row(String name) => find.ancestor(of: txt(name), matching: find.byType(InkWell)).first;
|
||||
Finder tip(String s) => find.byWidgetPredicate((x) => x is Tooltip && (x.message ?? '').startsWith(s)).first;
|
||||
// 줄 끝 표시 단추: 이름 글과 같은 높이의 것
|
||||
|
|
@ -264,6 +268,76 @@ void main() {
|
|||
await t.tap(txt('report.pdf'));
|
||||
await settle(4);
|
||||
});
|
||||
// TC 전용 (관리자 지시 10/23): 목록 들어가고 나오기 (..) · 아래 단추 줄 F5 복사 · F6 이동 · F8 삭제 · 지운 뒤 초점
|
||||
if (style == 'totalcmd') {
|
||||
// 복사 · 이동 · 지우기는 받는 폴더의 시각을 지금으로 바꾼다 - TC 는 폴더 날짜 칸이 있어 다시 고정하고 새로 읽는다 (Ctrl+R)
|
||||
Future<void> fixDirTimes() async {
|
||||
await t.runAsync(() => Process.run('powershell', [
|
||||
'-NoProfile',
|
||||
'-Command',
|
||||
"Get-ChildItem -LiteralPath '${tmp.path}' -Recurse -Directory | ForEach-Object { \$_.LastWriteTime = [datetime]'2026-10-22 09:00' }",
|
||||
]));
|
||||
await t.sendKeyDownEvent(LogicalKeyboardKey.controlLeft);
|
||||
await t.sendKeyEvent(LogicalKeyboardKey.keyR);
|
||||
await t.sendKeyUpEvent(LogicalKeyboardKey.controlLeft);
|
||||
await settle();
|
||||
}
|
||||
|
||||
Future<void> fkey(String tip) async {
|
||||
await t.tap(find.byTooltip(tip));
|
||||
await settle();
|
||||
if (find.byType(Dialog).evaluate().isNotEmpty) await okDialog();
|
||||
await jobsGone();
|
||||
await fixDirTimes();
|
||||
}
|
||||
|
||||
await noticeTimesOut();
|
||||
// 선택 모드면 끈다 (선택 모드의 누르기는 표시 - 들어가기를 보려면 끈다)
|
||||
await step('TC - 선택 끝', () async {
|
||||
if (find.text('선택 끝').evaluate().isNotEmpty) await t.tap(find.text('선택 끝').first);
|
||||
});
|
||||
// 맨 위 (Videos) 로: [Docs] 가 왼쪽 창에 보일 때까지 Backspace (위 폴더 키) - 두 번 누르기는 위로 간 뒤 같은 자리의 다른 줄을 열었다
|
||||
await step('TC - Backspace 로 맨 위까지', () async {
|
||||
// 왼쪽 창을 지금 창으로 (창 위 남은 공간 줄 - 누르기 동작이 없는 자리)
|
||||
await t.tap(leftOf(find.textContaining('남은')));
|
||||
await settle(10);
|
||||
bool top() => find.text(shown('Docs')).evaluate().any((e) => t.getCenter(find.byWidget(e.widget).first).dx < w / 2);
|
||||
for (var i = 0; i < 4 && !top(); i++) {
|
||||
await t.sendKeyEvent(LogicalKeyboardKey.backspace);
|
||||
await settle();
|
||||
}
|
||||
});
|
||||
// 들어가기 · 나오기 (".." 두 번 누르기는 단위 약속 시험 explorer_tc_look_test 가 본다 - 이 실제 시간 시험에서는 두 번 누르기가
|
||||
// 열기로 잡히지 않았다): 줄 메뉴 [열기] 로 들어가고, 위 폴더 키 (Backspace) 로 나온다
|
||||
await step('TC - [Docs] 메뉴 열기 (들어가기)', () async {
|
||||
await t.tap(txt('Docs'), buttons: kSecondaryMouseButton);
|
||||
await settle();
|
||||
await t.tap(find.text('열기').last);
|
||||
await settle();
|
||||
});
|
||||
await step('TC - Backspace (나오기)', () async {
|
||||
await t.sendKeyEvent(LogicalKeyboardKey.backspace);
|
||||
await settle();
|
||||
});
|
||||
await step('TC - notes 고르고 F5 복사', () async {
|
||||
await t.tap(txt('notes.txt'));
|
||||
await settle(10); // 한 번 누르기는 두 번 누르기 기다림 (약 300ms) 뒤에 처리된다
|
||||
await fkey('F5 복사');
|
||||
});
|
||||
await noticeTimesOut();
|
||||
await step('TC - report 고르고 F6 이동', () async {
|
||||
await t.tap(txt('report.pdf'));
|
||||
await settle(10); // 한 번 누르기는 두 번 누르기 기다림 (약 300ms) 뒤에 처리된다
|
||||
await fkey('F6 이동');
|
||||
});
|
||||
await noticeTimesOut();
|
||||
await step('TC - notes 고르고 F8 삭제 (지운 뒤 초점)', () async {
|
||||
await t.tap(txt('notes.txt'));
|
||||
await settle(10); // 한 번 누르기는 두 번 누르기 기다림 (약 300ms) 뒤에 처리된다
|
||||
await fkey('F8 삭제');
|
||||
});
|
||||
files('TC 단계 뒤');
|
||||
}
|
||||
// 끝 상태: 실제 파일
|
||||
final left = [
|
||||
for (final e in tmp.listSync(recursive: true))
|
||||
|
|
|
|||
|
|
@ -0,0 +1,32 @@
|
|||
/// 약속 (관리자 결정 10/23, Total Commander · X-plore 와 같음): 지운 뒤 지금 줄 (초점) 을 어디로 둘지.
|
||||
/// [shown] 은 지우기 전 그 폴더에 보이던 차례 (경로), [gone] 은 지운 것 (그 안의 것도 지운 것으로 본다), [focused] 는 지금 줄.
|
||||
/// - 지금 줄이 남아 있으면 그대로 (null = 바꾸지 않음)
|
||||
/// - 지워졌으면 지운 줄의 다음 줄 가운데 남은 첫째로, 다음이 없으면 (마지막이었으면) 앞 줄 가운데 남은 마지막으로
|
||||
/// - 남은 줄이 하나도 없으면 '' (초점 없음)
|
||||
/// 경로 비교는 [same] (같은 경로인지) · [inside] (안에 있는지 - 같아도 참) 로 받는다 (Windows 대소문자 · dav:// 를 부르는 쪽이 맞춤)
|
||||
library;
|
||||
|
||||
String? focusAfterDelete(
|
||||
List<String> shown,
|
||||
Iterable<String> gone,
|
||||
String? focused, {
|
||||
required bool Function(String a, String b) same,
|
||||
required bool Function(String path, String dir) inside,
|
||||
}) {
|
||||
if (focused == null) return null;
|
||||
bool removed(String x) => gone.any((g) => inside(x, g));
|
||||
if (!removed(focused)) return null;
|
||||
var at = shown.indexWhere((x) => same(x, focused));
|
||||
if (at < 0) {
|
||||
// 지금 줄이 지운 폴더 안에 있었다 - 그 폴더 줄의 자리에서
|
||||
at = shown.indexWhere((x) => inside(focused, x) && removed(x));
|
||||
}
|
||||
if (at < 0) return null;
|
||||
for (var i = at + 1; i < shown.length; i++) {
|
||||
if (!removed(shown[i])) return shown[i];
|
||||
}
|
||||
for (var i = at - 1; i >= 0; i--) {
|
||||
if (!removed(shown[i])) return shown[i];
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
|
@ -5,6 +5,7 @@ import 'package:path/path.dart' as p;
|
|||
|
||||
import '../../app/delete_ops.dart';
|
||||
import '../../core/app_trash.dart';
|
||||
import '../../core/explorer/focus_after_delete.dart';
|
||||
import '../../core/file_ops.dart';
|
||||
import '../../core/vfs.dart';
|
||||
import '../../l10n/tr.dart';
|
||||
|
|
@ -120,6 +121,9 @@ class ExplorerDeleteFlow {
|
|||
]),
|
||||
);
|
||||
if (!ok) return;
|
||||
// 관리자 결정 10/23: 지운 뒤 초점은 지운 줄의 다음 줄로 (마지막이었으면 앞 줄로) - 지우기 전의 차례를 적어 둔다
|
||||
final focusedBefore = pane.focused;
|
||||
final shownBefore = focusedBefore == null ? const <String>[] : [for (final e in sel.shown(pane, vDirname(focusedBefore))) e.path];
|
||||
// 하나씩: 실패해도 나머지는 계속. 실제로 어떻게 됐는지 세어 그대로 알린다 (94) - 지우기 규칙은 delete_ops (E5b)
|
||||
final report = await deletePaths(paths, permanent: permanent, trash: !permanent && canTrash ? trash : null);
|
||||
final failed = report.failed, tooLong = report.tooLong, sent = report.sent, trashed = report.trashed;
|
||||
|
|
@ -228,6 +232,14 @@ class ExplorerDeleteFlow {
|
|||
if (paths.any((s) => isSameOrInside(x.current, s))) x.current = vDirname(paths.first);
|
||||
}
|
||||
await ctl.refreshAll(paths.map(p.dirname));
|
||||
final gone = [for (final x in paths) if (!await vExists(x)) x];
|
||||
final next = focusAfterDelete(shownBefore, gone, focusedBefore, same: samePath, inside: isSameOrInside);
|
||||
// 그사이 사용자가 다른 줄로 옮겼으면 그대로 둔다 (새로 읽으며 지운 줄의 초점이 비워졌을 수도 있다)
|
||||
final cur = pane.focused;
|
||||
if (next != null && (cur == null || samePath(cur, focusedBefore!))) {
|
||||
pane.focused = next.isEmpty ? null : next;
|
||||
pane.changed();
|
||||
}
|
||||
}
|
||||
|
||||
/// X-plore 비교 e: Android 앱 휴지통 (보관 일이 0 이면 null - 바로 영구 삭제)
|
||||
|
|
|
|||
|
|
@ -92,8 +92,9 @@ class ExplorerRowView extends StatelessWidget {
|
|||
? '..'
|
||||
: row.isRoot
|
||||
? (ctl.volumes.firstWhere((v) => samePath(v.$1, e.path), orElse: () => (e.path, e.path)).$2)
|
||||
// TC: 확장자 칸이 있으면 이름에서 뺀다. 칸이 없으면 (좁은 줄 · 좁아 숨긴 열) 이름에 붙여 정보를 숨기지 않는다 (관리자 10/23)
|
||||
: look == ExplorerStyle.totalcmd
|
||||
? totalCmdName(e)
|
||||
? (!e.isDir && (tight || pane.colHide >= 1) ? e.name : totalCmdName(e))
|
||||
: e.name;
|
||||
final dim = TextStyle(fontSize: look.fontSize - 2, color: JjColors.textDim);
|
||||
final nameStyle = TextStyle(
|
||||
|
|
|
|||
|
|
@ -1,7 +1,10 @@
|
|||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:jj_mkvmaker/ui/explorer/tc_key_bar.dart';
|
||||
import 'package:jj_mkvmaker/ui/explorer_page.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
|
||||
import 'support/explorer_clock_keys.dart';
|
||||
|
||||
|
|
@ -56,7 +59,7 @@ void main() {
|
|||
t.view.devicePixelRatio = 1;
|
||||
addTearDown(t.view.reset);
|
||||
await t.runAsync(() => t.pumpWidget(MaterialApp(home: ExplorerPage(c: c, clock: () => testNow, device: device))));
|
||||
final name = style == 'totalcmd' ? 'doc' : 'doc.txt';
|
||||
const name = 'doc.txt'; // TC 도 좁은 줄은 확장자 칸이 없어 이름에 붙인다 (관리자 10/23)
|
||||
await settle(t, () => find.text(name).evaluate().isNotEmpty);
|
||||
// X-plore (줄 52) 는 크기 · 날짜가 이름 아래 한 줄, TC (줄 26) 는 두 줄이 들어가지 않아 이름만
|
||||
expect(find.textContaining('5B · '), style == 'xplore' ? findsWidgets : findsNothing, reason: '$style: 이름 아래 크기 · 날짜');
|
||||
|
|
@ -74,4 +77,25 @@ void main() {
|
|||
await t.pumpWidget(const SizedBox());
|
||||
}
|
||||
});
|
||||
|
||||
testWidgets('지운 뒤 초점: 지운 줄의 다음 줄로 (TC 목록 · F8)', (t) async {
|
||||
File(p.join(left, 'e.txt')).writeAsStringSync('e');
|
||||
c.settings.explorer.style = 'totalcmd';
|
||||
await open(t);
|
||||
await settle(t, () => find.text('[sub]').evaluate().isNotEmpty);
|
||||
await act(t, () => t.tap(find.text('doc').first));
|
||||
await settle(t, () => false, rounds: 15);
|
||||
// 아래 단추 줄의 [F8 삭제] (키 F8 과 같은 일) → [휴지통으로]
|
||||
await act(t, () => t.tap(find.byTooltip('F8 삭제')));
|
||||
await calm(t);
|
||||
expect(find.text('휴지통으로 보내기'), findsOneWidget);
|
||||
await act(t, () => t.tap(find.widgetWithText(FilledButton, '휴지통으로')));
|
||||
await settle(t, () => !File(p.join(left, 'doc.txt')).existsSync() && find.byType(SnackBar).evaluate().isNotEmpty);
|
||||
await calm(t);
|
||||
expect(File(p.join(left, 'doc.txt')).existsSync(), isFalse);
|
||||
// 차례: [sub] · doc · e - doc 을 지우면 다음 줄 e 가 지금 줄
|
||||
expect(find.text('doc'), findsNothing);
|
||||
final focusedRow = find.byWidgetPredicate((w) => w is Material && w.shape is Border);
|
||||
expect(find.descendant(of: focusedRow, matching: find.text('e')), findsOneWidget, reason: '지운 줄의 다음 줄 (e.txt) 이 지금 줄 (왼쪽 강조 막대)');
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -93,7 +93,8 @@ void main() {
|
|||
// 시험 글꼴 (Ahem) 은 글자마다 글자 크기만큼 넓어 실제보다 훨씬 길다 - 이름 칸 폭으로 본다 (실제 글꼴은 캡처 시험)
|
||||
expect(para.constraints.maxWidth, greaterThanOrEqualTo(20), reason: '트리의 폴더 이름 칸이 남는다 (예전 412 폭은 0, 실제 글꼴로 360 폭에서 한 줄 5글자쯤)');
|
||||
final treeRight = tester.getTopRight(tree).dx;
|
||||
final listLeft = tester.getTopLeft(find.text(style == 'totalcmd' ? 'a' : 'a.txt')).dx;
|
||||
// 관리자 결정 10/23 (E6 ③): 좁아 TC 의 확장자 칸이 없으면 이름에 확장자를 붙인다 - 이 폭에서는 TC 도 'a.txt'
|
||||
final listLeft = tester.getTopLeft(find.text('a.txt')).dx;
|
||||
expect(listLeft, greaterThan(treeRight), reason: '트리는 왼쪽, 목록은 오른쪽');
|
||||
// 트리 줄에는 ✔ 단추가 없다 (오른쪽 목록 줄에만) - X-plore 는 늘 ✔
|
||||
if (style == 'xplore') {
|
||||
|
|
@ -115,7 +116,8 @@ void main() {
|
|||
tester.view.devicePixelRatio = 1;
|
||||
addTearDown(tester.view.reset);
|
||||
final sub = style == 'totalcmd' ? '[sub]' : 'sub';
|
||||
final a = style == 'totalcmd' ? 'a' : 'a.txt';
|
||||
// 관리자 결정 10/23 (E6 ③): 좁아 TC 의 확장자 칸이 없으면 이름에 확장자를 붙인다 - 이 폭에서는 TC 도 'a.txt'
|
||||
const a = 'a.txt';
|
||||
final c = await open(tester, style, sub, device: 1.3, scale: 150, spacing: 'wide');
|
||||
await tester.pump();
|
||||
expect(tester.takeException(), isNull);
|
||||
|
|
@ -182,7 +184,8 @@ void main() {
|
|||
});
|
||||
|
||||
for (final style in ['xplore', 'windows', 'totalcmd']) {
|
||||
final a = style == 'totalcmd' ? 'a' : 'a.txt';
|
||||
// 관리자 결정 10/23 (E6 ③): 좁아 TC 의 확장자 칸이 없으면 이름에 확장자를 붙인다 - 이 폭에서는 TC 도 'a.txt'
|
||||
const a = 'a.txt';
|
||||
testWidgets('40: $style - 처음 값 (100% · 보통) 은 지금 그대로 · 150% · 넓게는 글자 1.5배 · 줄 높이 늘어남 · 412 폭에서 넘치지 않음', (tester) async {
|
||||
tester.view.physicalSize = const Size(412, 900);
|
||||
tester.view.devicePixelRatio = 1;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,32 @@
|
|||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:jj_mkvmaker/core/explorer/focus_after_delete.dart';
|
||||
|
||||
/// 약속 (관리자 결정 10/23): 지운 뒤 초점은 지운 줄의 다음 줄로, 마지막이었으면 앞 줄로 (TC · X-plore 와 같음)
|
||||
void main() {
|
||||
bool same(String a, String b) => a == b;
|
||||
bool inside(String x, String d) => x == d || x.startsWith('$d/');
|
||||
String? f(List<String> shown, List<String> gone, String? focused) =>
|
||||
focusAfterDelete(shown, gone, focused, same: same, inside: inside);
|
||||
const list = ['/d/a', '/d/b', '/d/c', '/d/e'];
|
||||
|
||||
test('지운 줄의 다음 줄 · 마지막이면 앞 줄', () {
|
||||
expect(f(list, ['/d/b'], '/d/b'), '/d/c');
|
||||
expect(f(list, ['/d/e'], '/d/e'), '/d/c');
|
||||
expect(f(list, ['/d/a'], '/d/a'), '/d/b');
|
||||
});
|
||||
|
||||
test('여럿을 지우면 남은 줄 가운데 다음 첫째 (없으면 앞의 마지막) · 다 지우면 빈 값', () {
|
||||
expect(f(list, ['/d/b', '/d/c'], '/d/b'), '/d/e');
|
||||
expect(f(list, ['/d/c', '/d/e'], '/d/e'), '/d/b');
|
||||
expect(f(list, list, '/d/a'), '');
|
||||
});
|
||||
|
||||
test('지금 줄이 남아 있거나 없으면 그대로 (null)', () {
|
||||
expect(f(list, ['/d/b'], '/d/a'), isNull);
|
||||
expect(f(list, ['/d/b'], null), isNull);
|
||||
});
|
||||
|
||||
test('지금 줄이 지운 폴더 안에 있었으면 그 폴더 줄의 다음', () {
|
||||
expect(f(list, ['/d/b'], '/d/b/inner.txt'), '/d/c');
|
||||
});
|
||||
}
|
||||
Loading…
Reference in New Issue