diff --git a/packages/file_selector/file_selector_macos/CHANGELOG.md b/packages/file_selector/file_selector_macos/CHANGELOG.md index af17db8ae3ef..2b3e097d7940 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+3 * Changes XTypeGroup initialization from final to const. 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 000000000000..63d6368396a9 --- /dev/null +++ b/packages/file_selector/file_selector_macos/example/lib/get_multiple_directories_page.dart @@ -0,0 +1,85 @@ +// 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({Key? key}) : super(key: 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; + } + 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( + // TODO(darrenaustin): Migrate to new API once it lands in stable: https://github.com/flutter/flutter/issues/105724 + // 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.directoryPath, {Key? key}) : super(key: key); + + /// The path selected in the dialog. + final String directoryPath; + + @override + Widget build(BuildContext context) { + return AlertDialog( + title: const Text('Selected Directories'), + content: Scrollbar( + child: SingleChildScrollView( + child: Text(directoryPath), + ), + ), + 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 a4b2ae1f63ea..80e16332a017 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 3e447104ef9f..fbc7f48f7b33 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 bffc3452c49d..37f46fb987b2 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 @@ -241,13 +241,13 @@ class exampleTests: XCTestCase { viewProvider: TestViewProvider(), panelController: panelController) - let returnPath = "/foo/bar" - panelController.openURLs = [URL(fileURLWithPath: returnPath)] + let returnPaths = ["/foo/bar"] + panelController.openURLs = returnPaths.map({ path in URL(fileURLWithPath: path) }) let called = XCTestExpectation() let call = FlutterMethodCall(methodName: "getDirectoryPath", arguments: [:]) plugin.handle(call) { result in - XCTAssertEqual(result as! String?, returnPath) + XCTAssertEqual(result as! [String]?, returnPaths) called.fulfill() } @@ -257,8 +257,7 @@ class exampleTests: XCTestCase { XCTAssertTrue(panel.canChooseDirectories) // For consistency across platforms, file selection is disabled. XCTAssertFalse(panel.canChooseFiles) - // The Dart API only allows a single directory to be returned, so users shouldn't be allowed - // to select multiple. + XCTAssertFalse(panel.allowsMultipleSelection) } } @@ -279,5 +278,46 @@ class exampleTests: XCTestCase { wait(for: [called], timeout: 0.5) XCTAssertNotNil(panelController.openPanel) } - + + func testGetDirectoryMultiple() 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 call = FlutterMethodCall(methodName: "getDirectoryPath", arguments: ["multiple": true]) + plugin.handle(call) { result in + XCTAssertEqual(result as! [String]?, returnPaths) + called.fulfill() + } + + wait(for: [called], timeout: 0.5) + XCTAssertNotNil(panelController.openPanel) + if let panel = panelController.openPanel { + XCTAssertTrue(panel.canChooseDirectories) + XCTAssertFalse(panel.canChooseFiles) + XCTAssertTrue(panel.allowsMultipleSelection) + } + } + + func testGetDirectoryMultipleCancel() throws { + let panelController = TestPanelController() + let plugin = FileSelectorPlugin( + viewProvider: TestViewProvider(), + panelController: panelController) + + let called = XCTestExpectation() + let call = FlutterMethodCall(methodName: "getDirectoryPath", arguments: ["multiple": true]) + plugin.handle(call) { result in + XCTAssertNil(result) + 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 d3f3114bb481..37583275c7a0 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 74ce2835d18c..9e409b775cf9 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 @@ -35,7 +35,8 @@ class FileSelectorMacOS extends FileSelectorPlatform { 'multiple': false, }, ); - return path == null ? null : XFile(path.first); + final String? filePath = _firstOrNull(path); + return filePath == null ? null : XFile(filePath); } @override @@ -79,13 +80,30 @@ class FileSelectorMacOS extends FileSelectorPlatform { String? initialDirectory, String? confirmButtonText, }) async { - return _channel.invokeMethod( + final List? pathList = await _channel.invokeListMethod( + 'getDirectoryPath', + { + 'initialDirectory': initialDirectory, + 'confirmButtonText': confirmButtonText, + }, + ); + return _firstOrNull(pathList); + } + + @override + Future> getDirectoryPaths({ + String? initialDirectory, + String? confirmButtonText, + }) async { + final List? pathList = await _channel.invokeListMethod( 'getDirectoryPath', { 'initialDirectory': initialDirectory, 'confirmButtonText': confirmButtonText, + 'multiple': true, }, ); + return pathList ?? []; } // Converts the type group list into a flat list of all allowed types, since @@ -126,4 +144,8 @@ class FileSelectorMacOS extends FileSelectorPlatform { return allowedTypes; } + + String? _firstOrNull(List? list) { + return (list?.isEmpty ?? true) ? null : list?.first; + } } diff --git a/packages/file_selector/file_selector_macos/macos/Classes/FileSelectorPlugin.swift b/packages/file_selector/file_selector_macos/macos/Classes/FileSelectorPlugin.swift index 9551671d1575..4d42b4305e41 100644 --- a/packages/file_selector/file_selector_macos/macos/Classes/FileSelectorPlugin.swift +++ b/packages/file_selector/file_selector_macos/macos/Classes/FileSelectorPlugin.swift @@ -73,11 +73,7 @@ public class FileSelectorPlugin: NSObject, FlutterPlugin { configure(panel: panel, with: arguments) configure(openPanel: panel, with: arguments, choosingDirectory: choosingDirectory) panelController.display(panel, for: viewProvider.view?.window) { (selection: [URL]?) in - if (choosingDirectory) { - result(selection?.first?.path) - } else { - result(selection?.map({ item in item.path })) - } + result(selection?.map({ item in item.path })) } case saveMethod: let panel = NSSavePanel() diff --git a/packages/file_selector/file_selector_macos/pubspec.yaml b/packages/file_selector/file_selector_macos/pubspec.yaml index 3fc3832d7280..91249234e689 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/plugins/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+3 +version: 0.9.1 environment: sdk: ">=2.12.0 <3.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 789d70a51777..6f8cbed078a4 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 @@ -46,52 +46,49 @@ void main() { await plugin.openFile(acceptedTypeGroups: [group, groupTwo]); - expect( + expectMethodCall( log, - [ - isMethodCall('openFile', arguments: { - 'acceptedTypes': { - 'extensions': ['txt', 'jpg'], - 'mimeTypes': ['text/plain', 'image/jpg'], - 'UTIs': ['public.text', 'public.image'], - }, - 'initialDirectory': null, - 'confirmButtonText': null, - 'multiple': false, - }), - ], + 'openFile', + arguments: { + 'acceptedTypes': { + 'extensions': ['txt', 'jpg'], + 'mimeTypes': ['text/plain', 'image/jpg'], + 'UTIs': ['public.text', 'public.image'], + }, + 'initialDirectory': null, + 'confirmButtonText': null, + 'multiple': false, + }, ); }); test('passes initialDirectory correctly', () async { await plugin.openFile(initialDirectory: '/example/directory'); - expect( + expectMethodCall( log, - [ - isMethodCall('openFile', arguments: { - 'acceptedTypes': null, - 'initialDirectory': '/example/directory', - 'confirmButtonText': null, - 'multiple': false, - }), - ], + 'openFile', + arguments: { + 'acceptedTypes': null, + 'initialDirectory': '/example/directory', + 'confirmButtonText': null, + 'multiple': false, + }, ); }); test('passes confirmButtonText correctly', () async { await plugin.openFile(confirmButtonText: 'Open File'); - expect( + expectMethodCall( log, - [ - isMethodCall('openFile', arguments: { - 'acceptedTypes': null, - 'initialDirectory': null, - 'confirmButtonText': 'Open File', - 'multiple': false, - }), - ], + 'openFile', + arguments: { + 'acceptedTypes': null, + 'initialDirectory': null, + 'confirmButtonText': 'Open File', + 'multiple': false, + }, ); }); @@ -134,52 +131,49 @@ void main() { await plugin.openFiles(acceptedTypeGroups: [group, groupTwo]); - expect( + expectMethodCall( log, - [ - isMethodCall('openFile', arguments: { - 'acceptedTypes': >{ - 'extensions': ['txt', 'jpg'], - 'mimeTypes': ['text/plain', 'image/jpg'], - 'UTIs': ['public.text', 'public.image'], - }, - 'initialDirectory': null, - 'confirmButtonText': null, - 'multiple': true, - }), - ], + 'openFile', + arguments: { + 'acceptedTypes': >{ + 'extensions': ['txt', 'jpg'], + 'mimeTypes': ['text/plain', 'image/jpg'], + 'UTIs': ['public.text', 'public.image'], + }, + 'initialDirectory': null, + 'confirmButtonText': null, + 'multiple': true, + }, ); }); test('passes initialDirectory correctly', () async { await plugin.openFiles(initialDirectory: '/example/directory'); - expect( + expectMethodCall( log, - [ - isMethodCall('openFile', arguments: { - 'acceptedTypes': null, - 'initialDirectory': '/example/directory', - 'confirmButtonText': null, - 'multiple': true, - }), - ], + 'openFile', + arguments: { + 'acceptedTypes': null, + 'initialDirectory': '/example/directory', + 'confirmButtonText': null, + 'multiple': true, + }, ); }); test('passes confirmButtonText correctly', () async { await plugin.openFiles(confirmButtonText: 'Open File'); - expect( + expectMethodCall( log, - [ - isMethodCall('openFile', arguments: { - 'acceptedTypes': null, - 'initialDirectory': null, - 'confirmButtonText': 'Open File', - 'multiple': true, - }), - ], + 'openFile', + arguments: { + 'acceptedTypes': null, + 'initialDirectory': null, + 'confirmButtonText': 'Open File', + 'multiple': true, + }, ); }); @@ -223,52 +217,49 @@ void main() { await plugin .getSavePath(acceptedTypeGroups: [group, groupTwo]); - expect( + expectMethodCall( log, - [ - isMethodCall('getSavePath', arguments: { - 'acceptedTypes': >{ - 'extensions': ['txt', 'jpg'], - 'mimeTypes': ['text/plain', 'image/jpg'], - 'UTIs': ['public.text', 'public.image'], - }, - 'initialDirectory': null, - 'suggestedName': null, - 'confirmButtonText': null, - }), - ], + 'getSavePath', + arguments: { + 'acceptedTypes': >{ + 'extensions': ['txt', 'jpg'], + 'mimeTypes': ['text/plain', 'image/jpg'], + 'UTIs': ['public.text', 'public.image'], + }, + 'initialDirectory': null, + 'suggestedName': null, + 'confirmButtonText': null, + }, ); }); test('passes initialDirectory correctly', () async { await plugin.getSavePath(initialDirectory: '/example/directory'); - expect( + expectMethodCall( log, - [ - isMethodCall('getSavePath', arguments: { - 'acceptedTypes': null, - 'initialDirectory': '/example/directory', - 'suggestedName': null, - 'confirmButtonText': null, - }), - ], + 'getSavePath', + arguments: { + 'acceptedTypes': null, + 'initialDirectory': '/example/directory', + 'suggestedName': null, + 'confirmButtonText': null, + }, ); }); test('passes confirmButtonText correctly', () async { await plugin.getSavePath(confirmButtonText: 'Open File'); - expect( + expectMethodCall( log, - [ - isMethodCall('getSavePath', arguments: { - 'acceptedTypes': null, - 'initialDirectory': null, - 'suggestedName': null, - 'confirmButtonText': 'Open File', - }), - ], + 'getSavePath', + arguments: { + 'acceptedTypes': null, + 'initialDirectory': null, + 'suggestedName': null, + 'confirmButtonText': 'Open File', + }, ); }); @@ -298,28 +289,26 @@ void main() { test('passes initialDirectory correctly', () async { await plugin.getDirectoryPath(initialDirectory: '/example/directory'); - expect( + expectMethodCall( log, - [ - isMethodCall('getDirectoryPath', arguments: { - 'initialDirectory': '/example/directory', - 'confirmButtonText': null, - }), - ], + 'getDirectoryPath', + arguments: { + 'initialDirectory': '/example/directory', + 'confirmButtonText': null, + }, ); }); test('passes confirmButtonText correctly', () async { await plugin.getDirectoryPath(confirmButtonText: 'Open File'); - expect( + expectMethodCall( log, - [ - isMethodCall('getDirectoryPath', arguments: { - 'initialDirectory': null, - 'confirmButtonText': 'Open File', - }), - ], + 'getDirectoryPath', + arguments: { + 'initialDirectory': null, + 'confirmButtonText': 'Open File', + }, ); }); }); @@ -343,16 +332,68 @@ void main() { ), ]); - expect( + expectMethodCall( log, - [ - isMethodCall('getSavePath', arguments: { - 'acceptedTypes': null, + 'getSavePath', + arguments: { + 'acceptedTypes': null, + 'initialDirectory': null, + 'suggestedName': null, + 'confirmButtonText': null, + }, + ); + }); + + group('getDirectoryPaths', () { + test('passes initialDirectory correctly', () async { + await plugin.getDirectoryPaths(initialDirectory: '/example/directory'); + + expectMethodCall( + log, + 'getDirectoryPath', + arguments: { + 'initialDirectory': '/example/directory', + 'confirmButtonText': null, + 'multiple': true + }, + ); + }); + + test('passes confirmButtonText correctly', () async { + await plugin.getDirectoryPaths(confirmButtonText: 'Open Directories'); + + expectMethodCall( + log, + 'getDirectoryPath', + arguments: { + 'initialDirectory': null, + 'confirmButtonText': 'Open Directories', + 'multiple': true + }, + ); + }); + + test('receives argument multiple as true even if the others are null', + () async { + await plugin.getDirectoryPaths(); + + expectMethodCall( + log, + 'getDirectoryPath', + arguments: { 'initialDirectory': null, - 'suggestedName': null, 'confirmButtonText': null, - }), - ], - ); + 'multiple': true + }, + ); + }); }); } + +void expectMethodCall( + List log, + String methodName, { + Map? arguments, +}) { + expect(log, [isMethodCall(methodName, arguments: arguments)]); +}