Skip to content
This repository was archived by the owner on Feb 22, 2023. It is now read-only.

Commit ea26eaf

Browse files
eugerossettoadpinola
authored andcommitted
Add win32 package and base dialog wrapper class.
Add methods of FileDialogController Add FileDialogController tests. Extract the interface conversion to a new class to make it testable. Add DialogWrapper constructor. Add test constructor to DialogWrapper and add two tests. Add mock for FileDialogController and tests for implemented methods. free memory allocations remove unused properties in dialog_wrapper use Arena Return the value of the path. fix extension string list to remove trailing semicolon
1 parent 44f30a5 commit ea26eaf

File tree

9 files changed

+774
-0
lines changed

9 files changed

+774
-0
lines changed
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
// Copyright 2013 The Flutter Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
/// The kind of file dialog to show.
6+
enum DialogMode {
7+
/// Used for chosing files.
8+
Open,
9+
10+
/// Used for chosing a directory to save a file.
11+
Save
12+
}
Lines changed: 197 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,197 @@
1+
// Copyright 2013 The Flutter Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
import 'dart:core';
6+
import 'dart:ffi';
7+
8+
import 'package:ffi/ffi.dart';
9+
import 'package:file_selector_platform_interface/file_selector_platform_interface.dart';
10+
import 'package:flutter/cupertino.dart';
11+
import 'package:win32/win32.dart';
12+
13+
import 'dialog_mode.dart';
14+
import 'file_dialog_controller.dart';
15+
import 'ifile_dialog_controller_factory.dart';
16+
import 'ifile_dialog_factory.dart';
17+
import 'shell_win32_api.dart';
18+
19+
/// Wraps an IFileDialog, managing object lifetime as a scoped object and
20+
/// providing a simplified API for interacting with it as needed for the plugin.
21+
class DialogWrapper {
22+
/// Creates a DialogWrapper using a [IFileDialogControllerFactory] and a [DialogMode].
23+
/// It is also responsible of creating a [IFileDialog].
24+
DialogWrapper(IFileDialogControllerFactory fileDialogControllerFactory,
25+
IFileDialogFactory fileDialogFactory, this._dialogMode)
26+
: _isOpenDialog = _dialogMode == DialogMode.Open {
27+
try {
28+
final IFileDialog dialog = fileDialogFactory.createInstace(_dialogMode);
29+
_dialogController = fileDialogControllerFactory.createController(dialog);
30+
_shellWin32Api = ShellWin32Api();
31+
} catch (ex) {
32+
if (ex is WindowsException) {
33+
_lastResult = ex.hr;
34+
}
35+
}
36+
}
37+
38+
/// Creates a DialogWrapper for testing purposes.
39+
@visibleForTesting
40+
DialogWrapper.withFakeDependencies(FileDialogController dialogController,
41+
this._dialogMode, this._shellWin32Api)
42+
: _isOpenDialog = _dialogMode == DialogMode.Open,
43+
_dialogController = dialogController;
44+
45+
int _lastResult = S_OK;
46+
47+
final DialogMode _dialogMode;
48+
49+
final bool _isOpenDialog;
50+
51+
final String _allowAnyValue = 'Any';
52+
53+
final String _allowAnyExtension = '*.*';
54+
55+
late FileDialogController _dialogController;
56+
57+
late ShellWin32Api _shellWin32Api;
58+
59+
/// Returns the result of the last Win32 API call related to this object.
60+
int get lastResult => _lastResult;
61+
62+
/// Attempts to set the default folder for the dialog to [path], if it exists.
63+
void setFolder(String path) {
64+
if (path == null || path.isEmpty) {
65+
return;
66+
}
67+
68+
using((Arena arena) {
69+
final Pointer<GUID> ptrGuid = GUIDFromString(IID_IShellItem);
70+
final Pointer<Pointer<COMObject>> ptrPath = arena<Pointer<COMObject>>();
71+
_lastResult =
72+
_shellWin32Api.createItemFromParsingName(path, ptrGuid, ptrPath);
73+
74+
if (!SUCCEEDED(_lastResult)) {
75+
return;
76+
}
77+
78+
_dialogController.setFolder(ptrPath.value);
79+
});
80+
}
81+
82+
/// Sets the file name that is initially shown in the dialog.
83+
void setFileName(String name) {
84+
_dialogController.setFileName(name);
85+
}
86+
87+
/// Sets the label of the confirmation button.
88+
void setOkButtonLabel(String label) {
89+
_dialogController.setOkButtonLabel(label);
90+
}
91+
92+
/// Adds the given options to the dialog's current [options](https://pub.dev/documentation/win32/latest/winrt/FILEOPENDIALOGOPTIONS-class.html).
93+
/// Both are bitfields.
94+
void addOptions(int newOptions) {
95+
using((Arena arena) {
96+
final Pointer<Uint32> currentOptions = arena<Uint32>();
97+
_lastResult = _dialogController.getOptions(currentOptions);
98+
if (!SUCCEEDED(_lastResult)) {
99+
return;
100+
}
101+
currentOptions.value |= newOptions;
102+
_lastResult = _dialogController.setOptions(currentOptions.value);
103+
});
104+
}
105+
106+
/// Sets the filters for allowed file types to select.
107+
/// filters -> std::optional<EncodableList>
108+
void setFileTypeFilters(List<XTypeGroup> filters) {
109+
final Map<String, String> filterSpecification = <String, String>{};
110+
111+
if (filters.isEmpty) {
112+
filterSpecification[_allowAnyValue] = _allowAnyExtension;
113+
} else {
114+
for (final XTypeGroup option in filters) {
115+
final String? label = option.label;
116+
if (option.allowsAny || option.extensions!.isEmpty) {
117+
filterSpecification[label ?? _allowAnyValue] = _allowAnyExtension;
118+
} else {
119+
final String extensionsForLabel = option.extensions!
120+
.map((String extension) => '*.$extension')
121+
.join(';');
122+
filterSpecification[label ?? extensionsForLabel] = extensionsForLabel;
123+
}
124+
}
125+
}
126+
127+
using((Arena arena) {
128+
final Pointer<COMDLG_FILTERSPEC> registerFilterSpecification =
129+
arena<COMDLG_FILTERSPEC>(filterSpecification.length);
130+
131+
int index = 0;
132+
for (final String key in filterSpecification.keys) {
133+
registerFilterSpecification[index]
134+
..pszName = TEXT(key)
135+
..pszSpec = TEXT(filterSpecification[key]!);
136+
index++;
137+
}
138+
139+
_lastResult = _dialogController.setFileTypes(
140+
filterSpecification.length, registerFilterSpecification);
141+
});
142+
}
143+
144+
/// Displays the dialog, and returns the selected files, or null on error.
145+
List<String?>? show(int parentWindow) {
146+
_lastResult = _dialogController.show(parentWindow);
147+
if (!SUCCEEDED(_lastResult)) {
148+
return null;
149+
}
150+
late List<String>? files;
151+
152+
using((Arena arena) {
153+
final Pointer<Pointer<COMObject>> shellItemArrayPtr =
154+
arena<Pointer<COMObject>>();
155+
final Pointer<Uint32> shellItemCountPtr = arena<Uint32>();
156+
final Pointer<Pointer<COMObject>> shellItemPtr =
157+
arena<Pointer<COMObject>>();
158+
159+
files =
160+
_getFilePathList(shellItemArrayPtr, shellItemCountPtr, shellItemPtr);
161+
});
162+
return files;
163+
}
164+
165+
List<String>? _getFilePathList(
166+
Pointer<Pointer<COMObject>> shellItemArrayPtr,
167+
Pointer<Uint32> shellItemCountPtr,
168+
Pointer<Pointer<COMObject>> shellItemPtr) {
169+
final List<String> files = <String>[];
170+
if (_isOpenDialog) {
171+
_lastResult = _dialogController.getResults(shellItemArrayPtr);
172+
if (!SUCCEEDED(_lastResult)) {
173+
return null;
174+
}
175+
176+
final IShellItemArray shellItemResources =
177+
IShellItemArray(shellItemArrayPtr.cast());
178+
_lastResult = shellItemResources.getCount(shellItemCountPtr);
179+
if (!SUCCEEDED(_lastResult)) {
180+
return null;
181+
}
182+
for (int index = 0; index < shellItemCountPtr.value; index++) {
183+
shellItemResources.getItemAt(index, shellItemPtr);
184+
final IShellItem shellItem = IShellItem(shellItemPtr.cast());
185+
files.add(_shellWin32Api.getPathForShellItem(shellItem));
186+
}
187+
} else {
188+
_lastResult = _dialogController.getResult(shellItemPtr);
189+
if (!SUCCEEDED(_lastResult)) {
190+
return null;
191+
}
192+
final IShellItem shellItem = IShellItem(shellItemPtr.cast());
193+
files.add(_shellWin32Api.getPathForShellItem(shellItem));
194+
}
195+
return files;
196+
}
197+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
// Copyright 2013 The Flutter Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
import 'package:win32/win32.dart';
6+
7+
import 'file_dialog_controller.dart';
8+
import 'ifile_dialog_controller_factory.dart';
9+
import 'ifile_open_dialog_factory.dart';
10+
11+
/// Implementation of FileDialogControllerFactory that makes standard
12+
/// FileDialogController instances.
13+
class FileDialogControllerFactory implements IFileDialogControllerFactory {
14+
@override
15+
FileDialogController createController(IFileDialog dialog) {
16+
return FileDialogController(dialog, IFileOpenDialogFactory());
17+
}
18+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
// Copyright 2013 The Flutter Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
import 'package:win32/win32.dart';
6+
7+
import 'file_dialog_controller.dart';
8+
9+
/// Interface for creating FileDialogControllers, to allow for dependency
10+
/// injection.
11+
abstract class IFileDialogControllerFactory {
12+
/// Returns a FileDialogController to interact with the given [IFileDialog].
13+
FileDialogController createController(IFileDialog dialog);
14+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
// Copyright 2013 The Flutter Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
import 'package:win32/win32.dart';
6+
7+
import 'dialog_mode.dart';
8+
9+
/// A factory for [IFileDialog] instances.
10+
class IFileDialogFactory {
11+
/// Creates the corresponding IFileDialog instace. The caller is responsible of releasing the resource.
12+
IFileDialog createInstace(DialogMode dialogMode) {
13+
if (dialogMode == DialogMode.Open) {
14+
return FileOpenDialog.createInstance();
15+
}
16+
17+
return FileSaveDialog.createInstance();
18+
}
19+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
// Copyright 2013 The Flutter Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
import 'dart:ffi';
6+
7+
import 'package:ffi/ffi.dart';
8+
import 'package:win32/win32.dart';
9+
10+
/// A thin wrapper for Win32 platform specific Shell methods.
11+
///
12+
/// The only purpose of this class is to decouple specific Win32 Api call from the bussiness logic so it can be init tested in any environment.
13+
class ShellWin32Api {
14+
/// 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.
15+
/// If the directory doesn't exist it will return an error result.
16+
int createItemFromParsingName(String initialDirectory, Pointer<GUID> ptrGuid,
17+
Pointer<Pointer<NativeType>> ptrPath) {
18+
return SHCreateItemFromParsingName(
19+
TEXT(initialDirectory), nullptr, ptrGuid, ptrPath);
20+
}
21+
22+
/// Returns the path for [shellItem] as a UTF-8 string, or an empty string on
23+
/// failure.
24+
String getPathForShellItem(IShellItem shellItem) {
25+
return using((Arena arena) {
26+
final Pointer<Pointer<Utf16>> ptrPath = arena<Pointer<Utf16>>();
27+
28+
if (!SUCCEEDED(
29+
shellItem.getDisplayName(SIGDN.SIGDN_FILESYSPATH, ptrPath.cast()))) {
30+
return '';
31+
}
32+
33+
return ptrPath.value.toDartString();
34+
});
35+
}
36+
}

0 commit comments

Comments
 (0)