diff --git a/packages/file_selector/file_selector_linux/CHANGELOG.md b/packages/file_selector/file_selector_linux/CHANGELOG.md index 88886301d3a..5c9dc1dbcd8 100644 --- a/packages/file_selector/file_selector_linux/CHANGELOG.md +++ b/packages/file_selector/file_selector_linux/CHANGELOG.md @@ -1,5 +1,6 @@ -## NEXT +## 0.9.2 +* Adds `getSaveLocation` and deprecates `getSavePath`. * Updates minimum supported SDK version to Flutter 3.3/Dart 2.18. ## 0.9.1+3 diff --git a/packages/file_selector/file_selector_linux/example/lib/save_text_page.dart b/packages/file_selector/file_selector_linux/example/lib/save_text_page.dart index 2d259a62f49..bfd83cfd239 100644 --- a/packages/file_selector/file_selector_linux/example/lib/save_text_page.dart +++ b/packages/file_selector/file_selector_linux/example/lib/save_text_page.dart @@ -17,14 +17,12 @@ class SaveTextPage extends StatelessWidget { Future _saveFile() async { final String fileName = _nameController.text; - // TODO(stuartmorgan): Update this to getSaveLocation in the next federated - // change PR. - // ignore: deprecated_member_use - final String? path = await FileSelectorPlatform.instance.getSavePath( - // Operation was canceled by the user. - suggestedName: fileName, + final FileSaveLocation? result = + await FileSelectorPlatform.instance.getSaveLocation( + options: SaveDialogOptions(suggestedName: fileName), ); - if (path == null) { + // Operation was canceled by the user. + if (result == null) { return; } final String text = _contentController.text; @@ -32,7 +30,7 @@ class SaveTextPage extends StatelessWidget { const String fileMimeType = 'text/plain'; final XFile textFile = XFile.fromData(fileData, mimeType: fileMimeType, name: fileName); - await textFile.saveTo(path); + await textFile.saveTo(result.path); } @override diff --git a/packages/file_selector/file_selector_linux/example/pubspec.yaml b/packages/file_selector/file_selector_linux/example/pubspec.yaml index 1596eb3f769..da7410f1b11 100644 --- a/packages/file_selector/file_selector_linux/example/pubspec.yaml +++ b/packages/file_selector/file_selector_linux/example/pubspec.yaml @@ -10,7 +10,7 @@ environment: dependencies: file_selector_linux: path: ../ - file_selector_platform_interface: ^2.4.0 + file_selector_platform_interface: ^2.6.0 flutter: sdk: flutter diff --git a/packages/file_selector/file_selector_linux/lib/file_selector_linux.dart b/packages/file_selector/file_selector_linux/lib/file_selector_linux.dart index b8e3df6a11b..b06523b27d3 100644 --- a/packages/file_selector/file_selector_linux/lib/file_selector_linux.dart +++ b/packages/file_selector/file_selector_linux/lib/file_selector_linux.dart @@ -82,19 +82,37 @@ class FileSelectorLinux extends FileSelectorPlatform { String? initialDirectory, String? suggestedName, String? confirmButtonText, + }) async { + final FileSaveLocation? location = await getSaveLocation( + acceptedTypeGroups: acceptedTypeGroups, + options: SaveDialogOptions( + initialDirectory: initialDirectory, + suggestedName: suggestedName, + confirmButtonText: confirmButtonText, + )); + return location?.path; + } + + @override + Future getSaveLocation({ + List? acceptedTypeGroups, + SaveDialogOptions options = const SaveDialogOptions(), }) async { final List> serializedTypeGroups = _serializeTypeGroups(acceptedTypeGroups); - return _channel.invokeMethod( + // TODO(stuartmorgan): Add the selected type group here and return it. See + // https://github.com/flutter/flutter/issues/107093 + final String? path = await _channel.invokeMethod( _getSavePathMethod, { if (serializedTypeGroups.isNotEmpty) _acceptedTypeGroupsKey: serializedTypeGroups, - _initialDirectoryKey: initialDirectory, - _suggestedNameKey: suggestedName, - _confirmButtonTextKey: confirmButtonText, + _initialDirectoryKey: options.initialDirectory, + _suggestedNameKey: options.suggestedName, + _confirmButtonTextKey: options.confirmButtonText, }, ); + return path == null ? null : FileSaveLocation(path); } @override diff --git a/packages/file_selector/file_selector_linux/pubspec.yaml b/packages/file_selector/file_selector_linux/pubspec.yaml index 238e0aef741..0c9909f5b44 100644 --- a/packages/file_selector/file_selector_linux/pubspec.yaml +++ b/packages/file_selector/file_selector_linux/pubspec.yaml @@ -2,7 +2,7 @@ name: file_selector_linux description: Liunx implementation of the file_selector plugin. repository: https://github.com/flutter/packages/tree/main/packages/file_selector/file_selector_linux issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+file_selector%22 -version: 0.9.1+3 +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.4.0 + file_selector_platform_interface: ^2.6.0 flutter: sdk: flutter diff --git a/packages/file_selector/file_selector_linux/test/file_selector_linux_test.dart b/packages/file_selector/file_selector_linux/test/file_selector_linux_test.dart index 5127d28b7f6..f6b2cc66fe5 100644 --- a/packages/file_selector/file_selector_linux/test/file_selector_linux_test.dart +++ b/packages/file_selector/file_selector_linux/test/file_selector_linux_test.dart @@ -32,7 +32,7 @@ void main() { expect(FileSelectorPlatform.instance, isA()); }); - group('#openFile', () { + group('openFile', () { test('passes the accepted type groups correctly', () async { const XTypeGroup group = XTypeGroup( label: 'text', @@ -135,7 +135,7 @@ void main() { }); }); - group('#openFiles', () { + group('openFiles', () { test('passes the accepted type groups correctly', () async { const XTypeGroup group = XTypeGroup( label: 'text', @@ -209,7 +209,7 @@ void main() { ); await expectLater( - plugin.openFile(acceptedTypeGroups: [group]), + plugin.openFiles(acceptedTypeGroups: [group]), throwsArgumentError); }); @@ -218,7 +218,7 @@ void main() { label: 'any', ); - await plugin.openFile(acceptedTypeGroups: [group]); + await plugin.openFiles(acceptedTypeGroups: [group]); expectMethodCall( log, @@ -232,13 +232,120 @@ void main() { ], 'initialDirectory': null, 'confirmButtonText': null, - 'multiple': false, + 'multiple': true, + }, + ); + }); + }); + + group('getSaveLocation', () { + test('passes the accepted type groups correctly', () async { + const XTypeGroup group = XTypeGroup( + label: 'text', + extensions: ['txt'], + mimeTypes: ['text/plain'], + ); + + const XTypeGroup groupTwo = XTypeGroup( + label: 'image', + extensions: ['jpg'], + mimeTypes: ['image/jpg'], + ); + + await plugin + .getSaveLocation(acceptedTypeGroups: [group, groupTwo]); + + expectMethodCall( + log, + 'getSavePath', + arguments: { + 'acceptedTypeGroups': >[ + { + 'label': 'text', + 'extensions': ['*.txt'], + 'mimeTypes': ['text/plain'], + }, + { + 'label': 'image', + 'extensions': ['*.jpg'], + 'mimeTypes': ['image/jpg'], + }, + ], + 'initialDirectory': null, + 'suggestedName': null, + 'confirmButtonText': null, + }, + ); + }); + + test('passes initialDirectory correctly', () async { + await plugin.getSaveLocation( + options: + const SaveDialogOptions(initialDirectory: '/example/directory')); + + expectMethodCall( + log, + 'getSavePath', + arguments: { + 'initialDirectory': '/example/directory', + 'suggestedName': null, + 'confirmButtonText': null, + }, + ); + }); + + test('passes confirmButtonText correctly', () async { + await plugin.getSaveLocation( + options: const SaveDialogOptions(confirmButtonText: 'Open File')); + + expectMethodCall( + log, + 'getSavePath', + arguments: { + 'initialDirectory': null, + 'suggestedName': null, + 'confirmButtonText': 'Open File', + }, + ); + }); + + test('throws for a type group that does not support Linux', () async { + const XTypeGroup group = XTypeGroup( + label: 'images', + webWildCards: ['images/*'], + ); + + await expectLater( + plugin.getSaveLocation(acceptedTypeGroups: [group]), + throwsArgumentError); + }); + + test('passes a wildcard group correctly', () async { + const XTypeGroup group = XTypeGroup( + label: 'any', + ); + + await plugin.getSaveLocation(acceptedTypeGroups: [group]); + + expectMethodCall( + log, + 'getSavePath', + arguments: { + 'acceptedTypeGroups': >[ + { + 'label': 'any', + 'extensions': ['*'], + }, + ], + 'initialDirectory': null, + 'suggestedName': null, + 'confirmButtonText': null, }, ); }); }); - group('#getSavePath', () { + group('getSavePath (deprecated)', () { test('passes the accepted type groups correctly', () async { const XTypeGroup group = XTypeGroup( label: 'text', @@ -313,7 +420,7 @@ void main() { ); await expectLater( - plugin.openFile(acceptedTypeGroups: [group]), + plugin.getSavePath(acceptedTypeGroups: [group]), throwsArgumentError); }); @@ -322,11 +429,11 @@ void main() { label: 'any', ); - await plugin.openFile(acceptedTypeGroups: [group]); + await plugin.getSavePath(acceptedTypeGroups: [group]); expectMethodCall( log, - 'openFile', + 'getSavePath', arguments: { 'acceptedTypeGroups': >[ { @@ -335,14 +442,14 @@ void main() { }, ], 'initialDirectory': null, + 'suggestedName': null, 'confirmButtonText': null, - 'multiple': false, }, ); }); }); - group('#getDirectoryPath', () { + group('getDirectoryPath', () { test('passes initialDirectory correctly', () async { await plugin.getDirectoryPath(initialDirectory: '/example/directory'); @@ -369,7 +476,7 @@ void main() { }); }); - group('#getDirectoryPaths', () { + group('getDirectoryPaths', () { test('passes initialDirectory correctly', () async { await plugin.getDirectoryPaths(initialDirectory: '/example/directory'); diff --git a/packages/file_selector/file_selector_macos/CHANGELOG.md b/packages/file_selector/file_selector_macos/CHANGELOG.md index e697974a390..7a197838bb9 100644 --- a/packages/file_selector/file_selector_macos/CHANGELOG.md +++ b/packages/file_selector/file_selector_macos/CHANGELOG.md @@ -1,5 +1,6 @@ -## NEXT +## 0.9.3 +* Adds `getSaveLocation` and deprecates `getSavePath`. * Updates minimum supported macOS version to 10.14. ## 0.9.2 diff --git a/packages/file_selector/file_selector_macos/example/lib/save_text_page.dart b/packages/file_selector/file_selector_macos/example/lib/save_text_page.dart index 84180ac51df..6208e16ef7d 100644 --- a/packages/file_selector/file_selector_macos/example/lib/save_text_page.dart +++ b/packages/file_selector/file_selector_macos/example/lib/save_text_page.dart @@ -17,13 +17,11 @@ class SaveTextPage extends StatelessWidget { Future _saveFile() async { final String fileName = _nameController.text; - // TODO(stuartmorgan): Update this to getSaveLocation in the next federated - // change PR. - // ignore: deprecated_member_use - final String? path = await FileSelectorPlatform.instance.getSavePath( - suggestedName: fileName, + final FileSaveLocation? result = + await FileSelectorPlatform.instance.getSaveLocation( + options: SaveDialogOptions(suggestedName: fileName), ); - if (path == null) { + if (result == null) { // Operation was canceled by the user. return; } @@ -32,7 +30,7 @@ class SaveTextPage extends StatelessWidget { const String fileMimeType = 'text/plain'; final XFile textFile = XFile.fromData(fileData, mimeType: fileMimeType, name: fileName); - await textFile.saveTo(path); + await textFile.saveTo(result.path); } @override diff --git a/packages/file_selector/file_selector_macos/example/pubspec.yaml b/packages/file_selector/file_selector_macos/example/pubspec.yaml index 9e2d831f75c..baa27c56848 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.4.0 + file_selector_platform_interface: ^2.6.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 293c1e20e77..3489938a73a 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 @@ -60,12 +60,28 @@ class FileSelectorMacOS extends FileSelectorPlatform { String? suggestedName, String? confirmButtonText, }) async { - return _hostApi.displaySavePanel(SavePanelOptions( + final FileSaveLocation? location = await getSaveLocation( + acceptedTypeGroups: acceptedTypeGroups, + options: SaveDialogOptions( + initialDirectory: initialDirectory, + suggestedName: suggestedName, + confirmButtonText: confirmButtonText, + )); + return location?.path; + } + + @override + Future getSaveLocation({ + List? acceptedTypeGroups, + SaveDialogOptions options = const SaveDialogOptions(), + }) async { + final String? path = await _hostApi.displaySavePanel(SavePanelOptions( allowedFileTypes: _allowedTypesFromTypeGroups(acceptedTypeGroups), - directoryPath: initialDirectory, - nameFieldStringValue: suggestedName, - prompt: confirmButtonText, + directoryPath: options.initialDirectory, + nameFieldStringValue: options.suggestedName, + prompt: options.confirmButtonText, )); + return path == null ? null : FileSaveLocation(path); } @override diff --git a/packages/file_selector/file_selector_macos/pubspec.yaml b/packages/file_selector/file_selector_macos/pubspec.yaml index 731a5cc8eed..047c7caca4f 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.2 +version: 0.9.3 environment: sdk: ">=2.18.0 <4.0.0" @@ -18,7 +18,7 @@ flutter: dependencies: cross_file: ^0.3.1 - file_selector_platform_interface: ^2.4.0 + file_selector_platform_interface: ^2.6.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 6450e6f3b0a..c7268a6be89 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 @@ -227,7 +227,7 @@ void main() { }); }); - group('getSavePath', () { + group('getSavePath (deprecated)', () { test('works as expected with no arguments', () async { when(mockApi.displaySavePanel(any)).thenAnswer((_) async => 'foo'); @@ -344,6 +344,126 @@ void main() { }); }); + group('getSaveLocation', () { + test('works as expected with no arguments', () async { + when(mockApi.displaySavePanel(any)).thenAnswer((_) async => 'foo'); + + final FileSaveLocation? location = await plugin.getSaveLocation(); + + expect(location?.path, 'foo'); + final VerificationResult result = + verify(mockApi.displaySavePanel(captureAny)); + final SavePanelOptions options = result.captured[0] as SavePanelOptions; + expect(options.allowedFileTypes, null); + expect(options.directoryPath, null); + expect(options.nameFieldStringValue, null); + expect(options.prompt, null); + }); + + test('handles cancel', () async { + when(mockApi.displaySavePanel(any)).thenAnswer((_) async => null); + + final FileSaveLocation? location = await plugin.getSaveLocation(); + + expect(location, null); + }); + + test('passes the accepted type groups correctly', () async { + const XTypeGroup group = XTypeGroup( + label: 'text', + extensions: ['txt'], + mimeTypes: ['text/plain'], + uniformTypeIdentifiers: ['public.text'], + ); + + const XTypeGroup groupTwo = XTypeGroup( + label: 'image', + extensions: ['jpg'], + mimeTypes: ['image/jpg'], + uniformTypeIdentifiers: ['public.image'], + webWildCards: ['image/*']); + + await plugin + .getSaveLocation(acceptedTypeGroups: [group, groupTwo]); + + final VerificationResult result = + verify(mockApi.displaySavePanel(captureAny)); + final SavePanelOptions options = result.captured[0] as SavePanelOptions; + expect(options.allowedFileTypes!.extensions, ['txt', 'jpg']); + expect(options.allowedFileTypes!.mimeTypes, + ['text/plain', 'image/jpg']); + expect(options.allowedFileTypes!.utis, + ['public.text', 'public.image']); + }); + + test('passes initialDirectory correctly', () async { + await plugin.getSaveLocation( + options: + const SaveDialogOptions(initialDirectory: '/example/directory')); + + final VerificationResult result = + verify(mockApi.displaySavePanel(captureAny)); + final SavePanelOptions options = result.captured[0] as SavePanelOptions; + expect(options.directoryPath, '/example/directory'); + }); + + test('passes confirmButtonText correctly', () async { + await plugin.getSaveLocation( + options: const SaveDialogOptions(confirmButtonText: 'Open File')); + + final VerificationResult result = + verify(mockApi.displaySavePanel(captureAny)); + final SavePanelOptions options = result.captured[0] as SavePanelOptions; + expect(options.prompt, 'Open File'); + }); + + test('throws for a type group that does not support macOS', () async { + const XTypeGroup group = XTypeGroup( + label: 'images', + webWildCards: ['images/*'], + ); + + await expectLater( + plugin.getSaveLocation(acceptedTypeGroups: [group]), + throwsArgumentError); + }); + + test('allows a wildcard group', () async { + const XTypeGroup group = XTypeGroup( + label: 'text', + ); + + await expectLater( + plugin.getSaveLocation(acceptedTypeGroups: [group]), + completes); + }); + + test('ignores all type groups if any of them is a wildcard', () async { + await plugin.getSaveLocation(acceptedTypeGroups: [ + const XTypeGroup( + label: 'text', + extensions: ['txt'], + mimeTypes: ['text/plain'], + uniformTypeIdentifiers: ['public.text'], + ), + const XTypeGroup( + label: 'image', + extensions: ['jpg'], + mimeTypes: ['image/jpg'], + uniformTypeIdentifiers: ['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', () { test('works as expected with no arguments', () async { when(mockApi.displayOpenPanel(any)) diff --git a/packages/file_selector/file_selector_web/CHANGELOG.md b/packages/file_selector/file_selector_web/CHANGELOG.md index 43ad4962f04..073f1c20ed8 100644 --- a/packages/file_selector/file_selector_web/CHANGELOG.md +++ b/packages/file_selector/file_selector_web/CHANGELOG.md @@ -1,5 +1,6 @@ -## NEXT +## 0.9.1 +* Adds `getSaveLocation` and deprecates `getSavePath`. * Updates minimum supported SDK version to Flutter 3.3/Dart 2.18. ## 0.9.0+4 diff --git a/packages/file_selector/file_selector_web/example/pubspec.yaml b/packages/file_selector/file_selector_web/example/pubspec.yaml index bc8b984e367..fb3e71ed3f3 100644 --- a/packages/file_selector/file_selector_web/example/pubspec.yaml +++ b/packages/file_selector/file_selector_web/example/pubspec.yaml @@ -6,7 +6,7 @@ environment: flutter: ">=3.3.0" dependencies: - file_selector_platform_interface: ^2.2.0 + file_selector_platform_interface: ^2.6.0 file_selector_web: path: ../ flutter: diff --git a/packages/file_selector/file_selector_web/lib/file_selector_web.dart b/packages/file_selector/file_selector_web/lib/file_selector_web.dart index 748bb3aa0df..2380e274c46 100644 --- a/packages/file_selector/file_selector_web/lib/file_selector_web.dart +++ b/packages/file_selector/file_selector_web/lib/file_selector_web.dart @@ -60,6 +60,16 @@ class FileSelectorWeb extends FileSelectorPlatform { }) async => ''; + @override + Future getSaveLocation({ + List? acceptedTypeGroups, + SaveDialogOptions options = const SaveDialogOptions(), + }) async { + // This is intended to be passed to XFile, which ignores the path, so + // provide a non-null dummy value. + return const FileSaveLocation(''); + } + @override Future getDirectoryPath({ String? initialDirectory, diff --git a/packages/file_selector/file_selector_web/pubspec.yaml b/packages/file_selector/file_selector_web/pubspec.yaml index 8a601cbb366..81ba11a9634 100644 --- a/packages/file_selector/file_selector_web/pubspec.yaml +++ b/packages/file_selector/file_selector_web/pubspec.yaml @@ -2,7 +2,7 @@ name: file_selector_web description: Web platform implementation of file_selector repository: https://github.com/flutter/packages/tree/main/packages/file_selector/file_selector_web issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+file_selector%22 -version: 0.9.0+4 +version: 0.9.1 environment: sdk: ">=2.18.0 <4.0.0" @@ -17,7 +17,7 @@ flutter: fileName: file_selector_web.dart dependencies: - file_selector_platform_interface: ^2.3.0 + file_selector_platform_interface: ^2.6.0 flutter: sdk: flutter flutter_web_plugins: diff --git a/packages/file_selector/file_selector_windows/CHANGELOG.md b/packages/file_selector/file_selector_windows/CHANGELOG.md index 35163430e92..b747a45723a 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.3 + +* Adds `getSaveLocation` and deprecates `getSavePath`. + ## 0.9.2 * Adds `getDirectoryPaths` implementation. diff --git a/packages/file_selector/file_selector_windows/example/lib/save_text_page.dart b/packages/file_selector/file_selector_windows/example/lib/save_text_page.dart index 2d259a62f49..c92aff05569 100644 --- a/packages/file_selector/file_selector_windows/example/lib/save_text_page.dart +++ b/packages/file_selector/file_selector_windows/example/lib/save_text_page.dart @@ -2,6 +2,7 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'dart:io'; import 'dart:typed_data'; import 'package:file_selector_platform_interface/file_selector_platform_interface.dart'; import 'package:flutter/material.dart'; @@ -17,22 +18,39 @@ class SaveTextPage extends StatelessWidget { Future _saveFile() async { final String fileName = _nameController.text; - // TODO(stuartmorgan): Update this to getSaveLocation in the next federated - // change PR. - // ignore: deprecated_member_use - final String? path = await FileSelectorPlatform.instance.getSavePath( - // Operation was canceled by the user. - suggestedName: fileName, + final FileSaveLocation? result = + await FileSelectorPlatform.instance.getSaveLocation( + options: SaveDialogOptions(suggestedName: fileName), + acceptedTypeGroups: const [ + XTypeGroup( + label: 'Plain text', + extensions: ['txt'], + ), + XTypeGroup( + label: 'JSON', + extensions: ['json'], + ), + ], ); - if (path == null) { + // Operation was canceled by the user. + if (result == null) { return; } + String path = result.path; + // Append an extension based on the selected type group if the user didn't + // include one. + if (!path.split(Platform.pathSeparator).last.contains('.')) { + final XTypeGroup? activeGroup = result.activeFilter; + if (activeGroup != null) { + // The group is one of the groups passed in above, each of which has + // exactly one `extensions` entry. + path = '$path.${activeGroup.extensions!.first}'; + } + } final String text = _contentController.text; final Uint8List fileData = Uint8List.fromList(text.codeUnits); - const String fileMimeType = 'text/plain'; - final XFile textFile = - XFile.fromData(fileData, mimeType: fileMimeType, name: fileName); - await textFile.saveTo(path); + final XFile textFile = XFile.fromData(fileData, name: fileName); + await textFile.saveTo(result.path); } @override diff --git a/packages/file_selector/file_selector_windows/example/pubspec.yaml b/packages/file_selector/file_selector_windows/example/pubspec.yaml index a22b6dc19aa..02a1475775e 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.4.0 + file_selector_platform_interface: ^2.6.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/example/windows/runner/Runner.rc b/packages/file_selector/file_selector_windows/example/windows/runner/Runner.rc index 51812dcd487..e5666e0223f 100644 --- a/packages/file_selector/file_selector_windows/example/windows/runner/Runner.rc +++ b/packages/file_selector/file_selector_windows/example/windows/runner/Runner.rc @@ -60,14 +60,14 @@ IDI_APP_ICON ICON "resources\\app_icon.ico" // Version // -#ifdef FLUTTER_BUILD_NUMBER -#define VERSION_AS_NUMBER FLUTTER_BUILD_NUMBER +#if defined(FLUTTER_VERSION_MAJOR) && defined(FLUTTER_VERSION_MINOR) && defined(FLUTTER_VERSION_PATCH) && defined(FLUTTER_VERSION_BUILD) +#define VERSION_AS_NUMBER FLUTTER_VERSION_MAJOR,FLUTTER_VERSION_MINOR,FLUTTER_VERSION_PATCH,FLUTTER_VERSION_BUILD #else -#define VERSION_AS_NUMBER 1,0,0 +#define VERSION_AS_NUMBER 1,0,0,0 #endif -#ifdef FLUTTER_BUILD_NAME -#define VERSION_AS_STRING #FLUTTER_BUILD_NAME +#if defined(FLUTTER_VERSION) +#define VERSION_AS_STRING FLUTTER_VERSION #else #define VERSION_AS_STRING "1.0.0" #endif 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 0e935ccdb9d..b96886bd0df 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 @@ -21,7 +21,7 @@ class FileSelectorWindows extends FileSelectorPlatform { String? initialDirectory, String? confirmButtonText, }) async { - final List paths = await _hostApi.showOpenDialog( + final FileDialogResult result = await _hostApi.showOpenDialog( SelectionOptions( allowMultiple: false, selectFolders: false, @@ -29,7 +29,7 @@ class FileSelectorWindows extends FileSelectorPlatform { ), initialDirectory, confirmButtonText); - return paths.isEmpty ? null : XFile(paths.first!); + return result.paths.isEmpty ? null : XFile(result.paths.first!); } @override @@ -38,7 +38,7 @@ class FileSelectorWindows extends FileSelectorPlatform { String? initialDirectory, String? confirmButtonText, }) async { - final List paths = await _hostApi.showOpenDialog( + final FileDialogResult result = await _hostApi.showOpenDialog( SelectionOptions( allowMultiple: true, selectFolders: false, @@ -46,7 +46,7 @@ class FileSelectorWindows extends FileSelectorPlatform { ), initialDirectory, confirmButtonText); - return paths.map((String? path) => XFile(path!)).toList(); + return result.paths.map((String? path) => XFile(path!)).toList(); } @override @@ -56,16 +56,36 @@ class FileSelectorWindows extends FileSelectorPlatform { String? suggestedName, String? confirmButtonText, }) async { - final List paths = await _hostApi.showSaveDialog( + final FileSaveLocation? location = await getSaveLocation( + acceptedTypeGroups: acceptedTypeGroups, + options: SaveDialogOptions( + initialDirectory: initialDirectory, + suggestedName: suggestedName, + confirmButtonText: confirmButtonText, + )); + return location?.path; + } + + @override + Future getSaveLocation({ + List? acceptedTypeGroups, + SaveDialogOptions options = const SaveDialogOptions(), + }) async { + final FileDialogResult result = await _hostApi.showSaveDialog( SelectionOptions( allowMultiple: false, selectFolders: false, allowedTypes: _typeGroupsFromXTypeGroups(acceptedTypeGroups), ), - initialDirectory, - suggestedName, - confirmButtonText); - return paths.isEmpty ? null : paths.first!; + options.initialDirectory, + options.suggestedName, + options.confirmButtonText); + final int? groupIndex = result.typeGroupIndex; + return result.paths.isEmpty + ? null + : FileSaveLocation(result.paths.first!, + activeFilter: + groupIndex == null ? null : acceptedTypeGroups?[groupIndex]); } @override @@ -73,7 +93,7 @@ class FileSelectorWindows extends FileSelectorPlatform { String? initialDirectory, String? confirmButtonText, }) async { - final List paths = await _hostApi.showOpenDialog( + final FileDialogResult result = await _hostApi.showOpenDialog( SelectionOptions( allowMultiple: false, selectFolders: true, @@ -81,7 +101,7 @@ class FileSelectorWindows extends FileSelectorPlatform { ), initialDirectory, confirmButtonText); - return paths.isEmpty ? null : paths.first!; + return result.paths.isEmpty ? null : result.paths.first!; } @override @@ -89,7 +109,7 @@ class FileSelectorWindows extends FileSelectorPlatform { String? initialDirectory, String? confirmButtonText, }) async { - final List paths = await _hostApi.showOpenDialog( + final FileDialogResult result = await _hostApi.showOpenDialog( SelectionOptions( allowMultiple: true, selectFolders: true, @@ -97,7 +117,7 @@ class FileSelectorWindows extends FileSelectorPlatform { ), initialDirectory, confirmButtonText); - return paths.isEmpty ? [] : List.from(paths); + return result.paths.isEmpty ? [] : List.from(result.paths); } } diff --git a/packages/file_selector/file_selector_windows/lib/src/messages.g.dart b/packages/file_selector/file_selector_windows/lib/src/messages.g.dart index a61076b97b3..b5b4a794388 100644 --- a/packages/file_selector/file_selector_windows/lib/src/messages.g.dart +++ b/packages/file_selector/file_selector_windows/lib/src/messages.g.dart @@ -1,7 +1,7 @@ // 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. -// Autogenerated from Pigeon (v9.1.1), do not edit directly. +// Autogenerated from Pigeon (v10.0.1), do not edit directly. // See also: https://pub.dev/packages/pigeon // ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import, unnecessary_parenthesis, prefer_null_aware_operators, omit_local_variable_types, unused_shown_name, unnecessary_import @@ -68,16 +68,53 @@ class SelectionOptions { } } +/// The result from an open or save dialog. +class FileDialogResult { + FileDialogResult({ + required this.paths, + this.typeGroupIndex, + }); + + /// The selected paths. + /// + /// Empty if the dialog was canceled. + List paths; + + /// The type group index (into the list provided in [SelectionOptions]) of + /// the group that was selected when the dialog was confirmed. + /// + /// Null if no type groups were provided, or the dialog was canceled. + int? typeGroupIndex; + + Object encode() { + return [ + paths, + typeGroupIndex, + ]; + } + + static FileDialogResult decode(Object result) { + result as List; + return FileDialogResult( + paths: (result[0] as List?)!.cast(), + typeGroupIndex: result[1] as int?, + ); + } +} + class _FileSelectorApiCodec extends StandardMessageCodec { const _FileSelectorApiCodec(); @override void writeValue(WriteBuffer buffer, Object? value) { - if (value is SelectionOptions) { + if (value is FileDialogResult) { buffer.putUint8(128); writeValue(buffer, value.encode()); - } else if (value is TypeGroup) { + } else if (value is SelectionOptions) { buffer.putUint8(129); writeValue(buffer, value.encode()); + } else if (value is TypeGroup) { + buffer.putUint8(130); + writeValue(buffer, value.encode()); } else { super.writeValue(buffer, value); } @@ -87,8 +124,10 @@ class _FileSelectorApiCodec extends StandardMessageCodec { Object? readValueOfType(int type, ReadBuffer buffer) { switch (type) { case 128: - return SelectionOptions.decode(readValue(buffer)!); + return FileDialogResult.decode(readValue(buffer)!); case 129: + return SelectionOptions.decode(readValue(buffer)!); + case 130: return TypeGroup.decode(readValue(buffer)!); default: return super.readValueOfType(type, buffer); @@ -106,7 +145,7 @@ class FileSelectorApi { static const MessageCodec codec = _FileSelectorApiCodec(); - Future> showOpenDialog(SelectionOptions arg_options, + Future showOpenDialog(SelectionOptions arg_options, String? arg_initialDirectory, String? arg_confirmButtonText) async { final BasicMessageChannel channel = BasicMessageChannel( 'dev.flutter.pigeon.FileSelectorApi.showOpenDialog', codec, @@ -131,11 +170,11 @@ class FileSelectorApi { message: 'Host platform returned null value for non-null return value.', ); } else { - return (replyList[0] as List?)!.cast(); + return (replyList[0] as FileDialogResult?)!; } } - Future> showSaveDialog( + Future showSaveDialog( SelectionOptions arg_options, String? arg_initialDirectory, String? arg_suggestedName, @@ -166,7 +205,7 @@ class FileSelectorApi { message: 'Host platform returned null value for non-null return value.', ); } else { - return (replyList[0] as List?)!.cast(); + return (replyList[0] as FileDialogResult?)!; } } } diff --git a/packages/file_selector/file_selector_windows/pigeons/messages.dart b/packages/file_selector/file_selector_windows/pigeons/messages.dart index c3b3aff192b..8f82aec0b83 100644 --- a/packages/file_selector/file_selector_windows/pigeons/messages.dart +++ b/packages/file_selector/file_selector_windows/pigeons/messages.dart @@ -37,14 +37,33 @@ class SelectionOptions { List allowedTypes; } +/// The result from an open or save dialog. +class FileDialogResult { + FileDialogResult({required this.paths, this.typeGroupIndex}); + + /// The selected paths. + /// + /// Empty if the dialog was canceled. + // TODO(stuartmorgan): Make the generic type non-nullable once supported. + // https://github.com/flutter/flutter/issues/97848 + // The Dart code treats the values as non-nullable. + List paths; + + /// The type group index (into the list provided in [SelectionOptions]) of + /// the group that was selected when the dialog was confirmed. + /// + /// Null if no type groups were provided, or the dialog was canceled. + int? typeGroupIndex; +} + @HostApi(dartHostTestHandler: 'TestFileSelectorApi') abstract class FileSelectorApi { - List showOpenDialog( + FileDialogResult showOpenDialog( SelectionOptions options, String? initialDirectory, String? confirmButtonText, ); - List showSaveDialog( + FileDialogResult showSaveDialog( SelectionOptions options, String? initialDirectory, String? suggestedName, diff --git a/packages/file_selector/file_selector_windows/pubspec.yaml b/packages/file_selector/file_selector_windows/pubspec.yaml index 39d5cf7abf6..4cd3cc52154 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.2 +version: 0.9.3 environment: sdk: ">=2.18.0 <4.0.0" @@ -18,7 +18,7 @@ flutter: dependencies: cross_file: ^0.3.1 - file_selector_platform_interface: ^2.4.0 + file_selector_platform_interface: ^2.6.0 flutter: sdk: flutter @@ -27,4 +27,4 @@ dev_dependencies: flutter_test: sdk: flutter mockito: 5.4.1 - pigeon: ^9.1.0 + pigeon: ^10.0.0 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 fbe3683af37..4f455ee3088 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 @@ -30,9 +30,10 @@ void main() { expect(FileSelectorPlatform.instance, isA()); }); - group('#openFile', () { + group('openFile', () { setUp(() { - when(mockApi.showOpenDialog(any, any, any)).thenReturn(['foo']); + when(mockApi.showOpenDialog(any, any, any)) + .thenReturn(FileDialogResult(paths: ['foo'])); }); test('simple call works', () async { @@ -105,10 +106,10 @@ void main() { }); }); - group('#openFiles', () { + group('openFiles', () { setUp(() { when(mockApi.showOpenDialog(any, any, any)) - .thenReturn(['foo', 'bar']); + .thenReturn(FileDialogResult(paths: ['foo', 'bar'])); }); test('simple call works', () async { @@ -182,9 +183,10 @@ void main() { }); }); - group('#getDirectoryPath', () { + group('getDirectoryPath', () { setUp(() { - when(mockApi.showOpenDialog(any, any, any)).thenReturn(['foo']); + when(mockApi.showOpenDialog(any, any, any)) + .thenReturn(FileDialogResult(paths: ['foo'])); }); test('simple call works', () async { @@ -211,10 +213,10 @@ void main() { }); }); - group('#getDirectoryPaths', () { + group('getDirectoryPaths', () { setUp(() { when(mockApi.showOpenDialog(any, any, any)) - .thenReturn(['foo', 'bar']); + .thenReturn(FileDialogResult(paths: ['foo', 'bar'])); }); test('simple call works', () async { @@ -242,10 +244,122 @@ void main() { }); }); - group('#getSavePath', () { + group('getSaveLocation', () { + setUp(() { + when(mockApi.showSaveDialog(any, any, any, any)) + .thenReturn(FileDialogResult(paths: ['foo'])); + }); + + test('simple call works', () async { + final FileSaveLocation? location = await plugin.getSaveLocation(); + + expect(location?.path, 'foo'); + expect(location?.activeFilter, null); + final VerificationResult result = + verify(mockApi.showSaveDialog(captureAny, null, null, null)); + final SelectionOptions options = result.captured[0] as SelectionOptions; + expect(options.allowMultiple, false); + expect(options.selectFolders, false); + }); + + test('passes the accepted type groups correctly', () async { + const XTypeGroup group = XTypeGroup( + label: 'text', + extensions: ['txt'], + mimeTypes: ['text/plain'], + ); + + const XTypeGroup groupTwo = XTypeGroup( + label: 'image', + extensions: ['jpg'], + mimeTypes: ['image/jpg'], + ); + + await plugin + .getSaveLocation(acceptedTypeGroups: [group, groupTwo]); + + final VerificationResult result = + verify(mockApi.showSaveDialog(captureAny, null, null, null)); + final SelectionOptions options = result.captured[0] as SelectionOptions; + expect( + _typeGroupListsMatch(options.allowedTypes, [ + TypeGroup(label: 'text', extensions: ['txt']), + TypeGroup(label: 'image', extensions: ['jpg']), + ]), + true); + }); + + test('returns the selected type group correctly', () async { + when(mockApi.showSaveDialog(any, any, any, any)).thenReturn( + FileDialogResult(paths: ['foo'], typeGroupIndex: 1)); + const XTypeGroup group = XTypeGroup( + label: 'text', + extensions: ['txt'], + mimeTypes: ['text/plain'], + ); + + const XTypeGroup groupTwo = XTypeGroup( + label: 'image', + extensions: ['jpg'], + mimeTypes: ['image/jpg'], + ); + + final FileSaveLocation? result = await plugin + .getSaveLocation(acceptedTypeGroups: [group, groupTwo]); + + verify(mockApi.showSaveDialog(captureAny, null, null, null)); + + expect(result?.activeFilter, groupTwo); + }); + + test('passes initialDirectory correctly', () async { + await plugin.getSaveLocation( + options: + const SaveDialogOptions(initialDirectory: '/example/directory')); + + verify(mockApi.showSaveDialog(any, '/example/directory', null, null)); + }); + + test('passes suggestedName correctly', () async { + await plugin.getSaveLocation( + options: const SaveDialogOptions(suggestedName: 'baz.txt')); + + verify(mockApi.showSaveDialog(any, null, 'baz.txt', null)); + }); + + test('passes confirmButtonText correctly', () async { + await plugin.getSaveLocation( + options: const SaveDialogOptions(confirmButtonText: 'Save File')); + + verify(mockApi.showSaveDialog(any, null, null, 'Save File')); + }); + + test('throws for a type group that does not support Windows', () async { + const XTypeGroup group = XTypeGroup( + label: 'text', + mimeTypes: ['text/plain'], + ); + + await expectLater( + plugin.getSaveLocation(acceptedTypeGroups: [group]), + throwsArgumentError); + }); + + test('allows a wildcard group', () async { + const XTypeGroup group = XTypeGroup( + label: 'text', + ); + + await expectLater( + plugin.getSaveLocation(acceptedTypeGroups: [group]), + completes); + }); + }); + + group('getSavePath (deprecated)', () { setUp(() { when(mockApi.showSaveDialog(any, any, any, any)) - .thenReturn(['foo']); + .thenReturn(FileDialogResult(paths: ['foo'])); }); test('simple call works', () async { diff --git a/packages/file_selector/file_selector_windows/test/file_selector_windows_test.mocks.dart b/packages/file_selector/file_selector_windows/test/file_selector_windows_test.mocks.dart index ae55f2e301d..7168e0c8d81 100644 --- a/packages/file_selector/file_selector_windows/test/file_selector_windows_test.mocks.dart +++ b/packages/file_selector/file_selector_windows/test/file_selector_windows_test.mocks.dart @@ -1,12 +1,14 @@ -// Mocks generated by Mockito 5.4.0 from annotations +// Mocks generated by Mockito 5.4.1 from annotations // in file_selector_windows/test/file_selector_windows_test.dart. // Do not manually edit this file. +// @dart=2.19 + // ignore_for_file: no_leading_underscores_for_library_prefixes -import 'package:file_selector_windows/src/messages.g.dart' as _i3; +import 'package:file_selector_windows/src/messages.g.dart' as _i2; import 'package:mockito/mockito.dart' as _i1; -import 'test_api.g.dart' as _i2; +import 'test_api.g.dart' as _i3; // ignore_for_file: type=lint // ignore_for_file: avoid_redundant_argument_values @@ -19,18 +21,29 @@ import 'test_api.g.dart' as _i2; // ignore_for_file: camel_case_types // ignore_for_file: subtype_of_sealed_class +class _FakeFileDialogResult_0 extends _i1.SmartFake + implements _i2.FileDialogResult { + _FakeFileDialogResult_0( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + /// A class which mocks [TestFileSelectorApi]. /// /// See the documentation for Mockito's code generation for more information. class MockTestFileSelectorApi extends _i1.Mock - implements _i2.TestFileSelectorApi { + implements _i3.TestFileSelectorApi { MockTestFileSelectorApi() { _i1.throwOnMissingStub(this); } @override - List showOpenDialog( - _i3.SelectionOptions? options, + _i2.FileDialogResult showOpenDialog( + _i2.SelectionOptions? options, String? initialDirectory, String? confirmButtonText, ) => @@ -43,11 +56,21 @@ class MockTestFileSelectorApi extends _i1.Mock confirmButtonText, ], ), - returnValue: [], - ) as List); + returnValue: _FakeFileDialogResult_0( + this, + Invocation.method( + #showOpenDialog, + [ + options, + initialDirectory, + confirmButtonText, + ], + ), + ), + ) as _i2.FileDialogResult); @override - List showSaveDialog( - _i3.SelectionOptions? options, + _i2.FileDialogResult showSaveDialog( + _i2.SelectionOptions? options, String? initialDirectory, String? suggestedName, String? confirmButtonText, @@ -62,6 +85,17 @@ class MockTestFileSelectorApi extends _i1.Mock confirmButtonText, ], ), - returnValue: [], - ) as List); + returnValue: _FakeFileDialogResult_0( + this, + Invocation.method( + #showSaveDialog, + [ + options, + initialDirectory, + suggestedName, + confirmButtonText, + ], + ), + ), + ) as _i2.FileDialogResult); } diff --git a/packages/file_selector/file_selector_windows/test/test_api.g.dart b/packages/file_selector/file_selector_windows/test/test_api.g.dart index f9ed8e5b63d..778ae4fc16c 100644 --- a/packages/file_selector/file_selector_windows/test/test_api.g.dart +++ b/packages/file_selector/file_selector_windows/test/test_api.g.dart @@ -1,7 +1,7 @@ // 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. -// Autogenerated from Pigeon (v9.1.1), do not edit directly. +// Autogenerated from Pigeon (v10.0.1), do not edit directly. // See also: https://pub.dev/packages/pigeon // ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import, unnecessary_parenthesis, unnecessary_import // ignore_for_file: avoid_relative_lib_imports @@ -17,12 +17,15 @@ class _TestFileSelectorApiCodec extends StandardMessageCodec { const _TestFileSelectorApiCodec(); @override void writeValue(WriteBuffer buffer, Object? value) { - if (value is SelectionOptions) { + if (value is FileDialogResult) { buffer.putUint8(128); writeValue(buffer, value.encode()); - } else if (value is TypeGroup) { + } else if (value is SelectionOptions) { buffer.putUint8(129); writeValue(buffer, value.encode()); + } else if (value is TypeGroup) { + buffer.putUint8(130); + writeValue(buffer, value.encode()); } else { super.writeValue(buffer, value); } @@ -32,8 +35,10 @@ class _TestFileSelectorApiCodec extends StandardMessageCodec { Object? readValueOfType(int type, ReadBuffer buffer) { switch (type) { case 128: - return SelectionOptions.decode(readValue(buffer)!); + return FileDialogResult.decode(readValue(buffer)!); case 129: + return SelectionOptions.decode(readValue(buffer)!); + case 130: return TypeGroup.decode(readValue(buffer)!); default: return super.readValueOfType(type, buffer); @@ -42,12 +47,14 @@ class _TestFileSelectorApiCodec extends StandardMessageCodec { } abstract class TestFileSelectorApi { + static TestDefaultBinaryMessengerBinding? get _testBinaryMessengerBinding => + TestDefaultBinaryMessengerBinding.instance; static const MessageCodec codec = _TestFileSelectorApiCodec(); - List showOpenDialog(SelectionOptions options, + FileDialogResult showOpenDialog(SelectionOptions options, String? initialDirectory, String? confirmButtonText); - List showSaveDialog( + FileDialogResult showSaveDialog( SelectionOptions options, String? initialDirectory, String? suggestedName, @@ -60,9 +67,12 @@ abstract class TestFileSelectorApi { 'dev.flutter.pigeon.FileSelectorApi.showOpenDialog', codec, binaryMessenger: binaryMessenger); if (api == null) { - channel.setMockMessageHandler(null); + _testBinaryMessengerBinding!.defaultBinaryMessenger + .setMockDecodedMessageHandler(channel, null); } else { - channel.setMockMessageHandler((Object? message) async { + _testBinaryMessengerBinding!.defaultBinaryMessenger + .setMockDecodedMessageHandler(channel, + (Object? message) async { assert(message != null, 'Argument for dev.flutter.pigeon.FileSelectorApi.showOpenDialog was null.'); final List args = (message as List?)!; @@ -71,7 +81,7 @@ abstract class TestFileSelectorApi { 'Argument for dev.flutter.pigeon.FileSelectorApi.showOpenDialog was null, expected non-null SelectionOptions.'); final String? arg_initialDirectory = (args[1] as String?); final String? arg_confirmButtonText = (args[2] as String?); - final List output = api.showOpenDialog( + final FileDialogResult output = api.showOpenDialog( arg_options!, arg_initialDirectory, arg_confirmButtonText); return [output]; }); @@ -82,9 +92,12 @@ abstract class TestFileSelectorApi { 'dev.flutter.pigeon.FileSelectorApi.showSaveDialog', codec, binaryMessenger: binaryMessenger); if (api == null) { - channel.setMockMessageHandler(null); + _testBinaryMessengerBinding!.defaultBinaryMessenger + .setMockDecodedMessageHandler(channel, null); } else { - channel.setMockMessageHandler((Object? message) async { + _testBinaryMessengerBinding!.defaultBinaryMessenger + .setMockDecodedMessageHandler(channel, + (Object? message) async { assert(message != null, 'Argument for dev.flutter.pigeon.FileSelectorApi.showSaveDialog was null.'); final List args = (message as List?)!; @@ -94,7 +107,7 @@ abstract class TestFileSelectorApi { final String? arg_initialDirectory = (args[1] as String?); final String? arg_suggestedName = (args[2] as String?); final String? arg_confirmButtonText = (args[3] as String?); - final List output = api.showSaveDialog(arg_options!, + final FileDialogResult output = api.showSaveDialog(arg_options!, arg_initialDirectory, arg_suggestedName, arg_confirmButtonText); return [output]; }); diff --git a/packages/file_selector/file_selector_windows/windows/file_dialog_controller.cpp b/packages/file_selector/file_selector_windows/windows/file_dialog_controller.cpp index 5820c4a5da4..af2a9affecc 100644 --- a/packages/file_selector/file_selector_windows/windows/file_dialog_controller.cpp +++ b/packages/file_selector/file_selector_windows/windows/file_dialog_controller.cpp @@ -51,6 +51,10 @@ HRESULT FileDialogController::GetResult(IShellItem** out_item) const { return dialog_->GetResult(out_item); } +HRESULT FileDialogController::GetFileTypeIndex(UINT* out_index) const { + return dialog_->GetFileTypeIndex(out_index); +} + HRESULT FileDialogController::GetResults(IShellItemArray** out_items) const { IFileOpenDialogPtr open_dialog; HRESULT result = dialog_->QueryInterface(IID_PPV_ARGS(&open_dialog)); diff --git a/packages/file_selector/file_selector_windows/windows/file_dialog_controller.h b/packages/file_selector/file_selector_windows/windows/file_dialog_controller.h index f5c93974cbe..ab4929287e9 100644 --- a/packages/file_selector/file_selector_windows/windows/file_dialog_controller.h +++ b/packages/file_selector/file_selector_windows/windows/file_dialog_controller.h @@ -38,6 +38,7 @@ class FileDialogController { virtual HRESULT SetOptions(FILEOPENDIALOGOPTIONS options); virtual HRESULT Show(HWND parent); virtual HRESULT GetResult(IShellItem** out_item) const; + virtual HRESULT GetFileTypeIndex(UINT* out_index) const; // IFileOpenDialog wrapper. This will fail if the IFileDialog* provided to the // constructor was not an IFileOpenDialog instance. diff --git a/packages/file_selector/file_selector_windows/windows/file_selector_plugin.cpp b/packages/file_selector/file_selector_windows/windows/file_selector_plugin.cpp index b9e6d211b2d..35697983108 100644 --- a/packages/file_selector/file_selector_windows/windows/file_selector_plugin.cpp +++ b/packages/file_selector/file_selector_windows/windows/file_selector_plugin.cpp @@ -29,8 +29,8 @@ namespace file_selector_windows { namespace { +using flutter::CustomEncodableValue; using flutter::EncodableList; -using flutter::EncodableMap; using flutter::EncodableValue; // The kind of file dialog to show. @@ -137,7 +137,7 @@ class DialogWrapper { for (const EncodableValue& filter_info_value : filters) { const auto& type_group = std::any_cast( - std::get(filter_info_value)); + std::get(filter_info_value)); filter_names.push_back(Utf16FromUtf8(type_group.label())); filter_extensions.push_back(L""); std::wstring& spec = filter_extensions.back(); @@ -158,8 +158,8 @@ class DialogWrapper { static_cast(filter_specs.size()), filter_specs.data()); } - // Displays the dialog, and returns the selected files, or nullopt on error. - std::optional Show(HWND parent_window) { + // Displays the dialog, and returns the result, or nullopt on error. + std::optional Show(HWND parent_window) { assert(dialog_controller_); last_result_ = dialog_controller_->Show(parent_window); if (!SUCCEEDED(last_result_)) { @@ -190,7 +190,14 @@ class DialogWrapper { } files.push_back(EncodableValue(GetPathForShellItem(shell_item))); } - return files; + FileDialogResult result(files, nullptr); + UINT file_type_index; + if (SUCCEEDED(dialog_controller_->GetFileTypeIndex(&file_type_index)) && + file_type_index > 0) { + // Convert from the one-based index to a Dart index. + result.set_type_group_index(file_type_index - 1); + } + return result; } // Returns the result of the last Win32 API call related to this object. @@ -205,7 +212,7 @@ class DialogWrapper { HRESULT last_result_; }; -ErrorOr ShowDialog( +ErrorOr ShowDialog( const FileDialogControllerFactory& dialog_factory, HWND parent_window, DialogMode mode, const SelectionOptions& options, const std::string* initial_directory, const std::string* suggested_name, @@ -243,16 +250,16 @@ ErrorOr ShowDialog( dialog.SetFileTypeFilters(options.allowed_types()); } - std::optional files = dialog.Show(parent_window); - if (!files) { + std::optional result = dialog.Show(parent_window); + if (!result) { if (dialog.last_result() != HRESULT_FROM_WIN32(ERROR_CANCELLED)) { return FlutterError("System error", "Could not show dialog", EncodableValue(dialog.last_result())); } else { - return EncodableList(); + return FileDialogResult(EncodableList(), nullptr); } } - return std::move(files.value()); + return std::move(result.value()); } // Returns the top-level window that owns |view|. @@ -282,14 +289,14 @@ FileSelectorPlugin::FileSelectorPlugin( FileSelectorPlugin::~FileSelectorPlugin() = default; -ErrorOr FileSelectorPlugin::ShowOpenDialog( +ErrorOr FileSelectorPlugin::ShowOpenDialog( const SelectionOptions& options, const std::string* initialDirectory, const std::string* confirmButtonText) { return ShowDialog(*controller_factory_, get_root_window_(), DialogMode::open, options, initialDirectory, nullptr, confirmButtonText); } -ErrorOr FileSelectorPlugin::ShowSaveDialog( +ErrorOr FileSelectorPlugin::ShowSaveDialog( const SelectionOptions& options, const std::string* initialDirectory, const std::string* suggestedName, const std::string* confirmButtonText) { return ShowDialog(*controller_factory_, get_root_window_(), DialogMode::save, diff --git a/packages/file_selector/file_selector_windows/windows/file_selector_plugin.h b/packages/file_selector/file_selector_windows/windows/file_selector_plugin.h index 1388bfd3898..2f17f949049 100644 --- a/packages/file_selector/file_selector_windows/windows/file_selector_plugin.h +++ b/packages/file_selector/file_selector_windows/windows/file_selector_plugin.h @@ -32,10 +32,10 @@ class FileSelectorPlugin : public flutter::Plugin, public FileSelectorApi { virtual ~FileSelectorPlugin(); // FileSelectorApi - ErrorOr ShowOpenDialog( + ErrorOr ShowOpenDialog( const SelectionOptions& options, const std::string* initial_directory, const std::string* confirm_button_text) override; - ErrorOr ShowSaveDialog( + ErrorOr ShowSaveDialog( const SelectionOptions& options, const std::string* initialDirectory, const std::string* suggestedName, const std::string* confirmButtonText) override; diff --git a/packages/file_selector/file_selector_windows/windows/messages.g.cpp b/packages/file_selector/file_selector_windows/windows/messages.g.cpp index 24b831e292e..a60fd92a974 100644 --- a/packages/file_selector/file_selector_windows/windows/messages.g.cpp +++ b/packages/file_selector/file_selector_windows/windows/messages.g.cpp @@ -1,7 +1,7 @@ // 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. -// Autogenerated from Pigeon (v9.1.1), do not edit directly. +// Autogenerated from Pigeon (v10.0.1), do not edit directly. // See also: https://pub.dev/packages/pigeon #undef _HAS_EXCEPTIONS @@ -26,10 +26,15 @@ using flutter::EncodableValue; // TypeGroup +TypeGroup::TypeGroup(const std::string& label, const EncodableList& extensions) + : label_(label), extensions_(extensions) {} + const std::string& TypeGroup::label() const { return label_; } + void TypeGroup::set_label(std::string_view value_arg) { label_ = value_arg; } const EncodableList& TypeGroup::extensions() const { return extensions_; } + void TypeGroup::set_extensions(const EncodableList& value_arg) { extensions_ = value_arg; } @@ -42,29 +47,28 @@ EncodableList TypeGroup::ToEncodableList() const { return list; } -TypeGroup::TypeGroup() {} - -TypeGroup::TypeGroup(const EncodableList& list) { - auto& encodable_label = list[0]; - if (const std::string* pointer_label = - std::get_if(&encodable_label)) { - label_ = *pointer_label; - } - auto& encodable_extensions = list[1]; - if (const EncodableList* pointer_extensions = - std::get_if(&encodable_extensions)) { - extensions_ = *pointer_extensions; - } +TypeGroup TypeGroup::FromEncodableList(const EncodableList& list) { + TypeGroup decoded(std::get(list[0]), + std::get(list[1])); + return decoded; } // SelectionOptions +SelectionOptions::SelectionOptions(bool allow_multiple, bool select_folders, + const EncodableList& allowed_types) + : allow_multiple_(allow_multiple), + select_folders_(select_folders), + allowed_types_(allowed_types) {} + bool SelectionOptions::allow_multiple() const { return allow_multiple_; } + void SelectionOptions::set_allow_multiple(bool value_arg) { allow_multiple_ = value_arg; } bool SelectionOptions::select_folders() const { return select_folders_; } + void SelectionOptions::set_select_folders(bool value_arg) { select_folders_ = value_arg; } @@ -72,6 +76,7 @@ void SelectionOptions::set_select_folders(bool value_arg) { const EncodableList& SelectionOptions::allowed_types() const { return allowed_types_; } + void SelectionOptions::set_allowed_types(const EncodableList& value_arg) { allowed_types_ = value_arg; } @@ -85,36 +90,77 @@ EncodableList SelectionOptions::ToEncodableList() const { return list; } -SelectionOptions::SelectionOptions() {} +SelectionOptions SelectionOptions::FromEncodableList( + const EncodableList& list) { + SelectionOptions decoded(std::get(list[0]), std::get(list[1]), + std::get(list[2])); + return decoded; +} -SelectionOptions::SelectionOptions(const EncodableList& list) { - auto& encodable_allow_multiple = list[0]; - if (const bool* pointer_allow_multiple = - std::get_if(&encodable_allow_multiple)) { - allow_multiple_ = *pointer_allow_multiple; - } - auto& encodable_select_folders = list[1]; - if (const bool* pointer_select_folders = - std::get_if(&encodable_select_folders)) { - select_folders_ = *pointer_select_folders; - } - auto& encodable_allowed_types = list[2]; - if (const EncodableList* pointer_allowed_types = - std::get_if(&encodable_allowed_types)) { - allowed_types_ = *pointer_allowed_types; +// FileDialogResult + +FileDialogResult::FileDialogResult(const EncodableList& paths) + : paths_(paths) {} + +FileDialogResult::FileDialogResult(const EncodableList& paths, + const int64_t* type_group_index) + : paths_(paths), + type_group_index_(type_group_index + ? std::optional(*type_group_index) + : std::nullopt) {} + +const EncodableList& FileDialogResult::paths() const { return paths_; } + +void FileDialogResult::set_paths(const EncodableList& value_arg) { + paths_ = value_arg; +} + +const int64_t* FileDialogResult::type_group_index() const { + return type_group_index_ ? &(*type_group_index_) : nullptr; +} + +void FileDialogResult::set_type_group_index(const int64_t* value_arg) { + type_group_index_ = + value_arg ? std::optional(*value_arg) : std::nullopt; +} + +void FileDialogResult::set_type_group_index(int64_t value_arg) { + type_group_index_ = value_arg; +} + +EncodableList FileDialogResult::ToEncodableList() const { + EncodableList list; + list.reserve(2); + list.push_back(EncodableValue(paths_)); + list.push_back(type_group_index_ ? EncodableValue(*type_group_index_) + : EncodableValue()); + return list; +} + +FileDialogResult FileDialogResult::FromEncodableList( + const EncodableList& list) { + FileDialogResult decoded(std::get(list[0])); + auto& encodable_type_group_index = list[1]; + if (!encodable_type_group_index.IsNull()) { + decoded.set_type_group_index(encodable_type_group_index.LongValue()); } + return decoded; } FileSelectorApiCodecSerializer::FileSelectorApiCodecSerializer() {} + EncodableValue FileSelectorApiCodecSerializer::ReadValueOfType( uint8_t type, flutter::ByteStreamReader* stream) const { switch (type) { case 128: - return CustomEncodableValue( - SelectionOptions(std::get(ReadValue(stream)))); + return CustomEncodableValue(FileDialogResult::FromEncodableList( + std::get(ReadValue(stream)))); case 129: - return CustomEncodableValue( - TypeGroup(std::get(ReadValue(stream)))); + return CustomEncodableValue(SelectionOptions::FromEncodableList( + std::get(ReadValue(stream)))); + case 130: + return CustomEncodableValue(TypeGroup::FromEncodableList( + std::get(ReadValue(stream)))); default: return flutter::StandardCodecSerializer::ReadValueOfType(type, stream); } @@ -124,8 +170,16 @@ void FileSelectorApiCodecSerializer::WriteValue( const EncodableValue& value, flutter::ByteStreamWriter* stream) const { if (const CustomEncodableValue* custom_value = std::get_if(&value)) { - if (custom_value->type() == typeid(SelectionOptions)) { + if (custom_value->type() == typeid(FileDialogResult)) { stream->WriteByte(128); + WriteValue( + EncodableValue( + std::any_cast(*custom_value).ToEncodableList()), + stream); + return; + } + if (custom_value->type() == typeid(SelectionOptions)) { + stream->WriteByte(129); WriteValue( EncodableValue( std::any_cast(*custom_value).ToEncodableList()), @@ -133,7 +187,7 @@ void FileSelectorApiCodecSerializer::WriteValue( return; } if (custom_value->type() == typeid(TypeGroup)) { - stream->WriteByte(129); + stream->WriteByte(130); WriteValue(EncodableValue( std::any_cast(*custom_value).ToEncodableList()), stream); @@ -176,14 +230,15 @@ void FileSelectorApi::SetUp(flutter::BinaryMessenger* binary_messenger, const auto& encodable_confirm_button_text_arg = args.at(2); const auto* confirm_button_text_arg = std::get_if(&encodable_confirm_button_text_arg); - ErrorOr output = api->ShowOpenDialog( + ErrorOr output = api->ShowOpenDialog( options_arg, initial_directory_arg, confirm_button_text_arg); if (output.has_error()) { reply(WrapError(output.error())); return; } EncodableList wrapped; - wrapped.push_back(EncodableValue(std::move(output).TakeValue())); + wrapped.push_back( + CustomEncodableValue(std::move(output).TakeValue())); reply(EncodableValue(std::move(wrapped))); } catch (const std::exception& exception) { reply(WrapError(exception.what())); @@ -219,7 +274,7 @@ void FileSelectorApi::SetUp(flutter::BinaryMessenger* binary_messenger, const auto& encodable_confirm_button_text_arg = args.at(3); const auto* confirm_button_text_arg = std::get_if(&encodable_confirm_button_text_arg); - ErrorOr output = api->ShowSaveDialog( + ErrorOr output = api->ShowSaveDialog( options_arg, initial_directory_arg, suggested_name_arg, confirm_button_text_arg); if (output.has_error()) { @@ -227,7 +282,8 @@ void FileSelectorApi::SetUp(flutter::BinaryMessenger* binary_messenger, return; } EncodableList wrapped; - wrapped.push_back(EncodableValue(std::move(output).TakeValue())); + wrapped.push_back( + CustomEncodableValue(std::move(output).TakeValue())); reply(EncodableValue(std::move(wrapped))); } catch (const std::exception& exception) { reply(WrapError(exception.what())); @@ -244,6 +300,7 @@ EncodableValue FileSelectorApi::WrapError(std::string_view error_message) { EncodableList{EncodableValue(std::string(error_message)), EncodableValue("Error"), EncodableValue()}); } + EncodableValue FileSelectorApi::WrapError(const FlutterError& error) { return EncodableValue(EncodableList{EncodableValue(error.code()), EncodableValue(error.message()), diff --git a/packages/file_selector/file_selector_windows/windows/messages.g.h b/packages/file_selector/file_selector_windows/windows/messages.g.h index 248ca89c977..ab8afd7d703 100644 --- a/packages/file_selector/file_selector_windows/windows/messages.g.h +++ b/packages/file_selector/file_selector_windows/windows/messages.g.h @@ -1,7 +1,7 @@ // 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. -// Autogenerated from Pigeon (v9.1.1), do not edit directly. +// Autogenerated from Pigeon (v10.0.1), do not edit directly. // See also: https://pub.dev/packages/pigeon #ifndef PIGEON_MESSAGES_G_H_ @@ -41,10 +41,10 @@ class FlutterError { template class ErrorOr { public: - ErrorOr(const T& rhs) { new (&v_) T(rhs); } - ErrorOr(const T&& rhs) { v_ = std::move(rhs); } - ErrorOr(const FlutterError& rhs) { new (&v_) FlutterError(rhs); } - ErrorOr(const FlutterError&& rhs) { v_ = std::move(rhs); } + ErrorOr(const T& rhs) : v_(rhs) {} + ErrorOr(const T&& rhs) : v_(std::move(rhs)) {} + ErrorOr(const FlutterError& rhs) : v_(rhs) {} + ErrorOr(const FlutterError&& rhs) : v_(std::move(rhs)) {} bool has_error() const { return std::holds_alternative(v_); } const T& value() const { return std::get(v_); }; @@ -61,7 +61,10 @@ class ErrorOr { // Generated class from Pigeon that represents data sent in messages. class TypeGroup { public: - TypeGroup(); + // Constructs an object setting all fields. + explicit TypeGroup(const std::string& label, + const flutter::EncodableList& extensions); + const std::string& label() const; void set_label(std::string_view value_arg); @@ -69,7 +72,7 @@ class TypeGroup { void set_extensions(const flutter::EncodableList& value_arg); private: - TypeGroup(const flutter::EncodableList& list); + static TypeGroup FromEncodableList(const flutter::EncodableList& list); flutter::EncodableList ToEncodableList() const; friend class FileSelectorApi; friend class FileSelectorApiCodecSerializer; @@ -80,7 +83,10 @@ class TypeGroup { // Generated class from Pigeon that represents data sent in messages. class SelectionOptions { public: - SelectionOptions(); + // Constructs an object setting all fields. + explicit SelectionOptions(bool allow_multiple, bool select_folders, + const flutter::EncodableList& allowed_types); + bool allow_multiple() const; void set_allow_multiple(bool value_arg); @@ -91,7 +97,7 @@ class SelectionOptions { void set_allowed_types(const flutter::EncodableList& value_arg); private: - SelectionOptions(const flutter::EncodableList& list); + static SelectionOptions FromEncodableList(const flutter::EncodableList& list); flutter::EncodableList ToEncodableList() const; friend class FileSelectorApi; friend class FileSelectorApiCodecSerializer; @@ -100,16 +106,49 @@ class SelectionOptions { flutter::EncodableList allowed_types_; }; +// The result from an open or save dialog. +// +// Generated class from Pigeon that represents data sent in messages. +class FileDialogResult { + public: + // Constructs an object setting all non-nullable fields. + explicit FileDialogResult(const flutter::EncodableList& paths); + + // Constructs an object setting all fields. + explicit FileDialogResult(const flutter::EncodableList& paths, + const int64_t* type_group_index); + + // The selected paths. + // + // Empty if the dialog was canceled. + const flutter::EncodableList& paths() const; + void set_paths(const flutter::EncodableList& value_arg); + + // The type group index (into the list provided in [SelectionOptions]) of + // the group that was selected when the dialog was confirmed. + // + // Null if no type groups were provided, or the dialog was canceled. + const int64_t* type_group_index() const; + void set_type_group_index(const int64_t* value_arg); + void set_type_group_index(int64_t value_arg); + + private: + static FileDialogResult FromEncodableList(const flutter::EncodableList& list); + flutter::EncodableList ToEncodableList() const; + friend class FileSelectorApi; + friend class FileSelectorApiCodecSerializer; + flutter::EncodableList paths_; + std::optional type_group_index_; +}; + class FileSelectorApiCodecSerializer : public flutter::StandardCodecSerializer { public: + FileSelectorApiCodecSerializer(); inline static FileSelectorApiCodecSerializer& GetInstance() { static FileSelectorApiCodecSerializer sInstance; return sInstance; } - FileSelectorApiCodecSerializer(); - - public: void WriteValue(const flutter::EncodableValue& value, flutter::ByteStreamWriter* stream) const override; @@ -125,10 +164,10 @@ class FileSelectorApi { FileSelectorApi(const FileSelectorApi&) = delete; FileSelectorApi& operator=(const FileSelectorApi&) = delete; virtual ~FileSelectorApi() {} - virtual ErrorOr ShowOpenDialog( + virtual ErrorOr ShowOpenDialog( const SelectionOptions& options, const std::string* initial_directory, const std::string* confirm_button_text) = 0; - virtual ErrorOr ShowSaveDialog( + virtual ErrorOr ShowSaveDialog( const SelectionOptions& options, const std::string* initial_directory, const std::string* suggested_name, const std::string* confirm_button_text) = 0; 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 8efeb54f860..ee6d4dcded7 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 @@ -28,37 +28,8 @@ namespace { using flutter::CustomEncodableValue; using flutter::EncodableList; -using flutter::EncodableMap; using flutter::EncodableValue; -// These structs and classes are a workaround for -// https://github.com/flutter/flutter/issues/104286 and -// https://github.com/flutter/flutter/issues/104653. -struct AllowMultipleArg { - bool value = false; - AllowMultipleArg(bool val) : value(val) {} -}; -struct SelectFoldersArg { - bool value = false; - SelectFoldersArg(bool val) : value(val) {} -}; -SelectionOptions CreateOptions(AllowMultipleArg allow_multiple, - SelectFoldersArg select_folders, - const EncodableList& allowed_types) { - SelectionOptions options; - options.set_allow_multiple(allow_multiple.value); - options.set_select_folders(select_folders.value); - options.set_allowed_types(allowed_types); - return options; -} -TypeGroup CreateTypeGroup(std::string_view label, - const EncodableList& extensions) { - TypeGroup group; - group.set_label(label); - group.set_extensions(extensions); - return group; -} - } // namespace TEST(FileSelectorPlugin, TestOpenSimple) { @@ -87,17 +58,18 @@ TEST(FileSelectorPlugin, TestOpenSimple) { FileSelectorPlugin plugin( [fake_window] { return fake_window; }, std::make_unique(show_validator)); - ErrorOr result = plugin.ShowOpenDialog( - CreateOptions(AllowMultipleArg(false), SelectFoldersArg(false), - EncodableList()), + ErrorOr result = plugin.ShowOpenDialog( + SelectionOptions(/* allow multiple = */ false, + /* select folders = */ false, EncodableList()), nullptr, nullptr); EXPECT_TRUE(shown); ASSERT_FALSE(result.has_error()); - const EncodableList& paths = result.value(); - EXPECT_EQ(paths.size(), 1); + const EncodableList& paths = result.value().paths(); + ASSERT_EQ(paths.size(), 1); EXPECT_EQ(std::get(paths[0]), Utf8FromUtf16(fake_selected_file.path())); + EXPECT_EQ(result.value().type_group_index(), nullptr); } TEST(FileSelectorPlugin, TestOpenWithArguments) { @@ -129,17 +101,18 @@ TEST(FileSelectorPlugin, TestOpenWithArguments) { // This directory must exist. std::string initial_directory("C:\\Program Files"); std::string confirm_button("Open it!"); - ErrorOr result = plugin.ShowOpenDialog( - CreateOptions(AllowMultipleArg(false), SelectFoldersArg(false), - EncodableList()), + ErrorOr result = plugin.ShowOpenDialog( + SelectionOptions(/* allow multiple = */ false, + /* select folders = */ false, EncodableList()), &initial_directory, &confirm_button); EXPECT_TRUE(shown); ASSERT_FALSE(result.has_error()); - const EncodableList& paths = result.value(); - EXPECT_EQ(paths.size(), 1); + const EncodableList& paths = result.value().paths(); + ASSERT_EQ(paths.size(), 1); EXPECT_EQ(std::get(paths[0]), Utf8FromUtf16(fake_selected_file.path())); + EXPECT_EQ(result.value().type_group_index(), nullptr); } TEST(FileSelectorPlugin, TestOpenMultiple) { @@ -173,19 +146,20 @@ TEST(FileSelectorPlugin, TestOpenMultiple) { FileSelectorPlugin plugin( [fake_window] { return fake_window; }, std::make_unique(show_validator)); - ErrorOr result = plugin.ShowOpenDialog( - CreateOptions(AllowMultipleArg(true), SelectFoldersArg(false), - EncodableList()), + ErrorOr result = plugin.ShowOpenDialog( + SelectionOptions(/* allow multiple = */ true, + /* select folders = */ false, EncodableList()), nullptr, nullptr); EXPECT_TRUE(shown); ASSERT_FALSE(result.has_error()); - const EncodableList& paths = result.value(); - EXPECT_EQ(paths.size(), 2); + const EncodableList& paths = result.value().paths(); + ASSERT_EQ(paths.size(), 2); EXPECT_EQ(std::get(paths[0]), Utf8FromUtf16(fake_selected_file_1.path())); EXPECT_EQ(std::get(paths[1]), Utf8FromUtf16(fake_selected_file_2.path())); + EXPECT_EQ(result.value().type_group_index(), nullptr); } TEST(FileSelectorPlugin, TestOpenWithFilter) { @@ -196,18 +170,18 @@ TEST(FileSelectorPlugin, TestOpenWithFilter) { IID_PPV_ARGS(&fake_result_array)); const EncodableValue text_group = - CustomEncodableValue(CreateTypeGroup("Text", EncodableList({ - EncodableValue("txt"), - EncodableValue("json"), - }))); + CustomEncodableValue(TypeGroup("Text", EncodableList({ + EncodableValue("txt"), + EncodableValue("json"), + }))); const EncodableValue image_group = - CustomEncodableValue(CreateTypeGroup("Images", EncodableList({ - EncodableValue("png"), - EncodableValue("gif"), - EncodableValue("jpeg"), - }))); + CustomEncodableValue(TypeGroup("Images", EncodableList({ + EncodableValue("png"), + EncodableValue("gif"), + EncodableValue("jpeg"), + }))); const EncodableValue any_group = - CustomEncodableValue(CreateTypeGroup("Any", EncodableList())); + CustomEncodableValue(TypeGroup("Any", EncodableList())); bool shown = false; MockShow show_validator = [&shown, fake_result_array, fake_window]( @@ -234,21 +208,26 @@ TEST(FileSelectorPlugin, TestOpenWithFilter) { FileSelectorPlugin plugin( [fake_window] { return fake_window; }, std::make_unique(show_validator)); - ErrorOr result = plugin.ShowOpenDialog( - CreateOptions(AllowMultipleArg(false), SelectFoldersArg(false), - EncodableList({ - text_group, - image_group, - any_group, - })), - nullptr, nullptr); + ErrorOr result = + plugin.ShowOpenDialog(SelectionOptions(/* allow multiple = */ false, + /* select folders = */ false, + EncodableList({ + text_group, + image_group, + any_group, + })), + nullptr, nullptr); EXPECT_TRUE(shown); ASSERT_FALSE(result.has_error()); - const EncodableList& paths = result.value(); - EXPECT_EQ(paths.size(), 1); + const EncodableList& paths = result.value().paths(); + ASSERT_EQ(paths.size(), 1); EXPECT_EQ(std::get(paths[0]), Utf8FromUtf16(fake_selected_file.path())); + // The test dialog controller always reports the last group as + // selected, so that should be what the plugin returns. + ASSERT_NE(result.value().type_group_index(), nullptr); + EXPECT_EQ(*(result.value().type_group_index()), 2); } TEST(FileSelectorPlugin, TestOpenCancel) { @@ -265,15 +244,16 @@ TEST(FileSelectorPlugin, TestOpenCancel) { FileSelectorPlugin plugin( [fake_window] { return fake_window; }, std::make_unique(show_validator)); - ErrorOr result = plugin.ShowOpenDialog( - CreateOptions(AllowMultipleArg(false), SelectFoldersArg(false), - EncodableList()), + ErrorOr result = plugin.ShowOpenDialog( + SelectionOptions(/* allow multiple = */ false, + /* select folders = */ false, EncodableList()), nullptr, nullptr); EXPECT_TRUE(shown); ASSERT_FALSE(result.has_error()); - const EncodableList& paths = result.value(); + const EncodableList& paths = result.value().paths(); EXPECT_EQ(paths.size(), 0); + EXPECT_EQ(result.value().type_group_index(), nullptr); } TEST(FileSelectorPlugin, TestSaveSimple) { @@ -299,17 +279,18 @@ TEST(FileSelectorPlugin, TestSaveSimple) { FileSelectorPlugin plugin( [fake_window] { return fake_window; }, std::make_unique(show_validator)); - ErrorOr result = plugin.ShowSaveDialog( - CreateOptions(AllowMultipleArg(false), SelectFoldersArg(false), - EncodableList()), + ErrorOr result = plugin.ShowSaveDialog( + SelectionOptions(/* allow multiple = */ false, + /* select folders = */ false, EncodableList()), nullptr, nullptr, nullptr); EXPECT_TRUE(shown); ASSERT_FALSE(result.has_error()); - const EncodableList& paths = result.value(); - EXPECT_EQ(paths.size(), 1); + const EncodableList& paths = result.value().paths(); + ASSERT_EQ(paths.size(), 1); EXPECT_EQ(std::get(paths[0]), Utf8FromUtf16(fake_selected_file.path())); + EXPECT_EQ(result.value().type_group_index(), nullptr); } TEST(FileSelectorPlugin, TestSaveWithArguments) { @@ -341,17 +322,78 @@ TEST(FileSelectorPlugin, TestSaveWithArguments) { std::string initial_directory("C:\\Program Files"); std::string suggested_name("a name"); std::string confirm_button("Save it!"); - ErrorOr result = plugin.ShowSaveDialog( - CreateOptions(AllowMultipleArg(false), SelectFoldersArg(false), - EncodableList()), + ErrorOr result = plugin.ShowSaveDialog( + SelectionOptions(/* allow multiple = */ false, + /* select folders = */ false, EncodableList()), &initial_directory, &suggested_name, &confirm_button); EXPECT_TRUE(shown); ASSERT_FALSE(result.has_error()); - const EncodableList& paths = result.value(); - EXPECT_EQ(paths.size(), 1); + const EncodableList& paths = result.value().paths(); + ASSERT_EQ(paths.size(), 1); + EXPECT_EQ(std::get(paths[0]), + Utf8FromUtf16(fake_selected_file.path())); + EXPECT_EQ(result.value().type_group_index(), nullptr); +} + +TEST(FileSelectorPlugin, TestSaveWithFilter) { + const HWND fake_window = reinterpret_cast(1337); + ScopedTestShellItem fake_selected_file; + + const EncodableValue text_group = + CustomEncodableValue(TypeGroup("Text", EncodableList({ + EncodableValue("txt"), + EncodableValue("json"), + }))); + const EncodableValue image_group = + CustomEncodableValue(TypeGroup("Images", EncodableList({ + EncodableValue("png"), + EncodableValue("gif"), + EncodableValue("jpeg"), + }))); + + bool shown = false; + MockShow show_validator = + [&shown, fake_result = fake_selected_file.file(), fake_window]( + const TestFileDialogController& dialog, HWND parent) { + shown = true; + EXPECT_EQ(parent, fake_window); + + // Validate filter. + const std::vector& filters = dialog.GetFileTypes(); + EXPECT_EQ(filters.size(), 2U); + if (filters.size() == 2U) { + EXPECT_EQ(filters[0].name, L"Text"); + EXPECT_EQ(filters[0].spec, L"*.txt;*.json"); + EXPECT_EQ(filters[1].name, L"Images"); + EXPECT_EQ(filters[1].spec, L"*.png;*.gif;*.jpeg"); + } + + return MockShowResult(fake_result); + }; + + FileSelectorPlugin plugin( + [fake_window] { return fake_window; }, + std::make_unique(show_validator)); + ErrorOr result = + plugin.ShowSaveDialog(SelectionOptions(/* allow multiple = */ false, + /* select folders = */ false, + EncodableList({ + text_group, + image_group, + })), + nullptr, nullptr, nullptr); + + EXPECT_TRUE(shown); + ASSERT_FALSE(result.has_error()); + const EncodableList& paths = result.value().paths(); + ASSERT_EQ(paths.size(), 1); EXPECT_EQ(std::get(paths[0]), Utf8FromUtf16(fake_selected_file.path())); + // The test dialog controller always reports the last group as + // selected, so that should be what the plugin returns. + ASSERT_NE(result.value().type_group_index(), nullptr); + EXPECT_EQ(*(result.value().type_group_index()), 1); } TEST(FileSelectorPlugin, TestSaveCancel) { @@ -368,15 +410,16 @@ TEST(FileSelectorPlugin, TestSaveCancel) { FileSelectorPlugin plugin( [fake_window] { return fake_window; }, std::make_unique(show_validator)); - ErrorOr result = plugin.ShowSaveDialog( - CreateOptions(AllowMultipleArg(false), SelectFoldersArg(false), - EncodableList()), + ErrorOr result = plugin.ShowSaveDialog( + SelectionOptions(/* allow multiple = */ false, + /* select folders = */ false, EncodableList()), nullptr, nullptr, nullptr); EXPECT_TRUE(shown); ASSERT_FALSE(result.has_error()); - const EncodableList& paths = result.value(); + const EncodableList& paths = result.value().paths(); EXPECT_EQ(paths.size(), 0); + EXPECT_EQ(result.value().type_group_index(), nullptr); } TEST(FileSelectorPlugin, TestGetDirectorySimple) { @@ -408,16 +451,17 @@ TEST(FileSelectorPlugin, TestGetDirectorySimple) { FileSelectorPlugin plugin( [fake_window] { return fake_window; }, std::make_unique(show_validator)); - ErrorOr result = plugin.ShowOpenDialog( - CreateOptions(AllowMultipleArg(false), SelectFoldersArg(true), - EncodableList()), + ErrorOr result = plugin.ShowOpenDialog( + SelectionOptions(/* allow multiple = */ false, + /* select folders = */ true, EncodableList()), nullptr, nullptr); EXPECT_TRUE(shown); ASSERT_FALSE(result.has_error()); - const EncodableList& paths = result.value(); - EXPECT_EQ(paths.size(), 1); + const EncodableList& paths = result.value().paths(); + ASSERT_EQ(paths.size(), 1); EXPECT_EQ(std::get(paths[0]), "C:\\Program Files"); + EXPECT_EQ(result.value().type_group_index(), nullptr); } TEST(FileSelectorPlugin, TestGetDirectoryMultiple) { @@ -454,19 +498,20 @@ TEST(FileSelectorPlugin, TestGetDirectoryMultiple) { FileSelectorPlugin plugin( [fake_window] { return fake_window; }, std::make_unique(show_validator)); - ErrorOr result = plugin.ShowOpenDialog( - CreateOptions(AllowMultipleArg(true), SelectFoldersArg(true), - EncodableList()), + ErrorOr result = plugin.ShowOpenDialog( + SelectionOptions(/* allow multiple = */ true, /* select folders = */ true, + EncodableList()), nullptr, nullptr); EXPECT_TRUE(shown); ASSERT_FALSE(result.has_error()); - const EncodableList& paths = result.value(); - EXPECT_EQ(paths.size(), 2); + const EncodableList& paths = result.value().paths(); + ASSERT_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())); + EXPECT_EQ(result.value().type_group_index(), nullptr); } TEST(FileSelectorPlugin, TestGetDirectoryCancel) { @@ -483,15 +528,16 @@ TEST(FileSelectorPlugin, TestGetDirectoryCancel) { FileSelectorPlugin plugin( [fake_window] { return fake_window; }, std::make_unique(show_validator)); - ErrorOr result = plugin.ShowOpenDialog( - CreateOptions(AllowMultipleArg(false), SelectFoldersArg(true), - EncodableList()), + ErrorOr result = plugin.ShowOpenDialog( + SelectionOptions(/* allow multiple = */ false, + /* select folders = */ true, EncodableList()), nullptr, nullptr); EXPECT_TRUE(shown); ASSERT_FALSE(result.has_error()); - const EncodableList& paths = result.value(); + const EncodableList& paths = result.value().paths(); EXPECT_EQ(paths.size(), 0); + EXPECT_EQ(result.value().type_group_index(), nullptr); } } // namespace test diff --git a/packages/file_selector/file_selector_windows/windows/test/test_file_dialog_controller.cpp b/packages/file_selector/file_selector_windows/windows/test/test_file_dialog_controller.cpp index 15065f916c8..e775aa627aa 100644 --- a/packages/file_selector/file_selector_windows/windows/test/test_file_dialog_controller.cpp +++ b/packages/file_selector/file_selector_windows/windows/test/test_file_dialog_controller.cpp @@ -60,6 +60,13 @@ HRESULT TestFileDialogController::GetResult(IShellItem** out_item) const { return S_OK; } +HRESULT TestFileDialogController::GetFileTypeIndex(UINT* out_index) const { + // Arbitrarily always return the last group. (No -1 because the return value + // from GetFileTypeIndex is defined to be one-indexed.) + *out_index = static_cast(filter_groups_.size()); + return S_OK; +} + HRESULT TestFileDialogController::GetResults( IShellItemArray** out_items) const { *out_items = std::get(mock_result_); diff --git a/packages/file_selector/file_selector_windows/windows/test/test_file_dialog_controller.h b/packages/file_selector/file_selector_windows/windows/test/test_file_dialog_controller.h index 1c221fc219f..e3aa0936e76 100644 --- a/packages/file_selector/file_selector_windows/windows/test/test_file_dialog_controller.h +++ b/packages/file_selector/file_selector_windows/windows/test/test_file_dialog_controller.h @@ -56,6 +56,7 @@ class TestFileDialogController : public FileDialogController { HRESULT SetOkButtonLabel(const wchar_t* text) override; HRESULT Show(HWND parent) override; HRESULT GetResult(IShellItem** out_item) const override; + HRESULT GetFileTypeIndex(UINT* out_index) const override; HRESULT GetResults(IShellItemArray** out_items) const override; // Accessors for validating IFileDialogController setter calls.