diff --git a/integration_test/explorer_text_shot_test.dart b/integration_test/explorer_text_shot_test.dart index cb4edd9..20a208c 100644 --- a/integration_test/explorer_text_shot_test.dart +++ b/integration_test/explorer_text_shot_test.dart @@ -1,6 +1,7 @@ import 'dart:io'; import 'dart:ui' as ui; +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter_test/flutter_test.dart'; @@ -28,7 +29,9 @@ void main() { final c = AppController(PlatformServices.create()); c.settings.explorerPaths = [tmp.path, tmp.path]; final key = GlobalKey(); - for (final layout in ['single', 'split']) { + // 폰처럼 (Android 는 트리 펼치기 화살표가 44 로 넓다 - Windows 로 그리면 폰보다 넉넉해 보인다) + debugDefaultTargetPlatformOverride = TargetPlatform.android; + for (final (layout, width) in [('single', 412.0), ('split', 412.0), ('split', 360.0)]) { for (final style in ['xplore', 'windows', 'totalcmd']) { for (final (scale, spacing) in [(100, 'normal'), (150, 'wide'), (80, 'compact')]) { c.settings @@ -36,7 +39,7 @@ void main() { ..explorerStyle = style ..explorerTextScale = scale ..explorerRowSpacing = spacing; - tester.view.physicalSize = const Size(412, 900); + tester.view.physicalSize = Size(width, 900); tester.view.devicePixelRatio = 1; await tester.runAsync(() => tester.pumpWidget(MaterialApp( theme: buildTheme(), @@ -46,16 +49,17 @@ void main() { await tester.runAsync(() => Future.delayed(const Duration(milliseconds: 250))); await tester.pump(); } - expect(tester.takeException(), isNull, reason: '$layout $style $scale% $spacing 넘치지 않음'); + expect(tester.takeException(), isNull, reason: '$layout $width $style $scale% $spacing 넘치지 않음'); final boundary = key.currentContext!.findRenderObject()! as RenderRepaintBoundary; final image = await tester.runAsync(() => boundary.toImage()); final bytes = await tester.runAsync(() => image!.toByteData(format: ui.ImageByteFormat.png)); - File('$out/explorer_text_${layout}_${style}_${scale}_$spacing.png').writeAsBytesSync(bytes!.buffer.asUint8List()); + File('$out/explorer_text_$layout${width.round()}_${style}_${scale}_$spacing.png').writeAsBytesSync(bytes!.buffer.asUint8List()); await tester.pumpWidget(const SizedBox()); await tester.runAsync(() => Future.delayed(const Duration(milliseconds: 300))); } } } + debugDefaultTargetPlatformOverride = null; tester.view.reset(); }); } diff --git a/lib/ui/explorer_page.dart b/lib/ui/explorer_page.dart index 2400dbf..ecac2a9 100644 --- a/lib/ui/explorer_page.dart +++ b/lib/ui/explorer_page.dart @@ -1153,6 +1153,11 @@ class _ExplorerPageState extends State { /// 왼쪽 폴더 트리 + 오른쪽 파일 목록 배치 bool get _split => !widget.rsync && c.settings.explorerLayout == 'split'; + /// 관리자 지시 10/20: 좁은 화면 (폭 < [narrowSplitWidth]) 의 "폴더 + 파일 목록" 좌우 배치 - 트리 창에 최소 폭을 두고 + /// ✔ 칸 · 크기 줄을 숨긴다 (412 폭에서 트리가 77px 이라 폴더 이름이 한 글자도 안 보였다). [_body] 가 정한다 + bool _narrowSplit = false; + static const narrowSplitWidth = 600.0; + /// 좌우 두 창 (Rsync 화면은 늘) bool get _dual => widget.rsync || @@ -4589,6 +4594,7 @@ class _ExplorerPageState extends State { // 191: Rsync 화면이 좁으면 (폴드 접은 화면 · 폰 세로의 좌우 두 창) 창 하나씩 - [왼쪽 | 오른쪽] 으로 바꿔 본다 // (두 창 + 가운데 버튼 줄이면 창이 140px 남짓이라 폴더 이름이 잘렸다) final rsyncOne = widget.rsync && side && box.maxWidth < rsyncTwoPaneMinWidth; + _narrowSplit = split && side && box.maxWidth < narrowSplitWidth; final bar = s.explorerToolbar == 'hidden' && !widget.rsync ? null : _Toolbar( @@ -4627,8 +4633,14 @@ class _ExplorerPageState extends State { Expanded(child: keep(1, _paneView(1, foldersOnly: widget.rsync))), if (edge) bar, ] else if (split) ...[ - // 왼쪽 폴더 트리 (좁게) · 오른쪽 그 폴더의 파일 목록 - Expanded(flex: 2, child: keep(0, _paneView(0, foldersOnly: true))), + // 왼쪽 폴더 트리 (좁게) · 오른쪽 그 폴더의 파일 목록. 좁은 화면은 트리에 최소 폭 (130px 또는 버튼 줄 (72) 을 뺀 폭의 42%) - + // 버튼 줄까지 넣어 38% 로 하면 360 폭에서 오른쪽 목록의 파일 이름이 두세 글자만 남았다 + if (_narrowSplit) + SizedBox( + width: math.max(130.0, (box.maxWidth - (middle || edge ? 72 : 0)) * 0.42), + child: keep(0, _paneView(0, foldersOnly: true))) + else + Expanded(flex: 2, child: keep(0, _paneView(0, foldersOnly: true))), if (middle) bar, Expanded(flex: 3, child: keep(2, _listView(_panes[0]))), if (edge) bar, @@ -5476,6 +5488,9 @@ class _ExplorerPageState extends State { Widget _rowView(_Pane pane, _Row row, {bool list = false, bool compact = false}) { final e = row.entry; final look = _look; + // 좁은 "폴더 + 파일 목록" 의 트리 (관리자 지시 10/20): ✔ 칸 · 크기 줄 없이 이름만, 줄에 두 줄이 들어가면 이름을 두 줄까지 + final slim = compact && _narrowSplit && !list; + final nameLines = slim && _rowH() >= MediaQuery.textScalerOf(context).scale(look.fontSize) * _listTextScale * 2.5 + 6 ? 2 : 1; final isCurrent = e.isDir && !list && !row.isUp && samePath(pane.current, e.path); final isFocused = !row.isUp && pane.focused != null && samePath(pane.focused!, e.path); final marked = _isMarked(pane, e.path); @@ -5495,7 +5510,8 @@ class _ExplorerPageState extends State { color: isCurrent ? JjColors.accent : null, ); - final chevronWidth = treeChevronWidth(defaultTargetPlatform); + // 좁은 트리 (slim) 는 조금 좁게 (36 - 줄 높이 전체를 누르므로 손가락으로도 눌림) + final chevronWidth = slim ? math.min(36.0, treeChevronWidth(defaultTargetPlatform)) : treeChevronWidth(defaultTargetPlatform); // 트리의 펼치기 화살표 (따로 누름). 189: 접기 · 펼치기는 이것으로만 하므로 줄 높이 전체를 누를 수 있게, // 손가락으로 누르는 기기 (Android) 는 폭도 넓게 Widget chevron() => SizedBox( @@ -5525,11 +5541,17 @@ class _ExplorerPageState extends State { // 좁은 창 (폰 세로의 좌우 두 창) 에서 깊은 폴더: 들여쓰기는 이름 자리를 남기는 만큼만 (넘치지 않게) Widget nameCell() => LayoutBuilder(builder: (context, box) { final fixed = (list ? 0 : chevronWidth) + (look.rich ? 64 : look.iconSize + 12) + 48.0; - final indent = list ? 0.0 : (row.depth * look.indent).clamp(0.0, (box.maxWidth - fixed).clamp(0.0, double.infinity)); + // 좁은 트리 (slim): 들여쓰기는 반만 (폭의 1/10 까지) - 깊은 폴더에서 이름이 한 글자도 안 보이지 않게 + final indent = list + ? 0.0 + : slim + ? (row.depth * look.indent / 2).clamp(0.0, box.maxWidth * 0.1).toDouble() + : (row.depth * look.indent).clamp(0.0, (box.maxWidth - fixed).clamp(0.0, double.infinity)).toDouble(); // 아주 좁은 창 (412 폭 "폴더 + 파일 목록" 의 트리, 77px 쯤): 아이콘 칸 · 틈을 남은 폭만큼만 (아이콘은 줄여 그림) - 넘치지 않게 final room = (box.maxWidth - indent - (list ? 0 : chevronWidth)).clamp(0.0, double.infinity); - final iconW = math.min(look.rich ? 56.0 : look.iconSize + 6, room); - final gapW = math.min(look.rich ? 8.0 : 6.0, room - iconW); + // 좁은 트리 (slim) 는 큰 썸네일 칸 (56) 대신 작은 아이콘 칸 (28 까지, 아이콘은 줄여 그림) + final iconW = math.min(slim ? math.min(look.iconSize, 24.0) + 4 : look.rich ? 56.0 : look.iconSize + 6, room); + final gapW = math.min(slim ? 4.0 : look.rich ? 8.0 : 6.0, room - iconW); return Row(children: [ SizedBox(width: indent), if (!list) chevron(), @@ -5557,8 +5579,10 @@ class _ExplorerPageState extends State { crossAxisAlignment: CrossAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.center, children: [ - Text(name, maxLines: 1, overflow: TextOverflow.ellipsis, style: nameStyle), - if (look.rich && e.isDir && pane.errors[e.path] != null) + Text(name, maxLines: nameLines, overflow: TextOverflow.ellipsis, style: nameStyle), + if (slim) + const SizedBox.shrink() + else if (look.rich && e.isDir && pane.errors[e.path] != null) Text(tr('읽을 수 없음'), style: dim.copyWith(color: Colors.redAccent)) // 80: 열 없는 모양 (X-plore · 좁은 화면) 은 폴더 이름 아래에 크기 else if (e.isDir && !row.isRoot && !row.isUp && (!look.columns || compact) && _dirSize(pane, e) != null) @@ -5630,7 +5654,9 @@ class _ExplorerPageState extends State { ), ), // 표시 동그라미는 두 번 누르기 영역 밖 (기다리지 않고 바로) - if ((real || (widget.rsync && row.isRoot)) && (_selecting || widget.rsync || _alwaysMarks)) + if (slim) + const SizedBox.shrink() + else if ((real || (widget.rsync && row.isRoot)) && (_selecting || widget.rsync || _alwaysMarks)) SizedBox( width: 36, child: IconButton( diff --git a/test/explorer_text_scale_test.dart b/test/explorer_text_scale_test.dart index 2c2a70f..6fa2cf3 100644 --- a/test/explorer_text_scale_test.dart +++ b/test/explorer_text_scale_test.dart @@ -75,6 +75,46 @@ void main() { await tester.pump(const Duration(seconds: 1)); } + // 관리자 지시 10/20: 좁은 화면의 "폴더 + 파일 목록" - 트리 창 최소 폭 (120px 또는 38%), 트리에는 ✔ 칸 · 크기 줄 없이 이름이 보임 + for (final width in [412.0, 360.0]) { + for (final style in ['xplore', 'windows', 'totalcmd']) { + testWidgets('좁은 폴더 + 파일 목록 ($width 폭, $style): 트리 창이 넓어 폴더 이름이 보이고 ✔ 칸은 오른쪽 목록에만', (tester) async { + tester.view.physicalSize = Size(width, 900); + tester.view.devicePixelRatio = 1; + addTearDown(tester.view.reset); + await open(tester, style, 'sub'); + expect(tester.takeException(), isNull); + // 트리 (먼저 그려짐) 의 'sub' 이름이 다 보인다 + final tree = find.text(style == 'totalcmd' ? '[sub]' : 'sub').first; + final para = tester.renderObject(tree); + // 시험 글꼴 (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; + expect(listLeft, greaterThan(treeRight), reason: '트리는 왼쪽, 목록은 오른쪽'); + // 트리 줄에는 ✔ 단추가 없다 (오른쪽 목록 줄에만) - X-plore 는 늘 ✔ + if (style == 'xplore') { + final marks = find.byTooltip('표시 (여러 개 고르기)'); + expect(marks, findsWidgets); + for (final m in marks.evaluate()) { + expect(tester.getTopLeft(find.byWidget(m.widget)).dx, greaterThan(treeRight), reason: '✔ 는 오른쪽 목록에만'); + } + } + await close(tester); + }); + } + } + + testWidgets('넓은 화면 (1280) 의 폴더 + 파일 목록은 그대로 (2 : 3, 트리에도 ✔)', (tester) async { + tester.view.physicalSize = const Size(1280, 900); + tester.view.devicePixelRatio = 1; + addTearDown(tester.view.reset); + await open(tester, 'xplore', 'sub'); + final treeMarks = find.byTooltip('표시 (여러 개 고르기)').evaluate().where((m) => tester.getTopLeft(find.byWidget(m.widget)).dx < 500); + expect(treeMarks, isNotEmpty, reason: '넓은 화면은 지금 그대로'); + await close(tester); + }); + for (final style in ['xplore', 'windows', 'totalcmd']) { final a = style == 'totalcmd' ? 'a' : 'a.txt'; testWidgets('40: $style - 처음 값 (100% · 보통) 은 지금 그대로 · 150% · 넓게는 글자 1.5배 · 줄 높이 늘어남 · 412 폭에서 넘치지 않음', (tester) async {