Skip to content

Commit cac847f

Browse files
committed
Added multiple file/folder selection support (closed #10)
Note that the Scripting API has also changed slightly with this new feature. The OnSuccess callback now returns string[] instead of string and dialog functions now take an allowMultiSelection parameter (false by default)
1 parent 2863db1 commit cac847f

13 files changed

+1000
-342
lines changed

.github/README.md

Lines changed: 31 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,8 @@
1616
- Simple user interface
1717
- Draggable and resizable
1818
- Ability to choose folders instead of files
19-
- Supports runtime permissions on Android M+
19+
- Supports selecting multiple files/folders
20+
- Supports runtime permissions on Android M+ and *Storage Access Framework* on Android Q+
2021
- Optimized using a recycled list view (makes *Instantiate* calls sparingly)
2122

2223
**NOTE:** Universal Windows Platform (UWP) is not supported!
@@ -41,28 +42,28 @@ There are 5 ways to install this plugin:
4142

4243
First, add `using SimpleFileBrowser;` to your script.
4344

44-
The file browser can be shown either as a **save dialog** or a **load dialog**. In load mode, the returned path always leads to an existing file or folder. In save mode, the returned path can point to a non-existing file, as well. You can use the following functions to show the file browser:
45+
The file browser can be shown either as a **save dialog** or a **load dialog**. In load mode, the returned path(s) always lead to existing files or folders. In save mode, the returned path(s) can point to non-existing files, as well. You can use the following functions to show the file browser:
4546

4647
```csharp
47-
public static bool ShowSaveDialog( OnSuccess onSuccess, OnCancel onCancel, bool folderMode = false, string initialPath = null, string title = "Save", string saveButtonText = "Save" );
48-
public static bool ShowLoadDialog( OnSuccess onSuccess, OnCancel onCancel, bool folderMode = false, string initialPath = null, string title = "Load", string loadButtonText = "Select" );
48+
public static bool ShowSaveDialog( OnSuccess onSuccess, OnCancel onCancel, bool folderMode = false, bool allowMultiSelection = false, string initialPath = null, string title = "Save", string saveButtonText = "Save" );
49+
public static bool ShowLoadDialog( OnSuccess onSuccess, OnCancel onCancel, bool folderMode = false, bool allowMultiSelection = false, string initialPath = null, string title = "Load", string loadButtonText = "Select" );
4950

50-
public delegate void OnSuccess( string path );
51+
public delegate void OnSuccess( string[] paths );
5152
public delegate void OnCancel();
5253
```
5354

5455
There can only be one dialog active at a time. These functions will return *true* if the dialog is shown successfully (if no other dialog is active), *false* otherwise. You can query the **FileBrowser.IsOpen** property to see if there is an active dialog at the moment.
5556

56-
If user presses the *Cancel* button, **onCancel** callback is called. Otherwise, **onSuccess** callback is called with the path of the selected file/folder as parameter. When **folderMode** is set to *true*, the file browser will show only folders and the user will pick a folder instead of a file.
57+
If user presses the *Cancel* button, **onCancel** callback is called. Otherwise, **onSuccess** callback is called with the paths of the selected files/folders as parameter. When **folderMode** is set to *true*, the file browser will show only folders and the user will pick folders instead of files. Setting **allowMultiSelection** to *true* will allow picking multiple files/folders.
5758

5859
There are also coroutine variants of these functions that will yield while the dialog is active:
5960

6061
```csharp
61-
public static IEnumerator WaitForSaveDialog( bool folderMode = false, string initialPath = null, string title = "Save", string saveButtonText = "Save" );
62-
public static IEnumerator WaitForLoadDialog( bool folderMode = false, string initialPath = null, string title = "Load", string loadButtonText = "Select" );
62+
public static IEnumerator WaitForSaveDialog( bool folderMode = false, bool allowMultiSelection = false, string initialPath = null, string title = "Save", string saveButtonText = "Save" );
63+
public static IEnumerator WaitForLoadDialog( bool folderMode = false, bool allowMultiSelection = false, string initialPath = null, string title = "Load", string loadButtonText = "Select" );
6364
```
6465

65-
After the dialog is closed, you can check the **FileBrowser.Success** property to see whether the user selected a file/folder or cancelled the operation and if FileBrowser.Success was set to *true*, you can use the **FileBrowser.Result** property to get the path of the selected file/folder.
66+
After the dialog is closed, you can check the **FileBrowser.Success** property to see whether the user has selected some files/folders or cancelled the operation and if FileBrowser.Success is set to *true*, you can use the **FileBrowser.Result** property to get the paths of the selected files/folders.
6667

6768
You can force close an open dialog using the following function:
6869

@@ -86,7 +87,7 @@ By default, the file browser doesn't show files with *.lnk* or *.tmp* extensions
8687
public static void SetExcludedExtensions( params string[] excludedExtensions );
8788
```
8889

89-
Lastly, you can use the following functions to set the file filters:
90+
Lastly, you can use the following functions to set the file filters (filters should include the period, e.g. "*.jpg*" instead of "*jpg*"):
9091

9192
```csharp
9293
public static void SetFilters( bool showAllFilesFilter, IEnumerable<string> filters );
@@ -189,16 +190,18 @@ public class FileBrowserTest : MonoBehaviour
189190
// Show a save file dialog
190191
// onSuccess event: not registered (which means this dialog is pretty useless)
191192
// onCancel event: not registered
192-
// Save file/folder: file, Initial path: "C:\", Title: "Save As", submit button text: "Save"
193-
// FileBrowser.ShowSaveDialog( null, null, false, "C:\\", "Save As", "Save" );
193+
// Save file/folder: file, Allow multiple selection: false
194+
// Initial path: "C:\", Title: "Save As", submit button text: "Save"
195+
// FileBrowser.ShowSaveDialog( null, null, false, false, "C:\\", "Save As", "Save" );
194196
195197
// Show a select folder dialog
196198
// onSuccess event: print the selected folder's path
197199
// onCancel event: print "Canceled"
198-
// Load file/folder: folder, Initial path: default (Documents), Title: "Select Folder", submit button text: "Select"
199-
// FileBrowser.ShowLoadDialog( (path) => { Debug.Log( "Selected: " + path ); },
200-
// () => { Debug.Log( "Canceled" ); },
201-
// true, null, "Select Folder", "Select" );
200+
// Load file/folder: folder, Allow multiple selection: false
201+
// Initial path: default (Documents), Title: "Select Folder", submit button text: "Select"
202+
// FileBrowser.ShowLoadDialog( ( paths ) => { Debug.Log( "Selected: " + paths[0] ); },
203+
// () => { Debug.Log( "Canceled" ); },
204+
// true, false, null, "Select Folder", "Select" );
202205
203206
// Coroutine example
204207
StartCoroutine( ShowLoadDialogCoroutine() );
@@ -207,19 +210,23 @@ public class FileBrowserTest : MonoBehaviour
207210
IEnumerator ShowLoadDialogCoroutine()
208211
{
209212
// Show a load file dialog and wait for a response from user
210-
// Load file/folder: file, Initial path: default (Documents), Title: "Load File", submit button text: "Load"
211-
yield return FileBrowser.WaitForLoadDialog( false, null, "Load File", "Load" );
213+
// Load file/folder: file, Allow multiple selection: true
214+
// Initial path: default (Documents), Title: "Load File", submit button text: "Load"
215+
yield return FileBrowser.WaitForLoadDialog( false, true, null, "Load File", "Load" );
212216

213217
// Dialog is closed
214-
// Print whether a file is chosen (FileBrowser.Success)
215-
// and the path to the selected file (FileBrowser.Result) (null, if FileBrowser.Success is false)
216-
Debug.Log( FileBrowser.Success + " " + FileBrowser.Result );
217-
218+
// Print whether the user has selected some files/folders or cancelled the operation (FileBrowser.Success)
219+
Debug.Log( FileBrowser.Success );
220+
218221
if( FileBrowser.Success )
219222
{
220-
// If a file was chosen, read its bytes via FileBrowserHelpers
223+
// Print paths of the selected files (FileBrowser.Result) (null, if FileBrowser.Success is false)
224+
for( int i = 0; i < FileBrowser.Result.Length; i++ )
225+
Debug.Log( FileBrowser.Result[i] );
226+
227+
// Read the bytes of the first file via FileBrowserHelpers
221228
// Contrary to File.ReadAllBytes, this function works on Android 10+, as well
222-
byte[] bytes = FileBrowserHelpers.ReadBytesFromFile( FileBrowser.Result );
229+
byte[] bytes = FileBrowserHelpers.ReadBytesFromFile( FileBrowser.Result[0] );
223230
}
224231
}
225232
}

