Skip to content

Commit 251010e

Browse files
[file_selector] Add getDirectoryPaths implementation for macOS (flutter#3703)
Implements the new `getDirectoryPaths` platform interface method. This is a recreation of flutter/plugins#7115 with very minor (pubspec version and CHANGELOG) updates for conflicts with main. Part of flutter#74323
1 parent de6131d commit 251010e

File tree

9 files changed

+251
-26
lines changed

9 files changed

+251
-26
lines changed

packages/file_selector/file_selector_macos/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
## 0.9.1
2+
3+
* Adds `getDirectoryPaths` implementation.
4+
15
## 0.9.0+8
26

37
* Updates pigeon for null value handling fixes.
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
// Copyright 2013 The Flutter Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
import 'package:file_selector_platform_interface/file_selector_platform_interface.dart';
6+
import 'package:flutter/material.dart';
7+
8+
/// Screen that allows the user to select one or more directories using `getDirectoryPaths`,
9+
/// then displays the selected directories in a dialog.
10+
class GetMultipleDirectoriesPage extends StatelessWidget {
11+
/// Default Constructor
12+
const GetMultipleDirectoriesPage({super.key});
13+
14+
Future<void> _getDirectoryPaths(BuildContext context) async {
15+
const String confirmButtonText = 'Choose';
16+
final List<String?> directoriesPaths =
17+
await FileSelectorPlatform.instance.getDirectoryPaths(
18+
confirmButtonText: confirmButtonText,
19+
);
20+
if (directoriesPaths.isEmpty) {
21+
// Operation was canceled by the user.
22+
return;
23+
}
24+
if (context.mounted) {
25+
await showDialog<void>(
26+
context: context,
27+
builder: (BuildContext context) =>
28+
TextDisplay(directoriesPaths.join('\n')),
29+
);
30+
}
31+
}
32+
33+
@override
34+
Widget build(BuildContext context) {
35+
return Scaffold(
36+
appBar: AppBar(
37+
title: const Text('Select multiple directories'),
38+
),
39+
body: Center(
40+
child: Column(
41+
mainAxisAlignment: MainAxisAlignment.center,
42+
children: <Widget>[
43+
ElevatedButton(
44+
style: ElevatedButton.styleFrom(
45+
// ignore: deprecated_member_use
46+
primary: Colors.blue,
47+
// ignore: deprecated_member_use
48+
onPrimary: Colors.white,
49+
),
50+
child: const Text(
51+
'Press to ask user to choose multiple directories'),
52+
onPressed: () => _getDirectoryPaths(context),
53+
),
54+
],
55+
),
56+
),
57+
);
58+
}
59+
}
60+
61+
/// Widget that displays a text file in a dialog.
62+
class TextDisplay extends StatelessWidget {
63+
/// Creates a `TextDisplay`.
64+
const TextDisplay(this.directoryPaths, {super.key});
65+
66+
/// The paths selected in the dialog.
67+
final String directoryPaths;
68+
69+
@override
70+
Widget build(BuildContext context) {
71+
return AlertDialog(
72+
title: const Text('Selected Directories'),
73+
content: Scrollbar(
74+
child: SingleChildScrollView(
75+
child: Text(directoryPaths),
76+
),
77+
),
78+
actions: <Widget>[
79+
TextButton(
80+
child: const Text('Close'),
81+
onPressed: () => Navigator.pop(context),
82+
),
83+
],
84+
);
85+
}
86+
}

packages/file_selector/file_selector_macos/example/lib/home_page.dart

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,13 @@ class HomePage extends StatelessWidget {
5555
child: const Text('Open a get directory dialog'),
5656
onPressed: () => Navigator.pushNamed(context, '/directory'),
5757
),
58+
const SizedBox(height: 10),
59+
ElevatedButton(
60+
style: style,
61+
child: const Text('Open a get directories dialog'),
62+
onPressed: () =>
63+
Navigator.pushNamed(context, '/multi-directories'),
64+
),
5865
],
5966
),
6067
),

packages/file_selector/file_selector_macos/example/lib/main.dart

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import 'package:flutter/material.dart';
66

77
import 'get_directory_page.dart';
8+
import 'get_multiple_directories_page.dart';
89
import 'home_page.dart';
910
import 'open_image_page.dart';
1011
import 'open_multiple_images_page.dart';
@@ -36,6 +37,8 @@ class MyApp extends StatelessWidget {
3637
'/open/text': (BuildContext context) => const OpenTextPage(),
3738
'/save/text': (BuildContext context) => SaveTextPage(),
3839
'/directory': (BuildContext context) => const GetDirectoryPage(),
40+
'/multi-directories': (BuildContext context) =>
41+
const GetMultipleDirectoriesPage()
3942
},
4043
);
4144
}

