From 24ae23bd42d4a259e4bd62f0b5f5423ce46627ce Mon Sep 17 00:00:00 2001 From: eugerossetto Date: Wed, 28 Sep 2022 11:56:25 -0300 Subject: [PATCH] migrate file_selector_windows from cpp to dart Co-authored-by: Alejandro Pinola --- .../file_selector_windows/CHANGELOG.md | 4 + .../example/lib/get_directory_page.dart | 4 +- .../example/lib/home_page.dart | 2 +- .../example/lib/main.dart | 2 +- .../example/lib/open_image_page.dart | 5 +- .../lib/open_multiple_images_page.dart | 4 +- .../example/lib/open_text_page.dart | 16 +- .../example/lib/save_text_page.dart | 10 +- .../example/pubspec.yaml | 6 +- .../windows/flutter/generated_plugins.cmake | 1 - .../lib/file_selector_windows.dart | 82 ++-- .../lib/src/file_selector_api.dart | 95 ++++ .../src/file_selector_dart/dialog_mode.dart | 12 + .../file_selector_dart/dialog_wrapper.dart | 220 +++++++++ .../dialog_wrapper_factory.dart | 31 ++ .../file_dialog_controller.dart | 96 ++++ .../file_dialog_controller_factory.dart | 18 + .../ifile_dialog_controller_factory.dart | 14 + .../ifile_dialog_factory.dart | 20 + .../ifile_open_dialog_factory.dart | 13 + .../file_selector_dart/selection_options.dart | 25 + .../file_selector_dart/shell_win32_api.dart | 45 ++ .../lib/src/messages.g.dart | 176 ------- .../pigeons/copyright.txt | 3 - .../pigeons/messages.dart | 53 --- .../file_selector_windows/pubspec.yaml | 10 +- .../test/file_selector_api_test.dart | 200 ++++++++ .../test/file_selector_api_test.mocks.dart | 130 +++++ .../dialog_wrapper_test.dart | 337 +++++++++++++ .../dialog_wrapper_test.mocks.dart | 168 +++++++ .../file_selector_dart/fake_file_dialog.dart | 112 +++++ .../fake_ifile_open_dialog.dart | 47 ++ .../fake_ifile_open_dialog_factory.dart | 40 ++ .../file_dialog_controller_factory_test.dart | 23 + .../file_dialog_controller_test.dart | 134 ++++++ .../test/file_selector_windows_test.dart | 66 ++- .../file_selector_windows_test.mocks.dart | 60 ++- .../file_selector_windows/test/test_api.dart | 105 ---- .../file_selector_windows/windows/.gitignore | 17 - .../windows/CMakeLists.txt | 86 ---- .../windows/file_dialog_controller.cpp | 66 --- .../windows/file_dialog_controller.h | 62 --- .../windows/file_selector_plugin.cpp | 300 ------------ .../windows/file_selector_plugin.h | 53 --- .../windows/file_selector_windows.cpp | 15 - .../file_selector_windows.h | 26 - .../windows/messages.g.cpp | 278 ----------- .../windows/messages.g.h | 149 ------ .../windows/string_utils.cpp | 60 --- .../windows/string_utils.h | 21 - .../test/file_selector_plugin_test.cpp | 449 ------------------ .../test/test_file_dialog_controller.cpp | 121 ----- .../test/test_file_dialog_controller.h | 110 ----- .../windows/test/test_main.cpp | 16 - .../windows/test/test_utils.cpp | 45 -- .../windows/test/test_utils.h | 91 ---- 56 files changed, 1932 insertions(+), 2422 deletions(-) create mode 100644 packages/file_selector/file_selector_windows/lib/src/file_selector_api.dart create mode 100644 packages/file_selector/file_selector_windows/lib/src/file_selector_dart/dialog_mode.dart create mode 100644 packages/file_selector/file_selector_windows/lib/src/file_selector_dart/dialog_wrapper.dart create mode 100644 packages/file_selector/file_selector_windows/lib/src/file_selector_dart/dialog_wrapper_factory.dart create mode 100644 packages/file_selector/file_selector_windows/lib/src/file_selector_dart/file_dialog_controller.dart create mode 100644 packages/file_selector/file_selector_windows/lib/src/file_selector_dart/file_dialog_controller_factory.dart create mode 100644 packages/file_selector/file_selector_windows/lib/src/file_selector_dart/ifile_dialog_controller_factory.dart create mode 100644 packages/file_selector/file_selector_windows/lib/src/file_selector_dart/ifile_dialog_factory.dart create mode 100644 packages/file_selector/file_selector_windows/lib/src/file_selector_dart/ifile_open_dialog_factory.dart create mode 100644 packages/file_selector/file_selector_windows/lib/src/file_selector_dart/selection_options.dart create mode 100644 packages/file_selector/file_selector_windows/lib/src/file_selector_dart/shell_win32_api.dart delete mode 100644 packages/file_selector/file_selector_windows/lib/src/messages.g.dart delete mode 100644 packages/file_selector/file_selector_windows/pigeons/copyright.txt delete mode 100644 packages/file_selector/file_selector_windows/pigeons/messages.dart create mode 100644 packages/file_selector/file_selector_windows/test/file_selector_api_test.dart create mode 100644 packages/file_selector/file_selector_windows/test/file_selector_api_test.mocks.dart create mode 100644 packages/file_selector/file_selector_windows/test/file_selector_dart/dialog_wrapper_test.dart create mode 100644 packages/file_selector/file_selector_windows/test/file_selector_dart/dialog_wrapper_test.mocks.dart create mode 100644 packages/file_selector/file_selector_windows/test/file_selector_dart/fake_file_dialog.dart create mode 100644 packages/file_selector/file_selector_windows/test/file_selector_dart/fake_ifile_open_dialog.dart create mode 100644 packages/file_selector/file_selector_windows/test/file_selector_dart/fake_ifile_open_dialog_factory.dart create mode 100644 packages/file_selector/file_selector_windows/test/file_selector_dart/file_dialog_controller_factory_test.dart create mode 100644 packages/file_selector/file_selector_windows/test/file_selector_dart/file_dialog_controller_test.dart delete mode 100644 packages/file_selector/file_selector_windows/test/test_api.dart delete mode 100644 packages/file_selector/file_selector_windows/windows/.gitignore delete mode 100644 packages/file_selector/file_selector_windows/windows/CMakeLists.txt delete mode 100644 packages/file_selector/file_selector_windows/windows/file_dialog_controller.cpp delete mode 100644 packages/file_selector/file_selector_windows/windows/file_dialog_controller.h delete mode 100644 packages/file_selector/file_selector_windows/windows/file_selector_plugin.cpp delete mode 100644 packages/file_selector/file_selector_windows/windows/file_selector_plugin.h delete mode 100644 packages/file_selector/file_selector_windows/windows/file_selector_windows.cpp delete mode 100644 packages/file_selector/file_selector_windows/windows/include/file_selector_windows/file_selector_windows.h delete mode 100644 packages/file_selector/file_selector_windows/windows/messages.g.cpp delete mode 100644 packages/file_selector/file_selector_windows/windows/messages.g.h delete mode 100644 packages/file_selector/file_selector_windows/windows/string_utils.cpp delete mode 100644 packages/file_selector/file_selector_windows/windows/string_utils.h delete mode 100644 packages/file_selector/file_selector_windows/windows/test/file_selector_plugin_test.cpp delete mode 100644 packages/file_selector/file_selector_windows/windows/test/test_file_dialog_controller.cpp delete mode 100644 packages/file_selector/file_selector_windows/windows/test/test_file_dialog_controller.h delete mode 100644 packages/file_selector/file_selector_windows/windows/test/test_main.cpp delete mode 100644 packages/file_selector/file_selector_windows/windows/test/test_utils.cpp delete mode 100644 packages/file_selector/file_selector_windows/windows/test/test_utils.h diff --git a/packages/file_selector/file_selector_windows/CHANGELOG.md b/packages/file_selector/file_selector_windows/CHANGELOG.md index 13e895ca46f1..a8bdedfa214b 100644 --- a/packages/file_selector/file_selector_windows/CHANGELOG.md +++ b/packages/file_selector/file_selector_windows/CHANGELOG.md @@ -1,3 +1,7 @@ +## 1.0.0 + +* Migrates implementation from Cpp to Dart. + ## 0.9.1+4 * Changes XTypeGroup initialization from final to const. diff --git a/packages/file_selector/file_selector_windows/example/lib/get_directory_page.dart b/packages/file_selector/file_selector_windows/example/lib/get_directory_page.dart index 0699dd121541..ebd72336ec2f 100644 --- a/packages/file_selector/file_selector_windows/example/lib/get_directory_page.dart +++ b/packages/file_selector/file_selector_windows/example/lib/get_directory_page.dart @@ -9,7 +9,7 @@ import 'package:flutter/material.dart'; /// then displays the selected directory in a dialog. class GetDirectoryPage extends StatelessWidget { /// Default Constructor - const GetDirectoryPage({Key? key}) : super(key: key); + const GetDirectoryPage({super.key}); Future _getDirectoryPath(BuildContext context) async { const String confirmButtonText = 'Choose'; @@ -58,7 +58,7 @@ class GetDirectoryPage extends StatelessWidget { /// Widget that displays a text file in a dialog. class TextDisplay extends StatelessWidget { /// Creates a `TextDisplay`. - const TextDisplay(this.directoryPath, {Key? key}) : super(key: key); + const TextDisplay(this.directoryPath, {super.key}); /// The path selected in the dialog. final String directoryPath; 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 a4b2ae1f63ea..366ff5144245 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 @@ -7,7 +7,7 @@ import 'package:flutter/material.dart'; /// Home Page of the application. class HomePage extends StatelessWidget { /// Default Constructor - const HomePage({Key? key}) : super(key: key); + const HomePage({super.key}); @override Widget build(BuildContext context) { 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 3e447104ef9f..bfd2c2fdbfc1 100644 --- a/packages/file_selector/file_selector_windows/example/lib/main.dart +++ b/packages/file_selector/file_selector_windows/example/lib/main.dart @@ -18,7 +18,7 @@ void main() { /// MyApp is the Main Application. class MyApp extends StatelessWidget { /// Default Constructor - const MyApp({Key? key}) : super(key: key); + const MyApp({super.key}); @override Widget build(BuildContext context) { diff --git a/packages/file_selector/file_selector_windows/example/lib/open_image_page.dart b/packages/file_selector/file_selector_windows/example/lib/open_image_page.dart index b6ada56ebb2b..2cc9fe677864 100644 --- a/packages/file_selector/file_selector_windows/example/lib/open_image_page.dart +++ b/packages/file_selector/file_selector_windows/example/lib/open_image_page.dart @@ -12,7 +12,7 @@ import 'package:flutter/material.dart'; /// `openFiles`, then displays the selected images in a gallery dialog. class OpenImagePage extends StatelessWidget { /// Default Constructor - const OpenImagePage({Key? key}) : super(key: key); + const OpenImagePage({super.key}); Future _openImageFile(BuildContext context) async { const XTypeGroup typeGroup = XTypeGroup( @@ -65,8 +65,7 @@ class OpenImagePage extends StatelessWidget { /// Widget that displays an image in a dialog. class ImageDisplay extends StatelessWidget { /// Default Constructor. - const ImageDisplay(this.fileName, this.filePath, {Key? key}) - : super(key: key); + const ImageDisplay(this.fileName, this.filePath, {super.key}); /// The name of the selected file. final String fileName; diff --git a/packages/file_selector/file_selector_windows/example/lib/open_multiple_images_page.dart b/packages/file_selector/file_selector_windows/example/lib/open_multiple_images_page.dart index c8e352a5b8bd..45201661fe60 100644 --- a/packages/file_selector/file_selector_windows/example/lib/open_multiple_images_page.dart +++ b/packages/file_selector/file_selector_windows/example/lib/open_multiple_images_page.dart @@ -12,7 +12,7 @@ import 'package:flutter/material.dart'; /// `openFiles`, then displays the selected images in a gallery dialog. class OpenMultipleImagesPage extends StatelessWidget { /// Default Constructor - const OpenMultipleImagesPage({Key? key}) : super(key: key); + const OpenMultipleImagesPage({super.key}); Future _openImageFile(BuildContext context) async { const XTypeGroup jpgsTypeGroup = XTypeGroup( @@ -69,7 +69,7 @@ class OpenMultipleImagesPage extends StatelessWidget { /// Widget that displays a text file in a dialog. class MultipleImagesDisplay extends StatelessWidget { /// Default Constructor. - const MultipleImagesDisplay(this.files, {Key? key}) : super(key: key); + const MultipleImagesDisplay(this.files, {super.key}); /// The files containing the images. final List files; diff --git a/packages/file_selector/file_selector_windows/example/lib/open_text_page.dart b/packages/file_selector/file_selector_windows/example/lib/open_text_page.dart index 4c88d7475049..054f6f62b7d9 100644 --- a/packages/file_selector/file_selector_windows/example/lib/open_text_page.dart +++ b/packages/file_selector/file_selector_windows/example/lib/open_text_page.dart @@ -2,6 +2,8 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'dart:typed_data'; + import 'package:file_selector_platform_interface/file_selector_platform_interface.dart'; import 'package:flutter/material.dart'; @@ -9,7 +11,7 @@ import 'package:flutter/material.dart'; /// displays its contents in a dialog. class OpenTextPage extends StatelessWidget { /// Default Constructor - const OpenTextPage({Key? key}) : super(key: key); + const OpenTextPage({super.key}); Future _openTextFile(BuildContext context) async { const XTypeGroup typeGroup = XTypeGroup( @@ -23,7 +25,14 @@ class OpenTextPage extends StatelessWidget { return; } final String fileName = file.name; - final String fileContent = await file.readAsString(); + + // This behavior defaults works when reading files encoded using UTF-16 LE. + // If you have files encoded with UTF-8 you can simply use file.readAsString() + // For other encodings, consider using Encoding.getByName() method, to get your encoder + // before calling file.readAsString() + final Uint8List bytes = await file.readAsBytes(); + final Uint16List utf16CodeUnits = bytes.buffer.asUint16List(); + final String fileContent = String.fromCharCodes(utf16CodeUnits); await showDialog( context: context, @@ -62,8 +71,7 @@ class OpenTextPage extends StatelessWidget { /// Widget that displays a text file in a dialog. class TextDisplay extends StatelessWidget { /// Default Constructor. - const TextDisplay(this.fileName, this.fileContent, {Key? key}) - : super(key: key); + const TextDisplay(this.fileName, this.fileContent, {super.key}); /// The name of the selected file. final String fileName; 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 9803f285a536..0a513e4f1cbf 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 @@ -10,7 +10,7 @@ import 'package:flutter/material.dart'; /// then writes text to a file at that location. class SaveTextPage extends StatelessWidget { /// Default Constructor - SaveTextPage({Key? key}) : super(key: key); + SaveTextPage({super.key}); final TextEditingController _nameController = TextEditingController(); final TextEditingController _contentController = TextEditingController(); @@ -25,10 +25,12 @@ class SaveTextPage extends StatelessWidget { return; } final String text = _contentController.text; - final Uint8List fileData = Uint8List.fromList(text.codeUnits); + + // This behavior saves an Utf-16 LE encoded file. + final Uint16List fileData = Uint16List.fromList(text.codeUnits); const String fileMimeType = 'text/plain'; - final XFile textFile = - XFile.fromData(fileData, mimeType: fileMimeType, name: fileName); + final XFile textFile = XFile.fromData(fileData.buffer.asUint8List(), + mimeType: fileMimeType, name: fileName); await textFile.saveTo(path); } diff --git a/packages/file_selector/file_selector_windows/example/pubspec.yaml b/packages/file_selector/file_selector_windows/example/pubspec.yaml index bc886d32c896..85d58604a27f 100644 --- a/packages/file_selector/file_selector_windows/example/pubspec.yaml +++ b/packages/file_selector/file_selector_windows/example/pubspec.yaml @@ -4,11 +4,11 @@ publish_to: 'none' version: 1.0.0 environment: - sdk: ">=2.12.0 <3.0.0" - flutter: ">=2.10.0" + sdk: ">=2.17.0 <3.0.0" + flutter: ">=3.0.0" dependencies: - file_selector_platform_interface: ^2.2.0 + file_selector_platform_interface: ^2.3.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/flutter/generated_plugins.cmake b/packages/file_selector/file_selector_windows/example/windows/flutter/generated_plugins.cmake index a423a02476a2..b93c4c30c167 100644 --- a/packages/file_selector/file_selector_windows/example/windows/flutter/generated_plugins.cmake +++ b/packages/file_selector/file_selector_windows/example/windows/flutter/generated_plugins.cmake @@ -3,7 +3,6 @@ # list(APPEND FLUTTER_PLUGIN_LIST - file_selector_windows ) list(APPEND FLUTTER_FFI_PLUGIN_LIST 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..5a29d1acea70 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 @@ -3,12 +3,31 @@ // found in the LICENSE file. import 'package:file_selector_platform_interface/file_selector_platform_interface.dart'; +import 'package:flutter/foundation.dart'; +import 'package:win32/win32.dart'; -import 'src/messages.g.dart'; +import 'src/file_selector_api.dart'; +import 'src/file_selector_dart/dialog_wrapper_factory.dart'; +import 'src/file_selector_dart/file_dialog_controller_factory.dart'; +import 'src/file_selector_dart/ifile_dialog_factory.dart'; +import 'src/file_selector_dart/selection_options.dart'; /// An implementation of [FileSelectorPlatform] for Windows. class FileSelectorWindows extends FileSelectorPlatform { - final FileSelectorApi _hostApi = FileSelectorApi(); + /// Creates a new instance of [FileSelectorApi]. + FileSelectorWindows() + : _hostApi = FileSelectorApi( + DialogWrapperFactory( + FileDialogControllerFactory(), + IFileDialogFactory(), + ), + GetActiveWindow()); + + /// Creates a fake implementation of [FileSelectorApi] for testing purposes. + @visibleForTesting + FileSelectorWindows.useFakeApi(this._hostApi); + + final FileSelectorApi _hostApi; /// Registers the Windows implementation. static void registerWith() { @@ -21,14 +40,11 @@ class FileSelectorWindows extends FileSelectorPlatform { String? initialDirectory, String? confirmButtonText, }) async { - final List paths = await _hostApi.showOpenDialog( - SelectionOptions( - allowMultiple: false, - selectFolders: false, - allowedTypes: _typeGroupsFromXTypeGroups(acceptedTypeGroups), - ), - initialDirectory, - confirmButtonText); + final List paths = _hostApi.showOpenDialog( + SelectionOptions(allowedTypes: _allowedXTypeGroups(acceptedTypeGroups)), + initialDirectory, + confirmButtonText, + ); return paths.isEmpty ? null : XFile(paths.first!); } @@ -38,14 +54,13 @@ class FileSelectorWindows extends FileSelectorPlatform { String? initialDirectory, String? confirmButtonText, }) async { - final List paths = await _hostApi.showOpenDialog( - SelectionOptions( + final List paths = _hostApi.showOpenDialog( + SelectionOptions( allowMultiple: true, - selectFolders: false, - allowedTypes: _typeGroupsFromXTypeGroups(acceptedTypeGroups), - ), - initialDirectory, - confirmButtonText); + allowedTypes: _allowedXTypeGroups(acceptedTypeGroups)), + initialDirectory, + confirmButtonText, + ); return paths.map((String? path) => XFile(path!)).toList(); } @@ -56,15 +71,12 @@ class FileSelectorWindows extends FileSelectorPlatform { String? suggestedName, String? confirmButtonText, }) async { - final List paths = await _hostApi.showSaveDialog( - SelectionOptions( - allowMultiple: false, - selectFolders: false, - allowedTypes: _typeGroupsFromXTypeGroups(acceptedTypeGroups), - ), - initialDirectory, - suggestedName, - confirmButtonText); + final List paths = _hostApi.showSaveDialog( + SelectionOptions(allowedTypes: _allowedXTypeGroups(acceptedTypeGroups)), + initialDirectory, + suggestedName, + confirmButtonText, + ); return paths.isEmpty ? null : paths.first!; } @@ -73,19 +85,16 @@ class FileSelectorWindows extends FileSelectorPlatform { String? initialDirectory, String? confirmButtonText, }) async { - final List paths = await _hostApi.showOpenDialog( - SelectionOptions( - allowMultiple: false, - selectFolders: true, - allowedTypes: [], - ), - initialDirectory, - confirmButtonText); + final List paths = _hostApi.showOpenDialog( + SelectionOptions(selectFolders: true, allowedTypes: []), + initialDirectory, + confirmButtonText, + ); return paths.isEmpty ? null : paths.first!; } } -List _typeGroupsFromXTypeGroups(List? xtypes) { +List _allowedXTypeGroups(List? xtypes) { return (xtypes ?? []).map((XTypeGroup xtype) { if (!xtype.allowsAny && (xtype.extensions?.isEmpty ?? true)) { throw ArgumentError('Provided type group $xtype does not allow ' @@ -93,7 +102,6 @@ List _typeGroupsFromXTypeGroups(List? xtypes) { 'categories. "extensions" must be non-empty for Windows if ' 'anything is non-empty.'); } - return TypeGroup( - label: xtype.label ?? '', extensions: xtype.extensions ?? []); + return xtype; }).toList(); } diff --git a/packages/file_selector/file_selector_windows/lib/src/file_selector_api.dart b/packages/file_selector/file_selector_windows/lib/src/file_selector_api.dart new file mode 100644 index 000000000000..fb57fc0a402a --- /dev/null +++ b/packages/file_selector/file_selector_windows/lib/src/file_selector_api.dart @@ -0,0 +1,95 @@ +// 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:win32/win32.dart'; + +import 'file_selector_dart/dialog_mode.dart'; +import 'file_selector_dart/dialog_wrapper.dart'; +import 'file_selector_dart/dialog_wrapper_factory.dart'; +import 'file_selector_dart/selection_options.dart'; + +/// File dialog handling for Open and Save operations. +class FileSelectorApi { + /// Creates a new instance of [FileSelectorApi]. + /// Allows Dependency Injection of a [DialogWrapperFactory] to handle dialog creation. + FileSelectorApi(this._dialogWrapperFactory, this._activeWindow); + + final DialogWrapperFactory _dialogWrapperFactory; + + final int _activeWindow; + + /// Displays a dialog window to open one or more files. + List showOpenDialog( + SelectionOptions options, + String? initialDirectory, + String? confirmButtonText, + ) => + _showDialog( + _activeWindow, + DialogMode.open, + options, + initialDirectory, + null, + confirmButtonText, + ); + + /// Displays a dialog used to save a file. + List showSaveDialog( + SelectionOptions options, + String? initialDirectory, + String? suggestedName, + String? confirmButtonText, + ) => + _showDialog( + _activeWindow, + DialogMode.save, + options, + initialDirectory, + suggestedName, + confirmButtonText, + ); + + List _showDialog( + int parentWindow, + DialogMode mode, + SelectionOptions options, + String? initialDirectory, + String? suggestedName, + String? confirmLabel, + ) { + final DialogWrapper dialogWrapper = + _dialogWrapperFactory.createInstance(mode); + if (!SUCCEEDED(dialogWrapper.lastResult)) { + throw WindowsException(dialogWrapper.lastResult); + } + int dialogOptions = 0; + if (options.selectFolders) { + dialogOptions |= FILEOPENDIALOGOPTIONS.FOS_PICKFOLDERS; + } + if (options.allowMultiple) { + dialogOptions |= FILEOPENDIALOGOPTIONS.FOS_ALLOWMULTISELECT; + } + if (dialogOptions != 0) { + dialogWrapper.addOptions(dialogOptions); + } + + if (initialDirectory != null) { + dialogWrapper.setFolder(initialDirectory); + } + if (suggestedName != null) { + dialogWrapper.setFileName(suggestedName); + } + if (confirmLabel != null) { + dialogWrapper.setOkButtonLabel(confirmLabel); + } + + if (options.allowedTypes.isNotEmpty) { + dialogWrapper.setFileTypeFilters(options.allowedTypes); + } + + final List? files = dialogWrapper.show(parentWindow); + dialogWrapper.release(); + return files ?? []; + } +} diff --git a/packages/file_selector/file_selector_windows/lib/src/file_selector_dart/dialog_mode.dart b/packages/file_selector/file_selector_windows/lib/src/file_selector_dart/dialog_mode.dart new file mode 100644 index 000000000000..4780c7ad8a54 --- /dev/null +++ b/packages/file_selector/file_selector_windows/lib/src/file_selector_dart/dialog_mode.dart @@ -0,0 +1,12 @@ +// 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. + +/// The kind of file dialog to show. +enum DialogMode { + /// Used for choosing files. + open, + + /// Used for choosing a directory to save a file. + save +} diff --git a/packages/file_selector/file_selector_windows/lib/src/file_selector_dart/dialog_wrapper.dart b/packages/file_selector/file_selector_windows/lib/src/file_selector_dart/dialog_wrapper.dart new file mode 100644 index 000000000000..a76739e23d3b --- /dev/null +++ b/packages/file_selector/file_selector_windows/lib/src/file_selector_dart/dialog_wrapper.dart @@ -0,0 +1,220 @@ +// 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 'dart:core'; +import 'dart:ffi'; + +import 'package:ffi/ffi.dart'; +import 'package:file_selector_platform_interface/file_selector_platform_interface.dart'; +import 'package:flutter/foundation.dart'; +import 'package:win32/win32.dart'; + +import 'dialog_mode.dart'; +import 'file_dialog_controller.dart'; +import 'ifile_dialog_controller_factory.dart'; +import 'ifile_dialog_factory.dart'; +import 'shell_win32_api.dart'; + +/// Wraps an IFileDialog, managing object lifetime as a scoped object and +/// providing a simplified API for interacting with it as needed for the plugin. +class DialogWrapper { + /// Creates a DialogWrapper using a [IFileDialogControllerFactory] and a [DialogMode]. + /// It is also responsible of creating a [IFileDialog]. + DialogWrapper( + IFileDialogControllerFactory fileDialogControllerFactory, + IFileDialogFactory fileDialogFactory, + this._dialogMode, + ) : _isOpenDialog = _dialogMode == DialogMode.open { + try { + final IFileDialog dialog = fileDialogFactory.createInstance(_dialogMode); + _dialogController = fileDialogControllerFactory.createController(dialog); + _shellWin32Api = ShellWin32Api(); + } catch (ex) { + if (ex is WindowsException) { + _lastResult = ex.hr; + } + } + } + + /// Creates a DialogWrapper for testing purposes. + @visibleForTesting + DialogWrapper.withFakeDependencies( + FileDialogController dialogController, + this._dialogMode, + this._shellWin32Api, + ) : _isOpenDialog = _dialogMode == DialogMode.open, + _dialogController = dialogController; + + final DialogMode _dialogMode; + + final bool _isOpenDialog; + + late final FileDialogController _dialogController; + + late final ShellWin32Api _shellWin32Api; + + static const String _allowAnyValue = 'Any'; + + static const String _allowAnyExtension = '*.*'; + + /// Returns the result of the last Win32 API call related to this object. + int get lastResult => _lastResult; + + int _lastResult = S_OK; + + /// Attempts to set the default folder for the dialog to [path], if it exists. + void setFolder(String path) { + if (path.isEmpty) { + return; + } + + using((Arena arena) { + final Pointer ptrGuid = GUIDFromString( + IID_IShellItem, + allocator: arena, + ); + final Pointer> ptrPath = arena>(); + _lastResult = _shellWin32Api.createItemFromParsingName( + path.toNativeUtf16(allocator: arena), + ptrGuid, + ptrPath, + ); + + if (!SUCCEEDED(_lastResult)) { + return; + } + + _lastResult = _dialogController.setFolder(ptrPath.value); + }); + } + + /// Sets the file name that is initially shown in the dialog. + void setFileName(String name) { + _lastResult = _dialogController.setFileName(name); + } + + /// Sets the label of the confirmation button. + void setOkButtonLabel(String label) { + _lastResult = _dialogController.setOkButtonLabel(label); + } + + /// Adds the given options to the dialog's current [options](https://pub.dev/documentation/win32/latest/winrt/FILEOPENDIALOGOPTIONS-class.html). + /// Both are bitfields. + void addOptions(int newOptions) { + using((Arena arena) { + final Pointer currentOptions = arena(); + _lastResult = _dialogController.getOptions(currentOptions); + if (!SUCCEEDED(_lastResult)) { + return; + } + currentOptions.value |= newOptions; + _lastResult = _dialogController.setOptions(currentOptions.value); + }); + } + + /// Sets the filters for allowed file types to select. + /// filters -> std::optional + void setFileTypeFilters(List filters) { + final Map filterSpecification = {}; + + if (filters.isEmpty) { + filterSpecification[_allowAnyValue] = _allowAnyExtension; + } else { + for (final XTypeGroup option in filters) { + final String? label = option.label; + if (option.allowsAny || option.extensions!.isEmpty) { + filterSpecification[label ?? _allowAnyValue] = _allowAnyExtension; + } else { + final String extensionsForLabel = option.extensions! + .map((String extension) => '*.$extension') + .join(';'); + filterSpecification[label ?? extensionsForLabel] = extensionsForLabel; + } + } + } + + using((Arena arena) { + final Pointer registerFilterSpecification = + arena(filterSpecification.length); + int index = 0; + for (final String key in filterSpecification.keys) { + registerFilterSpecification[index] + ..pszName = key.toNativeUtf16(allocator: arena) + ..pszSpec = filterSpecification[key]!.toNativeUtf16(allocator: arena); + index += 1; + } + + _lastResult = _dialogController.setFileTypes( + filterSpecification.length, + registerFilterSpecification, + ); + }); + } + + /// Releases the IFileDialog resource. + /// This method should be called after using the the DialogWrapper + int release() { + return _dialogController.release(); + } + + /// Displays the dialog, and returns the selected files, or null on error. + List? show(int parentWindow) { + _lastResult = _dialogController.show(parentWindow); + if (!SUCCEEDED(_lastResult)) { + return null; + } + + return using((Arena arena) { + final Pointer> shellItemArrayPtr = + arena>(); + final Pointer shellItemCountPtr = arena(); + final Pointer> shellItemPtr = + arena>(); + + return _getFilePathList( + shellItemArrayPtr, shellItemCountPtr, shellItemPtr); + }); + } + + List? _getFilePathList( + Pointer> shellItemArrayPtr, + Pointer shellItemCountPtr, + Pointer> shellItemPtr) { + try { + final List files = []; + int lastOperationResult; + if (_isOpenDialog) { + lastOperationResult = _dialogController.getResults(shellItemArrayPtr); + if (!SUCCEEDED(lastOperationResult)) { + throw WindowsException(lastOperationResult); + } + + final IShellItemArray shellItemResources = + IShellItemArray(shellItemArrayPtr.cast()); + lastOperationResult = shellItemResources.getCount(shellItemCountPtr); + if (!SUCCEEDED(lastOperationResult)) { + throw WindowsException(lastOperationResult); + } + for (int index = 0; index < shellItemCountPtr.value; index += 1) { + shellItemResources.getItemAt(index, shellItemPtr); + final IShellItem shellItem = IShellItem(shellItemPtr.cast()); + files.add(_shellWin32Api.getPathForShellItem(shellItem)); + _shellWin32Api.releaseShellItem(shellItem); + } + } else { + lastOperationResult = _dialogController.getResult(shellItemPtr); + if (!SUCCEEDED(lastOperationResult)) { + throw WindowsException(lastOperationResult); + } + final IShellItem shellItem = IShellItem(shellItemPtr.cast()); + files.add(_shellWin32Api.getPathForShellItem(shellItem)); + _shellWin32Api.releaseShellItem(shellItem); + } + return files; + } on WindowsException catch (ex) { + _lastResult = ex.hr; + return null; + } + } +} diff --git a/packages/file_selector/file_selector_windows/lib/src/file_selector_dart/dialog_wrapper_factory.dart b/packages/file_selector/file_selector_windows/lib/src/file_selector_dart/dialog_wrapper_factory.dart new file mode 100644 index 000000000000..1fc9ffccc519 --- /dev/null +++ b/packages/file_selector/file_selector_windows/lib/src/file_selector_dart/dialog_wrapper_factory.dart @@ -0,0 +1,31 @@ +// 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 'dialog_mode.dart'; +import 'dialog_wrapper.dart'; +import 'ifile_dialog_controller_factory.dart'; +import 'ifile_dialog_factory.dart'; + +/// Implementation of DialogWrapperFactory that provides [DialogWrapper] instances. +class DialogWrapperFactory { + /// Creates a [DialogWrapperFactory] that makes use of [IFileDialogControllerFactory] and [IFileDialogFactory] + /// to create [DialogWrapper] instances. + DialogWrapperFactory( + this._fileDialogControllerFactory, + this._fileDialogFactory, + ); + + final IFileDialogControllerFactory _fileDialogControllerFactory; + + final IFileDialogFactory _fileDialogFactory; + + /// Creates a [DialogWrapper] based on [dialogMode]. + DialogWrapper createInstance(DialogMode dialogMode) { + return DialogWrapper( + _fileDialogControllerFactory, + _fileDialogFactory, + dialogMode, + ); + } +} diff --git a/packages/file_selector/file_selector_windows/lib/src/file_selector_dart/file_dialog_controller.dart b/packages/file_selector/file_selector_windows/lib/src/file_selector_dart/file_dialog_controller.dart new file mode 100644 index 000000000000..9c9a403e29bd --- /dev/null +++ b/packages/file_selector/file_selector_windows/lib/src/file_selector_dart/file_dialog_controller.dart @@ -0,0 +1,96 @@ +// 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 'dart:ffi'; + +import 'package:ffi/ffi.dart'; +import 'package:win32/win32.dart'; + +import 'ifile_open_dialog_factory.dart'; + +/// A thin wrapper for IFileDialog to allow for faking and inspection in tests. +/// +/// Since this class defines the end of what can be unit tested, it should +/// contain as little logic as possible. +class FileDialogController { + /// Creates a controller managing [IFileDialog](https://pub.dev/documentation/win32/latest/winrt/IFileDialog-class.html). + /// It also receives an IFileOpenDialogFactory to construct [IFileOpenDialog] + /// instances. + FileDialogController( + IFileDialog fileDialog, + IFileOpenDialogFactory iFileOpenDialogFactory, + ) : _fileDialog = fileDialog, + _iFileOpenDialogFactory = iFileOpenDialogFactory; + + /// The [IFileDialog] to work with. + final IFileDialog _fileDialog; + + /// The [IFileOpenDialogFactory] to create [IFileOpenDialog] instances. + final IFileOpenDialogFactory _iFileOpenDialogFactory; + + /// Sets the default folder for the dialog to [path]. It also returns the operation result. + int setFolder(Pointer path) => _fileDialog.setFolder(path); + + /// Sets the file [name] that is initially shown in the IFileDialog. It also returns the operation result. + int setFileName(String name) { + return using((Arena arena) { + return _fileDialog.setFileName(name.toNativeUtf16(allocator: arena)); + }, malloc); + } + + /// Sets the allowed file type extensions in the IFileOpenDialog. It also returns the operation result. + int setFileTypes(int count, Pointer filters) { + return _fileDialog.setFileTypes(count, filters); + } + + /// Sets the label of the confirmation button. It also returns the operation result. It also returns the operation result. + int setOkButtonLabel(String text) { + return using((Arena arena) { + return _fileDialog.setOkButtonLabel(text.toNativeUtf16(allocator: arena)); + }, malloc); + } + + /// Gets the IFileDialog's [options](https://pub.dev/documentation/win32/latest/winrt/FILEOPENDIALOGOPTIONS-class.html), + /// which is a bitfield. It also returns the operation result. + int getOptions(Pointer outOptions) { + return _fileDialog.getOptions(outOptions); + } + + /// Sets the [options](https://pub.dev/documentation/win32/latest/winrt/FILEOPENDIALOGOPTIONS-class.html), + /// which is a bitfield, into the IFileDialog. It also returns the operation result. + int setOptions(int options) => _fileDialog.setOptions(options); + + /// Shows an IFileDialog using the given parent. It returns the operation result. + int show(int parent) => _fileDialog.show(parent); + + /// Return results from an IFileDialog. This should be used when selecting + /// single items. It also returns the operation result. + int getResult(Pointer> outItem) { + return _fileDialog.getResult(outItem); + } + + /// Return results from an IFileOpenDialog. This should be used when selecting + /// single items. This function will fail if the IFileDialog* provided to the + /// constructor was not an IFileOpenDialog instance, returning an E_FAIL + /// error. + int getResults(Pointer> outItems) { + IFileOpenDialog? fileOpenDialog; + try { + fileOpenDialog = _iFileOpenDialogFactory.from(_fileDialog); + return fileOpenDialog.getResults(outItems); + } catch (exception) { + return E_FAIL; + } finally { + fileOpenDialog?.release(); + if (fileOpenDialog != null) { + free(fileOpenDialog.ptr); + } + } + } + + /// Releases the IFileDialog resource + int release() { + return _fileDialog.release(); + } +} diff --git a/packages/file_selector/file_selector_windows/lib/src/file_selector_dart/file_dialog_controller_factory.dart b/packages/file_selector/file_selector_windows/lib/src/file_selector_dart/file_dialog_controller_factory.dart new file mode 100644 index 000000000000..296c9ba4529f --- /dev/null +++ b/packages/file_selector/file_selector_windows/lib/src/file_selector_dart/file_dialog_controller_factory.dart @@ -0,0 +1,18 @@ +// 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:win32/win32.dart'; + +import 'file_dialog_controller.dart'; +import 'ifile_dialog_controller_factory.dart'; +import 'ifile_open_dialog_factory.dart'; + +/// Implementation of FileDialogControllerFactory that makes standard +/// FileDialogController instances. +class FileDialogControllerFactory implements IFileDialogControllerFactory { + @override + FileDialogController createController(IFileDialog dialog) { + return FileDialogController(dialog, IFileOpenDialogFactory()); + } +} diff --git a/packages/file_selector/file_selector_windows/lib/src/file_selector_dart/ifile_dialog_controller_factory.dart b/packages/file_selector/file_selector_windows/lib/src/file_selector_dart/ifile_dialog_controller_factory.dart new file mode 100644 index 000000000000..63f081b82944 --- /dev/null +++ b/packages/file_selector/file_selector_windows/lib/src/file_selector_dart/ifile_dialog_controller_factory.dart @@ -0,0 +1,14 @@ +// 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:win32/win32.dart'; + +import 'file_dialog_controller.dart'; + +/// Interface for creating FileDialogControllers, to allow for dependency +/// injection. +abstract class IFileDialogControllerFactory { + /// Returns a FileDialogController to interact with the given [IFileDialog]. + FileDialogController createController(IFileDialog dialog); +} diff --git a/packages/file_selector/file_selector_windows/lib/src/file_selector_dart/ifile_dialog_factory.dart b/packages/file_selector/file_selector_windows/lib/src/file_selector_dart/ifile_dialog_factory.dart new file mode 100644 index 000000000000..b8e78f76171d --- /dev/null +++ b/packages/file_selector/file_selector_windows/lib/src/file_selector_dart/ifile_dialog_factory.dart @@ -0,0 +1,20 @@ +// 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:win32/win32.dart'; + +import 'dialog_mode.dart'; + +/// A factory for [IFileDialog] instances. +class IFileDialogFactory { + /// Creates the corresponding IFileDialog instance. The caller is responsible of releasing the resource. + IFileDialog createInstance(DialogMode dialogMode) { + switch (dialogMode) { + case DialogMode.open: + return FileOpenDialog.createInstance(); + case DialogMode.save: + return FileSaveDialog.createInstance(); + } + } +} diff --git a/packages/file_selector/file_selector_windows/lib/src/file_selector_dart/ifile_open_dialog_factory.dart b/packages/file_selector/file_selector_windows/lib/src/file_selector_dart/ifile_open_dialog_factory.dart new file mode 100644 index 000000000000..fca0095db413 --- /dev/null +++ b/packages/file_selector/file_selector_windows/lib/src/file_selector_dart/ifile_open_dialog_factory.dart @@ -0,0 +1,13 @@ +// 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:win32/win32.dart'; + +/// A wrapper of the IFileOpenDialog interface to use its from function. +class IFileOpenDialogFactory { + /// Wraps the IFileOpenDialog from function. + IFileOpenDialog from(IFileDialog fileDialog) { + return IFileOpenDialog.from(fileDialog); + } +} diff --git a/packages/file_selector/file_selector_windows/lib/src/file_selector_dart/selection_options.dart b/packages/file_selector/file_selector_windows/lib/src/file_selector_dart/selection_options.dart new file mode 100644 index 000000000000..d00e30198b1d --- /dev/null +++ b/packages/file_selector/file_selector_windows/lib/src/file_selector_dart/selection_options.dart @@ -0,0 +1,25 @@ +// 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'; + +/// Options for Dialog window +class SelectionOptions { + /// Creates a new [SelectionOptions] instance with the specified values. + /// It defaults [allowMultiple] to false, [selectFolders] to false and no [allowedTypes] + SelectionOptions({ + this.allowMultiple = false, + this.selectFolders = false, + this.allowedTypes = const [], + }); + + /// Indicates whether the user is able to select multiple items at the same time or not. + bool allowMultiple; + + /// Indicates whether the user is able to select folders or not. + bool selectFolders; + + /// A list of file types that can be selected. + List allowedTypes; +} diff --git a/packages/file_selector/file_selector_windows/lib/src/file_selector_dart/shell_win32_api.dart b/packages/file_selector/file_selector_windows/lib/src/file_selector_dart/shell_win32_api.dart new file mode 100644 index 000000000000..d5dd81c6b668 --- /dev/null +++ b/packages/file_selector/file_selector_windows/lib/src/file_selector_dart/shell_win32_api.dart @@ -0,0 +1,45 @@ +// 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 'dart:ffi'; + +import 'package:ffi/ffi.dart'; +import 'package:win32/win32.dart'; + +/// A thin wrapper for Win32 platform specific Shell methods. +/// +/// Allows for unit testing by decoupling win32 API calls from the business logic. +class ShellWin32Api { + /// Creates and [initializes](https://learn.microsoft.com/en-us/windows/win32/api/shobjidl_core/nf-shobjidl_core-shcreateitemfromparsingname) a Shell item object from a parsing name. + /// If the directory doesn't exist it will return an error result. + int createItemFromParsingName( + Pointer pszPath, + Pointer ptrGuid, + Pointer> ptrPath, + ) { + return SHCreateItemFromParsingName(pszPath, nullptr, ptrGuid, ptrPath); + } + + /// Returns the path for [shellItem] as a UTF-16 string, or an empty string on + /// failure. + String getPathForShellItem(IShellItem shellItem) { + return using((Arena arena) { + final Pointer> ptrPath = arena>(); + final int operationResult = shellItem.getDisplayName( + SIGDN.SIGDN_FILESYSPATH, + ptrPath.cast(), + ); + + if (!SUCCEEDED(operationResult)) { + throw WindowsException(operationResult); + } + return ptrPath.value.toDartString(); + }); + } + + /// Releases the given [shellItem] + int releaseShellItem(IShellItem shellItem) { + return shellItem.release(); + } +} 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 deleted file mode 100644 index ad3d5af83278..000000000000 --- a/packages/file_selector/file_selector_windows/lib/src/messages.g.dart +++ /dev/null @@ -1,176 +0,0 @@ -// 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 (v3.2.5), 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 -import 'dart:async'; -import 'dart:typed_data' show Uint8List, Int32List, Int64List, Float64List; - -import 'package:flutter/foundation.dart' show WriteBuffer, ReadBuffer; -import 'package:flutter/services.dart'; - -class TypeGroup { - TypeGroup({ - required this.label, - required this.extensions, - }); - - String label; - List extensions; - - Object encode() { - final Map pigeonMap = {}; - pigeonMap['label'] = label; - pigeonMap['extensions'] = extensions; - return pigeonMap; - } - - static TypeGroup decode(Object message) { - final Map pigeonMap = message as Map; - return TypeGroup( - label: pigeonMap['label']! as String, - extensions: (pigeonMap['extensions'] as List?)!.cast(), - ); - } -} - -class SelectionOptions { - SelectionOptions({ - required this.allowMultiple, - required this.selectFolders, - required this.allowedTypes, - }); - - bool allowMultiple; - bool selectFolders; - List allowedTypes; - - Object encode() { - final Map pigeonMap = {}; - pigeonMap['allowMultiple'] = allowMultiple; - pigeonMap['selectFolders'] = selectFolders; - pigeonMap['allowedTypes'] = allowedTypes; - return pigeonMap; - } - - static SelectionOptions decode(Object message) { - final Map pigeonMap = message as Map; - return SelectionOptions( - allowMultiple: pigeonMap['allowMultiple']! as bool, - selectFolders: pigeonMap['selectFolders']! as bool, - allowedTypes: - (pigeonMap['allowedTypes'] as List?)!.cast(), - ); - } -} - -class _FileSelectorApiCodec extends StandardMessageCodec { - const _FileSelectorApiCodec(); - @override - void writeValue(WriteBuffer buffer, Object? value) { - if (value is SelectionOptions) { - buffer.putUint8(128); - writeValue(buffer, value.encode()); - } else if (value is TypeGroup) { - buffer.putUint8(129); - writeValue(buffer, value.encode()); - } else { - super.writeValue(buffer, value); - } - } - - @override - Object? readValueOfType(int type, ReadBuffer buffer) { - switch (type) { - case 128: - return SelectionOptions.decode(readValue(buffer)!); - - case 129: - return TypeGroup.decode(readValue(buffer)!); - - default: - return super.readValueOfType(type, buffer); - } - } -} - -class FileSelectorApi { - /// Constructor for [FileSelectorApi]. The [binaryMessenger] named argument is - /// available for dependency injection. If it is left null, the default - /// BinaryMessenger will be used which routes to the host platform. - FileSelectorApi({BinaryMessenger? binaryMessenger}) - : _binaryMessenger = binaryMessenger; - - final BinaryMessenger? _binaryMessenger; - - static const MessageCodec codec = _FileSelectorApiCodec(); - - Future> showOpenDialog(SelectionOptions arg_options, - String? arg_initialDirectory, String? arg_confirmButtonText) async { - final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.FileSelectorApi.showOpenDialog', codec, - binaryMessenger: _binaryMessenger); - final Map? replyMap = await channel.send( - [arg_options, arg_initialDirectory, arg_confirmButtonText]) - as Map?; - if (replyMap == null) { - throw PlatformException( - code: 'channel-error', - message: 'Unable to establish connection on channel.', - ); - } else if (replyMap['error'] != null) { - final Map error = - (replyMap['error'] as Map?)!; - throw PlatformException( - code: (error['code'] as String?)!, - message: error['message'] as String?, - details: error['details'], - ); - } else if (replyMap['result'] == null) { - throw PlatformException( - code: 'null-error', - message: 'Host platform returned null value for non-null return value.', - ); - } else { - return (replyMap['result'] as List?)!.cast(); - } - } - - Future> showSaveDialog( - SelectionOptions arg_options, - String? arg_initialDirectory, - String? arg_suggestedName, - String? arg_confirmButtonText) async { - final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.FileSelectorApi.showSaveDialog', codec, - binaryMessenger: _binaryMessenger); - final Map? replyMap = await channel.send([ - arg_options, - arg_initialDirectory, - arg_suggestedName, - arg_confirmButtonText - ]) as Map?; - if (replyMap == null) { - throw PlatformException( - code: 'channel-error', - message: 'Unable to establish connection on channel.', - ); - } else if (replyMap['error'] != null) { - final Map error = - (replyMap['error'] as Map?)!; - throw PlatformException( - code: (error['code'] as String?)!, - message: error['message'] as String?, - details: error['details'], - ); - } else if (replyMap['result'] == null) { - throw PlatformException( - code: 'null-error', - message: 'Host platform returned null value for non-null return value.', - ); - } else { - return (replyMap['result'] as List?)!.cast(); - } - } -} diff --git a/packages/file_selector/file_selector_windows/pigeons/copyright.txt b/packages/file_selector/file_selector_windows/pigeons/copyright.txt deleted file mode 100644 index 1236b63caf3a..000000000000 --- a/packages/file_selector/file_selector_windows/pigeons/copyright.txt +++ /dev/null @@ -1,3 +0,0 @@ -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. diff --git a/packages/file_selector/file_selector_windows/pigeons/messages.dart b/packages/file_selector/file_selector_windows/pigeons/messages.dart deleted file mode 100644 index f2c9ab71bd82..000000000000 --- a/packages/file_selector/file_selector_windows/pigeons/messages.dart +++ /dev/null @@ -1,53 +0,0 @@ -// 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:pigeon/pigeon.dart'; - -@ConfigurePigeon(PigeonOptions( - dartOut: 'lib/src/messages.g.dart', - dartTestOut: 'test/test_api.dart', - cppOptions: CppOptions(namespace: 'file_selector_windows'), - cppHeaderOut: 'windows/messages.g.h', - cppSourceOut: 'windows/messages.g.cpp', - copyrightHeader: 'pigeons/copyright.txt', -)) -class TypeGroup { - TypeGroup(this.label, {required this.extensions}); - - String label; - // TODO(stuartmorgan): Make the generic type non-nullable once supported. - // https://github.com/flutter/flutter/issues/97848 - // The C++ code treats all of it as non-nullable. - List extensions; -} - -class SelectionOptions { - SelectionOptions({ - this.allowMultiple = false, - this.selectFolders = false, - this.allowedTypes = const [], - }); - bool allowMultiple; - bool selectFolders; - - // TODO(stuartmorgan): Make the generic type non-nullable once supported. - // https://github.com/flutter/flutter/issues/97848 - // The C++ code treats the values as non-nullable. - List allowedTypes; -} - -@HostApi(dartHostTestHandler: 'TestFileSelectorApi') -abstract class FileSelectorApi { - List showOpenDialog( - SelectionOptions options, - String? initialDirectory, - String? confirmButtonText, - ); - List showSaveDialog( - SelectionOptions options, - String? initialDirectory, - String? suggestedName, - String? confirmButtonText, - ); -} diff --git a/packages/file_selector/file_selector_windows/pubspec.yaml b/packages/file_selector/file_selector_windows/pubspec.yaml index ee0701b3fd30..179546bc9fb2 100644 --- a/packages/file_selector/file_selector_windows/pubspec.yaml +++ b/packages/file_selector/file_selector_windows/pubspec.yaml @@ -2,11 +2,11 @@ name: file_selector_windows description: Windows implementation of the file_selector plugin. repository: https://github.com/flutter/plugins/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+4 +version: 1.0.0 environment: - sdk: ">=2.12.0 <3.0.0" - flutter: ">=2.10.0" + sdk: ">=2.17.0 <3.0.0" + flutter: ">=3.0.0" flutter: plugin: @@ -14,17 +14,17 @@ flutter: platforms: windows: dartPluginClass: FileSelectorWindows - pluginClass: FileSelectorWindows dependencies: cross_file: ^0.3.1 + ffi: ^2.0.1 file_selector_platform_interface: ^2.2.0 flutter: sdk: flutter + win32: ^3.0.0 dev_dependencies: build_runner: 2.1.11 flutter_test: sdk: flutter mockito: ^5.1.0 - pigeon: ^3.2.5 diff --git a/packages/file_selector/file_selector_windows/test/file_selector_api_test.dart b/packages/file_selector/file_selector_windows/test/file_selector_api_test.dart new file mode 100644 index 000000000000..bfc35205a64f --- /dev/null +++ b/packages/file_selector/file_selector_windows/test/file_selector_api_test.dart @@ -0,0 +1,200 @@ +// 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:file_selector_windows/src/file_selector_api.dart'; +import 'package:file_selector_windows/src/file_selector_dart/dialog_mode.dart'; +import 'package:file_selector_windows/src/file_selector_dart/dialog_wrapper.dart'; +import 'package:file_selector_windows/src/file_selector_dart/dialog_wrapper_factory.dart'; +import 'package:file_selector_windows/src/file_selector_dart/selection_options.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; +import 'package:win32/win32.dart'; + +import 'file_selector_api_test.mocks.dart'; + +@GenerateMocks([DialogWrapperFactory, DialogWrapper]) +void main() { + const int parentWindow = 1; + final MockDialogWrapperFactory mockDialogWrapperFactory = + MockDialogWrapperFactory(); + late MockDialogWrapper mockDialogWrapper; + final FileSelectorApi fileSelectorApi = + FileSelectorApi(mockDialogWrapperFactory, parentWindow); + + const List expectedFileList = ['fileA', 'fileB']; + final SelectionOptions emptyOptions = SelectionOptions(); + + setUp(() { + mockDialogWrapper = MockDialogWrapper(); + when(mockDialogWrapper.lastResult).thenReturn(S_OK); + when(mockDialogWrapper.release()).thenReturn(0); + when(mockDialogWrapperFactory.createInstance(DialogMode.save)) + .thenReturn(mockDialogWrapper); + when(mockDialogWrapperFactory.createInstance(DialogMode.open)) + .thenReturn(mockDialogWrapper); + when(mockDialogWrapper.show(parentWindow)).thenReturn(expectedFileList); + }); + + tearDown(() { + reset(mockDialogWrapper); + reset(mockDialogWrapperFactory); + }); + + test('FileSelectorApi should not be null', () { + expect(fileSelectorApi, isNotNull); + }); + + group('showSaveDialog', () { + test('should call setFileName if a suggestedName is provided', () { + // Arrange + const String suggestedName = 'suggestedName'; + + // Act + fileSelectorApi.showSaveDialog(emptyOptions, null, suggestedName, null); + + // Assert + verify(mockDialogWrapper.setFileName(suggestedName)).called(1); + }); + + test('should create a DialogWrapper with DialogMode Save', () { + // Act + fileSelectorApi.showSaveDialog(emptyOptions, null, null, null); + + // Assert + verify(mockDialogWrapperFactory.createInstance(DialogMode.save)) + .called(1); + }); + + test('should return an empty list if operation is cancelled', () { + // Arrange + when(mockDialogWrapper.show(parentWindow)).thenReturn(null); + + // Act + final List result = + fileSelectorApi.showSaveDialog(emptyOptions, null, null, null); + + // Assert + expect(result.isEmpty, true); + }); + }); + group('showOpenDialog', () { + test('should create a DialogWrapper with DialogMode Open', () { + // Act + fileSelectorApi.showOpenDialog(emptyOptions, null, null); + + // Assert + verify(mockDialogWrapperFactory.createInstance(DialogMode.open)) + .called(1); + }); + + test('should return an empty list if operation is cancelled', () { + // Arrange + when(mockDialogWrapper.show(parentWindow)).thenReturn(null); + + // Act + final List result = + fileSelectorApi.showOpenDialog(emptyOptions, null, null); + + // Assert + expect(result.isEmpty, true); + }); + }); + group('Common behavior', () { + test('should throw a WindowsException is DialogWrapper can not be created', + () { + // Arrange + when(mockDialogWrapperFactory.createInstance(DialogMode.open)) + .thenReturn(mockDialogWrapper); + when(mockDialogWrapper.lastResult).thenReturn(E_FAIL); + + // Act - Assert + expect(() => fileSelectorApi.showOpenDialog(emptyOptions, null, null), + throwsA(const TypeMatcher())); + }); + + test('should not call AddOptions if no options are configured', () { + // Act + fileSelectorApi.showOpenDialog(emptyOptions, null, null); + + // Assert + verifyNever(mockDialogWrapper.addOptions(any)); + }); + + test('should call AddOptions with FOS_PICKFOLDERS configured', () { + // Arrange + final SelectionOptions options = SelectionOptions(selectFolders: true); + + // Act + fileSelectorApi.showOpenDialog(options, null, null); + + // Assert + verify(mockDialogWrapper + .addOptions(FILEOPENDIALOGOPTIONS.FOS_PICKFOLDERS)) + .called(1); + }); + + test('should call AddOptions with FOS_ALLOWMULTISELECT configured', () { + // Arrange + final SelectionOptions options = SelectionOptions(allowMultiple: true); + + // Act + fileSelectorApi.showOpenDialog(options, null, null); + + // Assert + verify(mockDialogWrapper + .addOptions(FILEOPENDIALOGOPTIONS.FOS_ALLOWMULTISELECT)) + .called(1); + }); + + test('should call setFolder if an initialDirectory is provided', () { + // Arrange + const String initialDirectory = 'path/to/dir'; + + // Act + fileSelectorApi.showOpenDialog(emptyOptions, initialDirectory, null); + + // Assert + verify(mockDialogWrapper.setFolder(initialDirectory)).called(1); + }); + + test('should call setOkButtonLabel if confirmButtonText is provided', () { + // Arrange + const String confirmButtonText = 'OK'; + + // Act + fileSelectorApi.showOpenDialog(emptyOptions, null, confirmButtonText); + + // Assert + verify(mockDialogWrapper.setOkButtonLabel(confirmButtonText)).called(1); + }); + + test('should call setFileTypeFilters with provided allowedTypes', () { + // Arrange + final SelectionOptions options = + SelectionOptions(allowedTypes: [ + const XTypeGroup(extensions: ['jpg', 'png'], label: 'Images'), + const XTypeGroup(extensions: ['txt', 'json'], label: 'Text'), + ]); + + // Act + fileSelectorApi.showOpenDialog(options, null, null); + + // Assert + verify(mockDialogWrapper.setFileTypeFilters(options.allowedTypes)) + .called(1); + }); + + test('should return the file list on success', () { + // Act + final List result = + fileSelectorApi.showOpenDialog(emptyOptions, null, null); + + // Assert + expect(result.length, expectedFileList.length); + expect(result, expectedFileList); + }); + }); +} diff --git a/packages/file_selector/file_selector_windows/test/file_selector_api_test.mocks.dart b/packages/file_selector/file_selector_windows/test/file_selector_api_test.mocks.dart new file mode 100644 index 000000000000..d2f38212730b --- /dev/null +++ b/packages/file_selector/file_selector_windows/test/file_selector_api_test.mocks.dart @@ -0,0 +1,130 @@ +// Mocks generated by Mockito 5.3.2 from annotations +// in file_selector_windows/test/file_selector_api_test.dart. +// Do not manually edit this file. + +// ignore_for_file: no_leading_underscores_for_library_prefixes +import 'package:file_selector_platform_interface/file_selector_platform_interface.dart' + as _i5; +import 'package:file_selector_windows/src/file_selector_dart/dialog_mode.dart' + as _i4; +import 'package:file_selector_windows/src/file_selector_dart/dialog_wrapper.dart' + as _i2; +import 'package:file_selector_windows/src/file_selector_dart/dialog_wrapper_factory.dart' + as _i3; +import 'package:mockito/mockito.dart' as _i1; + +// ignore_for_file: type=lint +// ignore_for_file: avoid_redundant_argument_values +// ignore_for_file: avoid_setters_without_getters +// ignore_for_file: comment_references +// ignore_for_file: implementation_imports +// ignore_for_file: invalid_use_of_visible_for_testing_member +// ignore_for_file: prefer_const_constructors +// ignore_for_file: unnecessary_parenthesis +// ignore_for_file: camel_case_types +// ignore_for_file: subtype_of_sealed_class + +class _FakeDialogWrapper_0 extends _i1.SmartFake implements _i2.DialogWrapper { + _FakeDialogWrapper_0( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +/// A class which mocks [DialogWrapperFactory]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockDialogWrapperFactory extends _i1.Mock + implements _i3.DialogWrapperFactory { + MockDialogWrapperFactory() { + _i1.throwOnMissingStub(this); + } + + @override + _i2.DialogWrapper createInstance(_i4.DialogMode? dialogMode) => + (super.noSuchMethod( + Invocation.method( + #createInstance, + [dialogMode], + ), + returnValue: _FakeDialogWrapper_0( + this, + Invocation.method( + #createInstance, + [dialogMode], + ), + ), + ) as _i2.DialogWrapper); +} + +/// A class which mocks [DialogWrapper]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockDialogWrapper extends _i1.Mock implements _i2.DialogWrapper { + MockDialogWrapper() { + _i1.throwOnMissingStub(this); + } + + @override + int get lastResult => (super.noSuchMethod( + Invocation.getter(#lastResult), + returnValue: 0, + ) as int); + @override + void setFolder(String? path) => super.noSuchMethod( + Invocation.method( + #setFolder, + [path], + ), + returnValueForMissingStub: null, + ); + @override + void setFileName(String? name) => super.noSuchMethod( + Invocation.method( + #setFileName, + [name], + ), + returnValueForMissingStub: null, + ); + @override + void setOkButtonLabel(String? label) => super.noSuchMethod( + Invocation.method( + #setOkButtonLabel, + [label], + ), + returnValueForMissingStub: null, + ); + @override + void addOptions(int? newOptions) => super.noSuchMethod( + Invocation.method( + #addOptions, + [newOptions], + ), + returnValueForMissingStub: null, + ); + @override + void setFileTypeFilters(List<_i5.XTypeGroup>? filters) => super.noSuchMethod( + Invocation.method( + #setFileTypeFilters, + [filters], + ), + returnValueForMissingStub: null, + ); + @override + int release() => (super.noSuchMethod( + Invocation.method( + #release, + [], + ), + returnValue: 0, + ) as int); + @override + List? show(int? parentWindow) => + (super.noSuchMethod(Invocation.method( + #show, + [parentWindow], + )) as List?); +} diff --git a/packages/file_selector/file_selector_windows/test/file_selector_dart/dialog_wrapper_test.dart b/packages/file_selector/file_selector_windows/test/file_selector_dart/dialog_wrapper_test.dart new file mode 100644 index 000000000000..151b9118330c --- /dev/null +++ b/packages/file_selector/file_selector_windows/test/file_selector_dart/dialog_wrapper_test.dart @@ -0,0 +1,337 @@ +// 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 'dart:ffi'; + +import 'package:ffi/ffi.dart'; +import 'package:file_selector_platform_interface/file_selector_platform_interface.dart'; +import 'package:file_selector_windows/src/file_selector_dart/dialog_mode.dart'; +import 'package:file_selector_windows/src/file_selector_dart/dialog_wrapper.dart'; +import 'package:file_selector_windows/src/file_selector_dart/file_dialog_controller.dart'; +import 'package:file_selector_windows/src/file_selector_dart/shell_win32_api.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; +import 'package:win32/win32.dart'; + +import 'dialog_wrapper_test.mocks.dart'; + +@GenerateMocks([FileDialogController, ShellWin32Api]) +void main() { + const int defaultReturnValue = S_OK; + late final MockFileDialogController mockFileDialogController = + MockFileDialogController(); + late final MockShellWin32Api mockShellWin32Api = MockShellWin32Api(); + const DialogMode dialogMode = DialogMode.open; + final DialogWrapper dialogWrapper = DialogWrapper.withFakeDependencies( + mockFileDialogController, + dialogMode, + mockShellWin32Api, + ); + + setUp(() { + setDefaultMocks(mockFileDialogController, defaultReturnValue); + }); + + tearDown(() { + reset(mockFileDialogController); + reset(mockShellWin32Api); + }); + + test('setFileName should call dialog setFileName', () { + const String folderName = 'Documents'; + dialogWrapper.setFileName(folderName); + verify(mockFileDialogController.setFileName(folderName)).called(1); + }); + + test('setOkButtonLabel should call dialog setOkButtonLabel', () { + const String okButtonLabel = 'Confirm'; + dialogWrapper.setOkButtonLabel(okButtonLabel); + verify(mockFileDialogController.setOkButtonLabel(okButtonLabel)).called(1); + }); + + test('addOptions should call dialog getOptions and setOptions', () { + const int newOptions = FILEOPENDIALOGOPTIONS.FOS_NOREADONLYRETURN; + dialogWrapper.addOptions(newOptions); + verify(mockFileDialogController.getOptions(any)).called(1); + verify(mockFileDialogController.setOptions(newOptions)).called(1); + }); + + test('addOptions should not call setOptions if getOptions returns an error', + () { + const int options = FILEOPENDIALOGOPTIONS.FOS_NOREADONLYRETURN; + when(mockFileDialogController.getOptions(any)).thenReturn(E_FAIL); + dialogWrapper.addOptions(options); + verifyNever(mockFileDialogController.setOptions(any)); + }); + + test( + 'setFileTypeFilters should call setFileTypes with expected typeGroups count', + () { + final List typeGroups = [ + const XTypeGroup(extensions: ['jpg', 'png'], label: 'Images'), + const XTypeGroup(extensions: ['txt', 'json'], label: 'Text'), + ]; + dialogWrapper.setFileTypeFilters(typeGroups); + verify(mockFileDialogController.setFileTypes(typeGroups.length, any)) + .called(1); + }); + + test('setFileTypeFilters should call setFileTypes with Any by default', () { + const String expectedPszName = 'Any'; + const String expectedPszSpec = '*.*'; + final List typeGroups = []; + mockSetFileTypesConditions( + mockFileDialogController, + expectedPszName, + expectedPszSpec, + ); + dialogWrapper.setFileTypeFilters(typeGroups); + verify(mockFileDialogController.setFileTypes(1, any)).called(1); + expect(dialogWrapper.lastResult, S_OK); + }); + + test( + 'setFileTypeFilters should call setFileTypes with a label and default extensions', + () { + const String label = 'All files'; + const String expectedPszSpec = '*.*'; + final List typeGroups = [ + const XTypeGroup(label: label), + ]; + mockSetFileTypesConditions( + mockFileDialogController, + label, + expectedPszSpec, + ); + dialogWrapper.setFileTypeFilters(typeGroups); + verify(mockFileDialogController.setFileTypes(1, any)).called(1); + expect(dialogWrapper.lastResult, S_OK); + }); + + test( + 'setFileTypeFilters should call setFileTypes with both default label and extensions', + () { + const String defaultLabel = 'Any'; + const String expectedPszSpec = '*.*'; + final List typeGroups = [ + const XTypeGroup(), + ]; + mockSetFileTypesConditions( + mockFileDialogController, + defaultLabel, + expectedPszSpec, + ); + dialogWrapper.setFileTypeFilters(typeGroups); + verify(mockFileDialogController.setFileTypes(1, any)).called(1); + expect(dialogWrapper.lastResult, S_OK); + }); + + test( + 'setFileTypeFilters should call setFileTypes with specific labels and extensions', + () { + const String jpg = 'jpg'; + const String png = 'png'; + const String imageLabel = 'Image'; + const String txt = 'txt'; + const String json = 'json'; + const String textLabel = 'Text'; + final Map expectedfilterSpecification = { + imageLabel: '*.$jpg;*.$png', + textLabel: '*.$txt;*.$json', + }; + final List typeGroups = [ + const XTypeGroup(extensions: [jpg, png], label: imageLabel), + const XTypeGroup(extensions: [txt, json], label: textLabel), + ]; + when(mockFileDialogController.setFileTypes(any, any)) + .thenAnswer((Invocation realInvocation) { + final Pointer pointer = + realInvocation.positionalArguments[1] as Pointer; + + int index = 0; + for (final String key in expectedfilterSpecification.keys) { + if (pointer[index].pszName.toDartString() != key || + pointer[index].pszSpec.toDartString() != + expectedfilterSpecification[key]) { + return E_FAIL; + } + index += 1; + } + return S_OK; + }); + + dialogWrapper.setFileTypeFilters(typeGroups); + verify(mockFileDialogController.setFileTypes(typeGroups.length, any)) + .called(1); + expect(dialogWrapper.lastResult, S_OK); + }); + + test( + 'setFileTypeFilters should call setFileTypes with specific extensions and No label', + () { + const String jpg = 'jpg'; + const String png = 'png'; + const String txt = 'txt'; + const String json = 'json'; + final Map expectedfilterSpecification = { + '*.$jpg;*.$png': '*.$jpg;*.$png', + '*.$txt;*.$json': '*.$txt;*.$json', + }; + final List typeGroups = [ + const XTypeGroup(extensions: [jpg, png]), + const XTypeGroup(extensions: [txt, json]), + ]; + when(mockFileDialogController.setFileTypes(any, any)) + .thenAnswer((Invocation realInvocation) { + final Pointer pointer = + realInvocation.positionalArguments[1] as Pointer; + + int index = 0; + for (final String key in expectedfilterSpecification.keys) { + if (pointer[index].pszName.toDartString() != key || + pointer[index].pszSpec.toDartString() != + expectedfilterSpecification[key]) { + return E_FAIL; + } + index += 1; + } + return S_OK; + }); + + dialogWrapper.setFileTypeFilters(typeGroups); + verify(mockFileDialogController.setFileTypes(typeGroups.length, any)) + .called(1); + expect(dialogWrapper.lastResult, S_OK); + }); + + test('setFolder should not call dialog setFolder if the path is empty', () { + const String emptyPath = ''; + dialogWrapper.setFolder(emptyPath); + verifyNever(mockFileDialogController.setFolder(any)); + }); + + test('setFolder should call dialog setFolder with the provided path', () { + const String path = 'path/to/my/folder'; + when(mockShellWin32Api.createItemFromParsingName(any, any, any)) + .thenReturn(S_OK); + dialogWrapper.setFolder(path); + verify(mockFileDialogController.setFolder(any)).called(1); + }); + + test('setFolder should not call dialog setFolder if createItem fails', () { + const String path = 'path/to/my/folder'; + when(mockShellWin32Api.createItemFromParsingName(any, any, any)) + .thenReturn(E_FAIL); + dialogWrapper.setFolder(path); + verifyNever(mockFileDialogController.setFolder(any)); + }); + + test( + '[DialogMode == Open] show should return null if parent window is not available', + () { + const int parentWindow = 0; + when(mockFileDialogController.show(parentWindow)).thenReturn(E_FAIL); + + final List? result = dialogWrapper.show(parentWindow); + + expect(result, null); + verify(mockFileDialogController.show(parentWindow)).called(1); + verifyNever(mockFileDialogController.getResults(any)); + }); + + test( + "[DialogMode == Open] show should return null if can't get results from dialog", + () { + const int parentWindow = 0; + when(mockFileDialogController.show(parentWindow)).thenReturn(S_OK); + when(mockFileDialogController.getResults(any)).thenReturn(E_FAIL); + + final List? result = dialogWrapper.show(parentWindow); + + expect(result, null); + verify(mockFileDialogController.show(parentWindow)).called(1); + verify(mockFileDialogController.getResults(any)).called(1); + }); + + test( + "[DialogMode == Save] show should return null if can't get result from dialog", + () { + final DialogWrapper dialogWrapperModeSave = + DialogWrapper.withFakeDependencies( + mockFileDialogController, DialogMode.save, mockShellWin32Api); + const int parentWindow = 0; + when(mockFileDialogController.show(parentWindow)).thenReturn(S_OK); + when(mockFileDialogController.getResult(any)).thenReturn(E_FAIL); + + final List? result = dialogWrapperModeSave.show(parentWindow); + + expect(result, null); + verify(mockFileDialogController.show(parentWindow)).called(1); + verify(mockFileDialogController.getResult(any)).called(1); + }); + + test( + '[DialogMode == Save] show should return null if cannot getPathForShellItem', + () { + final DialogWrapper dialogWrapperModeSave = + DialogWrapper.withFakeDependencies( + mockFileDialogController, DialogMode.save, mockShellWin32Api); + const int parentWindow = 0; + when(mockFileDialogController.show(parentWindow)).thenReturn(S_OK); + when(mockFileDialogController.getResult(any)).thenReturn(S_OK); + when(mockShellWin32Api.getPathForShellItem(any)) + .thenThrow(WindowsException(E_FAIL)); + + final List? result = dialogWrapperModeSave.show(parentWindow); + + expect(result, null); + }); + + test('[DialogMode == Save] show should return the file path', () { + const String filePath = 'path/to/file.txt'; + final DialogWrapper dialogWrapperModeSave = + DialogWrapper.withFakeDependencies( + mockFileDialogController, DialogMode.save, mockShellWin32Api); + const int parentWindow = 0; + when(mockFileDialogController.show(parentWindow)).thenReturn(S_OK); + when(mockFileDialogController.getResult(any)).thenReturn(S_OK); + when(mockShellWin32Api.getPathForShellItem(any)).thenReturn(filePath); + when(mockShellWin32Api.releaseShellItem(any)).thenReturn(0); + + final List? result = dialogWrapperModeSave.show(parentWindow); + + expect(result?.first, filePath); + }); +} + +void mockSetFileTypesConditions( + MockFileDialogController mockFileDialogController, + String expectedPszName, + String expectedPszSpec) { + when(mockFileDialogController.setFileTypes(1, any)) + .thenAnswer((Invocation realInvocation) { + final Pointer pointer = + realInvocation.positionalArguments[1] as Pointer; + + return pointer[0].pszName.toDartString() == expectedPszName && + pointer[0].pszSpec.toDartString() == expectedPszSpec + ? S_OK + : E_FAIL; + }); +} + +void setDefaultMocks( + MockFileDialogController mockFileDialogController, int defaultReturnValue) { + when(mockFileDialogController.setOptions(any)).thenReturn(defaultReturnValue); + when(mockFileDialogController.getOptions(any)).thenReturn(defaultReturnValue); + when(mockFileDialogController.setOkButtonLabel(any)) + .thenReturn(defaultReturnValue); + when(mockFileDialogController.setFileName(any)) + .thenReturn(defaultReturnValue); + when(mockFileDialogController.setFileTypes(any, any)) + .thenReturn(defaultReturnValue); + when(mockFileDialogController.setFolder(any)).thenReturn(defaultReturnValue); + when(mockFileDialogController.release()).thenReturn(defaultReturnValue); +} diff --git a/packages/file_selector/file_selector_windows/test/file_selector_dart/dialog_wrapper_test.mocks.dart b/packages/file_selector/file_selector_windows/test/file_selector_dart/dialog_wrapper_test.mocks.dart new file mode 100644 index 000000000000..c40a85829ebf --- /dev/null +++ b/packages/file_selector/file_selector_windows/test/file_selector_dart/dialog_wrapper_test.mocks.dart @@ -0,0 +1,168 @@ +// Mocks generated by Mockito 5.3.2 from annotations +// in file_selector_windows/example/windows/flutter/ephemeral/.plugin_symlinks/file_selector_windows/test/file_selector_dart/dialog_wrapper_test.dart. +// Do not manually edit this file. + +// ignore_for_file: no_leading_underscores_for_library_prefixes +import 'dart:ffi' as _i3; + +import 'package:ffi/ffi.dart' as _i6; +import 'package:file_selector_windows/src/file_selector_dart/file_dialog_controller.dart' + as _i2; +import 'package:file_selector_windows/src/file_selector_dart/shell_win32_api.dart' + as _i5; +import 'package:mockito/mockito.dart' as _i1; +import 'package:win32/win32.dart' as _i4; + +// ignore_for_file: type=lint +// ignore_for_file: avoid_redundant_argument_values +// ignore_for_file: avoid_setters_without_getters +// ignore_for_file: comment_references +// ignore_for_file: implementation_imports +// ignore_for_file: invalid_use_of_visible_for_testing_member +// ignore_for_file: prefer_const_constructors +// ignore_for_file: unnecessary_parenthesis +// ignore_for_file: camel_case_types +// ignore_for_file: subtype_of_sealed_class + +/// A class which mocks [FileDialogController]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockFileDialogController extends _i1.Mock + implements _i2.FileDialogController { + MockFileDialogController() { + _i1.throwOnMissingStub(this); + } + + @override + int setFolder(_i3.Pointer<_i4.COMObject>? path) => (super.noSuchMethod( + Invocation.method( + #setFolder, + [path], + ), + returnValue: 0, + ) as int); + @override + int setFileName(String? name) => (super.noSuchMethod( + Invocation.method( + #setFileName, + [name], + ), + returnValue: 0, + ) as int); + @override + int setFileTypes( + int? count, + _i3.Pointer<_i4.COMDLG_FILTERSPEC>? filters, + ) => + (super.noSuchMethod( + Invocation.method( + #setFileTypes, + [ + count, + filters, + ], + ), + returnValue: 0, + ) as int); + @override + int setOkButtonLabel(String? text) => (super.noSuchMethod( + Invocation.method( + #setOkButtonLabel, + [text], + ), + returnValue: 0, + ) as int); + @override + int getOptions(_i3.Pointer<_i3.Uint32>? outOptions) => (super.noSuchMethod( + Invocation.method( + #getOptions, + [outOptions], + ), + returnValue: 0, + ) as int); + @override + int setOptions(int? options) => (super.noSuchMethod( + Invocation.method( + #setOptions, + [options], + ), + returnValue: 0, + ) as int); + @override + int show(int? parent) => (super.noSuchMethod( + Invocation.method( + #show, + [parent], + ), + returnValue: 0, + ) as int); + @override + int getResult(_i3.Pointer<_i3.Pointer<_i4.COMObject>>? outItem) => + (super.noSuchMethod( + Invocation.method( + #getResult, + [outItem], + ), + returnValue: 0, + ) as int); + @override + int getResults(_i3.Pointer<_i3.Pointer<_i4.COMObject>>? outItems) => + (super.noSuchMethod( + Invocation.method( + #getResults, + [outItems], + ), + returnValue: 0, + ) as int); + @override + int release() => (super.noSuchMethod( + Invocation.method( + #release, + [], + ), + returnValue: 0, + ) as int); +} + +/// A class which mocks [ShellWin32Api]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockShellWin32Api extends _i1.Mock implements _i5.ShellWin32Api { + MockShellWin32Api() { + _i1.throwOnMissingStub(this); + } + + @override + int createItemFromParsingName( + _i3.Pointer<_i6.Utf16>? pszPath, + _i3.Pointer<_i4.GUID>? ptrGuid, + _i3.Pointer<_i3.Pointer<_i3.NativeType>>? ptrPath, + ) => + (super.noSuchMethod( + Invocation.method( + #createItemFromParsingName, + [ + pszPath, + ptrGuid, + ptrPath, + ], + ), + returnValue: 0, + ) as int); + @override + String getPathForShellItem(_i4.IShellItem? shellItem) => (super.noSuchMethod( + Invocation.method( + #getPathForShellItem, + [shellItem], + ), + returnValue: '', + ) as String); + @override + int releaseShellItem(_i4.IShellItem? shellItem) => (super.noSuchMethod( + Invocation.method( + #releaseShellItem, + [shellItem], + ), + returnValue: 0, + ) as int); +} diff --git a/packages/file_selector/file_selector_windows/test/file_selector_dart/fake_file_dialog.dart b/packages/file_selector/file_selector_windows/test/file_selector_dart/fake_file_dialog.dart new file mode 100644 index 000000000000..809c192ecfd6 --- /dev/null +++ b/packages/file_selector/file_selector_windows/test/file_selector_dart/fake_file_dialog.dart @@ -0,0 +1,112 @@ +// 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 'dart:ffi'; + +import 'package:ffi/ffi.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:win32/win32.dart'; + +// Fake IFileDialog class for testing purposes. +class FakeIFileDialog extends Fake implements IFileDialog { + int _getOptionsCalledTimes = 0; + int _getResultCalledTimes = 0; + int _setOptionsCalledTimes = 0; + int _setFolderCalledTimes = 0; + int _setFileNameCalledTimes = 0; + int _setFileTypesCalledTimes = 0; + int _setOkButtonLabelCalledTimes = 0; + int _showCalledTimes = 0; + + @override + int getOptions(Pointer pfos) { + _getOptionsCalledTimes += 1; + return S_OK; + } + + @override + int setOptions(int options) { + _setOptionsCalledTimes += 1; + return S_OK; + } + + @override + int getResult(Pointer> ppsi) { + _getResultCalledTimes += 1; + return S_OK; + } + + @override + int setFolder(Pointer psi) { + _setFolderCalledTimes += 1; + return S_OK; + } + + @override + int setFileTypes(int cFileTypes, Pointer rgFilterSpec) { + _setFileTypesCalledTimes += 1; + return S_OK; + } + + @override + int setFileName(Pointer pszName) { + _setFileNameCalledTimes += 1; + return S_OK; + } + + @override + int setOkButtonLabel(Pointer pszText) { + _setOkButtonLabelCalledTimes += 1; + return S_OK; + } + + @override + int show(int hwndOwner) { + _showCalledTimes += 1; + return S_OK; + } + + void resetCounters() { + _getOptionsCalledTimes = 0; + _getResultCalledTimes = 0; + _setOptionsCalledTimes = 0; + _setFolderCalledTimes = 0; + _setFileTypesCalledTimes = 0; + _setOkButtonLabelCalledTimes = 0; + _showCalledTimes = 0; + _setFileNameCalledTimes = 0; + } + + int getOptionsCalledTimes() { + return _getOptionsCalledTimes; + } + + int setOptionsCalledTimes() { + return _setOptionsCalledTimes; + } + + int getResultCalledTimes() { + return _getResultCalledTimes; + } + + int setFolderCalledTimes() { + return _setFolderCalledTimes; + } + + int setFileNameCalledTimes() { + return _setFileNameCalledTimes; + } + + int setFileTypesCalledTimes() { + return _setFileTypesCalledTimes; + } + + int setOkButtonLabelCalledTimes() { + return _setOkButtonLabelCalledTimes; + } + + int showCalledTimes() { + return _showCalledTimes; + } +} diff --git a/packages/file_selector/file_selector_windows/test/file_selector_dart/fake_ifile_open_dialog.dart b/packages/file_selector/file_selector_windows/test/file_selector_dart/fake_ifile_open_dialog.dart new file mode 100644 index 000000000000..1497d066c1bf --- /dev/null +++ b/packages/file_selector/file_selector_windows/test/file_selector_dart/fake_ifile_open_dialog.dart @@ -0,0 +1,47 @@ +// 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 'dart:ffi'; + +import 'package:flutter_test/flutter_test.dart'; +import 'package:win32/win32.dart'; + +// Fake IFileOpenDialog class for testing purposes. +class FakeIFileOpenDialog extends Fake implements IFileOpenDialog { + int _getResultsCalledTimes = 0; + int _getReleaseCalledTimes = 0; + bool _shouldFail = false; + + @override + Pointer get ptr => nullptr; + + @override + int release() { + _getReleaseCalledTimes += 1; + return S_OK; + } + + @override + int getResults(Pointer> ppsi) { + _getResultsCalledTimes += 1; + if (_shouldFail) { + throw WindowsException(E_FAIL); + } + + return S_OK; + } + + void resetCounters() { + _getResultsCalledTimes = 0; + _getReleaseCalledTimes = 0; + } + + int getResultsCalledTimes() => _getResultsCalledTimes; + + int getReleaseCalledTimes() => _getReleaseCalledTimes; + + void mockFailure() => _shouldFail = true; + + void mockSuccess() => _shouldFail = false; +} diff --git a/packages/file_selector/file_selector_windows/test/file_selector_dart/fake_ifile_open_dialog_factory.dart b/packages/file_selector/file_selector_windows/test/file_selector_dart/fake_ifile_open_dialog_factory.dart new file mode 100644 index 000000000000..5aba9e243fbd --- /dev/null +++ b/packages/file_selector/file_selector_windows/test/file_selector_dart/fake_ifile_open_dialog_factory.dart @@ -0,0 +1,40 @@ +// 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_windows/src/file_selector_dart/ifile_open_dialog_factory.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:win32/win32.dart'; + +import 'fake_ifile_open_dialog.dart'; + +// Fake FakeIFileOpenDialogFactory class for testing purposes. +class FakeIFileOpenDialogFactory extends Fake + implements IFileOpenDialogFactory { + int _fromCalledTimes = 0; + bool _shouldFail = false; + + final FakeIFileOpenDialog fakeIFileOpenDialog = FakeIFileOpenDialog(); + + @override + IFileOpenDialog from(IFileDialog dialog) { + _fromCalledTimes += 1; + if (_shouldFail) { + throw WindowsException(E_NOINTERFACE); + } + + return fakeIFileOpenDialog; + } + + int getFromCalledTimes() { + return _fromCalledTimes; + } + + void mockSuccess() { + _shouldFail = false; + } + + void mockFailure() { + _shouldFail = true; + } +} diff --git a/packages/file_selector/file_selector_windows/test/file_selector_dart/file_dialog_controller_factory_test.dart b/packages/file_selector/file_selector_windows/test/file_selector_dart/file_dialog_controller_factory_test.dart new file mode 100644 index 000000000000..da0f8b09a9a6 --- /dev/null +++ b/packages/file_selector/file_selector_windows/test/file_selector_dart/file_dialog_controller_factory_test.dart @@ -0,0 +1,23 @@ +// 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_windows/src/file_selector_dart/file_dialog_controller.dart'; +import 'package:file_selector_windows/src/file_selector_dart/file_dialog_controller_factory.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:win32/win32.dart'; + +import 'fake_file_dialog.dart'; + +void main() { + final FileDialogControllerFactory fileDialogControllerFactory = + FileDialogControllerFactory(); + final IFileDialog dialog = FakeIFileDialog(); + + test('createController should return a FileDialogController', () { + expect( + fileDialogControllerFactory.createController(dialog), + isA(), + ); + }); +} diff --git a/packages/file_selector/file_selector_windows/test/file_selector_dart/file_dialog_controller_test.dart b/packages/file_selector/file_selector_windows/test/file_selector_dart/file_dialog_controller_test.dart new file mode 100644 index 000000000000..9d391cdd7656 --- /dev/null +++ b/packages/file_selector/file_selector_windows/test/file_selector_dart/file_dialog_controller_test.dart @@ -0,0 +1,134 @@ +// 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 'dart:ffi'; + +import 'package:ffi/ffi.dart'; +import 'package:file_selector_windows/src/file_selector_dart/file_dialog_controller.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:win32/win32.dart'; + +import 'fake_file_dialog.dart'; +import 'fake_ifile_open_dialog_factory.dart'; + +void main() { + final FakeIFileDialog fakeFileOpenDialog = FakeIFileDialog(); + final FakeIFileOpenDialogFactory fakeIFileOpenDialogFactory = + FakeIFileOpenDialogFactory(); + final FileDialogController fileDialogController = + FileDialogController(fakeFileOpenDialog, fakeIFileOpenDialogFactory); + + setUp(() { + fakeIFileOpenDialogFactory.mockSuccess(); + fakeIFileOpenDialogFactory.fakeIFileOpenDialog.mockSuccess(); + }); + + tearDown(() { + fakeFileOpenDialog.resetCounters(); + fakeIFileOpenDialogFactory.fakeIFileOpenDialog.resetCounters(); + }); + + test('setFolder should call dialog setFolder', () { + final Pointer ptrFolder = calloc(); + fileDialogController.setFolder(ptrFolder); + free(ptrFolder); + expect(fakeFileOpenDialog.setFolderCalledTimes(), 1); + }); + + test('setFileName should call dialog setFileName', () { + fileDialogController.setFileName('fileName'); + expect(fakeFileOpenDialog.setFileNameCalledTimes(), 1); + }); + + test('setFileTypes should call dialog setFileTypes', () { + final Pointer ptrFilters = calloc(); + fileDialogController.setFileTypes(1, ptrFilters); + free(ptrFilters); + expect(fakeFileOpenDialog.setFileTypesCalledTimes(), 1); + }); + + test('setOkButtonLabel should call dialog setOkButtonLabel', () { + fileDialogController.setOkButtonLabel('button'); + expect(fakeFileOpenDialog.setOkButtonLabelCalledTimes(), 1); + }); + + test('show should call dialog show', () { + fileDialogController.show(0); + expect(fakeFileOpenDialog.showCalledTimes(), 1); + }); + + test('getOptions should call dialog getOptions', () { + final Pointer ptrOptions = calloc(); + fileDialogController.getOptions(ptrOptions); + free(ptrOptions); + expect(fakeFileOpenDialog.getOptionsCalledTimes(), 1); + }); + + test('setOptions should call dialog setOptions', () { + fileDialogController.setOptions(32); + expect(fakeFileOpenDialog.setOptionsCalledTimes(), 1); + }); + + test('getResult should call dialog getResult', () { + final Pointer> ptrCOMObject = + calloc>(); + fileDialogController.getResult(ptrCOMObject); + free(ptrCOMObject); + expect(fakeFileOpenDialog.getResultCalledTimes(), 1); + }); + + test('getResults should call the from method of the factory', () { + final Pointer> ptrCOMObject = + calloc>(); + fileDialogController.getResults(ptrCOMObject); + free(ptrCOMObject); + expect(fakeIFileOpenDialogFactory.getFromCalledTimes(), 1); + }); + + test('getResults should call dialog getResults', () { + final Pointer> ptrCOMObject = + calloc>(); + fileDialogController.getResults(ptrCOMObject); + free(ptrCOMObject); + expect( + fakeIFileOpenDialogFactory.fakeIFileOpenDialog.getResultsCalledTimes(), + 1, + ); + }); + + test( + 'getResults should return an error when building a file open dialog throws', + () { + final Pointer> ptrCOMObject = + calloc>(); + fakeIFileOpenDialogFactory.mockFailure(); + free(ptrCOMObject); + expect(fileDialogController.getResults(ptrCOMObject), E_FAIL); + }); + + test( + 'getResults should return an error and release the dialog when getting results throws', + () { + final Pointer> ptrCOMObject = + calloc>(); + fakeIFileOpenDialogFactory.fakeIFileOpenDialog.mockFailure(); + free(ptrCOMObject); + expect(fileDialogController.getResults(ptrCOMObject), E_FAIL); + expect( + fakeIFileOpenDialogFactory.fakeIFileOpenDialog.getReleaseCalledTimes(), + 1, + ); + }); + + test('getResults should call dialog release', () { + final Pointer> ptrCOMObject = + calloc>(); + fileDialogController.getResults(ptrCOMObject); + free(ptrCOMObject); + expect( + fakeIFileOpenDialogFactory.fakeIFileOpenDialog.getReleaseCalledTimes(), + 1, + ); + }); +} 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 f07c9b67618d..ad6e23739f73 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 @@ -4,31 +4,29 @@ import 'package:file_selector_platform_interface/file_selector_platform_interface.dart'; import 'package:file_selector_windows/file_selector_windows.dart'; -import 'package:file_selector_windows/src/messages.g.dart'; +import 'package:file_selector_windows/src/file_selector_api.dart'; +import 'package:file_selector_windows/src/file_selector_dart/selection_options.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mockito/annotations.dart'; import 'package:mockito/mockito.dart'; +import './file_selector_windows_test.mocks.dart'; -import 'file_selector_windows_test.mocks.dart'; -import 'test_api.dart'; - -@GenerateMocks([TestFileSelectorApi]) +@GenerateMocks([FileSelectorApi]) void main() { TestWidgetsFlutterBinding.ensureInitialized(); - final FileSelectorWindows plugin = FileSelectorWindows(); - late MockTestFileSelectorApi mockApi; + final MockFileSelectorApi mockApi = MockFileSelectorApi(); + final FileSelectorWindows plugin = FileSelectorWindows.useFakeApi(mockApi); - setUp(() { - mockApi = MockTestFileSelectorApi(); - TestFileSelectorApi.setup(mockApi); + tearDown(() { + reset(mockApi); }); test('registered instance', () { FileSelectorWindows.registerWith(); expect(FileSelectorPlatform.instance, isA()); - }); + }, testOn: 'windows'); group('#openFile', () { setUp(() { @@ -46,7 +44,7 @@ void main() { expect(options.selectFolders, false); }); - test('passes the accepted type groups correctly', () async { + test('passes the accepted xtype groups correctly', () async { const XTypeGroup group = XTypeGroup( label: 'text', extensions: ['txt'], @@ -66,9 +64,9 @@ void main() { verify(mockApi.showOpenDialog(captureAny, null, null)); final SelectionOptions options = result.captured[0] as SelectionOptions; expect( - _typeGroupListsMatch(options.allowedTypes, [ - TypeGroup(label: 'text', extensions: ['txt']), - TypeGroup(label: 'image', extensions: ['jpg']), + _xTypeGroupListsMatch(options.allowedTypes, const [ + XTypeGroup(label: 'text', extensions: ['txt']), + XTypeGroup(label: 'image', extensions: ['jpg']), ]), true); }); @@ -85,7 +83,7 @@ void main() { verify(mockApi.showOpenDialog(any, null, 'Open File')); }); - test('throws for a type group that does not support Windows', () async { + test('throws for a xtype group that does not support Windows', () async { const XTypeGroup group = XTypeGroup( label: 'text', mimeTypes: ['text/plain'], @@ -124,7 +122,7 @@ void main() { expect(options.selectFolders, false); }); - test('passes the accepted type groups correctly', () async { + test('passes the accepted xtype groups correctly', () async { const XTypeGroup group = XTypeGroup( label: 'text', extensions: ['txt'], @@ -144,9 +142,9 @@ void main() { verify(mockApi.showOpenDialog(captureAny, null, null)); final SelectionOptions options = result.captured[0] as SelectionOptions; expect( - _typeGroupListsMatch(options.allowedTypes, [ - TypeGroup(label: 'text', extensions: ['txt']), - TypeGroup(label: 'image', extensions: ['jpg']), + _xTypeGroupListsMatch(options.allowedTypes, const [ + XTypeGroup(label: 'text', extensions: ['txt']), + XTypeGroup(label: 'image', extensions: ['jpg']), ]), true); }); @@ -163,7 +161,7 @@ void main() { verify(mockApi.showOpenDialog(any, null, 'Open Files')); }); - test('throws for a type group that does not support Windows', () async { + test('throws for a xtype group that does not support Windows', () async { const XTypeGroup group = XTypeGroup( label: 'text', mimeTypes: ['text/plain'], @@ -230,7 +228,7 @@ void main() { expect(options.selectFolders, false); }); - test('passes the accepted type groups correctly', () async { + test('passes the accepted xtype groups correctly', () async { const XTypeGroup group = XTypeGroup( label: 'text', extensions: ['txt'], @@ -251,9 +249,9 @@ void main() { 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']), + _xTypeGroupListsMatch(options.allowedTypes, const [ + XTypeGroup(label: 'text', extensions: ['txt']), + XTypeGroup(label: 'image', extensions: ['jpg']), ]), true); }); @@ -276,7 +274,7 @@ void main() { verify(mockApi.showSaveDialog(any, null, null, 'Save File')); }); - test('throws for a type group that does not support Windows', () async { + test('throws for a xtype group that does not support Windows', () async { const XTypeGroup group = XTypeGroup( label: 'text', mimeTypes: ['text/plain'], @@ -300,25 +298,19 @@ void main() { } // True if the given options match. -// -// This is needed because Pigeon data classes don't have custom equality checks, -// so only match for identical instances. -bool _typeGroupListsMatch(List a, List b) { +bool _xTypeGroupListsMatch(List a, List b) { if (a.length != b.length) { return false; } - for (int i = 0; i < a.length; i++) { - if (!_typeGroupsMatch(a[i], b[i])) { + for (int i = 0; i < a.length; i += 1) { + if (!_xTypeGroupsMatch(a[i], b[i])) { return false; } } return true; } -// True if the given type groups match. -// -// This is needed because Pigeon data classes don't have custom equality checks, -// so only match for identical instances. -bool _typeGroupsMatch(TypeGroup? a, TypeGroup? b) { +// True if the given xtype groups match. +bool _xTypeGroupsMatch(XTypeGroup? a, XTypeGroup? b) { return a!.label == b!.label && listEquals(a.extensions, b.extensions); } 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 61e17fcdfeaa..2264016ea25e 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,13 @@ -// Mocks generated by Mockito 5.2.0 from annotations +// Mocks generated by Mockito 5.3.2 from annotations // in file_selector_windows/example/windows/flutter/ephemeral/.plugin_symlinks/file_selector_windows/test/file_selector_windows_test.dart. // Do not manually edit this file. -import 'package:file_selector_windows/src/messages.g.dart' as _i3; +// ignore_for_file: no_leading_underscores_for_library_prefixes +import 'package:file_selector_windows/src/file_selector_api.dart' as _i2; +import 'package:file_selector_windows/src/file_selector_dart/selection_options.dart' + as _i3; import 'package:mockito/mockito.dart' as _i1; -import 'test_api.dart' as _i2; - // ignore_for_file: type=lint // ignore_for_file: avoid_redundant_argument_values // ignore_for_file: avoid_setters_without_getters @@ -16,31 +17,50 @@ import 'test_api.dart' as _i2; // ignore_for_file: prefer_const_constructors // ignore_for_file: unnecessary_parenthesis // ignore_for_file: camel_case_types +// ignore_for_file: subtype_of_sealed_class -/// A class which mocks [TestFileSelectorApi]. +/// A class which mocks [FileSelectorApi]. /// /// See the documentation for Mockito's code generation for more information. -class MockTestFileSelectorApi extends _i1.Mock - implements _i2.TestFileSelectorApi { - MockTestFileSelectorApi() { +class MockFileSelectorApi extends _i1.Mock implements _i2.FileSelectorApi { + MockFileSelectorApi() { _i1.throwOnMissingStub(this); } @override - List showOpenDialog(_i3.SelectionOptions? options, - String? initialDirectory, String? confirmButtonText) => + List showOpenDialog( + _i3.SelectionOptions? options, + String? initialDirectory, + String? confirmButtonText, + ) => (super.noSuchMethod( - Invocation.method( - #showOpenDialog, [options, initialDirectory, confirmButtonText]), - returnValue: []) as List); + Invocation.method( + #showOpenDialog, + [ + options, + initialDirectory, + confirmButtonText, + ], + ), + returnValue: [], + ) as List); @override List showSaveDialog( - _i3.SelectionOptions? options, - String? initialDirectory, - String? suggestedName, - String? confirmButtonText) => + _i3.SelectionOptions? options, + String? initialDirectory, + String? suggestedName, + String? confirmButtonText, + ) => (super.noSuchMethod( - Invocation.method(#showSaveDialog, - [options, initialDirectory, suggestedName, confirmButtonText]), - returnValue: []) as List); + Invocation.method( + #showSaveDialog, + [ + options, + initialDirectory, + suggestedName, + confirmButtonText, + ], + ), + returnValue: [], + ) as List); } diff --git a/packages/file_selector/file_selector_windows/test/test_api.dart b/packages/file_selector/file_selector_windows/test/test_api.dart deleted file mode 100644 index f9b979f7b854..000000000000 --- a/packages/file_selector/file_selector_windows/test/test_api.dart +++ /dev/null @@ -1,105 +0,0 @@ -// 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 (v3.2.5), 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 -import 'dart:async'; -import 'dart:typed_data' show Uint8List, Int32List, Int64List, Float64List; -import 'package:flutter/foundation.dart' show WriteBuffer, ReadBuffer; -import 'package:flutter/services.dart'; -import 'package:flutter_test/flutter_test.dart'; - -// ignore: directives_ordering -import 'package:file_selector_windows/src/messages.g.dart'; - -class _TestFileSelectorApiCodec extends StandardMessageCodec { - const _TestFileSelectorApiCodec(); - @override - void writeValue(WriteBuffer buffer, Object? value) { - if (value is SelectionOptions) { - buffer.putUint8(128); - writeValue(buffer, value.encode()); - } else if (value is TypeGroup) { - buffer.putUint8(129); - writeValue(buffer, value.encode()); - } else { - super.writeValue(buffer, value); - } - } - - @override - Object? readValueOfType(int type, ReadBuffer buffer) { - switch (type) { - case 128: - return SelectionOptions.decode(readValue(buffer)!); - - case 129: - return TypeGroup.decode(readValue(buffer)!); - - default: - return super.readValueOfType(type, buffer); - } - } -} - -abstract class TestFileSelectorApi { - static const MessageCodec codec = _TestFileSelectorApiCodec(); - - List showOpenDialog(SelectionOptions options, - String? initialDirectory, String? confirmButtonText); - List showSaveDialog( - SelectionOptions options, - String? initialDirectory, - String? suggestedName, - String? confirmButtonText); - static void setup(TestFileSelectorApi? api, - {BinaryMessenger? binaryMessenger}) { - { - final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.FileSelectorApi.showOpenDialog', codec, - binaryMessenger: binaryMessenger); - if (api == null) { - channel.setMockMessageHandler(null); - } else { - channel.setMockMessageHandler((Object? message) async { - assert(message != null, - 'Argument for dev.flutter.pigeon.FileSelectorApi.showOpenDialog was null.'); - final List args = (message as List?)!; - final SelectionOptions? arg_options = (args[0] as SelectionOptions?); - assert(arg_options != null, - '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( - arg_options!, arg_initialDirectory, arg_confirmButtonText); - return {'result': output}; - }); - } - } - { - final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.FileSelectorApi.showSaveDialog', codec, - binaryMessenger: binaryMessenger); - if (api == null) { - channel.setMockMessageHandler(null); - } else { - channel.setMockMessageHandler((Object? message) async { - assert(message != null, - 'Argument for dev.flutter.pigeon.FileSelectorApi.showSaveDialog was null.'); - final List args = (message as List?)!; - final SelectionOptions? arg_options = (args[0] as SelectionOptions?); - assert(arg_options != null, - 'Argument for dev.flutter.pigeon.FileSelectorApi.showSaveDialog was null, expected non-null SelectionOptions.'); - 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!, - arg_initialDirectory, arg_suggestedName, arg_confirmButtonText); - return {'result': output}; - }); - } - } - } -} diff --git a/packages/file_selector/file_selector_windows/windows/.gitignore b/packages/file_selector/file_selector_windows/windows/.gitignore deleted file mode 100644 index b3eb2be169a5..000000000000 --- a/packages/file_selector/file_selector_windows/windows/.gitignore +++ /dev/null @@ -1,17 +0,0 @@ -flutter/ - -# Visual Studio user-specific files. -*.suo -*.user -*.userosscache -*.sln.docstates - -# Visual Studio build-related files. -x64/ -x86/ - -# Visual Studio cache files -# files ending in .cache can be ignored -*.[Cc]ache -# but keep track of directories ending in .cache -!*.[Cc]ache/ diff --git a/packages/file_selector/file_selector_windows/windows/CMakeLists.txt b/packages/file_selector/file_selector_windows/windows/CMakeLists.txt deleted file mode 100644 index e06f3749e0f7..000000000000 --- a/packages/file_selector/file_selector_windows/windows/CMakeLists.txt +++ /dev/null @@ -1,86 +0,0 @@ -cmake_minimum_required(VERSION 3.14) -set(PROJECT_NAME "file_selector_windows") -project(${PROJECT_NAME} LANGUAGES CXX) - -set(PLUGIN_NAME "${PROJECT_NAME}_plugin") - -list(APPEND PLUGIN_SOURCES - "file_dialog_controller.cpp" - "file_dialog_controller.h" - "file_selector_plugin.cpp" - "file_selector_plugin.h" - "messages.g.cpp" - "messages.g.h" - "string_utils.cpp" - "string_utils.h" -) - -add_library(${PLUGIN_NAME} SHARED - "file_selector_windows.cpp" - "include/file_selector_windows/file_selector_windows.h" - ${PLUGIN_SOURCES} -) -apply_standard_settings(${PLUGIN_NAME}) -set_target_properties(${PLUGIN_NAME} PROPERTIES CXX_VISIBILITY_PRESET hidden) -target_compile_definitions(${PLUGIN_NAME} PRIVATE FLUTTER_PLUGIN_IMPL) -target_include_directories(${PLUGIN_NAME} INTERFACE - "${CMAKE_CURRENT_SOURCE_DIR}/include") -target_link_libraries(${PLUGIN_NAME} PRIVATE flutter flutter_wrapper_plugin) -# Override apply_standard_settings for exceptions due to -# https://developercommunity.visualstudio.com/t/stdany-doesnt-link-when-exceptions-are-disabled/376072 -target_compile_definitions(${PLUGIN_NAME} PRIVATE "_HAS_EXCEPTIONS=1") - -# List of absolute paths to libraries that should be bundled with the plugin -set(file_selector_bundled_libraries - "" - PARENT_SCOPE -) - - -# === Tests === - -if (${include_${PROJECT_NAME}_tests}) -set(TEST_RUNNER "${PROJECT_NAME}_test") -enable_testing() -# TODO(stuartmorgan): Consider using a single shared, pre-checked-in googletest -# instance rather than downloading for each plugin. This approach makes sense -# for a template, but not for a monorepo with many plugins. -include(FetchContent) -FetchContent_Declare( - googletest - URL https://github.com/google/googletest/archive/release-1.11.0.zip -) -# Prevent overriding the parent project's compiler/linker settings -set(gtest_force_shared_crt ON CACHE BOOL "" FORCE) -# Disable install commands for gtest so it doesn't end up in the bundle. -set(INSTALL_GTEST OFF CACHE BOOL "Disable installation of googletest" FORCE) - -FetchContent_MakeAvailable(googletest) - -# The plugin's C API is not very useful for unit testing, so build the sources -# directly into the test binary rather than using the DLL. -add_executable(${TEST_RUNNER} - test/file_selector_plugin_test.cpp - test/test_main.cpp - test/test_file_dialog_controller.cpp - test/test_file_dialog_controller.h - test/test_utils.cpp - test/test_utils.h - ${PLUGIN_SOURCES} -) -apply_standard_settings(${TEST_RUNNER}) -target_include_directories(${TEST_RUNNER} PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}") -target_link_libraries(${TEST_RUNNER} PRIVATE flutter_wrapper_plugin) -target_link_libraries(${TEST_RUNNER} PRIVATE gtest gmock) -# Override apply_standard_settings for exceptions due to -# https://developercommunity.visualstudio.com/t/stdany-doesnt-link-when-exceptions-are-disabled/376072 -target_compile_definitions(${TEST_RUNNER} PRIVATE "_HAS_EXCEPTIONS=1") -# flutter_wrapper_plugin has link dependencies on the Flutter DLL. -add_custom_command(TARGET ${TEST_RUNNER} POST_BUILD - COMMAND ${CMAKE_COMMAND} -E copy_if_different - "${FLUTTER_LIBRARY}" $ -) - -include(GoogleTest) -gtest_discover_tests(${TEST_RUNNER}) -endif() 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 deleted file mode 100644 index 5820c4a5da40..000000000000 --- a/packages/file_selector/file_selector_windows/windows/file_dialog_controller.cpp +++ /dev/null @@ -1,66 +0,0 @@ -// 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. - -#include "file_dialog_controller.h" - -#include -#include -#include - -_COM_SMARTPTR_TYPEDEF(IFileOpenDialog, IID_IFileOpenDialog); - -namespace file_selector_windows { - -FileDialogController::FileDialogController(IFileDialog* dialog) - : dialog_(dialog) {} - -FileDialogController::~FileDialogController() {} - -HRESULT FileDialogController::SetFolder(IShellItem* folder) { - return dialog_->SetFolder(folder); -} - -HRESULT FileDialogController::SetFileName(const wchar_t* name) { - return dialog_->SetFileName(name); -} - -HRESULT FileDialogController::SetFileTypes(UINT count, - COMDLG_FILTERSPEC* filters) { - return dialog_->SetFileTypes(count, filters); -} - -HRESULT FileDialogController::SetOkButtonLabel(const wchar_t* text) { - return dialog_->SetOkButtonLabel(text); -} - -HRESULT FileDialogController::GetOptions( - FILEOPENDIALOGOPTIONS* out_options) const { - return dialog_->GetOptions(out_options); -} - -HRESULT FileDialogController::SetOptions(FILEOPENDIALOGOPTIONS options) { - return dialog_->SetOptions(options); -} - -HRESULT FileDialogController::Show(HWND parent) { - return dialog_->Show(parent); -} - -HRESULT FileDialogController::GetResult(IShellItem** out_item) const { - return dialog_->GetResult(out_item); -} - -HRESULT FileDialogController::GetResults(IShellItemArray** out_items) const { - IFileOpenDialogPtr open_dialog; - HRESULT result = dialog_->QueryInterface(IID_PPV_ARGS(&open_dialog)); - if (!SUCCEEDED(result)) { - return result; - } - result = open_dialog->GetResults(out_items); - return result; -} - -FileDialogControllerFactory::~FileDialogControllerFactory() {} - -} // namespace file_selector_windows 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 deleted file mode 100644 index f5c93974cbe9..000000000000 --- a/packages/file_selector/file_selector_windows/windows/file_dialog_controller.h +++ /dev/null @@ -1,62 +0,0 @@ -// 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. -#ifndef PACKAGES_FILE_SELECTOR_FILE_SELECTOR_WINDOWS_WINDOWS_FILE_DIALOG_CONTROLLER_H_ -#define PACKAGES_FILE_SELECTOR_FILE_SELECTOR_WINDOWS_WINDOWS_FILE_DIALOG_CONTROLLER_H_ - -#include -#include -#include -#include - -#include - -_COM_SMARTPTR_TYPEDEF(IFileDialog, IID_IFileDialog); - -namespace file_selector_windows { - -// A thin wrapper for IFileDialog to allow for faking and inspection in tests. -// -// Since this class defines the end of what can be unit tested, it should -// contain as little logic as possible. -class FileDialogController { - public: - // Creates a controller managing |dialog|. - FileDialogController(IFileDialog* dialog); - virtual ~FileDialogController(); - - // Disallow copy and assign. - FileDialogController(const FileDialogController&) = delete; - FileDialogController& operator=(const FileDialogController&) = delete; - - // IFileDialog wrappers: - virtual HRESULT SetFolder(IShellItem* folder); - virtual HRESULT SetFileName(const wchar_t* name); - virtual HRESULT SetFileTypes(UINT count, COMDLG_FILTERSPEC* filters); - virtual HRESULT SetOkButtonLabel(const wchar_t* text); - virtual HRESULT GetOptions(FILEOPENDIALOGOPTIONS* out_options) const; - virtual HRESULT SetOptions(FILEOPENDIALOGOPTIONS options); - virtual HRESULT Show(HWND parent); - virtual HRESULT GetResult(IShellItem** out_item) const; - - // IFileOpenDialog wrapper. This will fail if the IFileDialog* provided to the - // constructor was not an IFileOpenDialog instance. - virtual HRESULT GetResults(IShellItemArray** out_items) const; - - private: - IFileDialogPtr dialog_ = nullptr; -}; - -// Interface for creating FileDialogControllers, to allow for dependency -// injection. -class FileDialogControllerFactory { - public: - virtual ~FileDialogControllerFactory(); - - virtual std::unique_ptr CreateController( - IFileDialog* dialog) const = 0; -}; - -} // namespace file_selector_windows - -#endif // PACKAGES_FILE_SELECTOR_FILE_SELECTOR_WINDOWS_WINDOWS_FILE_DIALOG_CONTROLLER_H_ 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 deleted file mode 100644 index b9e6d211b2d1..000000000000 --- a/packages/file_selector/file_selector_windows/windows/file_selector_plugin.cpp +++ /dev/null @@ -1,300 +0,0 @@ -// 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. -#include "file_selector_plugin.h" - -#include -#include -#include -#include -#include -#include -#include -#include - -#include -#include -#include -#include - -#include "file_dialog_controller.h" -#include "string_utils.h" - -_COM_SMARTPTR_TYPEDEF(IEnumShellItems, IID_IEnumShellItems); -_COM_SMARTPTR_TYPEDEF(IFileDialog, IID_IFileDialog); -_COM_SMARTPTR_TYPEDEF(IShellItem, IID_IShellItem); -_COM_SMARTPTR_TYPEDEF(IShellItemArray, IID_IShellItemArray); - -namespace file_selector_windows { - -namespace { - -using flutter::EncodableList; -using flutter::EncodableMap; -using flutter::EncodableValue; - -// The kind of file dialog to show. -enum class DialogMode { open, save }; - -// Returns the path for |shell_item| as a UTF-8 string, or an -// empty string on failure. -std::string GetPathForShellItem(IShellItem* shell_item) { - if (shell_item == nullptr) { - return ""; - } - wchar_t* wide_path = nullptr; - if (!SUCCEEDED(shell_item->GetDisplayName(SIGDN_FILESYSPATH, &wide_path))) { - return ""; - } - std::string path = Utf8FromUtf16(wide_path); - ::CoTaskMemFree(wide_path); - return path; -} - -// Implementation of FileDialogControllerFactory that makes standard -// FileDialogController instances. -class DefaultFileDialogControllerFactory : public FileDialogControllerFactory { - public: - DefaultFileDialogControllerFactory() {} - virtual ~DefaultFileDialogControllerFactory() {} - - // Disallow copy and assign. - DefaultFileDialogControllerFactory( - const DefaultFileDialogControllerFactory&) = delete; - DefaultFileDialogControllerFactory& operator=( - const DefaultFileDialogControllerFactory&) = delete; - - std::unique_ptr CreateController( - IFileDialog* dialog) const override { - assert(dialog != nullptr); - return std::make_unique(dialog); - } -}; - -// Wraps an IFileDialog, managing object lifetime as a scoped object and -// providing a simplified API for interacting with it as needed for the plugin. -class DialogWrapper { - public: - explicit DialogWrapper(const FileDialogControllerFactory& dialog_factory, - IID type) { - is_open_dialog_ = type == CLSID_FileOpenDialog; - IFileDialogPtr dialog = nullptr; - last_result_ = CoCreateInstance(type, nullptr, CLSCTX_INPROC_SERVER, - IID_PPV_ARGS(&dialog)); - dialog_controller_ = dialog_factory.CreateController(dialog); - } - - // Attempts to set the default folder for the dialog to |path|, - // if it exists. - void SetFolder(std::string_view path) { - std::wstring wide_path = Utf16FromUtf8(path); - IShellItemPtr item; - last_result_ = SHCreateItemFromParsingName(wide_path.c_str(), nullptr, - IID_PPV_ARGS(&item)); - if (!SUCCEEDED(last_result_)) { - return; - } - dialog_controller_->SetFolder(item); - } - - // Sets the file name that is initially shown in the dialog. - void SetFileName(std::string_view name) { - std::wstring wide_name = Utf16FromUtf8(name); - last_result_ = dialog_controller_->SetFileName(wide_name.c_str()); - } - - // Sets the label of the confirmation button. - void SetOkButtonLabel(std::string_view label) { - std::wstring wide_label = Utf16FromUtf8(label); - last_result_ = dialog_controller_->SetOkButtonLabel(wide_label.c_str()); - } - - // Adds the given options to the dialog's current option set. - void AddOptions(FILEOPENDIALOGOPTIONS new_options) { - FILEOPENDIALOGOPTIONS options; - last_result_ = dialog_controller_->GetOptions(&options); - if (!SUCCEEDED(last_result_)) { - return; - } - options |= new_options; - if (options & FOS_PICKFOLDERS) { - opening_directory_ = true; - } - last_result_ = dialog_controller_->SetOptions(options); - } - - // Sets the filters for allowed file types to select. - void SetFileTypeFilters(const EncodableList& filters) { - const std::wstring spec_delimiter = L";"; - const std::wstring file_wildcard = L"*."; - std::vector filter_specs; - // Temporary ownership of the constructed strings whose data is used in - // filter_specs, so that they live until the call to SetFileTypes is done. - std::vector filter_names; - std::vector filter_extensions; - filter_extensions.reserve(filters.size()); - filter_names.reserve(filters.size()); - - for (const EncodableValue& filter_info_value : filters) { - const auto& type_group = std::any_cast( - std::get(filter_info_value)); - filter_names.push_back(Utf16FromUtf8(type_group.label())); - filter_extensions.push_back(L""); - std::wstring& spec = filter_extensions.back(); - if (type_group.extensions().empty()) { - spec += L"*.*"; - } else { - for (const EncodableValue& extension : type_group.extensions()) { - if (!spec.empty()) { - spec += spec_delimiter; - } - spec += - file_wildcard + Utf16FromUtf8(std::get(extension)); - } - } - filter_specs.push_back({filter_names.back().c_str(), spec.c_str()}); - } - last_result_ = dialog_controller_->SetFileTypes( - 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) { - assert(dialog_controller_); - last_result_ = dialog_controller_->Show(parent_window); - if (!SUCCEEDED(last_result_)) { - return std::nullopt; - } - - EncodableList files; - if (is_open_dialog_) { - IShellItemArrayPtr shell_items; - last_result_ = dialog_controller_->GetResults(&shell_items); - if (!SUCCEEDED(last_result_)) { - return std::nullopt; - } - IEnumShellItemsPtr item_enumerator; - last_result_ = shell_items->EnumItems(&item_enumerator); - if (!SUCCEEDED(last_result_)) { - return std::nullopt; - } - IShellItemPtr shell_item; - while (item_enumerator->Next(1, &shell_item, nullptr) == S_OK) { - files.push_back(EncodableValue(GetPathForShellItem(shell_item))); - } - } else { - IShellItemPtr shell_item; - last_result_ = dialog_controller_->GetResult(&shell_item); - if (!SUCCEEDED(last_result_)) { - return std::nullopt; - } - files.push_back(EncodableValue(GetPathForShellItem(shell_item))); - } - return files; - } - - // Returns the result of the last Win32 API call related to this object. - HRESULT last_result() { return last_result_; } - - private: - // The dialog controller that all interactions are mediated through, to allow - // for unit testing. - std::unique_ptr dialog_controller_; - bool is_open_dialog_; - bool opening_directory_ = false; - HRESULT last_result_; -}; - -ErrorOr ShowDialog( - const FileDialogControllerFactory& dialog_factory, HWND parent_window, - DialogMode mode, const SelectionOptions& options, - const std::string* initial_directory, const std::string* suggested_name, - const std::string* confirm_label) { - IID dialog_type = - mode == DialogMode::save ? CLSID_FileSaveDialog : CLSID_FileOpenDialog; - DialogWrapper dialog(dialog_factory, dialog_type); - if (!SUCCEEDED(dialog.last_result())) { - return FlutterError("System error", "Could not create dialog", - EncodableValue(dialog.last_result())); - } - - FILEOPENDIALOGOPTIONS dialog_options = 0; - if (options.select_folders()) { - dialog_options |= FOS_PICKFOLDERS; - } - if (options.allow_multiple()) { - dialog_options |= FOS_ALLOWMULTISELECT; - } - if (dialog_options != 0) { - dialog.AddOptions(dialog_options); - } - - if (initial_directory) { - dialog.SetFolder(*initial_directory); - } - if (suggested_name) { - dialog.SetFileName(*suggested_name); - } - if (confirm_label) { - dialog.SetOkButtonLabel(*confirm_label); - } - - if (!options.allowed_types().empty()) { - dialog.SetFileTypeFilters(options.allowed_types()); - } - - std::optional files = dialog.Show(parent_window); - if (!files) { - 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 std::move(files.value()); -} - -// Returns the top-level window that owns |view|. -HWND GetRootWindow(flutter::FlutterView* view) { - return ::GetAncestor(view->GetNativeWindow(), GA_ROOT); -} - -} // namespace - -// static -void FileSelectorPlugin::RegisterWithRegistrar( - flutter::PluginRegistrarWindows* registrar) { - std::unique_ptr plugin = - std::make_unique( - [registrar] { return GetRootWindow(registrar->GetView()); }, - std::make_unique()); - - FileSelectorApi::SetUp(registrar->messenger(), plugin.get()); - registrar->AddPlugin(std::move(plugin)); -} - -FileSelectorPlugin::FileSelectorPlugin( - FlutterRootWindowProvider window_provider, - std::unique_ptr dialog_controller_factory) - : get_root_window_(std::move(window_provider)), - controller_factory_(std::move(dialog_controller_factory)) {} - -FileSelectorPlugin::~FileSelectorPlugin() = default; - -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( - const SelectionOptions& options, const std::string* initialDirectory, - const std::string* suggestedName, const std::string* confirmButtonText) { - return ShowDialog(*controller_factory_, get_root_window_(), DialogMode::save, - options, initialDirectory, suggestedName, - confirmButtonText); -} - -} // namespace file_selector_windows 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 deleted file mode 100644 index 1388bfd3898d..000000000000 --- a/packages/file_selector/file_selector_windows/windows/file_selector_plugin.h +++ /dev/null @@ -1,53 +0,0 @@ -// 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. -#ifndef PACKAGES_FILE_SELECTOR_FILE_SELECTOR_WINDOWS_WINDOWS_FILE_SELECTOR_PLUGIN_H_ -#define PACKAGES_FILE_SELECTOR_FILE_SELECTOR_WINDOWS_WINDOWS_FILE_SELECTOR_PLUGIN_H_ - -#include -#include - -#include - -#include "file_dialog_controller.h" -#include "messages.g.h" - -namespace file_selector_windows { - -// Abstraction for accessing the Flutter view's root window, to allow for faking -// in unit tests without creating fake window hierarchies, as well as to work -// around https://github.com/flutter/flutter/issues/90694. -using FlutterRootWindowProvider = std::function; - -class FileSelectorPlugin : public flutter::Plugin, public FileSelectorApi { - public: - static void RegisterWithRegistrar(flutter::PluginRegistrarWindows* registrar); - - // Creates a new plugin instance for the given registar, using the given - // factory to create native dialog controllers. - FileSelectorPlugin( - FlutterRootWindowProvider window_provider, - std::unique_ptr dialog_controller_factory); - - virtual ~FileSelectorPlugin(); - - // FileSelectorApi - ErrorOr ShowOpenDialog( - const SelectionOptions& options, const std::string* initial_directory, - const std::string* confirm_button_text) override; - ErrorOr ShowSaveDialog( - const SelectionOptions& options, const std::string* initialDirectory, - const std::string* suggestedName, - const std::string* confirmButtonText) override; - - private: - // The provider for the root window to attach the dialog to. - FlutterRootWindowProvider get_root_window_; - - // The factory for creating dialog controller instances. - std::unique_ptr controller_factory_; -}; - -} // namespace file_selector_windows - -#endif // PACKAGES_FILE_SELECTOR_FILE_SELECTOR_WINDOWS_WINDOWS_FILE_SELECTOR_PLUGIN_H_ diff --git a/packages/file_selector/file_selector_windows/windows/file_selector_windows.cpp b/packages/file_selector/file_selector_windows/windows/file_selector_windows.cpp deleted file mode 100644 index e4d2c15fd89b..000000000000 --- a/packages/file_selector/file_selector_windows/windows/file_selector_windows.cpp +++ /dev/null @@ -1,15 +0,0 @@ -// 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. -#include "include/file_selector_windows/file_selector_windows.h" - -#include - -#include "file_selector_plugin.h" - -void FileSelectorWindowsRegisterWithRegistrar( - FlutterDesktopPluginRegistrarRef registrar) { - file_selector_windows::FileSelectorPlugin::RegisterWithRegistrar( - flutter::PluginRegistrarManager::GetInstance() - ->GetRegistrar(registrar)); -} diff --git a/packages/file_selector/file_selector_windows/windows/include/file_selector_windows/file_selector_windows.h b/packages/file_selector/file_selector_windows/windows/include/file_selector_windows/file_selector_windows.h deleted file mode 100644 index 7ee6ed3d29ff..000000000000 --- a/packages/file_selector/file_selector_windows/windows/include/file_selector_windows/file_selector_windows.h +++ /dev/null @@ -1,26 +0,0 @@ -// 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. -#ifndef PACKAGES_FILE_SELECTOR_FILE_SELECTOR_WINDOWS_WINDOWS_INCLUDE_FILE_SELECTOR_WINDOWS_FILE_SELECTOR_WINDOWS_H_ -#define PACKAGES_FILE_SELECTOR_FILE_SELECTOR_WINDOWS_WINDOWS_INCLUDE_FILE_SELECTOR_WINDOWS_FILE_SELECTOR_WINDOWS_H_ - -#include - -#ifdef FLUTTER_PLUGIN_IMPL -#define FLUTTER_PLUGIN_EXPORT __declspec(dllexport) -#else -#define FLUTTER_PLUGIN_EXPORT __declspec(dllimport) -#endif - -#if defined(__cplusplus) -extern "C" { -#endif - -FLUTTER_PLUGIN_EXPORT void FileSelectorWindowsRegisterWithRegistrar( - FlutterDesktopPluginRegistrarRef registrar); - -#if defined(__cplusplus) -} // extern "C" -#endif - -#endif // PACKAGES_FILE_SELECTOR_FILE_SELECTOR_WINDOWS_WINDOWS_INCLUDE_FILE_SELECTOR_WINDOWS_FILE_SELECTOR_WINDOWS_H_ diff --git a/packages/file_selector/file_selector_windows/windows/messages.g.cpp b/packages/file_selector/file_selector_windows/windows/messages.g.cpp deleted file mode 100644 index 04e529d8b35a..000000000000 --- a/packages/file_selector/file_selector_windows/windows/messages.g.cpp +++ /dev/null @@ -1,278 +0,0 @@ -// 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 (v3.2.5), do not edit directly. -// See also: https://pub.dev/packages/pigeon - -#undef _HAS_EXCEPTIONS - -#include "messages.g.h" - -#include -#include -#include -#include - -#include -#include -#include - -namespace file_selector_windows { - -/* TypeGroup */ - -const std::string& TypeGroup::label() const { return label_; } -void TypeGroup::set_label(std::string_view value_arg) { label_ = value_arg; } - -const flutter::EncodableList& TypeGroup::extensions() const { - return extensions_; -} -void TypeGroup::set_extensions(const flutter::EncodableList& value_arg) { - extensions_ = value_arg; -} - -flutter::EncodableMap TypeGroup::ToEncodableMap() const { - return flutter::EncodableMap{ - {flutter::EncodableValue("label"), flutter::EncodableValue(label_)}, - {flutter::EncodableValue("extensions"), - flutter::EncodableValue(extensions_)}, - }; -} - -TypeGroup::TypeGroup() {} - -TypeGroup::TypeGroup(flutter::EncodableMap map) { - auto& encodable_label = map.at(flutter::EncodableValue("label")); - if (const std::string* pointer_label = - std::get_if(&encodable_label)) { - label_ = *pointer_label; - } - auto& encodable_extensions = map.at(flutter::EncodableValue("extensions")); - if (const flutter::EncodableList* pointer_extensions = - std::get_if(&encodable_extensions)) { - extensions_ = *pointer_extensions; - } -} - -/* SelectionOptions */ - -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; -} - -const flutter::EncodableList& SelectionOptions::allowed_types() const { - return allowed_types_; -} -void SelectionOptions::set_allowed_types( - const flutter::EncodableList& value_arg) { - allowed_types_ = value_arg; -} - -flutter::EncodableMap SelectionOptions::ToEncodableMap() const { - return flutter::EncodableMap{ - {flutter::EncodableValue("allowMultiple"), - flutter::EncodableValue(allow_multiple_)}, - {flutter::EncodableValue("selectFolders"), - flutter::EncodableValue(select_folders_)}, - {flutter::EncodableValue("allowedTypes"), - flutter::EncodableValue(allowed_types_)}, - }; -} - -SelectionOptions::SelectionOptions() {} - -SelectionOptions::SelectionOptions(flutter::EncodableMap map) { - auto& encodable_allow_multiple = - map.at(flutter::EncodableValue("allowMultiple")); - if (const bool* pointer_allow_multiple = - std::get_if(&encodable_allow_multiple)) { - allow_multiple_ = *pointer_allow_multiple; - } - auto& encodable_select_folders = - map.at(flutter::EncodableValue("selectFolders")); - if (const bool* pointer_select_folders = - std::get_if(&encodable_select_folders)) { - select_folders_ = *pointer_select_folders; - } - auto& encodable_allowed_types = - map.at(flutter::EncodableValue("allowedTypes")); - if (const flutter::EncodableList* pointer_allowed_types = - std::get_if(&encodable_allowed_types)) { - allowed_types_ = *pointer_allowed_types; - } -} - -FileSelectorApiCodecSerializer::FileSelectorApiCodecSerializer() {} -flutter::EncodableValue FileSelectorApiCodecSerializer::ReadValueOfType( - uint8_t type, flutter::ByteStreamReader* stream) const { - switch (type) { - case 128: - return flutter::CustomEncodableValue( - SelectionOptions(std::get(ReadValue(stream)))); - - case 129: - return flutter::CustomEncodableValue( - TypeGroup(std::get(ReadValue(stream)))); - - default: - return flutter::StandardCodecSerializer::ReadValueOfType(type, stream); - } -} - -void FileSelectorApiCodecSerializer::WriteValue( - const flutter::EncodableValue& value, - flutter::ByteStreamWriter* stream) const { - if (const flutter::CustomEncodableValue* custom_value = - std::get_if(&value)) { - if (custom_value->type() == typeid(SelectionOptions)) { - stream->WriteByte(128); - WriteValue( - std::any_cast(*custom_value).ToEncodableMap(), - stream); - return; - } - if (custom_value->type() == typeid(TypeGroup)) { - stream->WriteByte(129); - WriteValue(std::any_cast(*custom_value).ToEncodableMap(), - stream); - return; - } - } - flutter::StandardCodecSerializer::WriteValue(value, stream); -} - -/** The codec used by FileSelectorApi. */ -const flutter::StandardMessageCodec& FileSelectorApi::GetCodec() { - return flutter::StandardMessageCodec::GetInstance( - &FileSelectorApiCodecSerializer::GetInstance()); -} - -/** Sets up an instance of `FileSelectorApi` to handle messages through the - * `binary_messenger`. */ -void FileSelectorApi::SetUp(flutter::BinaryMessenger* binary_messenger, - FileSelectorApi* api) { - { - auto channel = - std::make_unique>( - binary_messenger, - "dev.flutter.pigeon.FileSelectorApi.showOpenDialog", &GetCodec()); - if (api != nullptr) { - channel->SetMessageHandler( - [api](const flutter::EncodableValue& message, - const flutter::MessageReply& reply) { - flutter::EncodableMap wrapped; - try { - const auto& args = std::get(message); - const auto& encodable_options_arg = args.at(0); - if (encodable_options_arg.IsNull()) { - wrapped.emplace(flutter::EncodableValue("error"), - WrapError("options_arg unexpectedly null.")); - reply(wrapped); - return; - } - const auto& options_arg = std::any_cast( - std::get( - encodable_options_arg)); - const auto& encodable_initial_directory_arg = args.at(1); - const auto* initial_directory_arg = - std::get_if(&encodable_initial_directory_arg); - 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( - options_arg, initial_directory_arg, confirm_button_text_arg); - if (output.has_error()) { - wrapped.emplace(flutter::EncodableValue("error"), - WrapError(output.error())); - } else { - wrapped.emplace( - flutter::EncodableValue("result"), - flutter::EncodableValue(std::move(output).TakeValue())); - } - } catch (const std::exception& exception) { - wrapped.emplace(flutter::EncodableValue("error"), - WrapError(exception.what())); - } - reply(wrapped); - }); - } else { - channel->SetMessageHandler(nullptr); - } - } - { - auto channel = - std::make_unique>( - binary_messenger, - "dev.flutter.pigeon.FileSelectorApi.showSaveDialog", &GetCodec()); - if (api != nullptr) { - channel->SetMessageHandler( - [api](const flutter::EncodableValue& message, - const flutter::MessageReply& reply) { - flutter::EncodableMap wrapped; - try { - const auto& args = std::get(message); - const auto& encodable_options_arg = args.at(0); - if (encodable_options_arg.IsNull()) { - wrapped.emplace(flutter::EncodableValue("error"), - WrapError("options_arg unexpectedly null.")); - reply(wrapped); - return; - } - const auto& options_arg = std::any_cast( - std::get( - encodable_options_arg)); - const auto& encodable_initial_directory_arg = args.at(1); - const auto* initial_directory_arg = - std::get_if(&encodable_initial_directory_arg); - const auto& encodable_suggested_name_arg = args.at(2); - const auto* suggested_name_arg = - std::get_if(&encodable_suggested_name_arg); - 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( - options_arg, initial_directory_arg, suggested_name_arg, - confirm_button_text_arg); - if (output.has_error()) { - wrapped.emplace(flutter::EncodableValue("error"), - WrapError(output.error())); - } else { - wrapped.emplace( - flutter::EncodableValue("result"), - flutter::EncodableValue(std::move(output).TakeValue())); - } - } catch (const std::exception& exception) { - wrapped.emplace(flutter::EncodableValue("error"), - WrapError(exception.what())); - } - reply(wrapped); - }); - } else { - channel->SetMessageHandler(nullptr); - } - } -} - -flutter::EncodableMap FileSelectorApi::WrapError( - std::string_view error_message) { - return flutter::EncodableMap( - {{flutter::EncodableValue("message"), - flutter::EncodableValue(std::string(error_message))}, - {flutter::EncodableValue("code"), flutter::EncodableValue("Error")}, - {flutter::EncodableValue("details"), flutter::EncodableValue()}}); -} -flutter::EncodableMap FileSelectorApi::WrapError(const FlutterError& error) { - return flutter::EncodableMap( - {{flutter::EncodableValue("message"), - flutter::EncodableValue(error.message())}, - {flutter::EncodableValue("code"), flutter::EncodableValue(error.code())}, - {flutter::EncodableValue("details"), error.details()}}); -} - -} // namespace file_selector_windows diff --git a/packages/file_selector/file_selector_windows/windows/messages.g.h b/packages/file_selector/file_selector_windows/windows/messages.g.h deleted file mode 100644 index fb496d2d66e2..000000000000 --- a/packages/file_selector/file_selector_windows/windows/messages.g.h +++ /dev/null @@ -1,149 +0,0 @@ -// 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 (v3.2.5), do not edit directly. -// See also: https://pub.dev/packages/pigeon - -#ifndef PIGEON_MESSAGES_G_FILE_SELECTOR_WINDOWS_H_ -#define PIGEON_MESSAGES_G_FILE_SELECTOR_WINDOWS_H_ -#include -#include -#include -#include - -#include -#include -#include - -namespace file_selector_windows { - -/* Generated class from Pigeon. */ - -class FlutterError { - public: - FlutterError(const std::string& code) : code_(code) {} - FlutterError(const std::string& code, const std::string& message) - : code_(code), message_(message) {} - FlutterError(const std::string& code, const std::string& message, - const flutter::EncodableValue& details) - : code_(code), message_(message), details_(details) {} - - const std::string& code() const { return code_; } - const std::string& message() const { return message_; } - const flutter::EncodableValue& details() const { return details_; } - - private: - std::string code_; - std::string message_; - flutter::EncodableValue details_; -}; - -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); } - - bool has_error() const { return std::holds_alternative(v_); } - const T& value() const { return std::get(v_); }; - const FlutterError& error() const { return std::get(v_); }; - - private: - friend class FileSelectorApi; - ErrorOr() = default; - T TakeValue() && { return std::get(std::move(v_)); } - - std::variant v_; -}; - -/* Generated class from Pigeon that represents data sent in messages. */ -class TypeGroup { - public: - TypeGroup(); - const std::string& label() const; - void set_label(std::string_view value_arg); - - const flutter::EncodableList& extensions() const; - void set_extensions(const flutter::EncodableList& value_arg); - - private: - TypeGroup(flutter::EncodableMap map); - flutter::EncodableMap ToEncodableMap() const; - friend class FileSelectorApi; - friend class FileSelectorApiCodecSerializer; - std::string label_; - flutter::EncodableList extensions_; -}; - -/* Generated class from Pigeon that represents data sent in messages. */ -class SelectionOptions { - public: - SelectionOptions(); - bool allow_multiple() const; - void set_allow_multiple(bool value_arg); - - bool select_folders() const; - void set_select_folders(bool value_arg); - - const flutter::EncodableList& allowed_types() const; - void set_allowed_types(const flutter::EncodableList& value_arg); - - private: - SelectionOptions(flutter::EncodableMap map); - flutter::EncodableMap ToEncodableMap() const; - friend class FileSelectorApi; - friend class FileSelectorApiCodecSerializer; - bool allow_multiple_; - bool select_folders_; - flutter::EncodableList allowed_types_; -}; - -class FileSelectorApiCodecSerializer : public flutter::StandardCodecSerializer { - public: - inline static FileSelectorApiCodecSerializer& GetInstance() { - static FileSelectorApiCodecSerializer sInstance; - return sInstance; - } - - FileSelectorApiCodecSerializer(); - - public: - void WriteValue(const flutter::EncodableValue& value, - flutter::ByteStreamWriter* stream) const override; - - protected: - flutter::EncodableValue ReadValueOfType( - uint8_t type, flutter::ByteStreamReader* stream) const override; -}; - -/* Generated class from Pigeon that represents a handler of messages from - * Flutter. */ -class FileSelectorApi { - public: - FileSelectorApi(const FileSelectorApi&) = delete; - FileSelectorApi& operator=(const FileSelectorApi&) = delete; - virtual ~FileSelectorApi(){}; - virtual ErrorOr ShowOpenDialog( - const SelectionOptions& options, const std::string* initial_directory, - const std::string* confirm_button_text) = 0; - virtual ErrorOr ShowSaveDialog( - const SelectionOptions& options, const std::string* initial_directory, - const std::string* suggested_name, - const std::string* confirm_button_text) = 0; - - /** The codec used by FileSelectorApi. */ - static const flutter::StandardMessageCodec& GetCodec(); - /** Sets up an instance of `FileSelectorApi` to handle messages through the - * `binary_messenger`. */ - static void SetUp(flutter::BinaryMessenger* binary_messenger, - FileSelectorApi* api); - static flutter::EncodableMap WrapError(std::string_view error_message); - static flutter::EncodableMap WrapError(const FlutterError& error); - - protected: - FileSelectorApi() = default; -}; -} // namespace file_selector_windows -#endif // PIGEON_MESSAGES_G_FILE_SELECTOR_WINDOWS_H_ diff --git a/packages/file_selector/file_selector_windows/windows/string_utils.cpp b/packages/file_selector/file_selector_windows/windows/string_utils.cpp deleted file mode 100644 index 6fa7c18403a7..000000000000 --- a/packages/file_selector/file_selector_windows/windows/string_utils.cpp +++ /dev/null @@ -1,60 +0,0 @@ -// 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. - -#include "string_utils.h" - -#include -#include - -#include - -namespace file_selector_windows { - -// Converts the given UTF-16 string to UTF-8. -std::string Utf8FromUtf16(std::wstring_view utf16_string) { - if (utf16_string.empty()) { - return std::string(); - } - int target_length = ::WideCharToMultiByte( - CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string.data(), - static_cast(utf16_string.length()), nullptr, 0, nullptr, nullptr); - if (target_length == 0) { - return std::string(); - } - std::string utf8_string; - utf8_string.resize(target_length); - int converted_length = ::WideCharToMultiByte( - CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string.data(), - static_cast(utf16_string.length()), utf8_string.data(), - target_length, nullptr, nullptr); - if (converted_length == 0) { - return std::string(); - } - return utf8_string; -} - -// Converts the given UTF-8 string to UTF-16. -std::wstring Utf16FromUtf8(std::string_view utf8_string) { - if (utf8_string.empty()) { - return std::wstring(); - } - int target_length = - ::MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, utf8_string.data(), - static_cast(utf8_string.length()), nullptr, 0); - if (target_length == 0) { - return std::wstring(); - } - std::wstring utf16_string; - utf16_string.resize(target_length); - int converted_length = - ::MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, utf8_string.data(), - static_cast(utf8_string.length()), - utf16_string.data(), target_length); - if (converted_length == 0) { - return std::wstring(); - } - return utf16_string; -} - -} // namespace file_selector_windows diff --git a/packages/file_selector/file_selector_windows/windows/string_utils.h b/packages/file_selector/file_selector_windows/windows/string_utils.h deleted file mode 100644 index 2323a5a589d8..000000000000 --- a/packages/file_selector/file_selector_windows/windows/string_utils.h +++ /dev/null @@ -1,21 +0,0 @@ -// 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. -#ifndef PACKAGES_FILE_SELECTOR_FILE_SELECTOR_WINDOWS_WINDOWS_STRING_UTILS_H_ -#define PACKAGES_FILE_SELECTOR_FILE_SELECTOR_WINDOWS_WINDOWS_STRING_UTILS_H_ - -#include - -#include - -namespace file_selector_windows { - -// Converts the given UTF-16 string to UTF-8. -std::string Utf8FromUtf16(std::wstring_view utf16_string); - -// Converts the given UTF-8 string to UTF-16. -std::wstring Utf16FromUtf8(std::string_view utf8_string); - -} // namespace file_selector_windows - -#endif // PACKAGES_FILE_SELECTOR_FILE_SELECTOR_WINDOWS_WINDOWS_STRING_UTILS_H_ 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 deleted file mode 100644 index 2325a271b777..000000000000 --- a/packages/file_selector/file_selector_windows/windows/test/file_selector_plugin_test.cpp +++ /dev/null @@ -1,449 +0,0 @@ -// 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. -#include "file_selector_plugin.h" - -#include -#include -#include -#include -#include -#include -#include - -#include -#include -#include -#include - -#include "file_dialog_controller.h" -#include "string_utils.h" -#include "test/test_file_dialog_controller.h" -#include "test/test_utils.h" - -namespace file_selector_windows { -namespace test { - -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) { - const HWND fake_window = reinterpret_cast(1337); - ScopedTestShellItem fake_selected_file; - IShellItemArrayPtr fake_result_array; - ::SHCreateShellItemArrayFromShellItem(fake_selected_file.file(), - IID_PPV_ARGS(&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_EQ(options & FOS_ALLOWMULTISELECT, 0U); - EXPECT_EQ(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(false), SelectFoldersArg(false), - EncodableList()), - nullptr, nullptr); - - EXPECT_TRUE(shown); - ASSERT_FALSE(result.has_error()); - const EncodableList& paths = result.value(); - EXPECT_EQ(paths.size(), 1); - EXPECT_EQ(std::get(paths[0]), - Utf8FromUtf16(fake_selected_file.path())); -} - -TEST(FileSelectorPlugin, TestOpenWithArguments) { - const HWND fake_window = reinterpret_cast(1337); - ScopedTestShellItem fake_selected_file; - IShellItemArrayPtr fake_result_array; - ::SHCreateShellItemArrayFromShellItem(fake_selected_file.file(), - IID_PPV_ARGS(&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 arguments. - EXPECT_EQ(dialog.GetDialogFolderPath(), L"C:\\Program Files"); - // Make sure that the folder was called via SetFolder, not SetDefaultFolder. - EXPECT_EQ(dialog.GetSetFolderPath(), L"C:\\Program Files"); - EXPECT_EQ(dialog.GetOkButtonLabel(), L"Open it!"); - - return MockShowResult(fake_result_array); - }; - - FileSelectorPlugin plugin( - [fake_window] { return fake_window; }, - std::make_unique(show_validator)); - // 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()), - &initial_directory, &confirm_button); - - EXPECT_TRUE(shown); - ASSERT_FALSE(result.has_error()); - const EncodableList& paths = result.value(); - EXPECT_EQ(paths.size(), 1); - EXPECT_EQ(std::get(paths[0]), - Utf8FromUtf16(fake_selected_file.path())); -} - -TEST(FileSelectorPlugin, TestOpenMultiple) { - const HWND fake_window = reinterpret_cast(1337); - ScopedTestFileIdList fake_selected_file_1; - ScopedTestFileIdList fake_selected_file_2; - LPCITEMIDLIST fake_selected_files[] = { - fake_selected_file_1.file(), - fake_selected_file_2.file(), - }; - IShellItemArrayPtr fake_result_array; - ::SHCreateShellItemArrayFromIDLists(2, fake_selected_files, - &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_EQ(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(false), - 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_file_1.path())); - EXPECT_EQ(std::get(paths[1]), - Utf8FromUtf16(fake_selected_file_2.path())); -} - -TEST(FileSelectorPlugin, TestOpenWithFilter) { - const HWND fake_window = reinterpret_cast(1337); - ScopedTestShellItem fake_selected_file; - IShellItemArrayPtr fake_result_array; - ::SHCreateShellItemArrayFromShellItem(fake_selected_file.file(), - IID_PPV_ARGS(&fake_result_array)); - - const EncodableValue text_group = - CustomEncodableValue(CreateTypeGroup("Text", EncodableList({ - EncodableValue("txt"), - EncodableValue("json"), - }))); - const EncodableValue image_group = - CustomEncodableValue(CreateTypeGroup("Images", EncodableList({ - EncodableValue("png"), - EncodableValue("gif"), - EncodableValue("jpeg"), - }))); - const EncodableValue any_group = - CustomEncodableValue(CreateTypeGroup("Any", EncodableList())); - - 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 filter. - const std::vector& filters = dialog.GetFileTypes(); - EXPECT_EQ(filters.size(), 3U); - if (filters.size() == 3U) { - 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"); - EXPECT_EQ(filters[2].name, L"Any"); - EXPECT_EQ(filters[2].spec, L"*.*"); - } - - return MockShowResult(fake_result_array); - }; - - 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); - - EXPECT_TRUE(shown); - ASSERT_FALSE(result.has_error()); - const EncodableList& paths = result.value(); - EXPECT_EQ(paths.size(), 1); - EXPECT_EQ(std::get(paths[0]), - Utf8FromUtf16(fake_selected_file.path())); -} - -TEST(FileSelectorPlugin, TestOpenCancel) { - const HWND fake_window = reinterpret_cast(1337); - - bool shown = false; - MockShow show_validator = [&shown, fake_window]( - const TestFileDialogController& dialog, - HWND parent) { - shown = true; - return MockShowResult(); - }; - - FileSelectorPlugin plugin( - [fake_window] { return fake_window; }, - std::make_unique(show_validator)); - ErrorOr result = plugin.ShowOpenDialog( - CreateOptions(AllowMultipleArg(false), SelectFoldersArg(false), - EncodableList()), - nullptr, nullptr); - - EXPECT_TRUE(shown); - ASSERT_FALSE(result.has_error()); - const EncodableList& paths = result.value(); - EXPECT_EQ(paths.size(), 0); -} - -TEST(FileSelectorPlugin, TestSaveSimple) { - const HWND fake_window = reinterpret_cast(1337); - ScopedTestShellItem fake_selected_file; - - 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 options. - FILEOPENDIALOGOPTIONS options; - dialog.GetOptions(&options); - EXPECT_EQ(options & FOS_ALLOWMULTISELECT, 0U); - EXPECT_EQ(options & FOS_PICKFOLDERS, 0U); - - return MockShowResult(fake_result); - }; - - FileSelectorPlugin plugin( - [fake_window] { return fake_window; }, - std::make_unique(show_validator)); - ErrorOr result = plugin.ShowSaveDialog( - CreateOptions(AllowMultipleArg(false), SelectFoldersArg(false), - EncodableList()), - nullptr, nullptr, nullptr); - - EXPECT_TRUE(shown); - ASSERT_FALSE(result.has_error()); - const EncodableList& paths = result.value(); - EXPECT_EQ(paths.size(), 1); - EXPECT_EQ(std::get(paths[0]), - Utf8FromUtf16(fake_selected_file.path())); -} - -TEST(FileSelectorPlugin, TestSaveWithArguments) { - const HWND fake_window = reinterpret_cast(1337); - ScopedTestShellItem fake_selected_file; - - 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 arguments. - EXPECT_EQ(dialog.GetDialogFolderPath(), L"C:\\Program Files"); - // Make sure that the folder was called via SetFolder, not - // SetDefaultFolder. - EXPECT_EQ(dialog.GetSetFolderPath(), L"C:\\Program Files"); - EXPECT_EQ(dialog.GetFileName(), L"a name"); - EXPECT_EQ(dialog.GetOkButtonLabel(), L"Save it!"); - - return MockShowResult(fake_result); - }; - - FileSelectorPlugin plugin( - [fake_window] { return fake_window; }, - std::make_unique(show_validator)); - // This directory must exist. - 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()), - &initial_directory, &suggested_name, &confirm_button); - - EXPECT_TRUE(shown); - ASSERT_FALSE(result.has_error()); - const EncodableList& paths = result.value(); - EXPECT_EQ(paths.size(), 1); - EXPECT_EQ(std::get(paths[0]), - Utf8FromUtf16(fake_selected_file.path())); -} - -TEST(FileSelectorPlugin, TestSaveCancel) { - const HWND fake_window = reinterpret_cast(1337); - - bool shown = false; - MockShow show_validator = [&shown, fake_window]( - const TestFileDialogController& dialog, - HWND parent) { - shown = true; - return MockShowResult(); - }; - - FileSelectorPlugin plugin( - [fake_window] { return fake_window; }, - std::make_unique(show_validator)); - ErrorOr result = plugin.ShowSaveDialog( - CreateOptions(AllowMultipleArg(false), SelectFoldersArg(false), - EncodableList()), - nullptr, nullptr, nullptr); - - EXPECT_TRUE(shown); - ASSERT_FALSE(result.has_error()); - const EncodableList& paths = result.value(); - EXPECT_EQ(paths.size(), 0); -} - -TEST(FileSelectorPlugin, TestGetDirectorySimple) { - const HWND fake_window = reinterpret_cast(1337); - IShellItemPtr fake_selected_directory; - // This must be a directory that actually exists. - ::SHCreateItemFromParsingName(L"C:\\Program Files", nullptr, - IID_PPV_ARGS(&fake_selected_directory)); - IShellItemArrayPtr fake_result_array; - ::SHCreateShellItemArrayFromShellItem(fake_selected_directory, - IID_PPV_ARGS(&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_EQ(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(false), SelectFoldersArg(true), - EncodableList()), - nullptr, nullptr); - - EXPECT_TRUE(shown); - ASSERT_FALSE(result.has_error()); - const EncodableList& paths = result.value(); - EXPECT_EQ(paths.size(), 1); - EXPECT_EQ(std::get(paths[0]), "C:\\Program Files"); -} - -TEST(FileSelectorPlugin, TestGetDirectoryCancel) { - const HWND fake_window = reinterpret_cast(1337); - - bool shown = false; - MockShow show_validator = [&shown, fake_window]( - const TestFileDialogController& dialog, - HWND parent) { - shown = true; - return MockShowResult(); - }; - - FileSelectorPlugin plugin( - [fake_window] { return fake_window; }, - std::make_unique(show_validator)); - ErrorOr result = plugin.ShowOpenDialog( - CreateOptions(AllowMultipleArg(false), SelectFoldersArg(true), - EncodableList()), - nullptr, nullptr); - - EXPECT_TRUE(shown); - ASSERT_FALSE(result.has_error()); - const EncodableList& paths = result.value(); - EXPECT_EQ(paths.size(), 0); -} - -} // namespace test -} // namespace file_selector_windows 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 deleted file mode 100644 index 15065f916c8b..000000000000 --- a/packages/file_selector/file_selector_windows/windows/test/test_file_dialog_controller.cpp +++ /dev/null @@ -1,121 +0,0 @@ -// 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. -#include "test/test_file_dialog_controller.h" - -#include - -#include -#include -#include - -namespace file_selector_windows { -namespace test { - -TestFileDialogController::TestFileDialogController(IFileDialog* dialog, - MockShow mock_show) - : dialog_(dialog), - mock_show_(std::move(mock_show)), - FileDialogController(dialog) {} - -TestFileDialogController::~TestFileDialogController() {} - -HRESULT TestFileDialogController::SetFolder(IShellItem* folder) { - wchar_t* path_chars = nullptr; - if (SUCCEEDED(folder->GetDisplayName(SIGDN_FILESYSPATH, &path_chars))) { - set_folder_path_ = path_chars; - } else { - set_folder_path_ = L""; - } - - return FileDialogController::SetFolder(folder); -} - -HRESULT TestFileDialogController::SetFileTypes(UINT count, - COMDLG_FILTERSPEC* filters) { - filter_groups_.clear(); - for (unsigned int i = 0; i < count; ++i) { - filter_groups_.push_back( - DialogFilter(filters[i].pszName, filters[i].pszSpec)); - } - return FileDialogController::SetFileTypes(count, filters); -} - -HRESULT TestFileDialogController::SetOkButtonLabel(const wchar_t* text) { - ok_button_label_ = text; - return FileDialogController::SetOkButtonLabel(text); -} - -HRESULT TestFileDialogController::Show(HWND parent) { - mock_result_ = mock_show_(*this, parent); - if (std::holds_alternative(mock_result_)) { - return HRESULT_FROM_WIN32(ERROR_CANCELLED); - } - return S_OK; -} - -HRESULT TestFileDialogController::GetResult(IShellItem** out_item) const { - *out_item = std::get(mock_result_); - (*out_item)->AddRef(); - return S_OK; -} - -HRESULT TestFileDialogController::GetResults( - IShellItemArray** out_items) const { - *out_items = std::get(mock_result_); - (*out_items)->AddRef(); - return S_OK; -} - -std::wstring TestFileDialogController::GetSetFolderPath() const { - return set_folder_path_; -} - -std::wstring TestFileDialogController::GetDialogFolderPath() const { - IShellItemPtr item; - if (!SUCCEEDED(dialog_->GetFolder(&item))) { - return L""; - } - - wchar_t* path_chars = nullptr; - if (!SUCCEEDED(item->GetDisplayName(SIGDN_FILESYSPATH, &path_chars))) { - return L""; - } - std::wstring path(path_chars); - ::CoTaskMemFree(path_chars); - return path; -} - -std::wstring TestFileDialogController::GetFileName() const { - wchar_t* name_chars = nullptr; - if (!SUCCEEDED(dialog_->GetFileName(&name_chars))) { - return L""; - } - std::wstring name(name_chars); - ::CoTaskMemFree(name_chars); - return name; -} - -const std::vector& TestFileDialogController::GetFileTypes() - const { - return filter_groups_; -} - -std::wstring TestFileDialogController::GetOkButtonLabel() const { - return ok_button_label_; -} - -// ---------------------------------------- - -TestFileDialogControllerFactory::TestFileDialogControllerFactory( - MockShow mock_show) - : mock_show_(std::move(mock_show)) {} -TestFileDialogControllerFactory::~TestFileDialogControllerFactory() {} - -std::unique_ptr -TestFileDialogControllerFactory::CreateController(IFileDialog* dialog) const { - return std::make_unique(dialog, mock_show_); -} - -} // namespace test -} // namespace file_selector_windows 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 deleted file mode 100644 index 1c221fc219f9..000000000000 --- a/packages/file_selector/file_selector_windows/windows/test/test_file_dialog_controller.h +++ /dev/null @@ -1,110 +0,0 @@ -// 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. -#ifndef PACKAGES_FILE_SELECTOR_FILE_SELECTOR_WINDOWS_WINDOWS_TEST_TEST_FILE_DIALOG_CONTROLLER_H_ -#define PACKAGES_FILE_SELECTOR_FILE_SELECTOR_WINDOWS_WINDOWS_TEST_TEST_FILE_DIALOG_CONTROLLER_H_ - -#include -#include -#include - -#include -#include -#include -#include - -#include "file_dialog_controller.h" -#include "test/test_utils.h" - -_COM_SMARTPTR_TYPEDEF(IFileDialog, IID_IFileDialog); - -namespace file_selector_windows { -namespace test { - -class TestFileDialogController; - -// A value to use for GetResult(s) in TestFileDialogController. The type depends -// on whether the dialog is an open or save dialog. -using MockShowResult = - std::variant; -// Called for TestFileDialogController::Show, to do validation and provide a -// mock return value for GetResult(s). -using MockShow = - std::function; - -// A C++-friendly version of a COMDLG_FILTERSPEC. -struct DialogFilter { - std::wstring name; - std::wstring spec; - - DialogFilter(const wchar_t* name, const wchar_t* spec) - : name(name), spec(spec) {} -}; - -// An extension of the normal file dialog controller that: -// - Allows for inspection of set values. -// - Allows faking the 'Show' interaction, providing tests an opportunity to -// validate the dialog settings and provide a return value, via MockShow. -class TestFileDialogController : public FileDialogController { - public: - TestFileDialogController(IFileDialog* dialog, MockShow mock_show); - ~TestFileDialogController(); - - // FileDialogController: - HRESULT SetFolder(IShellItem* folder) override; - HRESULT SetFileTypes(UINT count, COMDLG_FILTERSPEC* filters) override; - HRESULT SetOkButtonLabel(const wchar_t* text) override; - HRESULT Show(HWND parent) override; - HRESULT GetResult(IShellItem** out_item) const override; - HRESULT GetResults(IShellItemArray** out_items) const override; - - // Accessors for validating IFileDialogController setter calls. - // Gets the folder path set by FileDialogController::SetFolder. - // - // This exists because there are multiple ways that the value returned by - // GetDialogFolderPath can be changed, so this allows specifically validating - // calls to SetFolder. - std::wstring GetSetFolderPath() const; - // Gets dialog folder path by calling IFileDialog::GetFolder. - std::wstring GetDialogFolderPath() const; - std::wstring GetFileName() const; - const std::vector& GetFileTypes() const; - std::wstring GetOkButtonLabel() const; - - private: - IFileDialogPtr dialog_; - MockShow mock_show_; - MockShowResult mock_result_; - - // The last set values, for IFileDialog properties that have setters but no - // corresponding getters. - std::wstring set_folder_path_; - std::wstring ok_button_label_; - std::vector filter_groups_; -}; - -// A controller factory that vends TestFileDialogController instances. -class TestFileDialogControllerFactory : public FileDialogControllerFactory { - public: - // Creates a factory whose instances use mock_show for the Show callback. - TestFileDialogControllerFactory(MockShow mock_show); - virtual ~TestFileDialogControllerFactory(); - - // Disallow copy and assign. - TestFileDialogControllerFactory(const TestFileDialogControllerFactory&) = - delete; - TestFileDialogControllerFactory& operator=( - const TestFileDialogControllerFactory&) = delete; - - // FileDialogControllerFactory: - std::unique_ptr CreateController( - IFileDialog* dialog) const override; - - private: - MockShow mock_show_; -}; - -} // namespace test -} // namespace file_selector_windows - -#endif // PACKAGES_FILE_SELECTOR_FILE_SELECTOR_WINDOWS_WINDOWS_TEST_TEST_FILE_DIALOG_CONTROLLER_H_ diff --git a/packages/file_selector/file_selector_windows/windows/test/test_main.cpp b/packages/file_selector/file_selector_windows/windows/test/test_main.cpp deleted file mode 100644 index 5a49b52c1c76..000000000000 --- a/packages/file_selector/file_selector_windows/windows/test/test_main.cpp +++ /dev/null @@ -1,16 +0,0 @@ -// 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. -#include -#include - -int main(int argc, char** argv) { - ::CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED); - - testing::InitGoogleTest(&argc, argv); - int exit_code = RUN_ALL_TESTS(); - - ::CoUninitialize(); - - return exit_code; -} diff --git a/packages/file_selector/file_selector_windows/windows/test/test_utils.cpp b/packages/file_selector/file_selector_windows/windows/test/test_utils.cpp deleted file mode 100644 index 3e3ab98a734a..000000000000 --- a/packages/file_selector/file_selector_windows/windows/test/test_utils.cpp +++ /dev/null @@ -1,45 +0,0 @@ -// 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. -#include "test/test_utils.h" - -#include -#include - -#include - -namespace file_selector_windows { -namespace test { - -namespace { - -// Creates a temp file and returns its path. -std::wstring CreateTempFile() { - wchar_t temp_dir[MAX_PATH]; - wchar_t temp_file[MAX_PATH]; - wchar_t long_path[MAX_PATH]; - ::GetTempPath(MAX_PATH, temp_dir); - ::GetTempFileName(temp_dir, L"test", 0, temp_file); - // Convert to long form to match what IShellItem queries will return. - ::GetLongPathName(temp_file, long_path, MAX_PATH); - return long_path; -} - -} // namespace - -ScopedTestShellItem::ScopedTestShellItem() { - path_ = CreateTempFile(); - ::SHCreateItemFromParsingName(path_.c_str(), nullptr, IID_PPV_ARGS(&item_)); -} - -ScopedTestShellItem::~ScopedTestShellItem() { ::DeleteFile(path_.c_str()); } - -ScopedTestFileIdList::ScopedTestFileIdList() { - path_ = CreateTempFile(); - item_ = ItemIdListPtr(::ILCreateFromPath(path_.c_str())); -} - -ScopedTestFileIdList::~ScopedTestFileIdList() { ::DeleteFile(path_.c_str()); } - -} // namespace test -} // namespace file_selector_windows diff --git a/packages/file_selector/file_selector_windows/windows/test/test_utils.h b/packages/file_selector/file_selector_windows/windows/test/test_utils.h deleted file mode 100644 index 34106c50092f..000000000000 --- a/packages/file_selector/file_selector_windows/windows/test/test_utils.h +++ /dev/null @@ -1,91 +0,0 @@ -// 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. -#ifndef PACKAGES_FILE_SELECTOR_FILE_SELECTOR_WINDOWS_WINDOWS_TEST_TEST_UTILS_H_ -#define PACKAGES_FILE_SELECTOR_FILE_SELECTOR_WINDOWS_WINDOWS_TEST_TEST_UTILS_H_ - -#include -#include -#include -#include -#include - -#include -#include -#include -#include - -#include "file_dialog_controller.h" - -_COM_SMARTPTR_TYPEDEF(IShellItem, IID_IShellItem); -_COM_SMARTPTR_TYPEDEF(IShellItemArray, IID_IShellItemArray); - -namespace file_selector_windows { -namespace test { - -// Creates a temp file, managed as an IShellItem, which will be deleted when -// the instance goes out of scope. -// -// This creates a file on the filesystem since creating IShellItem instances for -// files that don't exist is non-trivial. -class ScopedTestShellItem { - public: - ScopedTestShellItem(); - ~ScopedTestShellItem(); - - // Disallow copy and assign. - ScopedTestShellItem(const ScopedTestShellItem&) = delete; - ScopedTestShellItem& operator=(const ScopedTestShellItem&) = delete; - - // Returns the file's IShellItem reference. - IShellItemPtr file() { return item_; } - - // Returns the file's path. - const std::wstring& path() { return path_; } - - private: - IShellItemPtr item_; - std::wstring path_; -}; - -// Creates a temp file, managed as an ITEMIDLIST, which will be deleted when -// the instance goes out of scope. -// -// This creates a file on the filesystem since creating IShellItem instances for -// files that don't exist is non-trivial, and this is intended for use in -// creating IShellItemArray instances. -class ScopedTestFileIdList { - public: - ScopedTestFileIdList(); - ~ScopedTestFileIdList(); - - // Disallow copy and assign. - ScopedTestFileIdList(const ScopedTestFileIdList&) = delete; - ScopedTestFileIdList& operator=(const ScopedTestFileIdList&) = delete; - - // Returns the file's ITEMIDLIST reference. - PIDLIST_ABSOLUTE file() { return item_.get(); } - - // Returns the file's path. - const std::wstring& path() { return path_; } - - private: - // Smart pointer for managing ITEMIDLIST instances. - struct ItemIdListDeleter { - void operator()(LPITEMIDLIST item) { - if (item) { - ::ILFree(item); - } - } - }; - using ItemIdListPtr = std::unique_ptr, - ItemIdListDeleter>; - - ItemIdListPtr item_; - std::wstring path_; -}; - -} // namespace test -} // namespace file_selector_windows - -#endif // PACKAGES_FILE_SELECTOR_FILE_SELECTOR_WINDOWS_WINDOWS_TEST_TEST_UTILS_H_