Plugins/SimpleFileBrowser/Prefabs/SimpleFileBrowserItem.prefab

Lines changed: 92 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,11 @@ GameObject:
1616
m_ObjectHideFlags: 0
1717
m_PrefabParentObject: {fileID: 0}
1818
m_PrefabInternal: {fileID: 100100000}
19-
serializedVersion: 4
19+
serializedVersion: 5
2020
m_Component:
21-
- 224: {fileID: 224000013702633820}
22-
- 222: {fileID: 222000010857273394}
23-
- 114: {fileID: 114000013995033306}
21+
- component: {fileID: 224000013702633820}
22+
- component: {fileID: 222000010857273394}
23+
- component: {fileID: 114000013995033306}
2424
m_Layer: 5
2525
m_Name: Name
2626
m_TagString: Untagged
@@ -33,12 +33,12 @@ GameObject:
3333
m_ObjectHideFlags: 0
3434
m_PrefabParentObject: {fileID: 0}
3535
m_PrefabInternal: {fileID: 100100000}
36-
serializedVersion: 4
36+
serializedVersion: 5
3737
m_Component:
38-
- 224: {fileID: 224000013952242090}
39-
- 222: {fileID: 222000013410998874}
40-
- 114: {fileID: 114000012602770182}
41-
- 114: {fileID: 114000012265032802}
38+
- component: {fileID: 224000013952242090}
39+
- component: {fileID: 222000013410998874}
40+
- component: {fileID: 114000012602770182}
41+
- component: {fileID: 114000012265032802}
4242
m_Layer: 5
4343
m_Name: SimpleFileBrowserItem
4444
m_TagString: Untagged
@@ -51,11 +51,28 @@ GameObject:
5151
m_ObjectHideFlags: 0
5252
m_PrefabParentObject: {fileID: 0}
5353
m_PrefabInternal: {fileID: 100100000}
54-
serializedVersion: 4
54+
serializedVersion: 5
5555
m_Component:
56-
- 224: {fileID: 224000012393993334}
57-
- 222: {fileID: 222000012809932278}
58-
- 114: {fileID: 114000013208375990}
56+
- component: {fileID: 224000012393993334}
57+
- component: {fileID: 222000012809932278}
58+
- component: {fileID: 114000013208375990}
59+
m_Layer: 5
60+
m_Name: MultiSelectionToggle
61+
m_TagString: Untagged
62+
m_Icon: {fileID: 0}
63+
m_NavMeshLayer: 0
64+
m_StaticEditorFlags: 0
65+
m_IsActive: 0
66+
--- !u!1 &1268973638054374
67+
GameObject:
68+
m_ObjectHideFlags: 0
69+
m_PrefabParentObject: {fileID: 0}
70+
m_PrefabInternal: {fileID: 100100000}
71+
serializedVersion: 5
72+
m_Component:
73+
- component: {fileID: 224447975432582502}
74+
- component: {fileID: 222113778025760416}
75+
- component: {fileID: 114807149938652920}
5976
m_Layer: 5
6077
m_Name: Icon
6178
m_TagString: Untagged
@@ -75,7 +92,8 @@ MonoBehaviour:
7592
m_Name:
7693
m_EditorClassIdentifier:
7794
background: {fileID: 114000012602770182}
78-
icon: {fileID: 114000013208375990}
95+
icon: {fileID: 114807149938652920}
96+
multiSelectionToggle: {fileID: 114000013208375990}
7997
nameText: {fileID: 114000013995033306}
8098
--- !u!114 &114000012602770182
8199
MonoBehaviour:
@@ -123,7 +141,7 @@ MonoBehaviour:
123141
m_Calls: []
124142
m_TypeName: UnityEngine.UI.MaskableGraphic+CullStateChangedEvent, UnityEngine.UI,
125143
Version=1.0.0.0, Culture=neutral, PublicKeyToken=null
126-
m_Sprite: {fileID: 0}
144+
m_Sprite: {fileID: 21300000, guid: d6beaeac8de2af749a48581db778df3e, type: 3}
127145
m_Type: 0
128146
m_PreserveAspect: 1
129147
m_FillCenter: 1
@@ -160,10 +178,37 @@ MonoBehaviour:
160178
m_Alignment: 3
161179
m_AlignByGeometry: 0
162180
m_RichText: 1
163-
m_HorizontalOverflow: 0
181+
m_HorizontalOverflow: 1
164182
m_VerticalOverflow: 0
165183
m_LineSpacing: 1
166184
m_Text: Filename
185+
--- !u!114 &114807149938652920
186+
MonoBehaviour:
187+
m_ObjectHideFlags: 1
188+
m_PrefabParentObject: {fileID: 0}
189+
m_PrefabInternal: {fileID: 100100000}
190+
m_GameObject: {fileID: 1268973638054374}
191+
m_Enabled: 1
192+
m_EditorHideFlags: 0
193+
m_Script: {fileID: -765806418, guid: f5f67c52d1564df4a8936ccd202a3bd8, type: 3}
194+
m_Name:
195+
m_EditorClassIdentifier:
196+
m_Material: {fileID: 0}
197+
m_Color: {r: 1, g: 1, b: 1, a: 1}
198+
m_RaycastTarget: 0
199+
m_OnCullStateChanged:
200+
m_PersistentCalls:
201+
m_Calls: []
202+
m_TypeName: UnityEngine.UI.MaskableGraphic+CullStateChangedEvent, UnityEngine.UI,
203+
Version=1.0.0.0, Culture=neutral, PublicKeyToken=null
204+
m_Sprite: {fileID: 0}
205+
m_Type: 0
206+
m_PreserveAspect: 1
207+
m_FillCenter: 1
208+
m_FillMethod: 4
209+
m_FillAmount: 1
210+
m_FillClockwise: 1
211+
m_FillOrigin: 0
167212
--- !u!222 &222000010857273394
168213
CanvasRenderer:
169214
m_ObjectHideFlags: 1
@@ -182,22 +227,28 @@ CanvasRenderer:
182227
m_PrefabParentObject: {fileID: 0}
183228
m_PrefabInternal: {fileID: 100100000}
184229
m_GameObject: {fileID: 1000011646011302}
230+
--- !u!222 &222113778025760416
231+
CanvasRenderer:
232+
m_ObjectHideFlags: 1
233+
m_PrefabParentObject: {fileID: 0}
234+
m_PrefabInternal: {fileID: 100100000}
235+
m_GameObject: {fileID: 1268973638054374}
185236
--- !u!224 &224000012393993334
186237
RectTransform:
187238
m_ObjectHideFlags: 1
188239
m_PrefabParentObject: {fileID: 0}
189240
m_PrefabInternal: {fileID: 100100000}
190241
m_GameObject: {fileID: 1000013967986654}
191-
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
242+
m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
192243
m_LocalPosition: {x: 0, y: 0, z: 0}
193244
m_LocalScale: {x: 1, y: 1, z: 1}
194-
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
195245
m_Children: []
196246
m_Father: {fileID: 224000013952242090}
197247
m_RootOrder: 0
248+
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
198249
m_AnchorMin: {x: 0, y: 0}
199250
m_AnchorMax: {x: 0, y: 1}
200-
m_AnchoredPosition: {x: 19, y: 0}
251+
m_AnchoredPosition: {x: 18.99997, y: 0}
201252
m_SizeDelta: {x: 30, y: -6}
202253
m_Pivot: {x: 0.5, y: 0.5}
203254
--- !u!224 &224000013702633820
@@ -209,10 +260,10 @@ RectTransform:
209260
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
210261
m_LocalPosition: {x: 0, y: 0, z: 0}
211262
m_LocalScale: {x: 1, y: 1, z: 1}
212-
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
213263
m_Children: []
214264
m_Father: {fileID: 224000013952242090}
215-
m_RootOrder: 1
265+
m_RootOrder: 2
266+
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
216267
m_AnchorMin: {x: 0, y: 0}
217268
m_AnchorMax: {x: 1, y: 1}
218269
m_AnchoredPosition: {x: 19, y: 0}
@@ -227,14 +278,33 @@ RectTransform:
227278
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
228279
m_LocalPosition: {x: 0, y: 0, z: 0}
229280
m_LocalScale: {x: 1, y: 1, z: 1}
230-
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
231281
m_Children:
232282
- {fileID: 224000012393993334}
283+
- {fileID: 224447975432582502}
233284
- {fileID: 224000013702633820}
234285
m_Father: {fileID: 0}
235286
m_RootOrder: 0
287+
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
236288
m_AnchorMin: {x: 0, y: 1}
237289
m_AnchorMax: {x: 1, y: 1}
238290
m_AnchoredPosition: {x: 0, y: 0}
239291
m_SizeDelta: {x: 0, y: 30}
240292
m_Pivot: {x: 0, y: 1}
293+
--- !u!224 &224447975432582502
294+
RectTransform:
295+
m_ObjectHideFlags: 1
296+
m_PrefabParentObject: {fileID: 0}
297+
m_PrefabInternal: {fileID: 100100000}
298+
m_GameObject: {fileID: 1268973638054374}
299+
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
300+
m_LocalPosition: {x: 0, y: 0, z: 0}
301+
m_LocalScale: {x: 1, y: 1, z: 1}
302+
m_Children: []
303+
m_Father: {fileID: 224000013952242090}
304+
m_RootOrder: 1
305+
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
306+
m_AnchorMin: {x: 0, y: 0}
307+
m_AnchorMax: {x: 0, y: 1}
308+
m_AnchoredPosition: {x: 19, y: 0}
309+
m_SizeDelta: {x: 30, y: -6}
310+
m_Pivot: {x: 0.5, y: 0.5}

0 commit comments

Comments
 (0)