packages/file_selector/file_selector_macos/example/macos/RunnerTests/RunnerTests.swift

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -346,4 +346,64 @@ class exampleTests: XCTestCase {
346346
XCTAssertNotNil(panelController.openPanel)
347347
}
348348

349+
func testGetDirectoriesMultiple() throws {
350+
let panelController = TestPanelController()
351+
let plugin = FileSelectorPlugin(
352+
viewProvider: TestViewProvider(),
353+
panelController: panelController)
354+
355+
let returnPaths = ["/foo/bar", "/foo/test"];
356+
panelController.openURLs = returnPaths.map({ path in URL(fileURLWithPath: path) })
357+
358+
let called = XCTestExpectation()
359+
let options = OpenPanelOptions(
360+
allowsMultipleSelection: true,
361+
canChooseDirectories: true,
362+
canChooseFiles: false,
363+
baseOptions: SavePanelOptions())
364+
plugin.displayOpenPanel(options: options) { result in
365+
switch result {
366+
case .success(let paths):
367+
XCTAssertEqual(paths, returnPaths)
368+
case .failure(let error):
369+
XCTFail("\(error)")
370+
}
371+
called.fulfill()
372+
}
373+
374+
wait(for: [called], timeout: 0.5)
375+
XCTAssertNotNil(panelController.openPanel)
376+
if let panel = panelController.openPanel {
377+
XCTAssertTrue(panel.canChooseDirectories)
378+
// For consistency across platforms, file selection is disabled.
379+
XCTAssertFalse(panel.canChooseFiles)
380+
XCTAssertTrue(panel.allowsMultipleSelection)
381+
}
382+
}
383+
384+
func testGetDirectoryMultipleCancel() throws {
385+
let panelController = TestPanelController()
386+
let plugin = FileSelectorPlugin(
387+
viewProvider: TestViewProvider(),
388+
panelController: panelController)
389+
390+
let called = XCTestExpectation()
391+
let options = OpenPanelOptions(
392+
allowsMultipleSelection: true,
393+
canChooseDirectories: true,
394+
canChooseFiles: false,
395+
baseOptions: SavePanelOptions())
396+
plugin.displayOpenPanel(options: options) { result in
397+
switch result {
398+
case .success(let paths):
399+
XCTAssertEqual(paths.count, 0)
400+
case .failure(let error):
401+
XCTFail("\(error)")
402+
}
403+
called.fulfill()
404+
}
405+
406+
wait(for: [called], timeout: 0.5)
407+
XCTAssertNotNil(panelController.openPanel)
408+
}
349409
}

packages/file_selector/file_selector_macos/example/pubspec.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ dependencies:
1515
# The example app is bundled with the plugin so we use a path dependency on
1616
# the parent directory to use the current plugin's version.
1717
path: ..
18-
file_selector_platform_interface: ^2.2.0
18+
file_selector_platform_interface: ^2.4.0
1919
flutter:
2020
sdk: flutter
2121

packages/file_selector/file_selector_macos/lib/file_selector_macos.dart

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,23 @@ class FileSelectorMacOS extends FileSelectorPlatform {
8585
return paths.isEmpty ? null : paths.first;
8686
}
8787

88+
@override
89+
Future<List<String>> getDirectoryPaths({
90+
String? initialDirectory,
91+
String? confirmButtonText,
92+
}) async {
93+
final List<String?> paths =
94+
await _hostApi.displayOpenPanel(OpenPanelOptions(
95+
allowsMultipleSelection: true,
96+
canChooseDirectories: true,
97+
canChooseFiles: false,
98+
baseOptions: SavePanelOptions(
99+
directoryPath: initialDirectory,
100+
prompt: confirmButtonText,
101+
)));
102+
return paths.isEmpty ? <String>[] : List<String>.from(paths);
103+
}
104+
88105
// Converts the type group list into a flat list of all allowed types, since
89106
// macOS doesn't support filter groups.
90107
AllowedTypes? _allowedTypesFromTypeGroups(List<XTypeGroup>? typeGroups) {

packages/file_selector/file_selector_macos/pubspec.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ name: file_selector_macos
22
description: macOS implementation of the file_selector plugin.
33
repository: https://github.com/flutter/packages/tree/main/packages/file_selector/file_selector_macos
44
issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+file_selector%22
5-
version: 0.9.0+8
5+
version: 0.9.1
66

77
environment:
88
sdk: ">=2.18.0 <4.0.0"
@@ -18,7 +18,7 @@ flutter:
1818

1919
dependencies:
2020
cross_file: ^0.3.1
21-
file_selector_platform_interface: ^2.2.0
21+
file_selector_platform_interface: ^2.4.0
2222
flutter:
2323
sdk: flutter
2424

packages/file_selector/file_selector_macos/test/file_selector_macos_test.dart

Lines changed: 71 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -317,6 +317,31 @@ void main() {
317317
plugin.getSavePath(acceptedTypeGroups: <XTypeGroup>[group]),
318318
completes);
319319
});
320+
321+
test('ignores all type groups if any of them is a wildcard', () async {
322+
await plugin.getSavePath(acceptedTypeGroups: <XTypeGroup>[
323+
const XTypeGroup(
324+
label: 'text',
325+
extensions: <String>['txt'],
326+
mimeTypes: <String>['text/plain'],
327+
macUTIs: <String>['public.text'],
328+
),
329+
const XTypeGroup(
330+
label: 'image',
331+
extensions: <String>['jpg'],
332+
mimeTypes: <String>['image/jpg'],
333+
macUTIs: <String>['public.image'],
334+
),
335+
const XTypeGroup(
336+
label: 'any',
337+
),
338+
]);
339+
340+
final VerificationResult result =
341+
verify(mockApi.displaySavePanel(captureAny));
342+
final SavePanelOptions options = result.captured[0] as SavePanelOptions;
343+
expect(options.allowedFileTypes, null);
344+
});
320345
});
321346

