diff --git a/packages/file_selector/file_selector_macos/CHANGELOG.md b/packages/file_selector/file_selector_macos/CHANGELOG.md index a83f52552fd..f309e7baf89 100644 --- a/packages/file_selector/file_selector_macos/CHANGELOG.md +++ b/packages/file_selector/file_selector_macos/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.9.1 + +* Adds `getDirectoryPaths` implementation. + ## 0.9.0+8 * Updates pigeon for null value handling fixes. diff --git a/packages/file_selector/file_selector_macos/example/lib/get_multiple_directories_page.dart b/packages/file_selector/file_selector_macos/example/lib/get_multiple_directories_page.dart new file mode 100644 index 00000000000..c5fcecec7c7 --- /dev/null +++ b/packages/file_selector/file_selector_macos/example/lib/get_multiple_directories_page.dart @@ -0,0 +1,86 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:file_selector_platform_interface/file_selector_platform_interface.dart'; +import 'package:flutter/material.dart'; + +/// Screen that allows the user to select one or more directories using `getDirectoryPaths`, +/// then displays the selected directories in a dialog. +class GetMultipleDirectoriesPage extends StatelessWidget { + /// Default Constructor + const GetMultipleDirectoriesPage({super.key}); + + Future _getDirectoryPaths(BuildContext context) async { + const String confirmButtonText = 'Choose'; + final List directoriesPaths = + await FileSelectorPlatform.instance.getDirectoryPaths( + confirmButtonText: confirmButtonText, + ); + if (directoriesPaths.isEmpty) { + // Operation was canceled by the user. + return; + } + if (context.mounted) { + await showDialog( + context: context, + builder: (BuildContext context) => + TextDisplay(directoriesPaths.join('\n')), + ); + } + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('Select multiple directories'), + ), + body: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + ElevatedButton( + style: ElevatedButton.styleFrom( + // ignore: deprecated_member_use + primary: Colors.blue, + // ignore: deprecated_member_use + onPrimary: Colors.white, + ), + child: const Text( + 'Press to ask user to choose multiple directories'), + onPressed: () => _getDirectoryPaths(context), + ), + ], + ), + ), + ); + } +} + +/// Widget that displays a text file in a dialog. +class TextDisplay extends StatelessWidget { + /// Creates a `TextDisplay`. + const TextDisplay(this.directoryPaths, {super.key}); + + /// The paths selected in the dialog. + final String directoryPaths; + + @override + Widget build(BuildContext context) { + return AlertDialog( + title: const Text('Selected Directories'), + content: Scrollbar( + child: SingleChildScrollView( + child: Text(directoryPaths), + ), + ), + actions: [ + TextButton( + child: const Text('Close'), + onPressed: () => Navigator.pop(context), + ), + ], + ); + } +} diff --git a/packages/file_selector/file_selector_macos/example/lib/home_page.dart b/packages/file_selector/file_selector_macos/example/lib/home_page.dart index 366ff514424..3c80f440578 100644 --- a/packages/file_selector/file_selector_macos/example/lib/home_page.dart +++ b/packages/file_selector/file_selector_macos/example/lib/home_page.dart @@ -55,6 +55,13 @@ class HomePage extends StatelessWidget { child: const Text('Open a get directory dialog'), onPressed: () => Navigator.pushNamed(context, '/directory'), ), + const SizedBox(height: 10), + ElevatedButton( + style: style, + child: const Text('Open a get directories dialog'), + onPressed: () => + Navigator.pushNamed(context, '/multi-directories'), + ), ], ), ), diff --git a/packages/file_selector/file_selector_macos/example/lib/main.dart b/packages/file_selector/file_selector_macos/example/lib/main.dart index bfd2c2fdbfc..a88f850f5d6 100644 --- a/packages/file_selector/file_selector_macos/example/lib/main.dart +++ b/packages/file_selector/file_selector_macos/example/lib/main.dart @@ -5,6 +5,7 @@ import 'package:flutter/material.dart'; import 'get_directory_page.dart'; +import 'get_multiple_directories_page.dart'; import 'home_page.dart'; import 'open_image_page.dart'; import 'open_multiple_images_page.dart'; @@ -36,6 +37,8 @@ class MyApp extends StatelessWidget { '/open/text': (BuildContext context) => const OpenTextPage(), '/save/text': (BuildContext context) => SaveTextPage(), '/directory': (BuildContext context) => const GetDirectoryPage(), + '/multi-directories': (BuildContext context) => + const GetMultipleDirectoriesPage() }, ); } diff --git a/packages/file_selector/file_selector_macos/example/macos/RunnerTests/RunnerTests.swift b/packages/file_selector/file_selector_macos/example/macos/RunnerTests/RunnerTests.swift index fcd39c61117..4e103431c9b 100644 --- a/packages/file_selector/file_selector_macos/example/macos/RunnerTests/RunnerTests.swift +++ b/packages/file_selector/file_selector_macos/example/macos/RunnerTests/RunnerTests.swift @@ -346,4 +346,64 @@ class exampleTests: XCTestCase { XCTAssertNotNil(panelController.openPanel) } + func testGetDirectoriesMultiple() throws { + let panelController = TestPanelController() + let plugin = FileSelectorPlugin( + viewProvider: TestViewProvider(), + panelController: panelController) + + let returnPaths = ["/foo/bar", "/foo/test"]; + panelController.openURLs = returnPaths.map({ path in URL(fileURLWithPath: path) }) + + let called = XCTestExpectation() + let options = OpenPanelOptions( + allowsMultipleSelection: true, + canChooseDirectories: true, + canChooseFiles: false, + baseOptions: SavePanelOptions()) + plugin.displayOpenPanel(options: options) { result in + switch result { + case .success(let paths): + XCTAssertEqual(paths, returnPaths) + case .failure(let error): + XCTFail("\(error)") + } + called.fulfill() + } + + wait(for: [called], timeout: 0.5) + XCTAssertNotNil(panelController.openPanel) + if let panel = panelController.openPanel { + XCTAssertTrue(panel.canChooseDirectories) + // For consistency across platforms, file selection is disabled. + XCTAssertFalse(panel.canChooseFiles) + XCTAssertTrue(panel.allowsMultipleSelection) + } + } + + func testGetDirectoryMultipleCancel() throws { + let panelController = TestPanelController() + let plugin = FileSelectorPlugin( + viewProvider: TestViewProvider(), + panelController: panelController) + + let called = XCTestExpectation() + let options = OpenPanelOptions( + allowsMultipleSelection: true, + canChooseDirectories: true, + canChooseFiles: false, + baseOptions: SavePanelOptions()) + plugin.displayOpenPanel(options: options) { result in + switch result { + case .success(let paths): + XCTAssertEqual(paths.count, 0) + case .failure(let error): + XCTFail("\(error)") + } + called.fulfill() + } + + wait(for: [called], timeout: 0.5) + XCTAssertNotNil(panelController.openPanel) + } } diff --git a/packages/file_selector/file_selector_macos/example/pubspec.yaml b/packages/file_selector/file_selector_macos/example/pubspec.yaml index b8e38bd4e8a..9e2d831f75c 100644 --- a/packages/file_selector/file_selector_macos/example/pubspec.yaml +++ b/packages/file_selector/file_selector_macos/example/pubspec.yaml @@ -15,7 +15,7 @@ dependencies: # The example app is bundled with the plugin so we use a path dependency on # the parent directory to use the current plugin's version. path: .. - file_selector_platform_interface: ^2.2.0 + file_selector_platform_interface: ^2.4.0 flutter: sdk: flutter diff --git a/packages/file_selector/file_selector_macos/lib/file_selector_macos.dart b/packages/file_selector/file_selector_macos/lib/file_selector_macos.dart index f8a087fa687..0fd4457a236 100644 --- a/packages/file_selector/file_selector_macos/lib/file_selector_macos.dart +++ b/packages/file_selector/file_selector_macos/lib/file_selector_macos.dart @@ -85,6 +85,23 @@ class FileSelectorMacOS extends FileSelectorPlatform { return paths.isEmpty ? null : paths.first; } + @override + Future> getDirectoryPaths({ + String? initialDirectory, + String? confirmButtonText, + }) async { + final List paths = + await _hostApi.displayOpenPanel(OpenPanelOptions( + allowsMultipleSelection: true, + canChooseDirectories: true, + canChooseFiles: false, + baseOptions: SavePanelOptions( + directoryPath: initialDirectory, + prompt: confirmButtonText, + ))); + return paths.isEmpty ? [] : List.from(paths); + } + // Converts the type group list into a flat list of all allowed types, since // macOS doesn't support filter groups. AllowedTypes? _allowedTypesFromTypeGroups(List? typeGroups) { diff --git a/packages/file_selector/file_selector_macos/pubspec.yaml b/packages/file_selector/file_selector_macos/pubspec.yaml index 4b922261bbe..6c6c3655b7a 100644 --- a/packages/file_selector/file_selector_macos/pubspec.yaml +++ b/packages/file_selector/file_selector_macos/pubspec.yaml @@ -2,7 +2,7 @@ name: file_selector_macos description: macOS implementation of the file_selector plugin. repository: https://github.com/flutter/packages/tree/main/packages/file_selector/file_selector_macos issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+file_selector%22 -version: 0.9.0+8 +version: 0.9.1 environment: sdk: ">=2.18.0 <4.0.0" @@ -18,7 +18,7 @@ flutter: dependencies: cross_file: ^0.3.1 - file_selector_platform_interface: ^2.2.0 + file_selector_platform_interface: ^2.4.0 flutter: sdk: flutter diff --git a/packages/file_selector/file_selector_macos/test/file_selector_macos_test.dart b/packages/file_selector/file_selector_macos/test/file_selector_macos_test.dart index 181409e6f1b..6d330619e75 100644 --- a/packages/file_selector/file_selector_macos/test/file_selector_macos_test.dart +++ b/packages/file_selector/file_selector_macos/test/file_selector_macos_test.dart @@ -317,6 +317,31 @@ void main() { plugin.getSavePath(acceptedTypeGroups: [group]), completes); }); + + test('ignores all type groups if any of them is a wildcard', () async { + await plugin.getSavePath(acceptedTypeGroups: [ + const XTypeGroup( + label: 'text', + extensions: ['txt'], + mimeTypes: ['text/plain'], + macUTIs: ['public.text'], + ), + const XTypeGroup( + label: 'image', + extensions: ['jpg'], + mimeTypes: ['image/jpg'], + macUTIs: ['public.image'], + ), + const XTypeGroup( + label: 'any', + ), + ]); + + final VerificationResult result = + verify(mockApi.displaySavePanel(captureAny)); + final SavePanelOptions options = result.captured[0] as SavePanelOptions; + expect(options.allowedFileTypes, null); + }); }); group('getDirectoryPath', () { @@ -366,28 +391,51 @@ void main() { }); }); - test('ignores all type groups if any of them is a wildcard', () async { - await plugin.getSavePath(acceptedTypeGroups: [ - const XTypeGroup( - label: 'text', - extensions: ['txt'], - mimeTypes: ['text/plain'], - macUTIs: ['public.text'], - ), - const XTypeGroup( - label: 'image', - extensions: ['jpg'], - mimeTypes: ['image/jpg'], - macUTIs: ['public.image'], - ), - const XTypeGroup( - label: 'any', - ), - ]); - - final VerificationResult result = - verify(mockApi.displaySavePanel(captureAny)); - final SavePanelOptions options = result.captured[0] as SavePanelOptions; - expect(options.allowedFileTypes, null); + group('getDirectoryPaths', () { + test('works as expected with no arguments', () async { + when(mockApi.displayOpenPanel(any)).thenAnswer((_) async => + ['firstDirectory', 'secondDirectory', 'thirdDirectory']); + + final List path = await plugin.getDirectoryPaths(); + + expect(path, + ['firstDirectory', 'secondDirectory', 'thirdDirectory']); + final VerificationResult result = + verify(mockApi.displayOpenPanel(captureAny)); + final OpenPanelOptions options = result.captured[0] as OpenPanelOptions; + expect(options.allowsMultipleSelection, true); + expect(options.canChooseFiles, false); + expect(options.canChooseDirectories, true); + expect(options.baseOptions.allowedFileTypes, null); + expect(options.baseOptions.directoryPath, null); + expect(options.baseOptions.nameFieldStringValue, null); + expect(options.baseOptions.prompt, null); + }); + + test('handles cancel', () async { + when(mockApi.displayOpenPanel(any)).thenAnswer((_) async => []); + + final List paths = await plugin.getDirectoryPaths(); + + expect(paths, []); + }); + + test('passes confirmButtonText correctly', () async { + await plugin.getDirectoryPaths(confirmButtonText: 'Select directories'); + + final VerificationResult result = + verify(mockApi.displayOpenPanel(captureAny)); + final OpenPanelOptions options = result.captured[0] as OpenPanelOptions; + expect(options.baseOptions.prompt, 'Select directories'); + }); + + test('passes initialDirectory correctly', () async { + await plugin.getDirectoryPaths(initialDirectory: '/example/directory'); + + final VerificationResult result = + verify(mockApi.displayOpenPanel(captureAny)); + final OpenPanelOptions options = result.captured[0] as OpenPanelOptions; + expect(options.baseOptions.directoryPath, '/example/directory'); + }); }); }