diff --git a/packages/file_selector/file_selector_windows/CHANGELOG.md b/packages/file_selector/file_selector_windows/CHANGELOG.md index 80151bf234d8..35163430e921 100644 --- a/packages/file_selector/file_selector_windows/CHANGELOG.md +++ b/packages/file_selector/file_selector_windows/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.9.2 + +* Adds `getDirectoryPaths` implementation. + ## 0.9.1+8 * Sets a cmake_policy compatibility version to fix build warnings. diff --git a/packages/file_selector/file_selector_windows/example/lib/get_multiple_directories_page.dart b/packages/file_selector/file_selector_windows/example/lib/get_multiple_directories_page.dart new file mode 100644 index 000000000000..c5fcecec7c77 --- /dev/null +++ b/packages/file_selector/file_selector_windows/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_windows/example/lib/home_page.dart b/packages/file_selector/file_selector_windows/example/lib/home_page.dart index 366ff5144245..3c80f4405785 100644 --- a/packages/file_selector/file_selector_windows/example/lib/home_page.dart +++ b/packages/file_selector/file_selector_windows/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_windows/example/lib/main.dart b/packages/file_selector/file_selector_windows/example/lib/main.dart index bfd2c2fdbfc1..a88f850f5d6d 100644 --- a/packages/file_selector/file_selector_windows/example/lib/main.dart +++ b/packages/file_selector/file_selector_windows/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_windows/example/pubspec.yaml b/packages/file_selector/file_selector_windows/example/pubspec.yaml index aa46b515f403..a22b6dc19aa9 100644 --- a/packages/file_selector/file_selector_windows/example/pubspec.yaml +++ b/packages/file_selector/file_selector_windows/example/pubspec.yaml @@ -8,7 +8,7 @@ environment: flutter: ">=3.3.0" dependencies: - file_selector_platform_interface: ^2.2.0 + file_selector_platform_interface: ^2.4.0 file_selector_windows: # When depending on this package from a real application you should use: # file_selector_windows: ^x.y.z diff --git a/packages/file_selector/file_selector_windows/lib/file_selector_windows.dart b/packages/file_selector/file_selector_windows/lib/file_selector_windows.dart index 4ce248343abb..0e935ccdb9d8 100644 --- a/packages/file_selector/file_selector_windows/lib/file_selector_windows.dart +++ b/packages/file_selector/file_selector_windows/lib/file_selector_windows.dart @@ -83,6 +83,22 @@ class FileSelectorWindows extends FileSelectorPlatform { confirmButtonText); return paths.isEmpty ? null : paths.first!; } + + @override + Future> getDirectoryPaths({ + String? initialDirectory, + String? confirmButtonText, + }) async { + final List paths = await _hostApi.showOpenDialog( + SelectionOptions( + allowMultiple: true, + selectFolders: true, + allowedTypes: [], + ), + initialDirectory, + confirmButtonText); + return paths.isEmpty ? [] : List.from(paths); + } } List _typeGroupsFromXTypeGroups(List? xtypes) { diff --git a/packages/file_selector/file_selector_windows/pubspec.yaml b/packages/file_selector/file_selector_windows/pubspec.yaml index be62b1fbda8f..88f0d99e0103 100644 --- a/packages/file_selector/file_selector_windows/pubspec.yaml +++ b/packages/file_selector/file_selector_windows/pubspec.yaml @@ -2,7 +2,7 @@ name: file_selector_windows description: Windows implementation of the file_selector plugin. repository: https://github.com/flutter/packages/tree/main/packages/file_selector/file_selector_windows issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+file_selector%22 -version: 0.9.1+8 +version: 0.9.2 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_windows/test/file_selector_windows_test.dart b/packages/file_selector/file_selector_windows/test/file_selector_windows_test.dart index 62745f7df707..c7a380d12028 100644 --- a/packages/file_selector/file_selector_windows/test/file_selector_windows_test.dart +++ b/packages/file_selector/file_selector_windows/test/file_selector_windows_test.dart @@ -213,6 +213,37 @@ void main() { }); }); + group('#getDirectoryPaths', () { + setUp(() { + when(mockApi.showOpenDialog(any, any, any)) + .thenReturn(['foo', 'bar']); + }); + + test('simple call works', () async { + final List paths = await plugin.getDirectoryPaths(); + + expect(paths[0], 'foo'); + expect(paths[1], 'bar'); + final VerificationResult result = + verify(mockApi.showOpenDialog(captureAny, null, null)); + final SelectionOptions options = result.captured[0] as SelectionOptions; + expect(options.allowMultiple, true); + expect(options.selectFolders, true); + }); + + test('passes initialDirectory correctly', () async { + await plugin.getDirectoryPath(initialDirectory: '/example/directory'); + + verify(mockApi.showOpenDialog(any, '/example/directory', null)); + }); + + test('passes confirmButtonText correctly', () async { + await plugin.getDirectoryPath(confirmButtonText: 'Open Directory'); + + verify(mockApi.showOpenDialog(any, null, 'Open Directory')); + }); + }); + group('#getSavePath', () { setUp(() { when(mockApi.showSaveDialog(any, any, any, any)) diff --git a/packages/file_selector/file_selector_windows/windows/test/file_selector_plugin_test.cpp b/packages/file_selector/file_selector_windows/windows/test/file_selector_plugin_test.cpp index 2325a271b777..8efeb54f8604 100644 --- a/packages/file_selector/file_selector_windows/windows/test/file_selector_plugin_test.cpp +++ b/packages/file_selector/file_selector_windows/windows/test/file_selector_plugin_test.cpp @@ -420,6 +420,55 @@ TEST(FileSelectorPlugin, TestGetDirectorySimple) { EXPECT_EQ(std::get(paths[0]), "C:\\Program Files"); } +TEST(FileSelectorPlugin, TestGetDirectoryMultiple) { + const HWND fake_window = reinterpret_cast(1337); + // These are actual files, but since the plugin implementation doesn't + // validate the types of items returned from the system dialog, they are fine + // to use for unit tests. + ScopedTestFileIdList fake_selected_dir_1; + ScopedTestFileIdList fake_selected_dir_2; + LPCITEMIDLIST fake_selected_dirs[] = { + fake_selected_dir_1.file(), + fake_selected_dir_2.file(), + }; + IShellItemArrayPtr fake_result_array; + ::SHCreateShellItemArrayFromIDLists(2, fake_selected_dirs, + &fake_result_array); + + bool shown = false; + MockShow show_validator = [&shown, fake_result_array, fake_window]( + const TestFileDialogController& dialog, + HWND parent) { + shown = true; + EXPECT_EQ(parent, fake_window); + + // Validate options. + FILEOPENDIALOGOPTIONS options; + dialog.GetOptions(&options); + EXPECT_NE(options & FOS_ALLOWMULTISELECT, 0U); + EXPECT_NE(options & FOS_PICKFOLDERS, 0U); + + return MockShowResult(fake_result_array); + }; + + FileSelectorPlugin plugin( + [fake_window] { return fake_window; }, + std::make_unique(show_validator)); + ErrorOr result = plugin.ShowOpenDialog( + CreateOptions(AllowMultipleArg(true), SelectFoldersArg(true), + EncodableList()), + nullptr, nullptr); + + EXPECT_TRUE(shown); + ASSERT_FALSE(result.has_error()); + const EncodableList& paths = result.value(); + EXPECT_EQ(paths.size(), 2); + EXPECT_EQ(std::get(paths[0]), + Utf8FromUtf16(fake_selected_dir_1.path())); + EXPECT_EQ(std::get(paths[1]), + Utf8FromUtf16(fake_selected_dir_2.path())); +} + TEST(FileSelectorPlugin, TestGetDirectoryCancel) { const HWND fake_window = reinterpret_cast(1337);