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

Commit 8f56715

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
1 parent 3cd4360 commit 8f56715

16 files changed

+1220
-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: 198 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,198 @@
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+
String extensionsForLabel = '';
120+
for (final String extensionFile in option.extensions!) {
121+
extensionsForLabel += '*.$extensionFile;';
122+
}
123+
filterSpecification[label ?? extensionsForLabel] = extensionsForLabel;
124+
}
125+
}
126+
}
127+
128+
using((Arena arena) {
129+
final Pointer<COMDLG_FILTERSPEC> registerFilterSpecification =
130+
arena<COMDLG_FILTERSPEC>(filterSpecification.length);
131+
132+
int index = 0;
133+
for (final String key in filterSpecification.keys) {
134+
registerFilterSpecification[index]
135+
..pszName = TEXT(key)
136+
..pszSpec = TEXT(filterSpecification[key]!);
137+
index++;
138+
}
139+
140+
_lastResult = _dialogController.setFileTypes(
141+
filterSpecification.length, registerFilterSpecification);
142+
});
143+
}
144+
145+
/// Displays the dialog, and returns the selected files, or null on error.
146+
List<String?>? show(int parentWindow) {
147+
_lastResult = _dialogController.show(parentWindow);
148+
if (!SUCCEEDED(_lastResult)) {
149+
return null;
150+
}
151+
late List<String>? files;
152+
153+
using((Arena arena) {
154+
final Pointer<Pointer<COMObject>> shellItemArrayPtr =
155+
arena<Pointer<COMObject>>();
156+
final Pointer<Uint32> shellItemCountPtr = arena<Uint32>();
157+
final Pointer<Pointer<COMObject>> shellItemPtr =
158+
arena<Pointer<COMObject>>();
159+
160+
files =
161+
_getFilePathList(shellItemArrayPtr, shellItemCountPtr, shellItemPtr);
162+
});
163+
return files;
164+
}
165+
166+
List<String>? _getFilePathList(
167+
Pointer<Pointer<COMObject>> shellItemArrayPtr,
168+
Pointer<Uint32> shellItemCountPtr,
169+
Pointer<Pointer<COMObject>> shellItemPtr) {
170+
final List<String> files = <String>[];
171+
if (_isOpenDialog) {
172+
_lastResult = _dialogController.getResults(shellItemArrayPtr);
173+
if (!SUCCEEDED(_lastResult)) {
174+
return null;
175+
}
176+
177+
final IShellItemArray shellItemResources =
178+
IShellItemArray(shellItemArrayPtr.cast());
179+
_lastResult = shellItemResources.getCount(shellItemCountPtr);
180+
if (!SUCCEEDED(_lastResult)) {
181+
return null;
182+
}
183+
for (int index = 0; index < shellItemCountPtr.value; index++) {
184+
shellItemResources.getItemAt(index, shellItemPtr);
185+
final IShellItem shellItem = IShellItem(shellItemPtr.cast());
186+
files.add(_shellWin32Api.getPathForShellItem(shellItem));
187+
}
188+
} else {
189+
_lastResult = _dialogController.getResult(shellItemPtr);
190+
if (!SUCCEEDED(_lastResult)) {
191+
return null;
192+
}
193+
final IShellItem shellItem = IShellItem(shellItemPtr.cast());
194+
files.add(_shellWin32Api.getPathForShellItem(shellItem));
195+
}
196+
return files;
197+
}
198+
}
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
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:win32/win32.dart';
8+
9+
import 'ifile_open_dialog_factory.dart';
10+
11+
/// A thin wrapper for IFileDialog to allow for faking and inspection in tests.
12+
///
13+
/// Since this class defines the end of what can be unit tested, it should
14+
/// contain as little logic as possible.
15+
class FileDialogController {
16+
/// Creates a controller managing [IFileDialog](https://pub.dev/documentation/win32/latest/winrt/IFileDialog-class.html).
17+
/// It also receives an IFileOpenDialogFactory to construct [IFileOpenDialog]
18+
/// instances.
19+
FileDialogController(
20+
IFileDialog fileDialog, IFileOpenDialogFactory iFileOpenDialogFactory)
21+
: _fileDialog = fileDialog,
22+
_iFileOpenDialogFactory = iFileOpenDialogFactory;
23+
24+
/// The [IFileDialog] to work with.
25+
final IFileDialog _fileDialog;
26+
27+
/// The [IFileOpenDialogFactory] to work construc [IFileOpenDialog] instances.
28+
final IFileOpenDialogFactory _iFileOpenDialogFactory;
29+
30+
/// Sets the default folder for the dialog to [path]. It also returns the operation result.
31+
int setFolder(Pointer<COMObject> path) {
32+
return _fileDialog.setFolder(path);
33+
}
34+
35+
/// Sets the file [name] that is initially shown in the IFileDialog. It also returns the operation result.
36+
int setFileName(String name) {
37+
return _fileDialog.setFileName(TEXT(name));
38+
}
39+
40+
/// Sets the allowed file type extensions in the IFileOpenDialog. It also returns the operation result.
41+
int setFileTypes(int count, Pointer<COMDLG_FILTERSPEC> filters) {
42+
return _fileDialog.setFileTypes(count, filters);
43+
}
44+
45+
/// Sets the label of the confirmation button. It also returns the operation result. It also returns the operation result.
46+
int setOkButtonLabel(String text) {
47+
return _fileDialog.setOkButtonLabel(TEXT(text));
48+
}
49+
50+
/// Gets the IFileDialog's [options](https://pub.dev/documentation/win32/latest/winrt/FILEOPENDIALOGOPTIONS-class.html),
51+
/// which is a bitfield. It also returns the operation result.
52+
int getOptions(Pointer<Uint32> outOptions) {
53+
return _fileDialog.getOptions(outOptions);
54+
}
55+
56+
/// Sets the [options](https://pub.dev/documentation/win32/latest/winrt/FILEOPENDIALOGOPTIONS-class.html),
57+
/// which is a bitfield, into the IFileDialog. It also returns the operation result.
58+
int setOptions(int options) {
59+
return _fileDialog.setOptions(options);
60+
}
61+
62+
/// Shows an IFileDialog using the given parent. It returns the operation result.
63+
int show(int parent) {
64+
return _fileDialog.show(parent);
65+
}
66+
67+
/// Return results from an IFileDialog. This should be used when selecting
68+
/// single items. It also returns the operation result.
69+
int getResult(Pointer<Pointer<COMObject>> outItem) {
70+
return _fileDialog.getResult(outItem);
71+
}
72+
73+
/// Return results from an IFileOpenDialog. This should be used when selecting
74+
/// single items. This function will fail if the IFileDialog* provided to the
75+
/// constructor was not an IFileOpenDialog instance, returning an E_FAIL
76+
/// error.
77+
int getResults(Pointer<Pointer<COMObject>> outItems) {
78+
IFileOpenDialog? fileOpenDialog;
79+
try {
80+
fileOpenDialog = _iFileOpenDialogFactory.from(_fileDialog);
81+
return fileOpenDialog.getResults(outItems);
82+
} catch (_) {
83+
return E_FAIL;
84+
} finally {
85+
fileOpenDialog?.release();
86+
if (fileOpenDialog != null) {
87+
free(fileOpenDialog.ptr);
88+
}
89+
}
90+
}
91+
}
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: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
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+
/// A wrapper of the IFileOpenDialog interface to use its from function.
8+
class IFileOpenDialogFactory {
9+
/// Wraps the IFileOpenDialog from function.
10+
IFileOpenDialog from(IFileDialog fileDialog) {
11+
return IFileOpenDialog.from(fileDialog);
12+
}
13+
}

0 commit comments

Comments
 (0)