322347
group('getDirectoryPath', () {
@@ -366,28 +391,51 @@ void main() {
366391
});
367392
});
368393

369-
test('ignores all type groups if any of them is a wildcard', () async {
370-
await plugin.getSavePath(acceptedTypeGroups: <XTypeGroup>[
371-
const XTypeGroup(
372-
label: 'text',
373-
extensions: <String>['txt'],
374-
mimeTypes: <String>['text/plain'],
375-
macUTIs: <String>['public.text'],
376-
),
377-
const XTypeGroup(
378-
label: 'image',
379-
extensions: <String>['jpg'],
380-
mimeTypes: <String>['image/jpg'],
381-
macUTIs: <String>['public.image'],
382-
),
383-
const XTypeGroup(
384-
label: 'any',
385-
),
386-
]);
387-
388-
final VerificationResult result =
389-
verify(mockApi.displaySavePanel(captureAny));
390-
final SavePanelOptions options = result.captured[0] as SavePanelOptions;
391-
expect(options.allowedFileTypes, null);
394+
group('getDirectoryPaths', () {
395+
test('works as expected with no arguments', () async {
396+
when(mockApi.displayOpenPanel(any)).thenAnswer((_) async =>
397+
<String>['firstDirectory', 'secondDirectory', 'thirdDirectory']);
398+
399+
final List<String> path = await plugin.getDirectoryPaths();
400+
401+
expect(path,
402+
<String>['firstDirectory', 'secondDirectory', 'thirdDirectory']);
403+
final VerificationResult result =
404+
verify(mockApi.displayOpenPanel(captureAny));
405+
final OpenPanelOptions options = result.captured[0] as OpenPanelOptions;
406+
expect(options.allowsMultipleSelection, true);
407+
expect(options.canChooseFiles, false);
408+
expect(options.canChooseDirectories, true);
409+
expect(options.baseOptions.allowedFileTypes, null);
410+
expect(options.baseOptions.directoryPath, null);
411+
expect(options.baseOptions.nameFieldStringValue, null);
412+
expect(options.baseOptions.prompt, null);
413+
});
414+
415+
test('handles cancel', () async {
416+
when(mockApi.displayOpenPanel(any)).thenAnswer((_) async => <String?>[]);
417+
418+
final List<String> paths = await plugin.getDirectoryPaths();
419+
420+
expect(paths, <String>[]);
421+
});
422+
423+
test('passes confirmButtonText correctly', () async {
424+
await plugin.getDirectoryPaths(confirmButtonText: 'Select directories');
425+
426+
final VerificationResult result =
427+
verify(mockApi.displayOpenPanel(captureAny));
428+
final OpenPanelOptions options = result.captured[0] as OpenPanelOptions;
429+
expect(options.baseOptions.prompt, 'Select directories');
430+
});
431+
432+
test('passes initialDirectory correctly', () async {
433+
await plugin.getDirectoryPaths(initialDirectory: '/example/directory');
434+
435+
final VerificationResult result =
436+
verify(mockApi.displayOpenPanel(captureAny));
437+
final OpenPanelOptions options = result.captured[0] as OpenPanelOptions;
438+
expect(options.baseOptions.directoryPath, '/example/directory');
439+
});
392440
});
393441
}

0 commit comments

Comments
 (0)