diff --git a/packages/file_selector/file_selector_windows/CHANGELOG.md b/packages/file_selector/file_selector_windows/CHANGELOG.md index e0e6152e9b2f..20b05b118b0e 100644 --- a/packages/file_selector/file_selector_windows/CHANGELOG.md +++ b/packages/file_selector/file_selector_windows/CHANGELOG.md @@ -1,5 +1,6 @@ -## NEXT +## 0.10.0 +* Migrates CCP implementation to Dart. * Updates minimum Flutter version to 2.10. ## 0.9.1+2 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..792c63aef8de 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,14 +3,21 @@ // found in the LICENSE file. import 'package:file_selector_platform_interface/file_selector_platform_interface.dart'; - +import 'src/file_selector.dart'; import 'src/messages.g.dart'; /// An implementation of [FileSelectorPlatform] for Windows. class FileSelectorWindows extends FileSelectorPlatform { - final FileSelectorApi _hostApi = FileSelectorApi(); + /// Constructor for FileSelectorWindows. It uses default parameters for the [FileSelector]. + FileSelectorWindows() : this.withFileSelectorAPI(null); + + /// Constructor for FileSelectorWindows. It receives a DartFileSelectorApi parameter allowing dependency injection. + FileSelectorWindows.withFileSelectorAPI(FileSelector? api) + : _api = api ?? FileSelector.withoutParameters(); + + final FileSelector _api; - /// Registers the Windows implementation. + /// Registers the Windows implementation. It uses default parameters for the [FileSelector]. static void registerWith() { FileSelectorPlatform.instance = FileSelectorWindows(); } @@ -21,15 +28,15 @@ class FileSelectorWindows extends FileSelectorPlatform { String? initialDirectory, String? confirmButtonText, }) async { - final List paths = await _hostApi.showOpenDialog( - SelectionOptions( + final List paths = _api.getFiles( + selectionOptions: SelectionOptions( allowMultiple: false, selectFolders: false, allowedTypes: _typeGroupsFromXTypeGroups(acceptedTypeGroups), ), - initialDirectory, - confirmButtonText); - return paths.isEmpty ? null : XFile(paths.first!); + initialDirectory: initialDirectory, + confirmButtonText: confirmButtonText); + return paths.isEmpty ? null : XFile(paths.first); } @override @@ -38,14 +45,14 @@ class FileSelectorWindows extends FileSelectorPlatform { String? initialDirectory, String? confirmButtonText, }) async { - final List paths = await _hostApi.showOpenDialog( - SelectionOptions( + final List paths = _api.getFiles( + selectionOptions: SelectionOptions( allowMultiple: true, selectFolders: false, allowedTypes: _typeGroupsFromXTypeGroups(acceptedTypeGroups), ), - initialDirectory, - confirmButtonText); + initialDirectory: initialDirectory, + confirmButtonText: confirmButtonText); return paths.map((String? path) => XFile(path!)).toList(); } @@ -56,16 +63,17 @@ 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); - return paths.isEmpty ? null : paths.first!; + final String? path = _api.getSavePath( + initialDirectory: initialDirectory, + suggestedFileName: suggestedName, + confirmButtonText: confirmButtonText, + selectionOptions: SelectionOptions( + allowMultiple: false, + selectFolders: false, + allowedTypes: _typeGroupsFromXTypeGroups(acceptedTypeGroups), + ), + ); + return Future.value(path); } @override @@ -73,15 +81,10 @@ class FileSelectorWindows extends FileSelectorPlatform { String? initialDirectory, String? confirmButtonText, }) async { - final List paths = await _hostApi.showOpenDialog( - SelectionOptions( - allowMultiple: false, - selectFolders: true, - allowedTypes: [], - ), - initialDirectory, - confirmButtonText); - return paths.isEmpty ? null : paths.first!; + final String? path = _api.getDirectoryPath( + initialDirectory: initialDirectory, + confirmButtonText: confirmButtonText); + return Future.value(path); } } diff --git a/packages/file_selector/file_selector_windows/lib/src/file_open_dialog_wrapper.dart b/packages/file_selector/file_selector_windows/lib/src/file_open_dialog_wrapper.dart new file mode 100644 index 000000000000..d58db3032e42 --- /dev/null +++ b/packages/file_selector/file_selector_windows/lib/src/file_open_dialog_wrapper.dart @@ -0,0 +1,109 @@ +// 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'; + +/// FileOpenDialogWrapper provides an abstraction to interact with IFileOpenDialog related methods. +class FileOpenDialogWrapper { + /// Sets the [options](https://pub.dev/documentation/win32/latest/winrt/FILEOPENDIALOGOPTIONS-class.html) given into an IFileOpenDialog. + int setOptions(int options, IFileOpenDialog dialog) { + return dialog.setOptions(options); + } + + /// Returns the IFileOpenDialog's [options](https://pub.dev/documentation/win32/latest/winrt/FILEOPENDIALOGOPTIONS-class.html). + int getOptions(Pointer ptrOptions, IFileOpenDialog dialog) { + return dialog.getOptions(ptrOptions); + } + + /// Sets confirmation button text on an IFileOpenDialog. If the [confirmationText] is null, 'Pick' will be used. + int setOkButtonLabel(String? confirmationText, IFileOpenDialog dialog) { + return dialog.setOkButtonLabel(TEXT(confirmationText ?? 'Pick')); + } + + /// Sets the allowed file type extensions in an IFileOpenDialog. + int setFileTypes( + Map filterSpecification, IFileOpenDialog dialog) { + int operationResult = 0; + using((Arena arena) { + final Pointer registerFilterSpecification = + arena(filterSpecification.length); + + int index = 0; + for (final String key in filterSpecification.keys) { + registerFilterSpecification[index] + ..pszName = TEXT(key) + ..pszSpec = TEXT(filterSpecification[key]!); + index++; + } + + operationResult = dialog.setFileTypes( + filterSpecification.length, registerFilterSpecification); + }); + + return operationResult; + } + + /// Shows an IFileOpenDialog using the given owner. + int show(int hwndOwner, IFileOpenDialog dialog) { + return dialog.show(hwndOwner); + } + + /// Release an IFileOpenDialog. + int release(IFileOpenDialog dialog) { + return dialog.release(); + } + + /// Return a result from an IFileOpenDialog. + int getResult( + Pointer> ptrCOMObject, IFileOpenDialog dialog) { + return dialog.getResult(ptrCOMObject); + } + + /// Return results from an IFileOpenDialog. This should be used when selecting multiple items. + int getResults( + Pointer> ptrCOMObject, IFileOpenDialog dialog) { + return dialog.getResults(ptrCOMObject); + } + + /// Sets the initial directory for an IFileOpenDialog. + int setFolder(Pointer> ptrPath, IFileOpenDialog dialog) { + return dialog.setFolder(ptrPath.value); + } + + /// Sets the suggested file name for an IFileOpenDialog. + int setFileName(String suggestedFileName, IFileOpenDialog dialog) { + return dialog.setFileName(TEXT(suggestedFileName)); + } + + /// 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(String initialDirectory, Pointer ptrGuid, + Pointer> ptrPath) { + return SHCreateItemFromParsingName( + TEXT(initialDirectory), nullptr, ptrGuid, ptrPath); + } + + /// Initilaize the COM library with the internal [CoInitializeEx](https://pub.dev/documentation/win32/latest/winrt/CoInitializeEx.html) method. + /// It uses the following parameters: + /// pvReserved (Pointer): nullptr + /// dwCoInit (int): COINIT_APARTMENTTHREADED | COINIT_DISABLE_OLE1DDE + /// COINIT_APARTMENTTHREADED: Initializes the thread for apartment-threaded object concurrency. + /// COINIT_DISABLE_OLE1DDE: Disables Dynamic Data Exchange for Ole1 [support](https://learn.microsoft.com/en-us/windows/win32/learnwin32/initializing-the-com-library). + int coInitializeEx() { + return CoInitializeEx( + nullptr, COINIT_APARTMENTTHREADED | COINIT_DISABLE_OLE1DDE); + } + + /// Creates instance of FileOpenDialog. + IFileOpenDialog createInstance() { + return FileOpenDialog.createInstance(); + } + + /// Closes the COM library on the current thread, unloads all DLLs loaded by the thread, frees any other resources that the thread maintains, and forces all RPC connections on the thread to close. + void coUninitialize() { + CoUninitialize(); + } +} diff --git a/packages/file_selector/file_selector_windows/lib/src/file_selector.dart b/packages/file_selector/file_selector_windows/lib/src/file_selector.dart new file mode 100644 index 000000000000..d9ecaeeccf62 --- /dev/null +++ b/packages/file_selector/file_selector_windows/lib/src/file_selector.dart @@ -0,0 +1,395 @@ +// 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/messages.g.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:win32/win32.dart'; +import 'file_open_dialog_wrapper.dart'; +import 'shell_item_wrapper.dart'; + +/// An abstraction that provides primitives to interact with the file system including: +/// * Open a file. +/// * Open multiple files. +/// * Select a directory. +/// * Return a file path to save a file. +class FileSelector { + /// Initializes a FileSelector instance. It receives a FileOpenDialogWrapper and a ShellItemWrapper allowing dependency injection, both of which can be null. + FileSelector(FileOpenDialogWrapper? fileOpenDialogWrapper, + ShellItemWrapper? shellItemWrapper) + : super() { + _fileOpenDialogWrapper = fileOpenDialogWrapper ?? FileOpenDialogWrapper(); + _shellItemWrapper = shellItemWrapper ?? ShellItemWrapper(); + } + + /// Initializes a FileSelector instance. It receives a FileOpenDialogWrapper and a ShellItemWrapper allowing dependency injection, both of which can be null. + FileSelector.withoutParameters() : this(null, null); + + /// Sets a filter for the file types shown. + /// + /// When using the Open dialog, the file types declared here are used to + /// filter the view. When using the Save dialog, these values determine which + /// file name extension is appended to the file name. + /// + /// The first value is the "friendly" name which is shown to the user (e.g. + /// `JPEG Files`); the second value is a filter, which may be a semicolon- + /// separated list (for example `*.jpg;*.jpeg`). + Map filterSpecification = {}; + + /// Sets the owner of the IFileDialog to be shown. + int hWndOwner = NULL; + + /// Whether the selected item should exist or not. This allows the user to select inexistent files. + bool fileMustExist = false; + + late FileOpenDialogWrapper _fileOpenDialogWrapper; + late ShellItemWrapper _shellItemWrapper; + + /// Returns a directory path from user selection. + /// A [WindowsException] is thrown if an error occurs. + String? getDirectoryPath({ + String? initialDirectory, + String? confirmButtonText, + }) { + fileMustExist = true; + final SelectionOptions selectionOptions = SelectionOptions( + allowMultiple: false, selectFolders: true, allowedTypes: []); + return _getDirectory( + initialDirectory: initialDirectory, + confirmButtonText: confirmButtonText, + selectionOptions: selectionOptions); + } + + /// Returns a full path, including file name and extension, from the user selection. + /// A [WindowsException] is thrown if an error occurs. + String? getSavePath({ + String? initialDirectory, + String? confirmButtonText, + String? suggestedFileName, + SelectionOptions? selectionOptions, + }) { + fileMustExist = false; + final SelectionOptions defaultSelectionOptions = SelectionOptions( + allowMultiple: false, selectFolders: true, allowedTypes: []); + return _getDirectory( + initialDirectory: initialDirectory, + confirmButtonText: confirmButtonText, + suggestedFileName: suggestedFileName, + selectionOptions: selectionOptions ?? defaultSelectionOptions); + } + + /// Returns a list of file paths form the user selection. + /// A [WindowsException] is thrown if an error occurs. + List getFiles( + {String? initialDirectory, + String? confirmButtonText, + required SelectionOptions selectionOptions}) { + IFileOpenDialog? dialog; + fileMustExist = false; + try { + int hResult = initializeComLibrary(); + dialog = _fileOpenDialogWrapper.createInstance(); + using((Arena arena) { + final Pointer ptrOptions = arena(); + + hResult = getOptions(ptrOptions, dialog!); + hResult = setDialogOptions(ptrOptions, selectionOptions, dialog); + }); + + hResult = setInitialDirectory(initialDirectory, dialog); + hResult = setFileTypeFilters(selectionOptions, dialog); + hResult = setOkButtonLabel(confirmButtonText, dialog); + hResult = _fileOpenDialogWrapper.show(hWndOwner, dialog); + + return returnSelectedElements(hResult, selectionOptions, dialog); + } finally { + _realeaseResources(dialog); + } + } + + /// Returns the IFileOpenDialog options which is a bitfield containing the union of options described in [FILEOPENDIALOGOPTIONS](https://pub.dev/documentation/win32/latest/winrt/FILEOPENDIALOGOPTIONS-class.html). + /// A [WindowsException] is thrown if an error occurs. + @visibleForTesting + int getOptions(Pointer ptrOptions, IFileOpenDialog dialog) { + final int hResult = _fileOpenDialogWrapper.getOptions(ptrOptions, dialog); + _validateResult(hResult); + + return hResult; + } + + /// Returns options based the given [options], which is a bitfield containing the union of options described in [FILEOPENDIALOGOPTIONS](https://pub.dev/documentation/win32/latest/winrt/FILEOPENDIALOGOPTIONS-class.html), and theh value of [selectionOptions]. + /// The [options](https://learn.microsoft.com/en-us/previous-versions/bb775856(v=vs.85)) that are used in this method are: + /// FOS_PATHMUSTEXIST: The item returned must exist. This is a default value. + /// FOS_FILEMUSTEXIST: The item returned must be in an existing folder. This is a default value. + /// FOS_PICKFOLDERS: Present the Open dialog offering a choice of folders rather than files. + /// FOS_ALLOWMULTISELECT: Enables the user to select multiple items in the open dialog. + @visibleForTesting + int getDialogOptions(int options, SelectionOptions selectionOptions) { + if (!fileMustExist) { + options &= ~FILEOPENDIALOGOPTIONS.FOS_PATHMUSTEXIST; + options &= ~FILEOPENDIALOGOPTIONS.FOS_FILEMUSTEXIST; + } + + if (selectionOptions.selectFolders) { + options |= FILEOPENDIALOGOPTIONS.FOS_PICKFOLDERS; + } + + if (selectionOptions.allowMultiple) { + options |= FILEOPENDIALOGOPTIONS.FOS_ALLOWMULTISELECT; + } + + return options; + } + + /// Sets the dialog options based on the fileMustExist value and the selectionOption given. + /// A [WindowsException] is thrown if an error occurs. + @visibleForTesting + int setDialogOptions(Pointer ptrOptions, + SelectionOptions selectionOptions, IFileOpenDialog dialog) { + final int options = getDialogOptions(ptrOptions.value, selectionOptions); + final int hResult = _fileOpenDialogWrapper.setOptions(options, dialog); + + _validateResult(hResult); + + return hResult; + } + + /// Sets the initial directory to a given dialog. It does nothing if the given directory is empty. + /// A [WindowsException] is thrown if an error occurs. + @visibleForTesting + int setInitialDirectory(String? initialDirectory, IFileOpenDialog dialog) { + int hResult = 0; + + if (initialDirectory == null || initialDirectory.isEmpty) { + return hResult; + } + + using((Arena arena) { + final Pointer ptrGuid = GUIDFromString(IID_IShellItem); + final Pointer> ptrPath = arena>(); + hResult = _fileOpenDialogWrapper.createItemFromParsingName( + initialDirectory, ptrGuid, ptrPath); + + _validateResult(hResult); + + hResult = _fileOpenDialogWrapper.setFolder(ptrPath, dialog); + + _validateResult(hResult); + }); + + return hResult; + } + + /// Initialize the COM library with the internal [CoInitializeEx](https://pub.dev/documentation/win32/latest/winrt/CoInitializeEx.html) method. + /// A [WindowsException] is thrown if an error occurs. + @visibleForTesting + int initializeComLibrary() { + final int hResult = _fileOpenDialogWrapper.coInitializeEx(); + _validateResult(hResult); + return hResult; + } + + /// Returns a list of directory paths from user interaction. It receives the IFileOpenDialog show result to verify whether the user has canceled the dialog or not. + /// A [WindowsException] is thrown if an error occurs. + @visibleForTesting + List returnSelectedElements( + int hResult, SelectionOptions selectionOptions, IFileOpenDialog dialog) { + final List selectedElements = []; + + if (FAILED(hResult)) { + if (hResult != HRESULT_FROM_WIN32(ERROR_CANCELLED)) { + throw WindowsException(hResult); + } + } else { + hResult = _getSelectedPathsFromUserInput( + selectionOptions, selectedElements, dialog); + } + + return selectedElements; + } + + /// Sets the confirmation button text on an IFileOpenDialog. If the [confirmationText] is null, 'Pick' will be used. + /// A [WindowsException] is thrown if an error occurs. + @visibleForTesting + int setOkButtonLabel( + String? confirmButtonText, + IFileOpenDialog dialog, + ) { + final int hResult = + _fileOpenDialogWrapper.setOkButtonLabel(confirmButtonText, dialog); + _validateResult(hResult); + return hResult; + } + + /// Sets file type filters for a given dialog. It deleted the previous filters. + /// A [WindowsException] is thrown if an error occurs. + @visibleForTesting + int setFileTypeFilters( + SelectionOptions selectionOptions, IFileOpenDialog fileDialog) { + _clearFilterSpecification(); + for (final TypeGroup? option in selectionOptions.allowedTypes) { + if (option == null || + option.extensions == null || + option.extensions.isEmpty) { + continue; + } + + final String label = option.label; + String extensionsForLabel = ''; + for (final String? extensionFile in option.extensions) { + if (extensionFile != null) { + extensionsForLabel += '*.$extensionFile;'; + } + } + filterSpecification[label] = extensionsForLabel; + } + + int hResult = 0; + if (filterSpecification.isNotEmpty) { + hResult = + _fileOpenDialogWrapper.setFileTypes(filterSpecification, fileDialog); + _validateResult(hResult); + } + + return hResult; + } + + /// Sets the suggested file name of the given dialog. It does nothing if the suggested file name is empty. + /// A [WindowsException] is thrown if an error occurs. + @visibleForTesting + int setSuggestedFileName( + String? suggestedFileName, IFileOpenDialog fileDialog) { + int hResult = 0; + if (suggestedFileName != null && suggestedFileName.isNotEmpty) { + hResult = + _fileOpenDialogWrapper.setFileName(suggestedFileName, fileDialog); + _validateResult(hResult); + } + + return hResult; + } + + /// Returns a directory path by opnening a dialog in which the user can pick a folder. It can be configured with a [initialDirectory], a customized text for the confirm button and a suggested file name. + /// A [WindowsException] is thrown if an error occurs. + String? _getDirectory({ + String? initialDirectory, + String? confirmButtonText, + String? suggestedFileName, + required SelectionOptions selectionOptions, + }) { + IFileOpenDialog? dialog; + try { + int hResult = initializeComLibrary(); + dialog = _fileOpenDialogWrapper.createInstance(); + using((Arena arena) { + final Pointer ptrOptions = arena(); + hResult = getOptions(ptrOptions, dialog!); + hResult = setDialogOptions(ptrOptions, selectionOptions, dialog); + }); + + hResult = setInitialDirectory(initialDirectory, dialog); + hResult = setFileTypeFilters(selectionOptions, dialog); + hResult = setOkButtonLabel(confirmButtonText, dialog); + hResult = setSuggestedFileName(suggestedFileName, dialog); + hResult = _fileOpenDialogWrapper.show(hWndOwner, dialog); + + final List selectedPaths = + returnSelectedElements(hResult, selectionOptions, dialog); + return selectedPaths.isEmpty ? null : selectedPaths.first; + } finally { + _realeaseResources(dialog); + } + } + + /// Releases the given dialog, if any, and uninitialize the COM library. + /// A [WindowsException] if thrown if an error occurs. + void _realeaseResources(IFileOpenDialog? dialog) { + int releaseResult = 0; + if (dialog != null) { + releaseResult = _fileOpenDialogWrapper.release(dialog); + } + _fileOpenDialogWrapper.coUninitialize(); + _validateResult(releaseResult); + } + + void _validateResult(int hResult) { + if (FAILED(hResult)) { + throw WindowsException(hResult); + } + } + + /// Returns the selected path form a given dialog. It uses the selectionOptions to determine if multiple or single items were selected. + /// A [WindowsException] if thrown if an error occurs. + int _getSelectedPathsFromUserInput( + SelectionOptions selectionOptions, + List selectedElements, + IFileOpenDialog dialog, + ) { + int hResult = 0; + using((Arena arena) { + final Pointer> ptrShellItemArray = + arena>(); + + if (selectionOptions.allowMultiple) { + hResult = _fileOpenDialogWrapper.getResults(ptrShellItemArray, dialog); + _validateResult(hResult); + final IShellItemArray iShellItemArray = + _shellItemWrapper.createShellItemArray(ptrShellItemArray); + final Pointer ptrNumberOfSelectedElements = arena(); + _shellItemWrapper.getCount( + ptrNumberOfSelectedElements, iShellItemArray); + + for (int index = 0; + index < ptrNumberOfSelectedElements.value; + index++) { + final Pointer> ptrShellItem = + arena>(); + + hResult = + _shellItemWrapper.getItemAt(index, ptrShellItem, iShellItemArray); + _validateResult(hResult); + + hResult = + _addSelectedPathFromPpsi(ptrShellItem, arena, selectedElements); + + _shellItemWrapper.release(iShellItemArray); + } + } else { + hResult = _fileOpenDialogWrapper.getResult(ptrShellItemArray, dialog); + _validateResult(hResult); + hResult = _addSelectedPathFromPpsi( + ptrShellItemArray, arena, selectedElements); + } + }); + + _validateResult(hResult); + + return hResult; + } + + /// Adds the selected path to a given list of paths, [selectedElements]. It uses given [ShellItem] pointer, and an [Arena] to allocate and release pointers. + /// A [WindowsException] if thrown if an error occurs. + int _addSelectedPathFromPpsi(Pointer> ptrShellItem, + Arena arena, List selectedElements) { + final IShellItem shellItem = + _shellItemWrapper.createShellItem(ptrShellItem); + final Pointer ptrPath = arena(); + + int hResult = _shellItemWrapper.getDisplayName(ptrPath, shellItem); + _validateResult(hResult); + + selectedElements.add(_shellItemWrapper.getUserSelectedPath(ptrPath)); + hResult = _shellItemWrapper.releaseItem(shellItem); + _validateResult(hResult); + + return hResult; + } + + /// Clears the current filter specification, this way a new filter can be specified. + void _clearFilterSpecification() { + filterSpecification = {}; + } +} diff --git a/packages/file_selector/file_selector_windows/lib/src/shell_item_wrapper.dart b/packages/file_selector/file_selector_windows/lib/src/shell_item_wrapper.dart new file mode 100644 index 000000000000..4a5dcaa83878 --- /dev/null +++ b/packages/file_selector/file_selector_windows/lib/src/shell_item_wrapper.dart @@ -0,0 +1,55 @@ +// 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'; + +/// ShellItemWrapper to interact with Shell Item related functions. +class ShellItemWrapper { + /// Create a shell item from a given pointer. + IShellItem createShellItem(Pointer> ptrShellItem) { + return IShellItem(ptrShellItem.cast()); + } + + /// Creates an array from a given pointer. + IShellItemArray createShellItemArray( + Pointer> ptrShellItemArray) { + return IShellItemArray(ptrShellItemArray.cast()); + } + + /// Gets display name for an item. + int getDisplayName(Pointer ptrPath, IShellItem item) { + return item.getDisplayName(SIGDN.SIGDN_FILESYSPATH, ptrPath.cast()); + } + + /// Returns the selected path by the user. + String getUserSelectedPath(Pointer ptrPath) { + final Pointer path = Pointer.fromAddress(ptrPath.value); + return path.toDartString(); + } + + /// Releases an IShellItem. + int releaseItem(IShellItem item) { + return item.release(); + } + + /// Gets the number of elements given a IShellItemArray. + void getCount(Pointer ptrNumberOfSelectedElements, + IShellItemArray iShellItemArray) { + iShellItemArray.getCount(ptrNumberOfSelectedElements); + } + + /// Gets the item at a giving position. + int getItemAt(int index, Pointer> ptrShellItem, + IShellItemArray iShellItemArray) { + return iShellItemArray.getItemAt(index, ptrShellItem); + } + + /// Releases the given IShellItemArray. + void release(IShellItemArray iShellItemArray) { + iShellItemArray.release(); + } +} diff --git a/packages/file_selector/file_selector_windows/pubspec.yaml b/packages/file_selector/file_selector_windows/pubspec.yaml index 52b9f97174b4..856c12f0a4e9 100644 --- a/packages/file_selector/file_selector_windows/pubspec.yaml +++ b/packages/file_selector/file_selector_windows/pubspec.yaml @@ -2,7 +2,7 @@ name: file_selector_windows description: Windows implementation of the file_selector plugin. repository: https://github.com/flutter/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+2 +version: 0.10.0 environment: sdk: ">=2.12.0 <3.0.0" @@ -14,13 +14,14 @@ flutter: platforms: windows: dartPluginClass: FileSelectorWindows - pluginClass: FileSelectorWindows dependencies: cross_file: ^0.3.1 + ffi: ^2.0.1 file_selector_platform_interface: ^2.1.0 flutter: sdk: flutter + win32: ^3.0.0 dev_dependencies: build_runner: 2.1.11 diff --git a/packages/file_selector/file_selector_windows/test/fake_open_file_dialog.dart b/packages/file_selector/file_selector_windows/test/fake_open_file_dialog.dart new file mode 100644 index 000000000000..92cfe2cd0f65 --- /dev/null +++ b/packages/file_selector/file_selector_windows/test/fake_open_file_dialog.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:flutter_test/flutter_test.dart'; +import 'package:win32/win32.dart'; + +// Fake IOpenFileDialog class +class FakeIOpenFileDialog extends Fake implements IFileOpenDialog { + int _getOptionsCalledTimes = 0; + int _getResultCalledTimes = 0; + int _setOptionsCalledTimes = 0; + + @override + int getOptions(Pointer pfos) { + _getOptionsCalledTimes++; + return 0; + } + + @override + int setOptions(int options) { + _setOptionsCalledTimes++; + return 0; + } + + @override + int getResult(Pointer> ppsi) { + _getResultCalledTimes++; + return 0; + } + + int getOptionsCalledTimes() { + return _getOptionsCalledTimes; + } + + int setOptionsCalledTimes() { + return _setOptionsCalledTimes; + } + + int getResultCalledTimes() { + return _getResultCalledTimes; + } +} diff --git a/packages/file_selector/file_selector_windows/test/fake_shell_item.dart b/packages/file_selector/file_selector_windows/test/fake_shell_item.dart new file mode 100644 index 000000000000..6983d27db8d4 --- /dev/null +++ b/packages/file_selector/file_selector_windows/test/fake_shell_item.dart @@ -0,0 +1,35 @@ +// 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 IShellItemArray class +class FakeIShellItem extends Fake implements IShellItem { + int _getDisplayNameCalledTimes = 0; + int _releaseCalledTimes = 0; + + @override + int getDisplayName(int sigdnName, Pointer> ppszName) { + _getDisplayNameCalledTimes++; + return 0; + } + + @override + int release() { + _releaseCalledTimes++; + return 0; + } + + int getDisplayNameCalledTimes() { + return _getDisplayNameCalledTimes; + } + + int releaseCalledTimes() { + return _releaseCalledTimes; + } +} diff --git a/packages/file_selector/file_selector_windows/test/fake_shell_item_array.dart b/packages/file_selector/file_selector_windows/test/fake_shell_item_array.dart new file mode 100644 index 000000000000..1fc486eeed7e --- /dev/null +++ b/packages/file_selector/file_selector_windows/test/fake_shell_item_array.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:flutter_test/flutter_test.dart'; +import 'package:win32/win32.dart'; + +// Fake IShellItemArray class +class FakeIShellItemArray extends Fake implements IShellItemArray { + int _getCountCalledTimes = 0; + int _getItemAtCalledTimes = 0; + int _releaseCalledTimes = 0; + + @override + int getCount(Pointer ptrCount) { + _getCountCalledTimes++; + return 0; + } + + @override + int getItemAt(int dwIndex, Pointer> ppsi) { + _getItemAtCalledTimes++; + return 0; + } + + @override + int release() { + _releaseCalledTimes++; + return 0; + } + + int getCountCalledTimes() { + return _getCountCalledTimes; + } + + int getItemAtCalledTimes() { + return _getItemAtCalledTimes; + } + + int releaseCalledTimes() { + return _releaseCalledTimes; + } +} diff --git a/packages/file_selector/file_selector_windows/test/file_open_dialog_wrapper_test.dart b/packages/file_selector/file_selector_windows/test/file_open_dialog_wrapper_test.dart new file mode 100644 index 000000000000..5b4360f13392 --- /dev/null +++ b/packages/file_selector/file_selector_windows/test/file_open_dialog_wrapper_test.dart @@ -0,0 +1,37 @@ +// 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_open_dialog_wrapper.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:win32/win32.dart'; + +import 'fake_open_file_dialog.dart'; + +void main() { + final FileOpenDialogWrapper fileOpenDialogWrapper = FileOpenDialogWrapper(); + final FakeIOpenFileDialog fakeFileOpenDialog = FakeIOpenFileDialog(); + + test('getOptions should call dialog getOptions', () { + final Pointer ptrOptions = calloc(); + fileOpenDialogWrapper.getOptions(ptrOptions, fakeFileOpenDialog); + expect(fakeFileOpenDialog.getOptionsCalledTimes(), 1); + free(ptrOptions); + }); + + test('setOptions should call dialog setOptions', () { + fileOpenDialogWrapper.setOptions(32, fakeFileOpenDialog); + expect(fakeFileOpenDialog.setOptionsCalledTimes(), 1); + }); + + test('getResult should call dialog getResult', () { + final Pointer> ptrCOMObject = + calloc>(); + fileOpenDialogWrapper.getResult(ptrCOMObject, fakeFileOpenDialog); + expect(fakeFileOpenDialog.getResultCalledTimes(), 1); + free(ptrCOMObject); + }); +} 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..02f3eea61a4d --- /dev/null +++ b/packages/file_selector/file_selector_windows/test/file_selector_api_test.dart @@ -0,0 +1,682 @@ +// 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_open_dialog_wrapper.dart'; +import 'package:file_selector_windows/src/file_selector.dart'; +import 'package:file_selector_windows/src/messages.g.dart'; +import 'package:file_selector_windows/src/shell_item_wrapper.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'; +import 'mock_open_file_dialog.dart'; + +@GenerateMocks([FileOpenDialogWrapper, ShellItemWrapper]) +void main() { + const int defaultReturnValue = 1; + const int successReturnValue = 0; + const String defaultPath = 'C:'; + const List expectedPaths = [defaultPath]; + const List expectedMultiplePaths = [defaultPath, defaultPath]; + TestWidgetsFlutterBinding.ensureInitialized(); + final MockFileOpenDialogWrapper mockFileOpenDialogWrapper = + MockFileOpenDialogWrapper(); + final MockShellItemWrapper mockShellItemWrapper = MockShellItemWrapper(); + late FileSelector api; + late Pointer ptrOptions; + late int hResult; + late IFileOpenDialog mockFileOpenDialog; + + tearDown(() { + reset(mockFileOpenDialogWrapper); + reset(mockShellItemWrapper); + }); + + group('#Isolated functions', () { + final TypeGroup imagesTypeGroup = + TypeGroup(extensions: [], label: 'Images'); + final SelectionOptions singleFileSelectionOptions = SelectionOptions( + allowMultiple: false, + selectFolders: false, + allowedTypes: [imagesTypeGroup], + ); + + setUp(() { + api = FileSelector(mockFileOpenDialogWrapper, mockShellItemWrapper); + ptrOptions = calloc(); + final Pointer ptrCOMObject = calloc(); + hResult = 0; + mockFileOpenDialog = MockOpenFileDialog(ptrCOMObject); + setDefaultMocks( + mockFileOpenDialogWrapper, + mockShellItemWrapper, + successReturnValue, + defaultReturnValue, + defaultPath, + mockFileOpenDialog); + }); + + test('setDirectoryOptions should call dialog setOptions', () { + final SelectionOptions selectionOptions = SelectionOptions( + allowMultiple: false, + selectFolders: true, + allowedTypes: []); + expect( + api.setDialogOptions( + ptrOptions, selectionOptions, mockFileOpenDialog), + defaultReturnValue); + verify(mockFileOpenDialogWrapper.setOptions(any, any)).called(1); + }); + + test('getOptions should call dialog getOptions', () { + expect( + api.getOptions(ptrOptions, mockFileOpenDialog), defaultReturnValue); + verify(mockFileOpenDialogWrapper.getOptions( + ptrOptions, mockFileOpenDialog)) + .called(defaultReturnValue); + }); + + test('addConfirmButtonLabel should call dialog setOkButtonLabel', () { + const String confirmationText = 'Text'; + expect(api.setOkButtonLabel(confirmationText, mockFileOpenDialog), + defaultReturnValue); + verify(mockFileOpenDialogWrapper.setOkButtonLabel( + confirmationText, mockFileOpenDialog)) + .called(defaultReturnValue); + }); + + test('addFileFilters should call dialog setFileTypes', () { + final TypeGroup typeGroup = + TypeGroup(extensions: ['jpg', 'png'], label: 'Images'); + + final SelectionOptions selectionOptions = SelectionOptions( + allowMultiple: true, + selectFolders: true, + allowedTypes: [typeGroup], + ); + + final Map filterSpecification = { + 'Images': '*.jpg;*.png;', + }; + + expect(api.setFileTypeFilters(selectionOptions, mockFileOpenDialog), + defaultReturnValue); + verify(mockFileOpenDialogWrapper.setFileTypes( + filterSpecification, mockFileOpenDialog)) + .called(1); + }); + + test( + 'invoking addFileFilters twice should call dialog setFileTypes with proper parameters', + () { + final TypeGroup typeGroup = + TypeGroup(extensions: ['jpg', 'png'], label: 'Images'); + + final SelectionOptions selectionOptions = SelectionOptions( + allowMultiple: true, + selectFolders: true, + allowedTypes: [typeGroup], + ); + + final Map filterSpecification = { + 'Images': '*.jpg;*.png;', + }; + + expect(api.setFileTypeFilters(selectionOptions, mockFileOpenDialog), + defaultReturnValue); + expect(api.setFileTypeFilters(selectionOptions, mockFileOpenDialog), + defaultReturnValue); + verify(mockFileOpenDialogWrapper.setFileTypes( + filterSpecification, mockFileOpenDialog)) + .called(2); + }); + + test( + 'addFileFilters should not call dialog setFileTypes if filterSpecification is empty', + () { + final TypeGroup typeGroup = + TypeGroup(extensions: [], label: 'Images'); + + final SelectionOptions selectionOptions = SelectionOptions( + allowMultiple: true, + selectFolders: true, + allowedTypes: [typeGroup], + ); + + expect(api.setFileTypeFilters(selectionOptions, mockFileOpenDialog), + successReturnValue); + verifyNever( + mockFileOpenDialogWrapper.setFileTypes(any, mockFileOpenDialog)); + }); + + test( + 'returnSelectedElements should call dialog getResult and should return selected path', + () { + expect( + api.returnSelectedElements( + hResult, singleFileSelectionOptions, mockFileOpenDialog), + expectedPaths); + verify(mockFileOpenDialogWrapper.getResult(any, mockFileOpenDialog)) + .called(1); + }); + + test( + 'returnSelectedElements should throw if dialog getResult returns an error', + () { + when(mockFileOpenDialogWrapper.getResult(any, any)).thenReturn(-1); + expect( + () => api.returnSelectedElements( + hResult, singleFileSelectionOptions, mockFileOpenDialog), + throwsA(predicate((Object? e) => e is WindowsException))); + + verify(mockFileOpenDialogWrapper.getResult(any, mockFileOpenDialog)) + .called(1); + verifyNever(mockShellItemWrapper.getDisplayName(any, any)); + }); + + test( + 'returnSelectedElements should throw if dialog getDisplayName returns an error', + () { + when(mockShellItemWrapper.getDisplayName(any, any)).thenReturn(-1); + expect( + () => api.returnSelectedElements( + hResult, singleFileSelectionOptions, mockFileOpenDialog), + throwsA(predicate((Object? e) => e is WindowsException))); + + verify(mockFileOpenDialogWrapper.getResult(any, mockFileOpenDialog)) + .called(1); + verify(mockShellItemWrapper.getDisplayName(any, any)).called(1); + }); + + test( + 'returnSelectedElements should throw if dialog releaseItem returns an error', + () { + when(mockShellItemWrapper.releaseItem(any)).thenReturn(-1); + expect( + () => api.returnSelectedElements( + hResult, singleFileSelectionOptions, mockFileOpenDialog), + throwsA(predicate((Object? e) => e is WindowsException))); + + verify(mockFileOpenDialogWrapper.getResult(any, mockFileOpenDialog)) + .called(1); + verify(mockShellItemWrapper.getDisplayName(any, any)).called(1); + verify(mockShellItemWrapper.releaseItem(any)).called(1); + }); + + test( + 'returnSelectedElements should return without a path when the user cancels interaction', + () { + const int cancelledhResult = -2147023673; + + expect( + api.returnSelectedElements( + cancelledhResult, singleFileSelectionOptions, mockFileOpenDialog), + []); + + verifyNever(mockFileOpenDialogWrapper.getResult(any, mockFileOpenDialog)); + verifyNever(mockShellItemWrapper.getDisplayName(any, any)); + verifyNever(mockShellItemWrapper.getUserSelectedPath(any)); + }); + + test('returnSelectedElements should call dialog getDisplayName', () { + expect( + api.returnSelectedElements( + hResult, singleFileSelectionOptions, mockFileOpenDialog), + expectedPaths); + verify(mockShellItemWrapper.getDisplayName(any, any)).called(1); + }); + + test('returnSelectedElements should call dialog getUserSelectedPath', () { + expect( + api.returnSelectedElements( + hResult, singleFileSelectionOptions, mockFileOpenDialog), + expectedPaths); + verify(mockShellItemWrapper.getUserSelectedPath(any)).called(1); + }); + + test('setInitialDirectory should return param if initialDirectory is empty', + () { + expect( + api.setInitialDirectory('', mockFileOpenDialog), successReturnValue); + }); + + test( + 'setInitialDirectory should return successReturnValue if initialDirectory is null', + () { + expect(api.setInitialDirectory(null, mockFileOpenDialog), + successReturnValue); + }); + + test('setInitialDirectory should success when initialDirectory is valid', + () { + expect(api.setInitialDirectory(defaultPath, mockFileOpenDialog), + successReturnValue); + }); + + test( + 'setInitialDirectory should throw WindowsException when initialDirectory is invalid', + () { + when(mockFileOpenDialogWrapper.createItemFromParsingName(any, any, any)) + .thenReturn(-1); + expect(() => api.setInitialDirectory(':/', mockFileOpenDialog), + throwsA(predicate((Object? e) => e is WindowsException))); + }); + + test('getSavePath should call setFileName', () { + const String fileName = 'fileName'; + expect( + api.getSavePath( + suggestedFileName: fileName, + ), + defaultPath); + verify(mockFileOpenDialogWrapper.setFileName(fileName, any)).called(1); + }); + + test('getSavePath should not call setFileName without a suggestedFileName', + () { + const String fileName = 'fileName'; + expect( + api.getSavePath( + confirmButtonText: 'Choose', + initialDirectory: defaultPath, + ), + defaultPath); + verifyNever(mockFileOpenDialogWrapper.setFileName(fileName, any)); + }); + + test('getOptions should return 8 if fileMustExist is false', () { + const int options = 6152; + expect(api.getDialogOptions(options, singleFileSelectionOptions), 8); + }); + + test( + 'getOptions should return 520 if fileMustExist is false and allowMultiple is true', + () { + const int options = 6152; + final SelectionOptions selectionOptions = SelectionOptions( + allowMultiple: true, + selectFolders: false, + allowedTypes: [imagesTypeGroup], + ); + expect(api.getDialogOptions(options, selectionOptions), 520); + }); + + test( + 'getOptions should return 40 if fileMustExist is false and selectFolders is true', + () { + const int options = 6152; + final SelectionOptions selectionOptions = SelectionOptions( + allowMultiple: false, + selectFolders: true, + allowedTypes: [imagesTypeGroup], + ); + expect(api.getDialogOptions(options, selectionOptions), 40); + }); + + test('getOptions should return 6152 if fileMustExist is true', () { + const int options = 6152; + final SelectionOptions selectionOptions = SelectionOptions( + allowMultiple: false, + selectFolders: false, + allowedTypes: [imagesTypeGroup], + ); + api.fileMustExist = true; + expect(api.getDialogOptions(options, selectionOptions), 6152); + }); + + test( + 'getOptions should return 6664 if fileMustExist is true and allowMultiple is true', + () { + const int options = 6152; + final SelectionOptions selectionOptions = SelectionOptions( + allowMultiple: true, + selectFolders: false, + allowedTypes: [imagesTypeGroup], + ); + api.fileMustExist = true; + expect(api.getDialogOptions(options, selectionOptions), 6664); + }); + + test( + 'getOptions should return 6184 if fileMustExist is true and selectFolders is true', + () { + const int options = 6152; + final SelectionOptions selectionOptions = SelectionOptions( + allowMultiple: false, + selectFolders: true, + allowedTypes: [imagesTypeGroup], + ); + api.fileMustExist = true; + expect(api.getDialogOptions(options, selectionOptions), 6184); + }); + + test( + 'getOptions should return 6696 if fileMustExist is true, allowMultiple is true and selectFolders is true', + () { + const int options = 6152; + final SelectionOptions selectionOptions = SelectionOptions( + allowMultiple: true, + selectFolders: true, + allowedTypes: [imagesTypeGroup], + ); + api.fileMustExist = true; + expect(api.getDialogOptions(options, selectionOptions), 6696); + }); + + test('getSavePath should call setFolder', () { + expect( + api.getSavePath( + confirmButtonText: 'Choose', + initialDirectory: defaultPath, + ), + defaultPath); + verify(mockFileOpenDialogWrapper.setFolder(any, any)).called(1); + }); + }); + + group('#Multi file selection', () { + final SelectionOptions multipleFileSelectionOptions = SelectionOptions( + allowMultiple: true, + selectFolders: false, + allowedTypes: [], + ); + setUp(() { + api = FileSelector(mockFileOpenDialogWrapper, mockShellItemWrapper); + ptrOptions = calloc(); + final Pointer ptrCOMObject = calloc(); + hResult = 0; + mockFileOpenDialog = MockOpenFileDialog(ptrCOMObject); + setDefaultMocks( + mockFileOpenDialogWrapper, + mockShellItemWrapper, + successReturnValue, + defaultReturnValue, + defaultPath, + mockFileOpenDialog); + }); + + test( + 'returnSelectedElements should call dialog getResults and return the paths', + () { + mockGetCount(mockShellItemWrapper, 1); + expect( + api.returnSelectedElements( + hResult, multipleFileSelectionOptions, mockFileOpenDialog), + expectedPaths); + verify(mockFileOpenDialogWrapper.getResults(any, any)).called(1); + }); + + test( + 'returnSelectedElements should call createShellItemArray and return the paths', + () { + mockGetCount(mockShellItemWrapper, 1); + expect( + api.returnSelectedElements( + hResult, multipleFileSelectionOptions, mockFileOpenDialog), + expectedPaths); + verify(mockShellItemWrapper.createShellItemArray(any)).called(1); + }); + + test('returnSelectedElements should call getCount and return the paths', + () { + mockGetCount(mockShellItemWrapper, 1); + expect( + api.returnSelectedElements( + hResult, multipleFileSelectionOptions, mockFileOpenDialog), + expectedPaths); + verify(mockShellItemWrapper.getCount(any, any)).called(1); + }); + + test('returnSelectedElements should call getItemAt and return the paths', + () { + const int selectedFiles = 2; + mockGetCount(mockShellItemWrapper, selectedFiles); + expect( + api.returnSelectedElements( + hResult, multipleFileSelectionOptions, mockFileOpenDialog), + expectedMultiplePaths); + verify(mockShellItemWrapper.getItemAt(any, any, any)) + .called(selectedFiles); + }); + + test('returnSelectedElements should call release and return the paths', () { + const int selectedFiles = 2; + mockGetCount(mockShellItemWrapper, selectedFiles); + expect( + api.returnSelectedElements( + hResult, multipleFileSelectionOptions, mockFileOpenDialog), + expectedMultiplePaths); + verify(mockShellItemWrapper.release(any)).called(selectedFiles); + }); + + test('returnSelectedElements should call createShellItem', () { + const int selectedFiles = 2; + mockGetCount(mockShellItemWrapper, selectedFiles); + expect( + api.returnSelectedElements( + hResult, multipleFileSelectionOptions, mockFileOpenDialog), + expectedMultiplePaths); + verify(mockShellItemWrapper.createShellItem(any)).called(selectedFiles); + }); + + test('returnSelectedElements should call getDisplayName', () { + const int selectedFiles = 2; + mockGetCount(mockShellItemWrapper, selectedFiles); + expect( + api.returnSelectedElements( + hResult, multipleFileSelectionOptions, mockFileOpenDialog), + expectedMultiplePaths); + verify(mockShellItemWrapper.getDisplayName(any, any)) + .called(selectedFiles); + }); + + test('returnSelectedElements should call getUserSelectedPath', () { + const int selectedFiles = 2; + mockGetCount(mockShellItemWrapper, selectedFiles); + expect( + api.returnSelectedElements( + hResult, multipleFileSelectionOptions, mockFileOpenDialog), + expectedMultiplePaths); + verify(mockShellItemWrapper.getUserSelectedPath(any)) + .called(selectedFiles); + }); + + test('returnSelectedElements should call releaseItem', () { + const int selectedFiles = 2; + mockGetCount(mockShellItemWrapper, selectedFiles); + expect( + api.returnSelectedElements( + hResult, multipleFileSelectionOptions, mockFileOpenDialog), + expectedMultiplePaths); + verify(mockShellItemWrapper.releaseItem(any)).called(selectedFiles); + }); + + test( + 'returnSelectedElements should throw if dialog getResults returns an error', + () { + when(mockFileOpenDialogWrapper.getResults(any, any)).thenReturn(-1); + + expect( + () => api.returnSelectedElements( + hResult, multipleFileSelectionOptions, mockFileOpenDialog), + throwsA(predicate((Object? e) => e is WindowsException))); + + verifyNever(mockShellItemWrapper.createShellItemArray(any)); + }); + + test('returnSelectedElements should throw if getItemAt returns an error', + () { + mockGetCount(mockShellItemWrapper, 1); + when(mockShellItemWrapper.getItemAt(any, any, any)).thenReturn(-1); + + expect( + () => api.returnSelectedElements( + hResult, multipleFileSelectionOptions, mockFileOpenDialog), + throwsA(predicate((Object? e) => e is WindowsException))); + + verifyNever(mockShellItemWrapper.createShellItem(any)); + }); + + test( + 'returnSelectedElements should throw if getDisplayName returns an error', + () { + mockGetCount(mockShellItemWrapper, 1); + when(mockShellItemWrapper.getDisplayName(any, any)).thenReturn(-1); + + expect( + () => api.returnSelectedElements( + hResult, multipleFileSelectionOptions, mockFileOpenDialog), + throwsA(predicate((Object? e) => e is WindowsException))); + + verifyNever(mockShellItemWrapper.getUserSelectedPath(any)); + }); + + test('returnSelectedElements should throw if releaseItem returns an error', + () { + mockGetCount(mockShellItemWrapper, 1); + when(mockShellItemWrapper.releaseItem(any)).thenReturn(-1); + + expect( + () => api.returnSelectedElements( + hResult, multipleFileSelectionOptions, mockFileOpenDialog), + throwsA(predicate((Object? e) => e is WindowsException))); + + verifyNever(mockShellItemWrapper.release(any)); + }); + }); + + group('#Public facing functions', () { + setUp(() { + api = FileSelector(mockFileOpenDialogWrapper, mockShellItemWrapper); + setDefaultMocks( + mockFileOpenDialogWrapper, + mockShellItemWrapper, + successReturnValue, + defaultReturnValue, + defaultPath, + mockFileOpenDialog); + }); + + test('getDirectory should return selected path', () { + expect(defaultPath, api.getDirectoryPath()); + }); + + test('getFile should return selected path', () { + final TypeGroup typeGroup = + TypeGroup(extensions: ['jpg'], label: 'Images'); + + final SelectionOptions selectionOptions = SelectionOptions( + allowMultiple: false, + selectFolders: false, + allowedTypes: [typeGroup], + ); + expect( + api.getFiles( + selectionOptions: selectionOptions, + initialDirectory: 'c:', + confirmButtonText: 'Choose'), + expectedPaths); + }); + + test('getFile with multiple selection should return selected paths', () { + mockGetCount(mockShellItemWrapper, 2); + final TypeGroup typeGroup = + TypeGroup(extensions: ['jpg'], label: 'Images'); + + final SelectionOptions selectionOptions = SelectionOptions( + allowMultiple: true, + selectFolders: false, + allowedTypes: [typeGroup], + ); + expect( + api.getFiles( + selectionOptions: selectionOptions, + initialDirectory: 'c:', + confirmButtonText: 'Choose'), + expectedMultiplePaths); + }); + + test('getSavePath should return full path with file name and extension', + () { + const String fileName = 'file.txt'; + when(mockShellItemWrapper.getUserSelectedPath(any)) + .thenReturn('$defaultPath$fileName'); + final TypeGroup typeGroup = + TypeGroup(extensions: ['txt'], label: 'Text'); + + final SelectionOptions selectionOptions = SelectionOptions( + allowMultiple: false, + selectFolders: false, + allowedTypes: [typeGroup], + ); + expect( + api.getSavePath( + confirmButtonText: 'Choose', + initialDirectory: defaultPath, + selectionOptions: selectionOptions, + suggestedFileName: fileName), + '$defaultPath$fileName'); + }); + }); +} + +void mockGetCount( + MockShellItemWrapper mockShellItemWrapper, int numberOfElements) { + when(mockShellItemWrapper.getCount(any, any)) + .thenAnswer((Invocation realInvocation) { + final Pointer pointer = + realInvocation.positionalArguments.first as Pointer; + pointer.value = numberOfElements; + }); +} + +void setDefaultMocks( + MockFileOpenDialogWrapper mockFileOpenDialogWrapper, + MockShellItemWrapper mockShellItemWrapper, + int successReturnValue, + int defaultReturnValue, + String defaultPath, + IFileOpenDialog dialog) { + final Pointer> ppsi = calloc>(); + when(mockFileOpenDialogWrapper.setOptions(any, any)) + .thenReturn(defaultReturnValue); + when(mockFileOpenDialogWrapper.getOptions(any, any)) + .thenReturn(defaultReturnValue); + when(mockFileOpenDialogWrapper.setOkButtonLabel(any, any)) + .thenReturn(defaultReturnValue); + when(mockFileOpenDialogWrapper.setFileTypes(any, any)) + .thenReturn(defaultReturnValue); + when(mockFileOpenDialogWrapper.show(any, any)).thenReturn(defaultReturnValue); + when(mockFileOpenDialogWrapper.getResult(any, any)) + .thenReturn(defaultReturnValue); + when(mockFileOpenDialogWrapper.getResults(any, any)) + .thenReturn(defaultReturnValue); + when(mockFileOpenDialogWrapper.release(any)).thenReturn(defaultReturnValue); + when(mockFileOpenDialogWrapper.setFolder(any, any)) + .thenReturn(successReturnValue); + when(mockFileOpenDialogWrapper.setFileName(any, any)) + .thenReturn(defaultReturnValue); + when(mockFileOpenDialogWrapper.createItemFromParsingName(any, any, any)) + .thenReturn(defaultReturnValue); + when(mockFileOpenDialogWrapper.coInitializeEx()) + .thenReturn(defaultReturnValue); + when(mockFileOpenDialogWrapper.createInstance()).thenReturn(dialog); + when(mockShellItemWrapper.createShellItem(any)) + .thenReturn(IShellItem(ppsi.cast())); + when(mockShellItemWrapper.createShellItemArray(any)) + .thenReturn(IShellItemArray(ppsi.cast())); + + when(mockShellItemWrapper.getDisplayName(any, any)) + .thenReturn(defaultReturnValue); + when(mockShellItemWrapper.getUserSelectedPath(any)).thenReturn(defaultPath); + when(mockShellItemWrapper.releaseItem(any)).thenReturn(defaultReturnValue); + when(mockShellItemWrapper.getItemAt(any, any, any)) + .thenReturn(defaultReturnValue); + free(ppsi); +} 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..16aa255b8163 --- /dev/null +++ b/packages/file_selector/file_selector_windows/test/file_selector_api_test.mocks.dart @@ -0,0 +1,369 @@ +// Mocks generated by Mockito 5.3.1 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 'dart:ffi' as _i4; + +import 'package:file_selector_windows/src/file_open_dialog_wrapper.dart' as _i3; +import 'package:file_selector_windows/src/shell_item_wrapper.dart' as _i5; +import 'package:mockito/mockito.dart' as _i1; +import 'package:win32/win32.dart' as _i2; + +// 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 _FakeIFileOpenDialog_0 extends _i1.SmartFake + implements _i2.IFileOpenDialog { + _FakeIFileOpenDialog_0( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +class _FakeIShellItem_1 extends _i1.SmartFake implements _i2.IShellItem { + _FakeIShellItem_1( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +class _FakeIShellItemArray_2 extends _i1.SmartFake + implements _i2.IShellItemArray { + _FakeIShellItemArray_2( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +/// A class which mocks [FileOpenDialogWrapper]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockFileOpenDialogWrapper extends _i1.Mock + implements _i3.FileOpenDialogWrapper { + MockFileOpenDialogWrapper() { + _i1.throwOnMissingStub(this); + } + + @override + int setOptions( + int? options, + _i2.IFileOpenDialog? dialog, + ) => + (super.noSuchMethod( + Invocation.method( + #setOptions, + [ + options, + dialog, + ], + ), + returnValue: 0, + ) as int); + @override + int getOptions( + _i4.Pointer<_i4.Uint32>? ptrOptions, + _i2.IFileOpenDialog? dialog, + ) => + (super.noSuchMethod( + Invocation.method( + #getOptions, + [ + ptrOptions, + dialog, + ], + ), + returnValue: 0, + ) as int); + @override + int setOkButtonLabel( + String? confirmationText, + _i2.IFileOpenDialog? dialog, + ) => + (super.noSuchMethod( + Invocation.method( + #setOkButtonLabel, + [ + confirmationText, + dialog, + ], + ), + returnValue: 0, + ) as int); + @override + int setFileTypes( + Map? filterSpecification, + _i2.IFileOpenDialog? dialog, + ) => + (super.noSuchMethod( + Invocation.method( + #setFileTypes, + [ + filterSpecification, + dialog, + ], + ), + returnValue: 0, + ) as int); + @override + int show( + int? hwndOwner, + _i2.IFileOpenDialog? dialog, + ) => + (super.noSuchMethod( + Invocation.method( + #show, + [ + hwndOwner, + dialog, + ], + ), + returnValue: 0, + ) as int); + @override + int release(_i2.IFileOpenDialog? dialog) => (super.noSuchMethod( + Invocation.method( + #release, + [dialog], + ), + returnValue: 0, + ) as int); + @override + int getResult( + _i4.Pointer<_i4.Pointer<_i2.COMObject>>? ptrCOMObject, + _i2.IFileOpenDialog? dialog, + ) => + (super.noSuchMethod( + Invocation.method( + #getResult, + [ + ptrCOMObject, + dialog, + ], + ), + returnValue: 0, + ) as int); + @override + int getResults( + _i4.Pointer<_i4.Pointer<_i2.COMObject>>? ptrCOMObject, + _i2.IFileOpenDialog? dialog, + ) => + (super.noSuchMethod( + Invocation.method( + #getResults, + [ + ptrCOMObject, + dialog, + ], + ), + returnValue: 0, + ) as int); + @override + int setFolder( + _i4.Pointer<_i4.Pointer<_i2.COMObject>>? ptrPath, + _i2.IFileOpenDialog? dialog, + ) => + (super.noSuchMethod( + Invocation.method( + #setFolder, + [ + ptrPath, + dialog, + ], + ), + returnValue: 0, + ) as int); + @override + int setFileName( + String? suggestedFileName, + _i2.IFileOpenDialog? dialog, + ) => + (super.noSuchMethod( + Invocation.method( + #setFileName, + [ + suggestedFileName, + dialog, + ], + ), + returnValue: 0, + ) as int); + @override + int createItemFromParsingName( + String? initialDirectory, + _i4.Pointer<_i2.GUID>? ptrGuid, + _i4.Pointer<_i4.Pointer<_i4.NativeType>>? ptrPath, + ) => + (super.noSuchMethod( + Invocation.method( + #createItemFromParsingName, + [ + initialDirectory, + ptrGuid, + ptrPath, + ], + ), + returnValue: 0, + ) as int); + @override + int coInitializeEx() => (super.noSuchMethod( + Invocation.method( + #coInitializeEx, + [], + ), + returnValue: 0, + ) as int); + @override + _i2.IFileOpenDialog createInstance() => (super.noSuchMethod( + Invocation.method( + #createInstance, + [], + ), + returnValue: _FakeIFileOpenDialog_0( + this, + Invocation.method( + #createInstance, + [], + ), + ), + ) as _i2.IFileOpenDialog); + @override + void coUninitialize() => super.noSuchMethod( + Invocation.method( + #coUninitialize, + [], + ), + returnValueForMissingStub: null, + ); +} + +/// A class which mocks [ShellItemWrapper]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockShellItemWrapper extends _i1.Mock implements _i5.ShellItemWrapper { + MockShellItemWrapper() { + _i1.throwOnMissingStub(this); + } + + @override + _i2.IShellItem createShellItem( + _i4.Pointer<_i4.Pointer<_i2.COMObject>>? ptrShellItem) => + (super.noSuchMethod( + Invocation.method( + #createShellItem, + [ptrShellItem], + ), + returnValue: _FakeIShellItem_1( + this, + Invocation.method( + #createShellItem, + [ptrShellItem], + ), + ), + ) as _i2.IShellItem); + @override + _i2.IShellItemArray createShellItemArray( + _i4.Pointer<_i4.Pointer<_i2.COMObject>>? ptrShellItemArray) => + (super.noSuchMethod( + Invocation.method( + #createShellItemArray, + [ptrShellItemArray], + ), + returnValue: _FakeIShellItemArray_2( + this, + Invocation.method( + #createShellItemArray, + [ptrShellItemArray], + ), + ), + ) as _i2.IShellItemArray); + @override + int getDisplayName( + _i4.Pointer<_i4.IntPtr>? ptrPath, + _i2.IShellItem? item, + ) => + (super.noSuchMethod( + Invocation.method( + #getDisplayName, + [ + ptrPath, + item, + ], + ), + returnValue: 0, + ) as int); + @override + String getUserSelectedPath(_i4.Pointer<_i4.IntPtr>? ptrPath) => + (super.noSuchMethod( + Invocation.method( + #getUserSelectedPath, + [ptrPath], + ), + returnValue: '', + ) as String); + @override + int releaseItem(_i2.IShellItem? item) => (super.noSuchMethod( + Invocation.method( + #releaseItem, + [item], + ), + returnValue: 0, + ) as int); + @override + void getCount( + _i4.Pointer<_i4.Uint32>? ptrNumberOfSelectedElements, + _i2.IShellItemArray? iShellItemArray, + ) => + super.noSuchMethod( + Invocation.method( + #getCount, + [ + ptrNumberOfSelectedElements, + iShellItemArray, + ], + ), + returnValueForMissingStub: null, + ); + @override + int getItemAt( + int? index, + _i4.Pointer<_i4.Pointer<_i2.COMObject>>? ptrShellItem, + _i2.IShellItemArray? iShellItemArray, + ) => + (super.noSuchMethod( + Invocation.method( + #getItemAt, + [ + index, + ptrShellItem, + iShellItemArray, + ], + ), + returnValue: 0, + ) as int); + @override + void release(_i2.IShellItemArray? iShellItemArray) => super.noSuchMethod( + Invocation.method( + #release, + [iShellItemArray], + ), + returnValueForMissingStub: null, + ); +} 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 79277fae472b..d3dda452df02 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,25 +4,30 @@ 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/file_selector.dart'; import 'package:file_selector_windows/src/messages.g.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 'test_api.dart'; -@GenerateMocks([TestFileSelectorApi]) +@GenerateMocks([TestFileSelectorApi, FileSelector]) void main() { TestWidgetsFlutterBinding.ensureInitialized(); - final FileSelectorWindows plugin = FileSelectorWindows(); - late MockTestFileSelectorApi mockApi; + late FileSelectorWindows plugin; + late MockFileSelector mockFileSelector; setUp(() { - mockApi = MockTestFileSelectorApi(); - TestFileSelectorApi.setup(mockApi); + mockFileSelector = MockFileSelector(); + plugin = FileSelectorWindows.withFileSelectorAPI(mockFileSelector); + }); + + tearDown(() { + reset(mockFileSelector); }); test('registered instance', () { @@ -32,15 +37,17 @@ void main() { group('#openFile', () { setUp(() { - when(mockApi.showOpenDialog(any, any, any)).thenReturn(['foo']); + when(mockFileSelector.getFiles( + selectionOptions: anyNamed('selectionOptions'))) + .thenReturn(['foo']); }); test('simple call works', () async { final XFile? file = await plugin.openFile(); expect(file!.path, 'foo'); - final VerificationResult result = - verify(mockApi.showOpenDialog(captureAny, null, null)); + final VerificationResult result = verify(mockFileSelector.getFiles( + selectionOptions: captureAnyNamed('selectionOptions'))); final SelectionOptions options = result.captured[0] as SelectionOptions; expect(options.allowMultiple, false); expect(options.selectFolders, false); @@ -62,8 +69,10 @@ void main() { await plugin.openFile(acceptedTypeGroups: [group, groupTwo]); - final VerificationResult result = - verify(mockApi.showOpenDialog(captureAny, null, null)); + final VerificationResult result = verify(mockFileSelector.getFiles( + selectionOptions: captureAnyNamed('selectionOptions'), + initialDirectory: anyNamed('initialDirectory'), + confirmButtonText: anyNamed('confirmButtonText'))); final SelectionOptions options = result.captured[0] as SelectionOptions; expect( _typeGroupListsMatch(options.allowedTypes, [ @@ -74,15 +83,29 @@ void main() { }); test('passes initialDirectory correctly', () async { + when(mockFileSelector.getFiles( + selectionOptions: anyNamed('selectionOptions'), + initialDirectory: '/example/directory')) + .thenReturn(['foo']); await plugin.openFile(initialDirectory: '/example/directory'); - verify(mockApi.showOpenDialog(any, '/example/directory', null)); + verify(mockFileSelector.getFiles( + selectionOptions: anyNamed('selectionOptions'), + initialDirectory: '/example/directory', + confirmButtonText: anyNamed('confirmButtonText'))); }); test('passes confirmButtonText correctly', () async { + when(mockFileSelector.getFiles( + selectionOptions: anyNamed('selectionOptions'), + confirmButtonText: 'Open File')) + .thenReturn(['foo']); await plugin.openFile(confirmButtonText: 'Open File'); - verify(mockApi.showOpenDialog(any, null, 'Open File')); + verify(mockFileSelector.getFiles( + selectionOptions: anyNamed('selectionOptions'), + initialDirectory: anyNamed('initialDirectory'), + confirmButtonText: 'Open File')); }); test('throws for a type group that does not support Windows', () async { @@ -108,8 +131,11 @@ void main() { group('#openFiles', () { setUp(() { - when(mockApi.showOpenDialog(any, any, any)) - .thenReturn(['foo', 'bar']); + when(mockFileSelector.getFiles( + selectionOptions: anyNamed('selectionOptions'), + initialDirectory: anyNamed('initialDirectory'), + confirmButtonText: anyNamed('confirmButtonText'))) + .thenReturn(['foo', 'bar']); }); test('simple call works', () async { @@ -117,9 +143,12 @@ void main() { expect(file[0].path, 'foo'); expect(file[1].path, 'bar'); - final VerificationResult result = - verify(mockApi.showOpenDialog(captureAny, null, null)); + + final VerificationResult result = verify(mockFileSelector.getFiles( + selectionOptions: captureAnyNamed('selectionOptions'))); + final SelectionOptions options = result.captured[0] as SelectionOptions; + expect(options.allowMultiple, true); expect(options.selectFolders, false); }); @@ -140,8 +169,8 @@ void main() { await plugin.openFiles(acceptedTypeGroups: [group, groupTwo]); - final VerificationResult result = - verify(mockApi.showOpenDialog(captureAny, null, null)); + final VerificationResult result = verify(mockFileSelector.getFiles( + selectionOptions: captureAnyNamed('selectionOptions'))); final SelectionOptions options = result.captured[0] as SelectionOptions; expect( _typeGroupListsMatch(options.allowedTypes, [ @@ -154,13 +183,18 @@ void main() { test('passes initialDirectory correctly', () async { await plugin.openFiles(initialDirectory: '/example/directory'); - verify(mockApi.showOpenDialog(any, '/example/directory', null)); + verify(mockFileSelector.getFiles( + selectionOptions: anyNamed('selectionOptions'), + initialDirectory: '/example/directory')); }); test('passes confirmButtonText correctly', () async { await plugin.openFiles(confirmButtonText: 'Open Files'); - verify(mockApi.showOpenDialog(any, null, 'Open Files')); + verify(mockFileSelector.getFiles( + selectionOptions: anyNamed('selectionOptions'), + initialDirectory: anyNamed('initialDirectory'), + confirmButtonText: 'Open Files')); }); test('throws for a type group that does not support Windows', () async { @@ -184,47 +218,62 @@ void main() { }); }); + const String mockedPath = 'c://folder/foo'; + const String confirmText = 'Open Directory'; + const String initialDirectory = 'c://example/directory'; group('#getDirectoryPath', () { setUp(() { - when(mockApi.showOpenDialog(any, any, any)).thenReturn(['foo']); + when(mockFileSelector.getDirectoryPath()).thenReturn(mockedPath); + when(mockFileSelector.getDirectoryPath(confirmButtonText: confirmText)) + .thenReturn(mockedPath); + when(mockFileSelector.getDirectoryPath( + initialDirectory: initialDirectory)) + .thenReturn(mockedPath); }); test('simple call works', () async { - final String? path = await plugin.getDirectoryPath(); + final String? actualPath = await plugin.getDirectoryPath(); - expect(path, 'foo'); - final VerificationResult result = - verify(mockApi.showOpenDialog(captureAny, null, null)); - final SelectionOptions options = result.captured[0] as SelectionOptions; - expect(options.allowMultiple, false); - expect(options.selectFolders, true); + expect(actualPath, mockedPath); + verify(mockFileSelector.getDirectoryPath()); }); test('passes initialDirectory correctly', () async { - await plugin.getDirectoryPath(initialDirectory: '/example/directory'); + final String? actualPath = + await plugin.getDirectoryPath(initialDirectory: initialDirectory); - verify(mockApi.showOpenDialog(any, '/example/directory', null)); + verify(mockFileSelector.getDirectoryPath( + initialDirectory: initialDirectory)); + expect(actualPath, mockedPath); }); test('passes confirmButtonText correctly', () async { - await plugin.getDirectoryPath(confirmButtonText: 'Open Directory'); - - verify(mockApi.showOpenDialog(any, null, 'Open Directory')); + final String? actualPath = + await plugin.getDirectoryPath(confirmButtonText: confirmText); + verify(mockFileSelector.getDirectoryPath(confirmButtonText: confirmText)); + expect(actualPath, mockedPath); }); }); group('#getSavePath', () { setUp(() { - when(mockApi.showSaveDialog(any, any, any, any)) - .thenReturn(['foo']); + when(mockFileSelector.getSavePath( + selectionOptions: anyNamed('selectionOptions'), + confirmButtonText: anyNamed('confirmButtonText'), + initialDirectory: anyNamed('initialDirectory'), + suggestedFileName: anyNamed('suggestedFileName'))) + .thenReturn(mockedPath); }); test('simple call works', () async { - final String? path = await plugin.getSavePath(); - - expect(path, 'foo'); - final VerificationResult result = - verify(mockApi.showSaveDialog(captureAny, null, null, null)); + final String? actualPath = await plugin.getSavePath(); + + expect(actualPath, mockedPath); + final VerificationResult result = verify(mockFileSelector.getSavePath( + selectionOptions: captureAnyNamed('selectionOptions'), + confirmButtonText: anyNamed('confirmButtonText'), + initialDirectory: anyNamed('initialDirectory'), + suggestedFileName: anyNamed('suggestedFileName'))); final SelectionOptions options = result.captured[0] as SelectionOptions; expect(options.allowMultiple, false); expect(options.selectFolders, false); @@ -247,8 +296,11 @@ void main() { await plugin .getSavePath(acceptedTypeGroups: [group, groupTwo]); - final VerificationResult result = - verify(mockApi.showSaveDialog(captureAny, null, null, null)); + final VerificationResult result = verify(mockFileSelector.getSavePath( + selectionOptions: captureAnyNamed('selectionOptions'), + confirmButtonText: anyNamed('confirmButtonText'), + initialDirectory: anyNamed('initialDirectory'), + suggestedFileName: anyNamed('suggestedFileName'))); final SelectionOptions options = result.captured[0] as SelectionOptions; expect( _typeGroupListsMatch(options.allowedTypes, [ @@ -260,20 +312,31 @@ void main() { test('passes initialDirectory correctly', () async { await plugin.getSavePath(initialDirectory: '/example/directory'); - - verify(mockApi.showSaveDialog(any, '/example/directory', null, null)); + verify(mockFileSelector.getSavePath( + selectionOptions: captureAnyNamed('selectionOptions'), + confirmButtonText: anyNamed('confirmButtonText'), + initialDirectory: '/example/directory', + suggestedFileName: anyNamed('suggestedFileName'))); }); test('passes suggestedName correctly', () async { await plugin.getSavePath(suggestedName: 'baz.txt'); - verify(mockApi.showSaveDialog(any, null, 'baz.txt', null)); + verify(mockFileSelector.getSavePath( + selectionOptions: captureAnyNamed('selectionOptions'), + confirmButtonText: anyNamed('confirmButtonText'), + initialDirectory: anyNamed('initialDirectory'), + suggestedFileName: 'baz.txt')); }); test('passes confirmButtonText correctly', () async { await plugin.getSavePath(confirmButtonText: 'Save File'); - verify(mockApi.showSaveDialog(any, null, null, 'Save File')); + verify(mockFileSelector.getSavePath( + selectionOptions: anyNamed('selectionOptions'), + confirmButtonText: 'Save File', + initialDirectory: anyNamed('initialDirectory'), + suggestedFileName: anyNamed('suggestedFileName'))); }); test('throws for a type group that does not support Windows', () async { diff --git a/packages/file_selector/file_selector_windows/test/file_selector_windows_test.mocks.dart b/packages/file_selector/file_selector_windows/test/file_selector_windows_test.mocks.dart index 61e17fcdfeaa..8c3b5686da27 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,9 +1,14 @@ -// Mocks generated by Mockito 5.2.0 from annotations -// in file_selector_windows/example/windows/flutter/ephemeral/.plugin_symlinks/file_selector_windows/test/file_selector_windows_test.dart. +// Mocks generated by Mockito 5.3.1 from annotations +// in file_selector_windows/test/file_selector_windows_test.dart. // Do not manually edit this file. +// ignore_for_file: no_leading_underscores_for_library_prefixes +import 'dart:ffi' as _i5; + +import 'package:file_selector_windows/src/file_selector.dart' as _i4; import 'package:file_selector_windows/src/messages.g.dart' as _i3; import 'package:mockito/mockito.dart' as _i1; +import 'package:win32/win32.dart' as _i6; import 'test_api.dart' as _i2; @@ -16,6 +21,7 @@ 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]. /// @@ -27,20 +33,239 @@ class MockTestFileSelectorApi extends _i1.Mock } @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) => - (super.noSuchMethod( - Invocation.method(#showSaveDialog, - [options, initialDirectory, suggestedName, confirmButtonText]), - returnValue: []) as List); + _i3.SelectionOptions? options, + String? initialDirectory, + String? suggestedName, + String? confirmButtonText, + ) => + (super.noSuchMethod( + Invocation.method( + #showSaveDialog, + [ + options, + initialDirectory, + suggestedName, + confirmButtonText, + ], + ), + returnValue: [], + ) as List); +} + +/// A class which mocks [FileSelector]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockFileSelector extends _i1.Mock implements _i4.FileSelector { + MockFileSelector() { + _i1.throwOnMissingStub(this); + } + + @override + Map get filterSpecification => (super.noSuchMethod( + Invocation.getter(#filterSpecification), + returnValue: {}, + ) as Map); + @override + set filterSpecification(Map? _filterSpecification) => + super.noSuchMethod( + Invocation.setter( + #filterSpecification, + _filterSpecification, + ), + returnValueForMissingStub: null, + ); + @override + int get hWndOwner => (super.noSuchMethod( + Invocation.getter(#hWndOwner), + returnValue: 0, + ) as int); + @override + set hWndOwner(int? _hWndOwner) => super.noSuchMethod( + Invocation.setter( + #hWndOwner, + _hWndOwner, + ), + returnValueForMissingStub: null, + ); + @override + bool get fileMustExist => (super.noSuchMethod( + Invocation.getter(#fileMustExist), + returnValue: false, + ) as bool); + @override + set fileMustExist(bool? _fileMustExist) => super.noSuchMethod( + Invocation.setter( + #fileMustExist, + _fileMustExist, + ), + returnValueForMissingStub: null, + ); + @override + List getFiles({ + String? initialDirectory, + String? confirmButtonText, + required _i3.SelectionOptions? selectionOptions, + }) => + (super.noSuchMethod( + Invocation.method( + #getFiles, + [], + { + #initialDirectory: initialDirectory, + #confirmButtonText: confirmButtonText, + #selectionOptions: selectionOptions, + }, + ), + returnValue: [], + ) as List); + @override + int getOptions( + _i5.Pointer<_i5.Uint32>? ptrOptions, + _i6.IFileOpenDialog? dialog, + ) => + (super.noSuchMethod( + Invocation.method( + #getOptions, + [ + ptrOptions, + dialog, + ], + ), + returnValue: 0, + ) as int); + @override + int getDialogOptions( + int? options, + _i3.SelectionOptions? selectionOptions, + ) => + (super.noSuchMethod( + Invocation.method( + #getDialogOptions, + [ + options, + selectionOptions, + ], + ), + returnValue: 0, + ) as int); + @override + int setDialogOptions( + _i5.Pointer<_i5.Uint32>? ptrOptions, + _i3.SelectionOptions? selectionOptions, + _i6.IFileOpenDialog? dialog, + ) => + (super.noSuchMethod( + Invocation.method( + #setDialogOptions, + [ + ptrOptions, + selectionOptions, + dialog, + ], + ), + returnValue: 0, + ) as int); + @override + int setInitialDirectory( + String? initialDirectory, + _i6.IFileOpenDialog? dialog, + ) => + (super.noSuchMethod( + Invocation.method( + #setInitialDirectory, + [ + initialDirectory, + dialog, + ], + ), + returnValue: 0, + ) as int); + @override + int initializeComLibrary() => (super.noSuchMethod( + Invocation.method( + #initializeComLibrary, + [], + ), + returnValue: 0, + ) as int); + @override + List returnSelectedElements( + int? hResult, + _i3.SelectionOptions? selectionOptions, + _i6.IFileOpenDialog? dialog, + ) => + (super.noSuchMethod( + Invocation.method( + #returnSelectedElements, + [ + hResult, + selectionOptions, + dialog, + ], + ), + returnValue: [], + ) as List); + @override + int setOkButtonLabel( + String? confirmButtonText, + _i6.IFileOpenDialog? dialog, + ) => + (super.noSuchMethod( + Invocation.method( + #setOkButtonLabel, + [ + confirmButtonText, + dialog, + ], + ), + returnValue: 0, + ) as int); + @override + int setFileTypeFilters( + _i3.SelectionOptions? selectionOptions, + _i6.IFileOpenDialog? fileDialog, + ) => + (super.noSuchMethod( + Invocation.method( + #setFileTypeFilters, + [ + selectionOptions, + fileDialog, + ], + ), + returnValue: 0, + ) as int); + @override + int setSuggestedFileName( + String? suggestedFileName, + _i6.IFileOpenDialog? fileDialog, + ) => + (super.noSuchMethod( + Invocation.method( + #setSuggestedFileName, + [ + suggestedFileName, + fileDialog, + ], + ), + returnValue: 0, + ) as int); } diff --git a/packages/file_selector/file_selector_windows/test/mock_open_file_dialog.dart b/packages/file_selector/file_selector_windows/test/mock_open_file_dialog.dart new file mode 100644 index 000000000000..4037700d433e --- /dev/null +++ b/packages/file_selector/file_selector_windows/test/mock_open_file_dialog.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. + +import 'dart:ffi'; +import 'package:win32/win32.dart'; + +/// MockOpenFileDialog provides an instance of [IFileOpenDialog](https://pub.dev/documentation/win32/latest/winrt/IFileOpenDialog-class.html) for testing purposes. +class MockOpenFileDialog extends IFileOpenDialog { + /// OpenFileDialogMock's constructor, it receives an [COMObject](https://pub.dev/documentation/win32/latest/winrt/COMObject-class.html) [Pointer](https://api.dart.dev/stable/2.18.1/dart-ffi/Pointer-class.html) which is used in the super constructor. + MockOpenFileDialog(Pointer ptr) : super(ptr); +} diff --git a/packages/file_selector/file_selector_windows/test/shell_item_wrapper_test.dart b/packages/file_selector/file_selector_windows/test/shell_item_wrapper_test.dart new file mode 100644 index 000000000000..9b00d8a539e9 --- /dev/null +++ b/packages/file_selector/file_selector_windows/test/shell_item_wrapper_test.dart @@ -0,0 +1,77 @@ +// 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/shell_item_wrapper.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:win32/win32.dart'; + +import 'fake_shell_item.dart'; +import 'fake_shell_item_array.dart'; + +void main() { + final ShellItemWrapper shellItemWrapper = ShellItemWrapper(); + + test('creates a shell item instance', () { + final Pointer> ptrComObject = + calloc>(); + expect(shellItemWrapper.createShellItem(ptrComObject), isA()); + free(ptrComObject); + }); + + test('creates a shell item array instance', () { + final Pointer> ptrComObject = + calloc>(); + shellItemWrapper.createShellItemArray(ptrComObject); + expect(shellItemWrapper.createShellItemArray(ptrComObject), + isA()); + free(ptrComObject); + }); + + test('getCount invokes shellItemArray getCount', () { + final FakeIShellItemArray shellItemArray = FakeIShellItemArray(); + final Pointer ptrNumberOfItems = calloc(); + + shellItemWrapper.getCount(ptrNumberOfItems, shellItemArray); + expect(shellItemArray.getCountCalledTimes(), 1); + free(ptrNumberOfItems); + }); + + test('getDisplayName invokes shellItem getDisplayName', () { + final FakeIShellItem shellItem = FakeIShellItem(); + final Pointer ptrInt = calloc(); + + shellItemWrapper.getDisplayName(ptrInt, shellItem); + expect(shellItem.getDisplayNameCalledTimes(), 1); + free(ptrInt); + }); + + test('getItemAt invokes shellItem getItemAt', () { + final FakeIShellItemArray shellItemArray = FakeIShellItemArray(); + final Pointer ptrNumberOfItems = calloc(); + final Pointer> ptrShellItem = + calloc>(); + + shellItemWrapper.getItemAt(4, ptrShellItem, shellItemArray); + expect(shellItemArray.getItemAtCalledTimes(), 1); + free(ptrNumberOfItems); + free(ptrShellItem); + }); + + test('release invokes shellItemArray release', () { + final FakeIShellItemArray shellItemArray = FakeIShellItemArray(); + + shellItemWrapper.release(shellItemArray); + expect(shellItemArray.releaseCalledTimes(), 1); + }); + + test('releaseItem invokes shellItem release', () { + final FakeIShellItem shellItem = FakeIShellItem(); + + shellItemWrapper.releaseItem(shellItem); + expect(shellItem.releaseCalledTimes(), 1); + }); +} 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_