Skip to content

Commit

Permalink
Merge pull request #971 from miguelpruivo/feature/#915-modern-directo…
Browse files Browse the repository at this point in the history
…ry-picker-on-windows

#915 modern directory picker on Windows
  • Loading branch information
Miguel Ruivo authored Feb 28, 2022
2 parents 03cedea + 3fb350e commit bb0683c
Show file tree
Hide file tree
Showing 5 changed files with 95 additions and 70 deletions.
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
## 4.5.0

#### Desktop (Windows)
Changes the implementation of `getDirectoryPath()` on Windows to provide a modern dialog that looks the same as a file picker dialog ([#915](https://github.com/miguelpruivo/flutter_file_picker/issues/915)).

## 4.4.0

#### Desktop (Linux, macOS, and Windows)
Expand Down
12 changes: 12 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,18 @@ A package that allows you to use the native file explorer to pick single or mult

If you have any feature that you want to see in this package, please feel free to issue a suggestion. 🎉

## Compatibility Chart

| API | Android | iOS | Linux | macOS | Windows | Web |
| --------------------- | ------------------ | ------------------ | ------------------ | ------------------ | ------------------ | ------------------ |
| clearTemporaryFiles() | :white_check_mark: | :white_check_mark: | :x: | :x: | :x: | :x: |
| getDirectoryPath() | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :x: |
| pickFiles() | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: |
| saveFile() | :x: | :x: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :x: |

See the [API section of the File Picker Wiki](https://github.com/miguelpruivo/flutter_file_picker/wiki/api) or the [official API reference on pub.dev](https://pub.dev/documentation/file_picker/latest/file_picker/FilePicker-class.html) for further details.


## Documentation
See the **[File Picker Wiki](https://github.com/miguelpruivo/flutter_file_picker/wiki)** for every detail on about how to install, setup and use it.

Expand Down
8 changes: 6 additions & 2 deletions lib/src/file_picker.dart
Original file line number Diff line number Diff line change
Expand Up @@ -136,10 +136,14 @@ abstract class FilePicker extends PlatformInterface {
/// window). This parameter works only on Windows desktop.
///
/// [initialDirectory] can be optionally set to an absolute path to specify
/// where the dialog should open. Only supported on Linux, macOS, and Windows.
/// where the dialog should open. Only supported on Linux and macOS.
///
/// Returns `null` if aborted or if the folder path couldn't be resolved.
/// Returns a [Future<String?>] which resolves to the absolute path of the selected directory,
/// if the user selected a directory. Returns `null` if the user aborted the dialog or if the
/// folder path couldn't be resolved.
///
/// Note: on Windows, throws a `WindowsException` with a detailed error message, if the dialog
/// could not be instantiated or the dialog result could not be interpreted.
/// Note: Some Android paths are protected, hence can't be accessed and will return `/` instead.
Future<String?> getDirectoryPath({
String? dialogTitle,
Expand Down
137 changes: 70 additions & 67 deletions lib/src/windows/file_picker_windows.dart
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import 'package:file_picker/src/utils.dart';
import 'package:file_picker/src/exceptions.dart';
import 'package:file_picker/src/windows/file_picker_windows_ffi_types.dart';
import 'package:path/path.dart';
import 'package:win32/win32.dart';

FilePicker filePickerWithFFI() => FilePickerWindows();

Expand Down Expand Up @@ -59,20 +60,82 @@ class FilePickerWindows extends FilePicker {
return returnValue;
}

/// See API spec:
/// https://docs.microsoft.com/en-us/windows/win32/api/shobjidl_core/nn-shobjidl_core-ifiledialog
/// See example implementation:
/// https://github.com/timsneath/win32/blob/main/example/dialogshow.dart
@override
Future<String?> getDirectoryPath({
String? dialogTitle,
bool lockParentWindow = false,
String? initialDirectory,
}) {
final pathIdPointer =
_pickDirectory(dialogTitle ?? defaultDialogTitle, lockParentWindow);
if (pathIdPointer == null) {
return Future.value(null);
int hr = CoInitializeEx(
nullptr, COINIT_APARTMENTTHREADED | COINIT_DISABLE_OLE1DDE);

if (!SUCCEEDED(hr)) throw WindowsException(hr);

final fileDialog = FileOpenDialog.createInstance();

final optionsPointer = calloc<Uint32>();
hr = fileDialog.GetOptions(optionsPointer);
if (!SUCCEEDED(hr)) throw WindowsException(hr);

final options = optionsPointer.value |
FILEOPENDIALOGOPTIONS.FOS_PICKFOLDERS |
FILEOPENDIALOGOPTIONS.FOS_FORCEFILESYSTEM;
hr = fileDialog.SetOptions(options);
if (!SUCCEEDED(hr)) throw WindowsException(hr);

final title = TEXT(dialogTitle ?? defaultDialogTitle);
hr = fileDialog.SetTitle(title);
if (!SUCCEEDED(hr)) throw WindowsException(hr);
free(title);

// TODO: figure out how to set the initial directory via SetDefaultFolder / SetFolder
// if (initialDirectory != null) {
// final folder = TEXT(initialDirectory);
// final riid = calloc<COMObject>();
// final item = IShellItem(riid);
// final location = item.ptr;
// SHCreateItemFromParsingName(folder, nullptr, riid.cast(), item.ptr.cast());
// hr = fileDialog.AddPlace(item.ptr, FDAP.FDAP_TOP);
// if (!SUCCEEDED(hr)) throw WindowsException(hr);
// hr = fileDialog.SetFolder(location);
// if (!SUCCEEDED(hr)) throw WindowsException(hr);
// free(folder);
// }

final hwndOwner = lockParentWindow ? GetForegroundWindow() : NULL;
hr = fileDialog.Show(hwndOwner);
if (!SUCCEEDED(hr)) {
fileDialog.Release();
CoUninitialize();

if (hr == HRESULT_FROM_WIN32(ERROR_CANCELLED)) {
return Future.value(null);
}
throw WindowsException(hr);
}
return Future.value(
_getPathFromItemIdentifierList(pathIdPointer),
);

final ppsi = calloc<COMObject>();
hr = fileDialog.GetResult(ppsi.cast());
if (!SUCCEEDED(hr)) throw WindowsException(hr);

final item = IShellItem(ppsi);
final pathPtr = calloc<Pointer<Utf16>>();
hr = item.GetDisplayName(SIGDN.SIGDN_FILESYSPATH, pathPtr);
if (!SUCCEEDED(hr)) throw WindowsException(hr);

final path = pathPtr.value.toDartString();

hr = item.Release();
if (!SUCCEEDED(hr)) throw WindowsException(hr);

hr = fileDialog.Release();
CoUninitialize();

return Future.value(path);
}

@override
Expand Down Expand Up @@ -138,66 +201,6 @@ class FilePickerWindows extends FilePicker {
}
}

/// Uses the Win32 API to display a dialog box that enables the user to select a folder.
///
/// Returns a PIDL that specifies the location of the selected folder relative to the root of the
/// namespace. Returns null, if the user clicked on the "Cancel" button in the dialog box.
Pointer? _pickDirectory(String dialogTitle, bool lockParentWindow) {
final shell32 = DynamicLibrary.open('shell32.dll');

final shBrowseForFolderW =
shell32.lookupFunction<SHBrowseForFolderW, SHBrowseForFolderW>(
'SHBrowseForFolderW');

final Pointer<BROWSEINFOA> browseInfo = calloc<BROWSEINFOA>();
if (lockParentWindow) {
browseInfo.ref.hwndOwner = _getWindowHandle();
}
browseInfo.ref.pidlRoot = nullptr;
browseInfo.ref.pszDisplayName = calloc.allocate<Utf16>(maximumPathLength);
browseInfo.ref.lpszTitle = dialogTitle.toNativeUtf16();
browseInfo.ref.ulFlags =
bifEditBox | bifNewDialogStyle | bifReturnOnlyFsDirs;

final Pointer<NativeType> itemIdentifierList =
shBrowseForFolderW(browseInfo);

calloc.free(browseInfo.ref.pszDisplayName);
calloc.free(browseInfo.ref.lpszTitle);
calloc.free(browseInfo);

if (itemIdentifierList == nullptr) {
return null;
}
return itemIdentifierList;
}

/// Uses the Win32 API to convert an item identifier list to a file system path.
///
/// [lpItem] must contain the address of an item identifier list that specifies a
/// file or directory location relative to the root of the namespace (the desktop).
/// Returns the file system path as a [String]. Throws an exception, if the
/// conversion wasn't successful.
String _getPathFromItemIdentifierList(Pointer lpItem) {
final shell32 = DynamicLibrary.open('shell32.dll');

final shGetPathFromIDListW =
shell32.lookupFunction<SHGetPathFromIDListW, SHGetPathFromIDListWDart>(
'SHGetPathFromIDListW');

final Pointer<Utf16> pszPath = calloc.allocate<Utf16>(maximumPathLength);

final int result = shGetPathFromIDListW(lpItem, pszPath);
if (result == 0x00000000) {
throw Exception(
'Failed to convert item identifier list to a file system path.');
}

final path = pszPath.toDartString();
calloc.free(pszPath);
return path;
}

/// Extracts the list of selected files from the Win32 API struct [OPENFILENAMEW].
///
/// After the user has closed the file picker dialog, Win32 API sets the property
Expand Down
3 changes: 2 additions & 1 deletion pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ description: A package that allows you to use a native file explorer to pick sin
homepage: https://github.com/miguelpruivo/plugins_flutter_file_picker
repository: https://github.com/miguelpruivo/flutter_file_picker
issue_tracker: https://github.com/miguelpruivo/flutter_file_picker/issues
version: 4.4.0
version: 4.5.0

dependencies:
flutter:
Expand All @@ -15,6 +15,7 @@ dependencies:
plugin_platform_interface: ^2.0.0
ffi: ^1.1.2
path: ^1.8.0
win32: ^2.4.1

dev_dependencies:
lints: ^1.0.1
Expand Down

0 comments on commit bb0683c

Please sign in to comment.