3149 lines
92 KiB
C#
3149 lines
92 KiB
C#
//#define WIN_DIR_CHECK_WITHOUT_TIMEOUT // When uncommented, Directory.Exists won't be wrapped inside a Task/Thread on Windows but we won't be able to set a timeout for unreachable directories/drives
|
|
|
|
using UnityEngine;
|
|
using UnityEngine.EventSystems;
|
|
using UnityEngine.UI;
|
|
using System;
|
|
using System.IO;
|
|
using System.Collections;
|
|
using System.Collections.Generic;
|
|
using System.Globalization;
|
|
using System.Text;
|
|
#if ENABLE_INPUT_SYSTEM && !ENABLE_LEGACY_INPUT_MANAGER
|
|
using UnityEngine.InputSystem;
|
|
#endif
|
|
|
|
namespace SimpleFileBrowser
|
|
{
|
|
public class FileBrowser : MonoBehaviour, IListViewAdapter
|
|
{
|
|
public enum Permission { Denied = 0, Granted = 1, ShouldAsk = 2 };
|
|
public enum PickMode { Files = 0, Folders = 1, FilesAndFolders = 2 };
|
|
|
|
#region Structs
|
|
#pragma warning disable 0649
|
|
[Serializable]
|
|
private struct FiletypeIcon
|
|
{
|
|
public string extension;
|
|
public Sprite icon;
|
|
}
|
|
|
|
[Serializable]
|
|
private struct QuickLink
|
|
{
|
|
#if UNITY_EDITOR || ( !UNITY_WSA && !UNITY_WSA_10_0 )
|
|
public Environment.SpecialFolder target;
|
|
#endif
|
|
public string name;
|
|
public Sprite icon;
|
|
}
|
|
#pragma warning restore 0649
|
|
#endregion
|
|
|
|
#region Inner Classes
|
|
public class Filter
|
|
{
|
|
public readonly string name;
|
|
public readonly string[] extensions;
|
|
public readonly HashSet<string> extensionsSet;
|
|
public readonly string defaultExtension;
|
|
public readonly bool allExtensionsHaveSingleSuffix; // 'false' when some extensions have multiple suffixes like ".tar.gz"
|
|
|
|
internal Filter( string name )
|
|
{
|
|
this.name = name;
|
|
extensions = null;
|
|
extensionsSet = null;
|
|
defaultExtension = null;
|
|
allExtensionsHaveSingleSuffix = true;
|
|
}
|
|
|
|
public Filter( string name, string extension )
|
|
{
|
|
this.name = name;
|
|
|
|
extension = extension.ToLowerInvariant();
|
|
if( extension[0] != '.' )
|
|
extension = "." + extension;
|
|
|
|
extensions = new string[1] { extension };
|
|
extensionsSet = new HashSet<string>() { extension };
|
|
defaultExtension = extension;
|
|
allExtensionsHaveSingleSuffix = ( extension.LastIndexOf( '.' ) == 0 );
|
|
}
|
|
|
|
public Filter( string name, params string[] extensions )
|
|
{
|
|
this.name = name;
|
|
allExtensionsHaveSingleSuffix = true;
|
|
|
|
for( int i = 0; i < extensions.Length; i++ )
|
|
{
|
|
extensions[i] = extensions[i].ToLowerInvariant();
|
|
if( extensions[i][0] != '.' )
|
|
extensions[i] = "." + extensions[i];
|
|
|
|
allExtensionsHaveSingleSuffix &= ( extensions[i].LastIndexOf( '.' ) == 0 );
|
|
}
|
|
|
|
this.extensions = extensions;
|
|
extensionsSet = new HashSet<string>( extensions );
|
|
defaultExtension = extensions[0];
|
|
}
|
|
|
|
public bool MatchesExtension( string extension, bool extensionMayHaveMultipleSuffixes )
|
|
{
|
|
if( extensionsSet == null || extensionsSet.Contains( extension ) )
|
|
return true;
|
|
|
|
// When the provided extension may have multiple suffixes (e.g. ".tar.gz"), check if it ends with any of the
|
|
// extensions in this filter (e.g. return true when this Filter has ".gz" and the provided extension is ".tar.gz")
|
|
if( extensionMayHaveMultipleSuffixes )
|
|
{
|
|
for( int i = 0; i < extensions.Length; i++ )
|
|
{
|
|
if( extension.EndsWith( extensions[i], StringComparison.Ordinal ) )
|
|
{
|
|
extensionsSet.Add( extension );
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
public override string ToString()
|
|
{
|
|
string result = string.Empty;
|
|
|
|
if( name != null )
|
|
result += name;
|
|
|
|
if( extensions != null )
|
|
{
|
|
if( name != null )
|
|
result += " (";
|
|
|
|
for( int i = 0; i < extensions.Length; i++ )
|
|
{
|
|
if( i > 0 )
|
|
result += ", " + extensions[i];
|
|
else
|
|
result += extensions[i];
|
|
}
|
|
|
|
if( name != null )
|
|
result += ")";
|
|
}
|
|
|
|
return result;
|
|
}
|
|
}
|
|
#endregion
|
|
|
|
#region Constants
|
|
private const int FILENAME_INPUT_FIELD_MAX_FILE_COUNT = 7;
|
|
private const string SAF_PICK_FOLDER_QUICK_LINK_PATH = "SAF_PICK_FOLDER";
|
|
#endregion
|
|
|
|
#region Static Variables
|
|
public static bool IsOpen { get; private set; }
|
|
|
|
public static bool Success { get; private set; }
|
|
public static string[] Result { get; private set; }
|
|
|
|
[SerializeField]
|
|
private UISkin m_skin;
|
|
#if UNITY_EDITOR
|
|
private UISkin prevSkin;
|
|
#endif
|
|
private int m_skinVersion = 0;
|
|
private Sprite m_skinPrevDriveIcon, m_skinPrevFolderIcon;
|
|
public static UISkin Skin
|
|
{
|
|
get { return Instance.m_skin; }
|
|
set
|
|
{
|
|
if( value && Instance.m_skin != value )
|
|
{
|
|
Instance.m_skin = value;
|
|
Instance.m_skinVersion = Instance.m_skin.Version;
|
|
Instance.RefreshSkin();
|
|
}
|
|
}
|
|
}
|
|
|
|
private static bool m_askPermissions = true;
|
|
public static bool AskPermissions
|
|
{
|
|
get { return m_askPermissions; }
|
|
set { m_askPermissions = value; }
|
|
}
|
|
|
|
private static bool m_singleClickMode = false;
|
|
public static bool SingleClickMode
|
|
{
|
|
get { return m_singleClickMode; }
|
|
set { m_singleClickMode = value; }
|
|
}
|
|
|
|
private static FileSystemEntryFilter m_displayedEntriesFilter;
|
|
public static event FileSystemEntryFilter DisplayedEntriesFilter
|
|
{
|
|
add
|
|
{
|
|
m_displayedEntriesFilter -= value;
|
|
m_displayedEntriesFilter += value;
|
|
|
|
if( m_instance )
|
|
{
|
|
m_instance.PersistFileEntrySelection();
|
|
m_instance.RefreshFiles( false );
|
|
}
|
|
}
|
|
remove
|
|
{
|
|
m_displayedEntriesFilter -= value;
|
|
|
|
if( m_instance )
|
|
{
|
|
m_instance.PersistFileEntrySelection();
|
|
m_instance.RefreshFiles( false );
|
|
}
|
|
}
|
|
}
|
|
|
|
private static bool m_showFileOverwriteDialog = true;
|
|
public static bool ShowFileOverwriteDialog
|
|
{
|
|
get { return m_showFileOverwriteDialog; }
|
|
set { m_showFileOverwriteDialog = value; }
|
|
}
|
|
|
|
private static bool m_checkWriteAccessToDestinationDirectory = false;
|
|
public static bool CheckWriteAccessToDestinationDirectory
|
|
{
|
|
get { return m_checkWriteAccessToDestinationDirectory; }
|
|
set { m_checkWriteAccessToDestinationDirectory = value; }
|
|
}
|
|
|
|
#if UNITY_EDITOR || ( !UNITY_ANDROID && !UNITY_IOS && !UNITY_WSA && !UNITY_WSA_10_0 )
|
|
private static float m_drivesRefreshInterval = 5f;
|
|
#else
|
|
private static float m_drivesRefreshInterval = -1f;
|
|
#endif
|
|
public static float DrivesRefreshInterval
|
|
{
|
|
get { return m_drivesRefreshInterval; }
|
|
set { m_drivesRefreshInterval = value; }
|
|
}
|
|
|
|
public static bool ShowHiddenFiles
|
|
{
|
|
get { return Instance.showHiddenFilesToggle.isOn; }
|
|
set { Instance.showHiddenFilesToggle.isOn = value; }
|
|
}
|
|
|
|
private static bool m_displayHiddenFilesToggle = true;
|
|
public static bool DisplayHiddenFilesToggle
|
|
{
|
|
get { return m_displayHiddenFilesToggle; }
|
|
set
|
|
{
|
|
if( m_displayHiddenFilesToggle != value )
|
|
{
|
|
m_displayHiddenFilesToggle = value;
|
|
|
|
if( m_instance )
|
|
{
|
|
if( !value )
|
|
m_instance.showHiddenFilesToggle.gameObject.SetActive( false );
|
|
else if( m_instance.windowTR.sizeDelta.x >= m_instance.narrowScreenWidth )
|
|
{
|
|
#if !UNITY_EDITOR && UNITY_ANDROID
|
|
if( !FileBrowserHelpers.ShouldUseSAF )
|
|
#endif
|
|
m_instance.showHiddenFilesToggle.gameObject.SetActive( true );
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private static string m_allFilesFilterText = "All Files (.*)";
|
|
public static string AllFilesFilterText
|
|
{
|
|
get { return m_allFilesFilterText; }
|
|
set
|
|
{
|
|
if( m_allFilesFilterText != value )
|
|
{
|
|
string oldValue = m_allFilesFilterText;
|
|
m_allFilesFilterText = value;
|
|
|
|
if( m_instance )
|
|
{
|
|
Filter oldAllFilesFilter = m_instance.allFilesFilter;
|
|
m_instance.allFilesFilter = new Filter( value );
|
|
|
|
if( m_instance.filters.Count > 0 && m_instance.filters[0] == oldAllFilesFilter )
|
|
m_instance.filters[0] = m_instance.allFilesFilter;
|
|
|
|
if( m_instance.filtersDropdown.options[0].text == oldValue )
|
|
{
|
|
m_instance.filtersDropdown.options[0].text = value;
|
|
m_instance.filtersDropdown.RefreshShownValue();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private static string m_foldersFilterText = "Folders";
|
|
public static string FoldersFilterText
|
|
{
|
|
get { return m_foldersFilterText; }
|
|
set
|
|
{
|
|
if( m_foldersFilterText != value )
|
|
{
|
|
string oldValue = m_foldersFilterText;
|
|
m_foldersFilterText = value;
|
|
|
|
if( m_instance && m_instance.filtersDropdown.options[0].text == oldValue )
|
|
{
|
|
m_instance.filtersDropdown.options[0].text = value;
|
|
m_instance.filtersDropdown.RefreshShownValue();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private static string m_pickFolderQuickLinkText = "Browse...";
|
|
public static string PickFolderQuickLinkText
|
|
{
|
|
get { return m_pickFolderQuickLinkText; }
|
|
set
|
|
{
|
|
if( m_pickFolderQuickLinkText != value )
|
|
{
|
|
m_pickFolderQuickLinkText = value;
|
|
|
|
if( m_instance )
|
|
{
|
|
for( int i = 0; i < m_instance.allQuickLinks.Count; i++ )
|
|
{
|
|
FileBrowserQuickLink quickLink = m_instance.allQuickLinks[i];
|
|
if( quickLink && quickLink.TargetPath == SAF_PICK_FOLDER_QUICK_LINK_PATH )
|
|
{
|
|
quickLink.SetQuickLink( Skin.DriveIcon, value, SAF_PICK_FOLDER_QUICK_LINK_PATH );
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private static FileBrowser m_instance = null;
|
|
private static FileBrowser Instance
|
|
{
|
|
get
|
|
{
|
|
if( !m_instance )
|
|
{
|
|
m_instance = Instantiate( Resources.Load<GameObject>( "SimpleFileBrowserCanvas" ) ).GetComponent<FileBrowser>();
|
|
DontDestroyOnLoad( m_instance.gameObject );
|
|
m_instance.gameObject.SetActive( false );
|
|
}
|
|
|
|
return m_instance;
|
|
}
|
|
}
|
|
#endregion
|
|
|
|
#region Variables
|
|
#pragma warning disable 0649
|
|
[Header( "Settings" )]
|
|
[SerializeField]
|
|
internal int minWidth = 380;
|
|
[SerializeField]
|
|
internal int minHeight = 300;
|
|
|
|
[SerializeField]
|
|
private float narrowScreenWidth = 380f;
|
|
|
|
[SerializeField]
|
|
private float quickLinksMaxWidthPercentage = 0.4f;
|
|
|
|
[SerializeField]
|
|
private bool sortFilesByName = true;
|
|
|
|
[SerializeField, UnityEngine.Serialization.FormerlySerializedAs( "excludeExtensions" )]
|
|
private string[] excludedExtensions;
|
|
|
|
#pragma warning disable 0414
|
|
[SerializeField]
|
|
private QuickLink[] quickLinks;
|
|
private static bool quickLinksInitialized;
|
|
#pragma warning restore 0414
|
|
|
|
private readonly HashSet<string> excludedExtensionsSet = new HashSet<string>();
|
|
|
|
[SerializeField]
|
|
private bool generateQuickLinksForDrives = true;
|
|
|
|
[SerializeField]
|
|
private bool contextMenuShowDeleteButton = true;
|
|
|
|
[SerializeField]
|
|
private bool contextMenuShowRenameButton = true;
|
|
|
|
[SerializeField]
|
|
private bool showResizeCursor = true;
|
|
|
|
[Header( "Internal References" )]
|
|
|
|
[SerializeField]
|
|
private FileBrowserMovement window;
|
|
private RectTransform windowTR;
|
|
|
|
[SerializeField]
|
|
private RectTransform topViewNarrowScreen;
|
|
|
|
[SerializeField]
|
|
private RectTransform middleView;
|
|
private Vector2 middleViewOriginalPosition;
|
|
private Vector2 middleViewOriginalSize;
|
|
|
|
[SerializeField]
|
|
private RectTransform middleViewQuickLinks;
|
|
private Vector2 middleViewQuickLinksOriginalSize;
|
|
|
|
[SerializeField]
|
|
private RectTransform middleViewFiles;
|
|
|
|
[SerializeField]
|
|
private RectTransform middleViewSeparator;
|
|
|
|
[SerializeField]
|
|
private FileBrowserItem itemPrefab;
|
|
private readonly List<FileBrowserItem> allItems = new List<FileBrowserItem>( 16 );
|
|
|
|
[SerializeField]
|
|
private FileBrowserQuickLink quickLinkPrefab;
|
|
private readonly List<FileBrowserQuickLink> allQuickLinks = new List<FileBrowserQuickLink>( 8 );
|
|
|
|
[SerializeField]
|
|
private Text titleText;
|
|
|
|
[SerializeField]
|
|
private Button backButton;
|
|
|
|
[SerializeField]
|
|
private Button forwardButton;
|
|
|
|
[SerializeField]
|
|
private Button upButton;
|
|
|
|
[SerializeField]
|
|
private Button moreOptionsButton;
|
|
|
|
[SerializeField]
|
|
private InputField pathInputField;
|
|
|
|
[SerializeField]
|
|
private RectTransform pathInputFieldSlotTop;
|
|
|
|
[SerializeField]
|
|
private RectTransform pathInputFieldSlotBottom;
|
|
|
|
[SerializeField]
|
|
private InputField searchInputField;
|
|
|
|
[SerializeField]
|
|
private RectTransform quickLinksContainer;
|
|
|
|
[SerializeField]
|
|
private ScrollRect quickLinksScrollRect;
|
|
|
|
[SerializeField]
|
|
private RectTransform filesContainer;
|
|
|
|
[SerializeField]
|
|
private ScrollRect filesScrollRect;
|
|
|
|
[SerializeField]
|
|
private RecycledListView listView;
|
|
|
|
[SerializeField]
|
|
private InputField filenameInputField;
|
|
|
|
[SerializeField]
|
|
private Text filenameInputFieldOverlayText;
|
|
|
|
[SerializeField]
|
|
private Image filenameImage;
|
|
|
|
[SerializeField]
|
|
private Dropdown filtersDropdown;
|
|
|
|
[SerializeField]
|
|
private RectTransform filtersDropdownContainer;
|
|
|
|
[SerializeField]
|
|
private Text filterItemTemplate;
|
|
|
|
[SerializeField]
|
|
private Toggle showHiddenFilesToggle;
|
|
|
|
[SerializeField]
|
|
private Text submitButtonText;
|
|
|
|
[SerializeField]
|
|
private Button[] allButtons;
|
|
|
|
[SerializeField]
|
|
private RectTransform moreOptionsContextMenuPosition;
|
|
|
|
[SerializeField]
|
|
private FileBrowserRenamedItem renameItem;
|
|
|
|
[SerializeField]
|
|
private FileBrowserContextMenu contextMenu;
|
|
|
|
[SerializeField]
|
|
private FileBrowserFileOperationConfirmationPanel fileOperationConfirmationPanel;
|
|
|
|
[SerializeField]
|
|
private FileBrowserAccessRestrictedPanel accessRestrictedPanel;
|
|
|
|
[SerializeField]
|
|
private FileBrowserCursorHandler resizeCursorHandler;
|
|
#pragma warning restore 0649
|
|
|
|
internal RectTransform rectTransform;
|
|
private Canvas canvas;
|
|
|
|
private FileAttributes ignoredFileAttributes = FileAttributes.System;
|
|
|
|
private FileSystemEntry[] allFileEntries;
|
|
private readonly List<FileSystemEntry> validFileEntries = new List<FileSystemEntry>();
|
|
private readonly List<int> selectedFileEntries = new List<int>( 4 );
|
|
private readonly List<string> pendingFileEntrySelection = new List<string>();
|
|
|
|
private readonly List<string> submittedFileEntryPaths = new List<string>( 4 );
|
|
private readonly List<string> submittedFolderPaths = new List<string>( 4 ); // Used to check if all destination folders have write access
|
|
private readonly List<FileSystemEntry> submittedFileEntriesToOverwrite = new List<FileSystemEntry>( 4 ); // Existing files selected by the user in save mode
|
|
|
|
#pragma warning disable 0414 // Value is assigned but never used on Android & iOS
|
|
private int multiSelectionPivotFileEntry;
|
|
#pragma warning restore 0414
|
|
private StringBuilder multiSelectionFilenameBuilder;
|
|
|
|
private readonly List<Filter> filters = new List<Filter>();
|
|
private Filter allFilesFilter;
|
|
|
|
private bool showAllFilesFilter = true;
|
|
|
|
// Single suffix: ".mp4", ".txt", etc.
|
|
// Multiple suffixes: ".tar.gz", etc.
|
|
private bool allFiltersHaveSingleSuffix = true;
|
|
private bool allExcludedExtensionsHaveSingleSuffix = true;
|
|
// When its value is 'true', file extensions will be handled in a more optimized way
|
|
private bool AllExtensionsHaveSingleSuffix { get { return allFiltersHaveSingleSuffix && allExcludedExtensionsHaveSingleSuffix && m_skin.AllIconExtensionsHaveSingleSuffix; } }
|
|
|
|
private string defaultInitialPath;
|
|
|
|
private int currentPathIndex = -1;
|
|
private readonly List<string> pathsFollowed = new List<string>();
|
|
|
|
private HashSet<char> invalidFilenameChars;
|
|
|
|
private float drivesNextRefreshTime;
|
|
#if !UNITY_EDITOR && UNITY_ANDROID
|
|
private string driveQuickLinks;
|
|
#else
|
|
private string[] driveQuickLinks;
|
|
#endif
|
|
private int numberOfDriveQuickLinks;
|
|
|
|
#if !WIN_DIR_CHECK_WITHOUT_TIMEOUT && ( UNITY_EDITOR_WIN || UNITY_STANDALONE_WIN )
|
|
private readonly List<string> timedOutDirectoryExistsRequests = new List<string>( 2 );
|
|
#endif
|
|
|
|
private bool canvasDimensionsChanged;
|
|
|
|
private readonly CompareInfo textComparer = new CultureInfo( "en-US" ).CompareInfo;
|
|
private readonly CompareOptions textCompareOptions = CompareOptions.IgnoreCase | CompareOptions.IgnoreNonSpace;
|
|
|
|
// Required in RefreshFiles() function
|
|
private PointerEventData nullPointerEventData;
|
|
#endregion
|
|
|
|
#region Properties
|
|
private string m_currentPath = string.Empty;
|
|
private string CurrentPath
|
|
{
|
|
get { return m_currentPath; }
|
|
set
|
|
{
|
|
if( value != null )
|
|
{
|
|
value = value.Trim();
|
|
#if !UNITY_EDITOR && UNITY_ANDROID
|
|
if( !FileBrowserHelpers.ShouldUseSAFForPath( value ) )
|
|
#endif
|
|
value = GetPathWithoutTrailingDirectorySeparator( value );
|
|
}
|
|
|
|
if( string.IsNullOrEmpty( value ) )
|
|
{
|
|
pathInputField.text = m_currentPath;
|
|
return;
|
|
}
|
|
|
|
if( m_currentPath != value )
|
|
{
|
|
if( !FileBrowserHelpers.DirectoryExists( value ) )
|
|
{
|
|
pathInputField.text = m_currentPath;
|
|
return;
|
|
}
|
|
|
|
m_currentPath = value;
|
|
pathInputField.text = m_currentPath;
|
|
|
|
if( currentPathIndex == -1 || pathsFollowed[currentPathIndex] != m_currentPath )
|
|
{
|
|
currentPathIndex++;
|
|
if( currentPathIndex < pathsFollowed.Count )
|
|
{
|
|
pathsFollowed[currentPathIndex] = value;
|
|
for( int i = pathsFollowed.Count - 1; i >= currentPathIndex + 1; i-- )
|
|
pathsFollowed.RemoveAt( i );
|
|
}
|
|
else
|
|
pathsFollowed.Add( m_currentPath );
|
|
}
|
|
|
|
backButton.interactable = currentPathIndex > 0;
|
|
forwardButton.interactable = currentPathIndex < pathsFollowed.Count - 1;
|
|
#if !UNITY_EDITOR && UNITY_ANDROID
|
|
if( FileBrowserHelpers.ShouldUseSAF )
|
|
{
|
|
string parentPath = FileBrowserHelpers.GetDirectoryName( m_currentPath );
|
|
upButton.interactable = !string.IsNullOrEmpty( parentPath ) && ( FileBrowserHelpers.ShouldUseSAFForPath( parentPath ) || FileBrowserHelpers.DirectoryExists( parentPath ) ); // DirectoryExists: Directory may not be accessible on Android 10+, this function checks that
|
|
}
|
|
else
|
|
#endif
|
|
{
|
|
try // When "C:/" or "C:" is typed instead of "C:\", an exception is thrown
|
|
{
|
|
upButton.interactable = Directory.GetParent( m_currentPath ) != null;
|
|
}
|
|
catch
|
|
{
|
|
upButton.interactable = false;
|
|
}
|
|
}
|
|
|
|
m_searchString = string.Empty;
|
|
searchInputField.text = m_searchString;
|
|
|
|
multiSelectionPivotFileEntry = 0;
|
|
filesScrollRect.verticalNormalizedPosition = 1;
|
|
|
|
filenameImage.color = m_skin.InputFieldNormalBackgroundColor;
|
|
if( m_pickerMode != PickMode.Files )
|
|
{
|
|
filenameInputField.text = string.Empty;
|
|
filenameInputField.interactable = true;
|
|
}
|
|
|
|
// If a quick link points to this directory, highlight it
|
|
#if !UNITY_EDITOR && UNITY_ANDROID
|
|
// Path strings aren't deterministic on Storage Access Framework but the paths' absolute parts usually are
|
|
if( FileBrowserHelpers.ShouldUseSAFForPath( m_currentPath ) )
|
|
{
|
|
int SAFAbsolutePathSeparatorIndex = m_currentPath.LastIndexOf( '/' );
|
|
if( SAFAbsolutePathSeparatorIndex >= 0 )
|
|
{
|
|
string absoluteSAFPath = m_currentPath.Substring( SAFAbsolutePathSeparatorIndex );
|
|
for( int i = 0; i < allQuickLinks.Count; i++ )
|
|
allQuickLinks[i].SetSelected( allQuickLinks[i].TargetPath == m_currentPath || allQuickLinks[i].TargetPath.EndsWith( absoluteSAFPath ) );
|
|
}
|
|
else
|
|
{
|
|
for( int i = 0; i < allQuickLinks.Count; i++ )
|
|
allQuickLinks[i].SetSelected( allQuickLinks[i].TargetPath == m_currentPath );
|
|
}
|
|
}
|
|
else
|
|
#endif
|
|
{
|
|
for( int i = 0; i < allQuickLinks.Count; i++ )
|
|
allQuickLinks[i].SetSelected( allQuickLinks[i].TargetPath == m_currentPath );
|
|
}
|
|
}
|
|
|
|
m_multiSelectionToggleSelectionMode = false;
|
|
RefreshFiles( true );
|
|
}
|
|
}
|
|
|
|
private string m_searchString = string.Empty;
|
|
private string SearchString
|
|
{
|
|
get { return m_searchString; }
|
|
set
|
|
{
|
|
if( m_searchString != value )
|
|
{
|
|
m_searchString = value;
|
|
searchInputField.text = m_searchString;
|
|
|
|
RefreshFiles( false );
|
|
}
|
|
}
|
|
}
|
|
|
|
private bool m_acceptNonExistingFilename = false; // Is set to true when showing save dialog for Files or FilesAndFolders, false otherwise
|
|
private bool AcceptNonExistingFilename
|
|
{
|
|
get { return m_acceptNonExistingFilename; }
|
|
set { m_acceptNonExistingFilename = value; }
|
|
}
|
|
|
|
private PickMode m_pickerMode = PickMode.Files;
|
|
internal PickMode PickerMode
|
|
{
|
|
get { return m_pickerMode; }
|
|
private set
|
|
{
|
|
m_pickerMode = value;
|
|
|
|
if( m_pickerMode == PickMode.Folders )
|
|
{
|
|
filtersDropdown.options[0].text = FoldersFilterText;
|
|
filtersDropdown.value = 0;
|
|
filtersDropdown.interactable = false;
|
|
}
|
|
else
|
|
{
|
|
filtersDropdown.options[0].text = filters[0].ToString();
|
|
filtersDropdown.interactable = true;
|
|
}
|
|
|
|
filtersDropdown.RefreshShownValue();
|
|
|
|
Text placeholder = filenameInputField.placeholder as Text;
|
|
if( placeholder )
|
|
placeholder.gameObject.SetActive( m_pickerMode != PickMode.Folders );
|
|
}
|
|
}
|
|
|
|
private bool m_allowMultiSelection;
|
|
internal bool AllowMultiSelection
|
|
{
|
|
get { return m_allowMultiSelection; }
|
|
private set { m_allowMultiSelection = value; }
|
|
}
|
|
|
|
private bool m_multiSelectionToggleSelectionMode;
|
|
internal bool MultiSelectionToggleSelectionMode
|
|
{
|
|
get { return m_multiSelectionToggleSelectionMode; }
|
|
private set
|
|
{
|
|
if( m_multiSelectionToggleSelectionMode != value )
|
|
{
|
|
m_multiSelectionToggleSelectionMode = value;
|
|
|
|
for( int i = 0; i < allItems.Count; i++ )
|
|
{
|
|
if( allItems[i].gameObject.activeSelf )
|
|
allItems[i].SetSelected( selectedFileEntries.Contains( allItems[i].Position ) );
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private string Title
|
|
{
|
|
get { return titleText.text; }
|
|
set { titleText.text = value; }
|
|
}
|
|
|
|
private string SubmitButtonText
|
|
{
|
|
get { return submitButtonText.text; }
|
|
set { submitButtonText.text = value; }
|
|
}
|
|
|
|
private string LastBrowsedFolder
|
|
{
|
|
get { return PlayerPrefs.GetString( "FBLastPath", null ); }
|
|
set { PlayerPrefs.SetString( "FBLastPath", value ); }
|
|
}
|
|
#endregion
|
|
|
|
#region Delegates
|
|
public delegate void OnSuccess( string[] paths );
|
|
public delegate void OnCancel();
|
|
public delegate bool FileSystemEntryFilter( FileSystemEntry entry );
|
|
#if UNITY_EDITOR || UNITY_ANDROID
|
|
public delegate void AndroidSAFDirectoryPickCallback( string rawUri, string name );
|
|
#endif
|
|
|
|
private OnSuccess onSuccess;
|
|
private OnCancel onCancel;
|
|
#endregion
|
|
|
|
#region Messages
|
|
private void Awake()
|
|
{
|
|
m_instance = this;
|
|
|
|
rectTransform = (RectTransform) transform;
|
|
windowTR = (RectTransform) window.transform;
|
|
canvas = GetComponent<Canvas>();
|
|
|
|
middleViewOriginalPosition = middleView.anchoredPosition;
|
|
middleViewOriginalSize = middleView.sizeDelta;
|
|
middleViewQuickLinksOriginalSize = middleViewQuickLinks.sizeDelta;
|
|
|
|
nullPointerEventData = new PointerEventData( null );
|
|
|
|
#if !UNITY_EDITOR && ( UNITY_ANDROID || UNITY_IOS || UNITY_WSA || UNITY_WSA_10_0 )
|
|
defaultInitialPath = Application.persistentDataPath;
|
|
#else
|
|
defaultInitialPath = Environment.GetFolderPath( Environment.SpecialFolder.MyDocuments );
|
|
#endif
|
|
|
|
#if !UNITY_EDITOR && UNITY_ANDROID
|
|
if( FileBrowserHelpers.ShouldUseSAF )
|
|
{
|
|
// These UI elements have no use in Storage Access Framework mode (Android 10+)
|
|
pathInputField.gameObject.SetActive( false );
|
|
showHiddenFilesToggle.gameObject.SetActive( false );
|
|
}
|
|
#endif
|
|
|
|
SetExcludedExtensions( excludedExtensions );
|
|
|
|
backButton.interactable = false;
|
|
forwardButton.interactable = false;
|
|
upButton.interactable = false;
|
|
|
|
filenameInputField.onValidateInput += OnValidateFilenameInput;
|
|
filenameInputField.onValueChanged.AddListener( OnFilenameInputChanged );
|
|
|
|
allFilesFilter = new Filter( AllFilesFilterText );
|
|
filters.Add( allFilesFilter );
|
|
|
|
invalidFilenameChars = new HashSet<char>( Path.GetInvalidFileNameChars() )
|
|
{
|
|
Path.DirectorySeparatorChar,
|
|
Path.AltDirectorySeparatorChar
|
|
};
|
|
|
|
window.Initialize( this );
|
|
listView.SetAdapter( this );
|
|
|
|
// Refresh the skin immediately
|
|
m_skinVersion = m_skin.Version;
|
|
RefreshSkin();
|
|
|
|
if( !showResizeCursor )
|
|
Destroy( resizeCursorHandler );
|
|
|
|
#if ENABLE_INPUT_SYSTEM && !ENABLE_LEGACY_INPUT_MANAGER
|
|
// On new Input System, scroll sensitivity is much higher than legacy Input system
|
|
filesScrollRect.scrollSensitivity *= 0.25f;
|
|
quickLinksContainer.GetComponentInParent<ScrollRect>().scrollSensitivity *= 0.25f;
|
|
filtersDropdownContainer.GetComponent<ScrollRect>().scrollSensitivity *= 0.25f;
|
|
#endif
|
|
}
|
|
|
|
private void OnRectTransformDimensionsChange()
|
|
{
|
|
canvasDimensionsChanged = true;
|
|
}
|
|
|
|
#if UNITY_EDITOR
|
|
protected virtual void OnValidate()
|
|
{
|
|
// Refresh the skin in the next Update if it is changed via Unity Inspector at runtime
|
|
if( UnityEditor.EditorApplication.isPlaying && m_skin != prevSkin )
|
|
{
|
|
if( !m_skin ) // Don't allow null UISkin
|
|
m_skin = prevSkin;
|
|
else
|
|
m_skinVersion = m_skin.Version - 1;
|
|
}
|
|
}
|
|
#endif
|
|
|
|
private void Update()
|
|
{
|
|
if( m_skin && m_skinVersion != m_skin.Version )
|
|
{
|
|
m_skinVersion = m_skin.Version;
|
|
RefreshSkin();
|
|
|
|
#if UNITY_EDITOR
|
|
prevSkin = m_skin;
|
|
#endif
|
|
}
|
|
}
|
|
|
|
private void LateUpdate()
|
|
{
|
|
if( canvasDimensionsChanged )
|
|
{
|
|
canvasDimensionsChanged = false;
|
|
|
|
Vector2 windowSize = windowTR.sizeDelta;
|
|
EnsureWindowIsWithinBounds();
|
|
if( windowTR.sizeDelta != windowSize )
|
|
OnWindowDimensionsChanged( windowTR.sizeDelta );
|
|
|
|
fileOperationConfirmationPanel.OnCanvasDimensionsChanged( rectTransform.sizeDelta );
|
|
|
|
if( contextMenu.gameObject.activeSelf )
|
|
contextMenu.Hide();
|
|
}
|
|
|
|
#if UNITY_EDITOR || UNITY_STANDALONE || UNITY_WEBGL || UNITY_WSA || UNITY_WSA_10_0
|
|
// Handle keyboard shortcuts
|
|
if( !EventSystem.current.currentSelectedGameObject )
|
|
{
|
|
#if ENABLE_INPUT_SYSTEM && !ENABLE_LEGACY_INPUT_MANAGER
|
|
if( Keyboard.current != null )
|
|
#endif
|
|
{
|
|
#if ENABLE_INPUT_SYSTEM && !ENABLE_LEGACY_INPUT_MANAGER
|
|
if( Keyboard.current[Key.Delete].wasPressedThisFrame )
|
|
#else
|
|
if( Input.GetKeyDown( KeyCode.Delete ) )
|
|
#endif
|
|
DeleteSelectedFiles();
|
|
|
|
#if ENABLE_INPUT_SYSTEM && !ENABLE_LEGACY_INPUT_MANAGER
|
|
if( Keyboard.current[Key.F2].wasPressedThisFrame )
|
|
#else
|
|
if( Input.GetKeyDown( KeyCode.F2 ) )
|
|
#endif
|
|
RenameSelectedFile();
|
|
|
|
#if ENABLE_INPUT_SYSTEM && !ENABLE_LEGACY_INPUT_MANAGER
|
|
if( Keyboard.current[Key.A].wasPressedThisFrame && IsCtrlKeyHeld() )
|
|
#else
|
|
if( Input.GetKeyDown( KeyCode.A ) && IsCtrlKeyHeld() )
|
|
#endif
|
|
SelectAllFiles();
|
|
}
|
|
}
|
|
#endif
|
|
|
|
// 2 Text objects are used in the filename input field:
|
|
// filenameInputField.textComponent: visible when editing the text, has Horizontal Overflow set to Wrap (cuts out words, ugly)
|
|
// filenameInputFieldOverlayText: visible when not editing the text, has Horizontal Overflow set to Overflow (doesn't cut out words)
|
|
if( EventSystem.current.currentSelectedGameObject == filenameInputField.gameObject )
|
|
{
|
|
if( filenameInputFieldOverlayText.enabled )
|
|
{
|
|
filenameInputFieldOverlayText.enabled = false;
|
|
filenameInputField.textComponent.color = m_skin.InputFieldTextColor;
|
|
}
|
|
}
|
|
else if( !filenameInputFieldOverlayText.enabled )
|
|
{
|
|
filenameInputFieldOverlayText.enabled = true;
|
|
|
|
Color c = m_skin.InputFieldTextColor;
|
|
c.a = 0f;
|
|
filenameInputField.textComponent.color = c;
|
|
}
|
|
|
|
// Refresh drive quick links
|
|
#if UNITY_EDITOR || ( !UNITY_IOS && !UNITY_WSA && !UNITY_WSA_10_0 )
|
|
#if !UNITY_EDITOR && UNITY_ANDROID
|
|
if( !FileBrowserHelpers.ShouldUseSAF )
|
|
#endif
|
|
if( quickLinksInitialized && generateQuickLinksForDrives && m_drivesRefreshInterval >= 0f && Time.realtimeSinceStartup >= drivesNextRefreshTime )
|
|
{
|
|
drivesNextRefreshTime = Time.realtimeSinceStartup + m_drivesRefreshInterval;
|
|
RefreshDriveQuickLinks();
|
|
}
|
|
#endif
|
|
}
|
|
|
|
private void OnApplicationFocus( bool focus )
|
|
{
|
|
if( !focus )
|
|
PersistFileEntrySelection();
|
|
else
|
|
RefreshFiles( true );
|
|
}
|
|
#endregion
|
|
|
|
#region Interface Methods
|
|
OnItemClickedHandler IListViewAdapter.OnItemClicked { get { return null; } set { } }
|
|
|
|
int IListViewAdapter.Count { get { return validFileEntries.Count; } }
|
|
float IListViewAdapter.ItemHeight { get { return m_skin.FileHeight; } }
|
|
|
|
ListItem IListViewAdapter.CreateItem()
|
|
{
|
|
FileBrowserItem item = (FileBrowserItem) Instantiate( itemPrefab, filesContainer, false );
|
|
item.SetFileBrowser( this, m_skin );
|
|
allItems.Add( item );
|
|
|
|
return item;
|
|
}
|
|
|
|
void IListViewAdapter.SetItemContent( ListItem item )
|
|
{
|
|
FileBrowserItem file = (FileBrowserItem) item;
|
|
FileSystemEntry fileInfo = validFileEntries[item.Position];
|
|
|
|
file.SetFile( GetIconForFileEntry( fileInfo ), fileInfo.Name, fileInfo.IsDirectory );
|
|
file.SetSelected( selectedFileEntries.Contains( file.Position ) );
|
|
file.SetHidden( ( fileInfo.Attributes & FileAttributes.Hidden ) == FileAttributes.Hidden );
|
|
}
|
|
#endregion
|
|
|
|
#region Initialization Functions
|
|
private void InitializeQuickLinks()
|
|
{
|
|
quickLinksInitialized = true;
|
|
drivesNextRefreshTime = Time.realtimeSinceStartup + m_drivesRefreshInterval;
|
|
|
|
#if !UNITY_EDITOR && UNITY_ANDROID
|
|
if( FileBrowserHelpers.ShouldUseSAF )
|
|
{
|
|
AddQuickLink( m_skin.DriveIcon, PickFolderQuickLinkText, SAF_PICK_FOLDER_QUICK_LINK_PATH );
|
|
|
|
try
|
|
{
|
|
FetchPersistedSAFQuickLinks();
|
|
}
|
|
catch( Exception e )
|
|
{
|
|
Debug.LogException( e );
|
|
}
|
|
|
|
return;
|
|
}
|
|
#endif
|
|
|
|
if( generateQuickLinksForDrives )
|
|
{
|
|
#if UNITY_EDITOR || ( !UNITY_IOS && !UNITY_WSA && !UNITY_WSA_10_0 )
|
|
RefreshDriveQuickLinks();
|
|
#else
|
|
AddQuickLink( m_skin.DriveIcon, "Files", Application.persistentDataPath );
|
|
#endif
|
|
|
|
#if UNITY_STANDALONE_OSX
|
|
// Add a quick link for user directory on Mac OS
|
|
string userDirectory = Environment.GetFolderPath( Environment.SpecialFolder.MyDocuments );
|
|
if( !string.IsNullOrEmpty( userDirectory ) )
|
|
AddQuickLink( m_skin.DriveIcon, userDirectory.Substring( userDirectory.LastIndexOf( '/' ) + 1 ), userDirectory );
|
|
#endif
|
|
}
|
|
|
|
#if UNITY_EDITOR || ( !UNITY_ANDROID && !UNITY_WSA && !UNITY_WSA_10_0 )
|
|
for( int i = 0; i < quickLinks.Length; i++ )
|
|
{
|
|
QuickLink quickLink = quickLinks[i];
|
|
string quickLinkPath = Environment.GetFolderPath( quickLink.target );
|
|
#if UNITY_STANDALONE_OSX
|
|
// Documents folder must be appended manually on Mac OS
|
|
if( quickLink.target == Environment.SpecialFolder.MyDocuments && !string.IsNullOrEmpty( quickLinkPath ) )
|
|
quickLinkPath = Path.Combine( quickLinkPath, "Documents" );
|
|
#endif
|
|
|
|
AddQuickLink( quickLink.icon, quickLink.name, quickLinkPath );
|
|
}
|
|
|
|
quickLinks = null;
|
|
#endif
|
|
}
|
|
|
|
private void RefreshDriveQuickLinks()
|
|
{
|
|
// Check if drives has changed since the last refresh
|
|
#if !UNITY_EDITOR && UNITY_ANDROID
|
|
string drivesList = FileBrowserHelpers.AJC.CallStatic<string>( "GetExternalDrives", FileBrowserHelpers.Context );
|
|
if( drivesList == driveQuickLinks || ( string.IsNullOrEmpty( drivesList ) && string.IsNullOrEmpty( driveQuickLinks ) ) )
|
|
return;
|
|
|
|
driveQuickLinks = drivesList;
|
|
#else
|
|
string[] drives = Directory.GetLogicalDrives();
|
|
|
|
if( driveQuickLinks != null && drives.Length == driveQuickLinks.Length )
|
|
{
|
|
bool drivesListHasntChanged = true;
|
|
for( int i = 0; i < drives.Length; i++ )
|
|
{
|
|
#if !WIN_DIR_CHECK_WITHOUT_TIMEOUT && ( UNITY_EDITOR_WIN || UNITY_STANDALONE_WIN )
|
|
if( timedOutDirectoryExistsRequests.Contains( drives[i] ) )
|
|
continue;
|
|
#endif
|
|
|
|
if( drives[i] != driveQuickLinks[i] )
|
|
{
|
|
drivesListHasntChanged = false;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if( drivesListHasntChanged )
|
|
return;
|
|
}
|
|
|
|
driveQuickLinks = drives;
|
|
#endif
|
|
|
|
// Drives has changed, remove previous drive quick links
|
|
for( ; numberOfDriveQuickLinks > 0; numberOfDriveQuickLinks-- )
|
|
{
|
|
Destroy( allQuickLinks[numberOfDriveQuickLinks - 1].gameObject );
|
|
allQuickLinks.RemoveAt( numberOfDriveQuickLinks - 1 );
|
|
}
|
|
|
|
FileBrowserQuickLink[] customQuickLinks = allQuickLinks.Count > 0 ? allQuickLinks.ToArray() : null;
|
|
allQuickLinks.Clear();
|
|
|
|
quickLinksContainer.sizeDelta = Vector2.zero;
|
|
|
|
// Create drive quick links
|
|
#if !UNITY_EDITOR && UNITY_ANDROID
|
|
if( drivesList != null && drivesList.Length > 0 )
|
|
{
|
|
bool defaultPathInitialized = false;
|
|
int driveIndex = 1;
|
|
string[] drives = drivesList.Split( ':' );
|
|
for( int i = 0; i < drives.Length; i++ )
|
|
{
|
|
try
|
|
{
|
|
//string driveName = new DirectoryInfo( drives[i] ).Name;
|
|
//if( driveName.Length <= 1 )
|
|
//{
|
|
// try
|
|
// {
|
|
// driveName = Directory.GetParent( drives[i] ).Name + "/" + driveName;
|
|
// }
|
|
// catch
|
|
// {
|
|
// driveName = "Drive " + driveIndex++;
|
|
// }
|
|
//}
|
|
|
|
string driveName;
|
|
if( !defaultPathInitialized )
|
|
{
|
|
defaultInitialPath = drives[i];
|
|
defaultPathInitialized = true;
|
|
|
|
driveName = "Primary Drive";
|
|
}
|
|
else
|
|
{
|
|
if( driveIndex == 1 )
|
|
driveName = "External Drive";
|
|
else
|
|
driveName = "External Drive " + driveIndex;
|
|
|
|
driveIndex++;
|
|
}
|
|
|
|
if( AddQuickLink( m_skin.DriveIcon, driveName, drives[i] ) )
|
|
numberOfDriveQuickLinks++;
|
|
}
|
|
catch { }
|
|
}
|
|
}
|
|
#else
|
|
for( int i = 0; i < drives.Length; i++ )
|
|
{
|
|
if( string.IsNullOrEmpty( drives[i] ) )
|
|
continue;
|
|
|
|
#if UNITY_STANDALONE_OSX
|
|
// There are a number of useless drives listed on Mac OS, filter them
|
|
if( drives[i] == "/" )
|
|
{
|
|
if( AddQuickLink( m_skin.DriveIcon, "Root", drives[i] ) )
|
|
numberOfDriveQuickLinks++;
|
|
}
|
|
else if( drives[i].StartsWith( "/Volumes/" ) && drives[i] != "/Volumes/Recovery" )
|
|
{
|
|
if( AddQuickLink( m_skin.DriveIcon, drives[i].Substring( drives[i].LastIndexOf( '/' ) + 1 ), drives[i] ) )
|
|
numberOfDriveQuickLinks++;
|
|
}
|
|
#else
|
|
if( AddQuickLink( m_skin.DriveIcon, drives[i], drives[i] ) )
|
|
numberOfDriveQuickLinks++;
|
|
#endif
|
|
}
|
|
#endif
|
|
|
|
// Reposition custom quick links
|
|
if( customQuickLinks != null )
|
|
{
|
|
Vector2 anchoredPos = new Vector2( 0f, -quickLinksContainer.sizeDelta.y );
|
|
for( int i = 0; i < customQuickLinks.Length; i++ )
|
|
{
|
|
customQuickLinks[i].TransformComponent.anchoredPosition = anchoredPos;
|
|
anchoredPos.y -= m_skin.FileHeight;
|
|
|
|
allQuickLinks.Add( customQuickLinks[i] );
|
|
}
|
|
|
|
quickLinksContainer.sizeDelta = new Vector2( 0f, -anchoredPos.y );
|
|
}
|
|
|
|
// Verify that current directory still exists
|
|
try
|
|
{
|
|
if( !string.IsNullOrEmpty( m_currentPath ) && !FileBrowserHelpers.DirectoryExists( m_currentPath ) )
|
|
{
|
|
string currentPathRoot = Path.GetPathRoot( m_currentPath );
|
|
if( !string.IsNullOrEmpty( currentPathRoot ) && FileBrowserHelpers.DirectoryExists( currentPathRoot ) )
|
|
CurrentPath = currentPathRoot;
|
|
else if( allQuickLinks.Count > 0 )
|
|
CurrentPath = allQuickLinks[0].TargetPath;
|
|
}
|
|
}
|
|
catch { }
|
|
}
|
|
|
|
private void RefreshSkin()
|
|
{
|
|
window.GetComponent<Image>().color = m_skin.WindowColor;
|
|
middleView.GetComponent<Image>().color = m_skin.FilesListColor;
|
|
middleViewSeparator.GetComponent<Image>().color = m_skin.FilesVerticalSeparatorColor;
|
|
|
|
titleText.transform.parent.GetComponent<Image>().color = m_skin.TitleBackgroundColor;
|
|
m_skin.ApplyTo( titleText, m_skin.TitleTextColor );
|
|
|
|
backButton.image.color = m_skin.HeaderButtonsColor;
|
|
forwardButton.image.color = m_skin.HeaderButtonsColor;
|
|
upButton.image.color = m_skin.HeaderButtonsColor;
|
|
moreOptionsButton.image.color = m_skin.HeaderButtonsColor;
|
|
|
|
backButton.image.sprite = m_skin.HeaderBackButton;
|
|
forwardButton.image.sprite = m_skin.HeaderForwardButton;
|
|
upButton.image.sprite = m_skin.HeaderUpButton;
|
|
moreOptionsButton.image.sprite = m_skin.HeaderContextMenuButton;
|
|
|
|
if( resizeCursorHandler )
|
|
{
|
|
Image windowResizeGizmo = resizeCursorHandler.GetComponent<Image>();
|
|
windowResizeGizmo.color = m_skin.WindowResizeGizmoColor;
|
|
windowResizeGizmo.sprite = m_skin.WindowResizeGizmo;
|
|
}
|
|
|
|
m_skin.ApplyTo( filenameInputField );
|
|
m_skin.ApplyTo( pathInputField );
|
|
m_skin.ApplyTo( searchInputField );
|
|
m_skin.ApplyTo( renameItem.InputField );
|
|
m_skin.ApplyTo( filenameInputFieldOverlayText, m_skin.InputFieldTextColor );
|
|
|
|
if( !EventSystem.current || EventSystem.current.currentSelectedGameObject != filenameInputField.gameObject )
|
|
{
|
|
Color c = m_skin.InputFieldTextColor;
|
|
c.a = 0f;
|
|
filenameInputField.textComponent.color = c;
|
|
}
|
|
|
|
for( int i = 0; i < allButtons.Length; i++ )
|
|
m_skin.ApplyTo( allButtons[i] );
|
|
|
|
m_skin.ApplyTo( filtersDropdown );
|
|
m_skin.ApplyTo( showHiddenFilesToggle );
|
|
|
|
m_skin.ApplyTo( quickLinksScrollRect.verticalScrollbar );
|
|
m_skin.ApplyTo( filesScrollRect.verticalScrollbar );
|
|
m_skin.ApplyTo( filtersDropdown.template.GetComponent<ScrollRect>().verticalScrollbar );
|
|
|
|
for( int i = 0; i < allQuickLinks.Count; i++ )
|
|
{
|
|
allQuickLinks[i].OnSkinRefreshed( m_skin );
|
|
allQuickLinks[i].TransformComponent.anchoredPosition = new Vector2( 0f, allQuickLinks[i].TransformComponent.GetSiblingIndex() * -m_skin.FileHeight );
|
|
|
|
if( allQuickLinks[i].Icon.sprite == m_skinPrevDriveIcon )
|
|
allQuickLinks[i].Icon.sprite = m_skin.DriveIcon;
|
|
else if( allQuickLinks[i].Icon.sprite == m_skinPrevFolderIcon )
|
|
allQuickLinks[i].Icon.sprite = m_skin.FolderIcon;
|
|
}
|
|
|
|
quickLinksContainer.sizeDelta = new Vector2( 0f, allQuickLinks.Count * m_skin.FileHeight );
|
|
|
|
for( int i = 0; i < allItems.Count; i++ )
|
|
allItems[i].OnSkinRefreshed( m_skin );
|
|
|
|
renameItem.TransformComponent.sizeDelta = new Vector2( renameItem.TransformComponent.sizeDelta.x, m_skin.FileHeight );
|
|
|
|
contextMenu.RefreshSkin( m_skin );
|
|
fileOperationConfirmationPanel.RefreshSkin( m_skin );
|
|
accessRestrictedPanel.RefreshSkin( m_skin );
|
|
|
|
listView.OnSkinRefreshed();
|
|
|
|
m_skinPrevDriveIcon = m_skin.DriveIcon;
|
|
m_skinPrevFolderIcon = m_skin.FolderIcon;
|
|
}
|
|
#endregion
|
|
|
|
#region Button Events
|
|
public void OnBackButtonPressed()
|
|
{
|
|
if( currentPathIndex > 0 )
|
|
CurrentPath = pathsFollowed[--currentPathIndex];
|
|
}
|
|
|
|
public void OnForwardButtonPressed()
|
|
{
|
|
if( currentPathIndex < pathsFollowed.Count - 1 )
|
|
CurrentPath = pathsFollowed[++currentPathIndex];
|
|
}
|
|
|
|
public void OnUpButtonPressed()
|
|
{
|
|
#if !UNITY_EDITOR && UNITY_ANDROID
|
|
if( FileBrowserHelpers.ShouldUseSAF )
|
|
{
|
|
string parentPath = FileBrowserHelpers.GetDirectoryName( m_currentPath );
|
|
if( !string.IsNullOrEmpty( parentPath ) && ( FileBrowserHelpers.ShouldUseSAFForPath( parentPath ) || FileBrowserHelpers.DirectoryExists( parentPath ) ) ) // DirectoryExists: Directory may not be accessible on Android 10+, this function checks that
|
|
CurrentPath = parentPath;
|
|
}
|
|
else
|
|
#endif
|
|
{
|
|
try // When "C:/" or "C:" is typed instead of "C:\", an exception is thrown
|
|
{
|
|
DirectoryInfo parentPath = Directory.GetParent( m_currentPath );
|
|
if( parentPath != null )
|
|
CurrentPath = parentPath.FullName;
|
|
}
|
|
catch
|
|
{
|
|
}
|
|
}
|
|
}
|
|
|
|
public void OnMoreOptionsButtonClicked()
|
|
{
|
|
ShowContextMenuAt( rectTransform.InverseTransformPoint( moreOptionsContextMenuPosition.position ), true );
|
|
}
|
|
|
|
internal void OnContextMenuTriggered( Vector2 pointerPos )
|
|
{
|
|
filesScrollRect.velocity = Vector2.zero;
|
|
|
|
Vector2 position;
|
|
RectTransformUtility.ScreenPointToLocalPointInRectangle( rectTransform, pointerPos, canvas.worldCamera, out position );
|
|
|
|
ShowContextMenuAt( position, false );
|
|
}
|
|
|
|
private void ShowContextMenuAt( Vector2 position, bool isMoreOptionsMenu )
|
|
{
|
|
if( string.IsNullOrEmpty( m_currentPath ) )
|
|
return;
|
|
|
|
bool selectAllButtonVisible = isMoreOptionsMenu && m_allowMultiSelection && validFileEntries.Count > 0;
|
|
bool deselectAllButtonVisible = isMoreOptionsMenu && selectedFileEntries.Count > 1;
|
|
bool deleteButtonVisible = contextMenuShowDeleteButton && selectedFileEntries.Count > 0;
|
|
bool renameButtonVisible = contextMenuShowRenameButton && selectedFileEntries.Count == 1;
|
|
|
|
if( selectAllButtonVisible && m_pickerMode == PickMode.Files )
|
|
{
|
|
// In file selection mode, if only folders exist in the current path, "Select All" option shouldn't be visible
|
|
selectAllButtonVisible = false;
|
|
for( int i = 0; i < validFileEntries.Count; i++ )
|
|
{
|
|
if( !validFileEntries[i].IsDirectory )
|
|
{
|
|
selectAllButtonVisible = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
contextMenu.Show( selectAllButtonVisible, deselectAllButtonVisible, deleteButtonVisible, renameButtonVisible, position, isMoreOptionsMenu );
|
|
}
|
|
|
|
public void OnSubmitButtonClicked()
|
|
{
|
|
string[] result = null;
|
|
string filenameInput = filenameInputField.text.Trim();
|
|
|
|
submittedFileEntryPaths.Clear();
|
|
submittedFolderPaths.Clear();
|
|
submittedFileEntriesToOverwrite.Clear();
|
|
|
|
if( filenameInput.Length == 0 )
|
|
{
|
|
if( m_pickerMode == PickMode.Files )
|
|
{
|
|
filenameImage.color = m_skin.InputFieldInvalidBackgroundColor;
|
|
return;
|
|
}
|
|
else
|
|
{
|
|
result = new string[1] { m_currentPath };
|
|
submittedFolderPaths.Add( m_currentPath );
|
|
}
|
|
}
|
|
|
|
if( result == null )
|
|
{
|
|
if( m_allowMultiSelection && selectedFileEntries.Count > 1 )
|
|
{
|
|
// When multiple files are selected via file browser UI, filenameInputField is not interactable and will show
|
|
// only the first FILENAME_INPUT_FIELD_MAX_FILE_COUNT entries for performance reasons. We should iterate over
|
|
// selectedFileEntries instead of filenameInputField
|
|
|
|
// Beforehand, check if a folder is selected in file selection mode. If so, open that directory
|
|
if( m_pickerMode == PickMode.Files )
|
|
{
|
|
for( int i = 0; i < selectedFileEntries.Count; i++ )
|
|
{
|
|
if( validFileEntries[selectedFileEntries[i]].IsDirectory )
|
|
{
|
|
CurrentPath = validFileEntries[selectedFileEntries[i]].Path;
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
result = new string[selectedFileEntries.Count];
|
|
for( int i = 0; i < selectedFileEntries.Count; i++ )
|
|
{
|
|
result[i] = validFileEntries[selectedFileEntries[i]].Path;
|
|
|
|
if( validFileEntries[selectedFileEntries[i]].IsDirectory )
|
|
submittedFolderPaths.Add( result[i] );
|
|
else if( m_acceptNonExistingFilename )
|
|
{
|
|
submittedFileEntriesToOverwrite.Add( validFileEntries[selectedFileEntries[i]] );
|
|
|
|
if( !submittedFolderPaths.Contains( m_currentPath ) )
|
|
submittedFolderPaths.Add( m_currentPath );
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// When multiple files aren't selected via file browser UI, we must consider the rare case where user manually enters
|
|
// multiple filenames to filenameInputField in format "file1" "file2" and so on. So, we must parse filenameInputField
|
|
|
|
for( int startIndex = 0, nextStartIndex = 0; startIndex < filenameInput.Length; startIndex = nextStartIndex )
|
|
{
|
|
int filenameLength = ExtractFilenameFromInput( filenameInput, ref startIndex, out nextStartIndex );
|
|
if( filenameLength == 0 )
|
|
continue;
|
|
|
|
string filename = filenameInput.Substring( startIndex, filenameLength ).Trim();
|
|
if( !VerifyFilename( filename ) )
|
|
{
|
|
// Check if user has entered a full path to input field instead of just a filename. Even if it's the case, don't immediately accept the full path,
|
|
// first verify that it doesn't point to a file/folder that is ignored by the file browser
|
|
try
|
|
{
|
|
if( FileBrowserHelpers.DirectoryExists( filename ) )
|
|
{
|
|
FileSystemEntry fileEntry = new FileSystemEntry( filename, FileBrowserHelpers.GetFilename( filename ), "", true );
|
|
if( FileSystemEntryMatchesFilters( fileEntry, AllExtensionsHaveSingleSuffix ) )
|
|
{
|
|
if( m_pickerMode == PickMode.Files )
|
|
{
|
|
CurrentPath = filename;
|
|
return;
|
|
}
|
|
else
|
|
{
|
|
submittedFileEntryPaths.Add( filename );
|
|
submittedFolderPaths.Add( filename );
|
|
|
|
continue;
|
|
}
|
|
}
|
|
}
|
|
else if( m_pickerMode != PickMode.Folders && FileBrowserHelpers.FileExists( filename ) )
|
|
{
|
|
string fullPathFilename = FileBrowserHelpers.GetFilename( filename );
|
|
FileSystemEntry fileEntry = new FileSystemEntry( filename, fullPathFilename, GetExtensionFromFilename( fullPathFilename, AllExtensionsHaveSingleSuffix ), false );
|
|
if( FileSystemEntryMatchesFilters( fileEntry, AllExtensionsHaveSingleSuffix ) )
|
|
{
|
|
submittedFileEntryPaths.Add( filename );
|
|
submittedFileEntriesToOverwrite.Add( fileEntry );
|
|
|
|
if( m_acceptNonExistingFilename )
|
|
submittedFolderPaths.Add( FileBrowserHelpers.GetDirectoryName( filename ) );
|
|
|
|
continue;
|
|
}
|
|
}
|
|
}
|
|
catch { }
|
|
|
|
// Filename contains invalid characters or is completely whitespace
|
|
filenameImage.color = m_skin.InputFieldInvalidBackgroundColor;
|
|
return;
|
|
}
|
|
|
|
try
|
|
{
|
|
int fileEntryIndex = FilenameToFileEntryIndex( filename );
|
|
if( fileEntryIndex < 0 )
|
|
{
|
|
if( m_pickerMode != PickMode.Folders )
|
|
{
|
|
bool isAllFilesFilterActive = filters[filtersDropdown.value].extensions == null;
|
|
if( !m_acceptNonExistingFilename || !isAllFilesFilterActive )
|
|
{
|
|
// File couldn't be found but perhaps filename is missing the extension, check if any of the files match the filename without extension
|
|
for( int i = 0; i < validFileEntries.Count; i++ )
|
|
{
|
|
if( !validFileEntries[i].IsDirectory && validFileEntries[i].Name.Length >= filename.Length + 2 && validFileEntries[i].Name[filename.Length] == '.' )
|
|
{
|
|
if( validFileEntries[i].Name.StartsWith( filename ) ) // Case-sensitive filename query
|
|
{
|
|
fileEntryIndex = i;
|
|
break;
|
|
}
|
|
else if( textComparer.IsPrefix( validFileEntries[i].Name, filename, textCompareOptions ) ) // Case-insensitive filename query
|
|
{
|
|
// Don't exit the loop immediately because case-sensitive query takes precedence, we need to check all files to see if there's a case-sensitive match
|
|
fileEntryIndex = i;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if( m_acceptNonExistingFilename && fileEntryIndex < 0 && !isAllFilesFilterActive )
|
|
{
|
|
// In file saving mode, make sure that nonexisting files' extensions match one of the required extensions
|
|
string fileExtension = GetExtensionFromFilename( filename, AllExtensionsHaveSingleSuffix );
|
|
if( string.IsNullOrEmpty( fileExtension ) || !filters[filtersDropdown.value].MatchesExtension( fileExtension, !AllExtensionsHaveSingleSuffix ) )
|
|
{
|
|
filename = Path.ChangeExtension( filename, filters[filtersDropdown.value].defaultExtension );
|
|
fileEntryIndex = FilenameToFileEntryIndex( filename );
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if( fileEntryIndex >= 0 ) // This is an existing file/folder
|
|
{
|
|
if( validFileEntries[fileEntryIndex].IsDirectory && m_pickerMode == PickMode.Files )
|
|
{
|
|
// Selected a directory in file selection mode, open that directory
|
|
CurrentPath = validFileEntries[fileEntryIndex].Path;
|
|
return;
|
|
}
|
|
else
|
|
{
|
|
submittedFileEntryPaths.Add( validFileEntries[fileEntryIndex].Path );
|
|
|
|
if( validFileEntries[fileEntryIndex].IsDirectory )
|
|
submittedFolderPaths.Add( validFileEntries[fileEntryIndex].Path );
|
|
else if( m_acceptNonExistingFilename )
|
|
{
|
|
submittedFileEntriesToOverwrite.Add( validFileEntries[fileEntryIndex] );
|
|
|
|
if( !submittedFolderPaths.Contains( m_currentPath ) )
|
|
submittedFolderPaths.Add( m_currentPath );
|
|
}
|
|
}
|
|
}
|
|
else // File/folder doesn't exist
|
|
{
|
|
if( !m_acceptNonExistingFilename )
|
|
{
|
|
filenameImage.color = m_skin.InputFieldInvalidBackgroundColor;
|
|
return;
|
|
}
|
|
else
|
|
{
|
|
#if !UNITY_EDITOR && UNITY_ANDROID
|
|
if( FileBrowserHelpers.ShouldUseSAFForPath( m_currentPath ) )
|
|
{
|
|
if( m_pickerMode == PickMode.Folders )
|
|
submittedFileEntryPaths.Add( FileBrowserHelpers.CreateFolderInDirectory( m_currentPath, filename ) );
|
|
else
|
|
submittedFileEntryPaths.Add( FileBrowserHelpers.CreateFileInDirectory( m_currentPath, filename ) );
|
|
}
|
|
else
|
|
#endif
|
|
{
|
|
submittedFileEntryPaths.Add( Path.Combine( m_currentPath, filename ) );
|
|
|
|
if( !submittedFolderPaths.Contains( m_currentPath ) )
|
|
submittedFolderPaths.Add( m_currentPath );
|
|
}
|
|
}
|
|
}
|
|
}
|
|
catch( ArgumentException e )
|
|
{
|
|
filenameImage.color = m_skin.InputFieldInvalidBackgroundColor;
|
|
Debug.LogException( e );
|
|
return;
|
|
}
|
|
}
|
|
|
|
if( submittedFileEntryPaths.Count == 0 )
|
|
{
|
|
filenameImage.color = m_skin.InputFieldInvalidBackgroundColor;
|
|
return;
|
|
}
|
|
|
|
result = submittedFileEntryPaths.ToArray();
|
|
}
|
|
}
|
|
|
|
if( result != null )
|
|
{
|
|
if( m_checkWriteAccessToDestinationDirectory )
|
|
{
|
|
for( int i = 0; i < submittedFolderPaths.Count; i++ )
|
|
{
|
|
if( !string.IsNullOrEmpty( submittedFolderPaths[i] ) && !CheckDirectoryWriteAccess( submittedFolderPaths[i] ) )
|
|
{
|
|
accessRestrictedPanel.Show();
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
if( m_showFileOverwriteDialog && submittedFileEntriesToOverwrite.Count > 0 )
|
|
{
|
|
fileOperationConfirmationPanel.Show( this, submittedFileEntriesToOverwrite, FileBrowserFileOperationConfirmationPanel.OperationType.Overwrite, () => OnOperationSuccessful( result ) );
|
|
return;
|
|
}
|
|
|
|
OnOperationSuccessful( result );
|
|
}
|
|
}
|
|
|
|
public void OnCancelButtonClicked()
|
|
{
|
|
OnOperationCanceled( true );
|
|
}
|
|
#endregion
|
|
|
|
#region Other Events
|
|
private void OnOperationSuccessful( string[] paths )
|
|
{
|
|
Success = true;
|
|
Result = paths;
|
|
|
|
Hide();
|
|
|
|
if( !string.IsNullOrEmpty( m_currentPath ) )
|
|
LastBrowsedFolder = m_currentPath;
|
|
|
|
OnSuccess _onSuccess = onSuccess;
|
|
onSuccess = null;
|
|
onCancel = null;
|
|
|
|
if( _onSuccess != null )
|
|
_onSuccess( paths );
|
|
}
|
|
|
|
private void OnOperationCanceled( bool invokeCancelCallback )
|
|
{
|
|
Success = false;
|
|
Result = null;
|
|
|
|
Hide();
|
|
|
|
if( !string.IsNullOrEmpty( m_currentPath ) )
|
|
LastBrowsedFolder = m_currentPath;
|
|
|
|
OnCancel _onCancel = onCancel;
|
|
onSuccess = null;
|
|
onCancel = null;
|
|
|
|
if( invokeCancelCallback && _onCancel != null )
|
|
_onCancel();
|
|
}
|
|
|
|
public void OnPathChanged( string newPath )
|
|
{
|
|
// Fixes harmless NullReferenceException that occurs when Play button is clicked while SimpleFileBrowserCanvas prefab is open in prefab mode
|
|
// https://github.com/yasirkula/UnitySimpleFileBrowser/issues/30
|
|
if( !canvas )
|
|
return;
|
|
|
|
CurrentPath = newPath;
|
|
}
|
|
|
|
public void OnSearchStringChanged( string newSearchString )
|
|
{
|
|
if( !canvas ) // Same as OnPathChanged
|
|
return;
|
|
|
|
PersistFileEntrySelection();
|
|
SearchString = newSearchString;
|
|
}
|
|
|
|
public void OnFilterChanged()
|
|
{
|
|
if( !canvas ) // Same as OnPathChanged
|
|
return;
|
|
|
|
bool extensionsSingleSuffixModeChanged = false;
|
|
|
|
if( filters != null && filtersDropdown.value < filters.Count )
|
|
{
|
|
bool allExtensionsHadSingleSuffix = AllExtensionsHaveSingleSuffix;
|
|
allFiltersHaveSingleSuffix = filters[filtersDropdown.value].allExtensionsHaveSingleSuffix;
|
|
extensionsSingleSuffixModeChanged = ( AllExtensionsHaveSingleSuffix != allExtensionsHadSingleSuffix );
|
|
}
|
|
|
|
PersistFileEntrySelection();
|
|
RefreshFiles( extensionsSingleSuffixModeChanged );
|
|
}
|
|
|
|
public void OnShowHiddenFilesToggleChanged()
|
|
{
|
|
if( !canvas ) // Same as OnPathChanged
|
|
return;
|
|
|
|
PersistFileEntrySelection();
|
|
RefreshFiles( false );
|
|
}
|
|
|
|
public void OnItemSelected( FileBrowserItem item, bool isDoubleClick )
|
|
{
|
|
if( item == null )
|
|
return;
|
|
|
|
if( item is FileBrowserQuickLink )
|
|
{
|
|
#if !UNITY_EDITOR && UNITY_ANDROID
|
|
if( ( (FileBrowserQuickLink) item ).TargetPath == SAF_PICK_FOLDER_QUICK_LINK_PATH )
|
|
FileBrowserHelpers.AJC.CallStatic( "PickSAFFolder", FileBrowserHelpers.Context, new FBDirectoryReceiveCallbackAndroid( OnSAFDirectoryPicked ) );
|
|
else
|
|
#endif
|
|
CurrentPath = ( (FileBrowserQuickLink) item ).TargetPath;
|
|
|
|
return;
|
|
}
|
|
|
|
if( m_multiSelectionToggleSelectionMode )
|
|
{
|
|
// In file selection mode, we shouldn't include folders in the multi-selection
|
|
if( item.IsDirectory && m_pickerMode == PickMode.Files && !selectedFileEntries.Contains( item.Position ) )
|
|
return;
|
|
|
|
// If a file/folder is double clicked in multi-selection mode, instead of opening that file/folder, we want to toggle its selected state
|
|
isDoubleClick = false;
|
|
}
|
|
|
|
if( !isDoubleClick )
|
|
{
|
|
if( !m_allowMultiSelection )
|
|
{
|
|
selectedFileEntries.Clear();
|
|
selectedFileEntries.Add( item.Position );
|
|
}
|
|
else
|
|
{
|
|
#if UNITY_EDITOR || UNITY_STANDALONE || UNITY_WEBGL || UNITY_WSA || UNITY_WSA_10_0
|
|
// When Shift key is held, all items from the pivot item to the clicked item will be selected
|
|
#if ENABLE_INPUT_SYSTEM && !ENABLE_LEGACY_INPUT_MANAGER
|
|
if( Keyboard.current != null && Keyboard.current.shiftKey.isPressed )
|
|
#else
|
|
if( Input.GetKey( KeyCode.LeftShift ) || Input.GetKey( KeyCode.RightShift ) )
|
|
#endif
|
|
{
|
|
multiSelectionPivotFileEntry = Mathf.Clamp( multiSelectionPivotFileEntry, 0, validFileEntries.Count - 1 );
|
|
|
|
selectedFileEntries.Clear();
|
|
|
|
for( int i = multiSelectionPivotFileEntry; i < item.Position; i++ )
|
|
selectedFileEntries.Add( i );
|
|
|
|
for( int i = multiSelectionPivotFileEntry; i > item.Position; i-- )
|
|
selectedFileEntries.Add( i );
|
|
|
|
selectedFileEntries.Add( item.Position );
|
|
}
|
|
else
|
|
#endif
|
|
{
|
|
multiSelectionPivotFileEntry = item.Position;
|
|
|
|
// When in toggle selection mode or Control/Command key is held, individual items can be multi-selected
|
|
#if UNITY_EDITOR || UNITY_STANDALONE || UNITY_WEBGL || UNITY_WSA || UNITY_WSA_10_0
|
|
if( m_multiSelectionToggleSelectionMode || IsCtrlKeyHeld() )
|
|
#else
|
|
if( m_multiSelectionToggleSelectionMode )
|
|
#endif
|
|
{
|
|
if( !selectedFileEntries.Contains( item.Position ) )
|
|
selectedFileEntries.Add( item.Position );
|
|
else
|
|
{
|
|
selectedFileEntries.Remove( item.Position );
|
|
|
|
if( selectedFileEntries.Count == 0 )
|
|
MultiSelectionToggleSelectionMode = false;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
selectedFileEntries.Clear();
|
|
selectedFileEntries.Add( item.Position );
|
|
}
|
|
}
|
|
}
|
|
|
|
UpdateFilenameInputFieldWithSelection();
|
|
}
|
|
|
|
for( int i = 0; i < allItems.Count; i++ )
|
|
{
|
|
if( allItems[i].gameObject.activeSelf )
|
|
allItems[i].SetSelected( selectedFileEntries.Contains( allItems[i].Position ) );
|
|
}
|
|
|
|
if( selectedFileEntries.Count > 0 && ( isDoubleClick || ( SingleClickMode && !m_multiSelectionToggleSelectionMode ) ) )
|
|
{
|
|
if( !item.IsDirectory )
|
|
{
|
|
// Submit selection
|
|
OnSubmitButtonClicked();
|
|
}
|
|
else
|
|
{
|
|
// Enter the directory
|
|
#if !UNITY_EDITOR && UNITY_ANDROID
|
|
if( FileBrowserHelpers.ShouldUseSAFForPath( m_currentPath ) )
|
|
{
|
|
for( int i = 0; i < validFileEntries.Count; i++ )
|
|
{
|
|
FileSystemEntry fileInfo = validFileEntries[i];
|
|
if( fileInfo.IsDirectory && fileInfo.Name == item.Name )
|
|
{
|
|
CurrentPath = fileInfo.Path;
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
else
|
|
#endif
|
|
CurrentPath = Path.Combine( m_currentPath, item.Name );
|
|
}
|
|
}
|
|
}
|
|
|
|
public void OnItemHeld( FileBrowserItem item )
|
|
{
|
|
if( item is FileBrowserQuickLink )
|
|
OnItemSelected( item, false );
|
|
else if( m_allowMultiSelection && ( !item.IsDirectory || m_pickerMode != PickMode.Files ) ) // Holding a folder in file selection mode should do nothing
|
|
{
|
|
if( !MultiSelectionToggleSelectionMode )
|
|
{
|
|
if( m_pickerMode == PickMode.Files )
|
|
{
|
|
// If some folders are selected in file selection mode, deselect these folders before enabling the selection toggles because otherwise,
|
|
// user won't be able to deselect the selected folders without exiting MultiSelectionToggleSelectionMode
|
|
for( int i = selectedFileEntries.Count - 1; i >= 0; i-- )
|
|
{
|
|
if( validFileEntries[selectedFileEntries[i]].IsDirectory )
|
|
selectedFileEntries.RemoveAt( i );
|
|
}
|
|
}
|
|
|
|
MultiSelectionToggleSelectionMode = true;
|
|
}
|
|
|
|
if( !selectedFileEntries.Contains( item.Position ) )
|
|
OnItemSelected( item, false );
|
|
}
|
|
}
|
|
|
|
#if !UNITY_EDITOR && UNITY_ANDROID
|
|
private void OnSAFDirectoryPicked( string rawUri, string name )
|
|
{
|
|
if( !string.IsNullOrEmpty( rawUri ) )
|
|
{
|
|
if( AddQuickLink( m_skin.FolderIcon, name, rawUri ) )
|
|
CurrentPath = rawUri;
|
|
}
|
|
}
|
|
|
|
private void FetchPersistedSAFQuickLinks()
|
|
{
|
|
string resultRaw = FileBrowserHelpers.AJC.CallStatic<string>( "FetchSAFQuickLinks", FileBrowserHelpers.Context );
|
|
if( resultRaw == "0" )
|
|
return;
|
|
|
|
int separatorIndex = resultRaw.LastIndexOf( "<>" );
|
|
if( separatorIndex <= 0 )
|
|
{
|
|
Debug.LogError( "Entry count does not exist" );
|
|
return;
|
|
}
|
|
|
|
int entryCount = 0;
|
|
for( int i = separatorIndex + 2; i < resultRaw.Length; i++ )
|
|
{
|
|
char ch = resultRaw[i];
|
|
if( ch < '0' && ch > '9' )
|
|
{
|
|
Debug.LogError( "Couldn't parse entry count" );
|
|
return;
|
|
}
|
|
|
|
entryCount = entryCount * 10 + ( ch - '0' );
|
|
}
|
|
|
|
if( entryCount <= 0 )
|
|
return;
|
|
|
|
bool defaultPathInitialized = false;
|
|
|
|
separatorIndex = 0;
|
|
for( int i = 0; i < entryCount; i++ )
|
|
{
|
|
int nextSeparatorIndex = resultRaw.IndexOf( "<>", separatorIndex );
|
|
if( nextSeparatorIndex <= 0 )
|
|
{
|
|
Debug.LogError( "Entry name is empty" );
|
|
return;
|
|
}
|
|
|
|
string entryName = resultRaw.Substring( separatorIndex, nextSeparatorIndex - separatorIndex );
|
|
|
|
separatorIndex = nextSeparatorIndex + 2;
|
|
nextSeparatorIndex = resultRaw.IndexOf( "<>", separatorIndex );
|
|
if( nextSeparatorIndex <= 0 )
|
|
{
|
|
Debug.LogError( "Entry rawUri is empty" );
|
|
return;
|
|
}
|
|
|
|
string rawUri = resultRaw.Substring( separatorIndex, nextSeparatorIndex - separatorIndex );
|
|
|
|
separatorIndex = nextSeparatorIndex + 2;
|
|
|
|
if( AddQuickLink( m_skin.FolderIcon, entryName, rawUri ) && !defaultPathInitialized )
|
|
{
|
|
defaultInitialPath = rawUri;
|
|
defaultPathInitialized = true;
|
|
}
|
|
}
|
|
}
|
|
#endif
|
|
|
|
private char OnValidateFilenameInput( string text, int charIndex, char addedChar )
|
|
{
|
|
if( addedChar == '\n' )
|
|
{
|
|
OnSubmitButtonClicked();
|
|
return '\0';
|
|
}
|
|
|
|
return addedChar;
|
|
}
|
|
|
|
private void OnFilenameInputChanged( string text )
|
|
{
|
|
filenameInputFieldOverlayText.text = text;
|
|
filenameImage.color = m_skin.InputFieldNormalBackgroundColor;
|
|
}
|
|
#endregion
|
|
|
|
#region Helper Functions
|
|
public void Show( string initialPath, string initialFilename )
|
|
{
|
|
if( AskPermissions )
|
|
RequestPermission();
|
|
|
|
if( !quickLinksInitialized )
|
|
InitializeQuickLinks();
|
|
|
|
selectedFileEntries.Clear();
|
|
m_multiSelectionToggleSelectionMode = false;
|
|
|
|
m_searchString = string.Empty;
|
|
searchInputField.text = m_searchString;
|
|
|
|
filesScrollRect.verticalNormalizedPosition = 1;
|
|
|
|
IsOpen = true;
|
|
Success = false;
|
|
Result = null;
|
|
|
|
gameObject.SetActive( true );
|
|
|
|
CurrentPath = GetInitialPath( initialPath );
|
|
|
|
filenameInputField.text = initialFilename ?? string.Empty;
|
|
filenameInputField.interactable = true;
|
|
filenameImage.color = m_skin.InputFieldNormalBackgroundColor;
|
|
}
|
|
|
|
public void Hide()
|
|
{
|
|
IsOpen = false;
|
|
|
|
currentPathIndex = -1;
|
|
pathsFollowed.Clear();
|
|
|
|
backButton.interactable = false;
|
|
forwardButton.interactable = false;
|
|
upButton.interactable = false;
|
|
|
|
gameObject.SetActive( false );
|
|
}
|
|
|
|
public void RefreshFiles( bool pathChanged )
|
|
{
|
|
bool allExtensionsHaveSingleSuffix = AllExtensionsHaveSingleSuffix;
|
|
|
|
if( pathChanged )
|
|
{
|
|
if( !string.IsNullOrEmpty( m_currentPath ) )
|
|
allFileEntries = FileBrowserHelpers.GetEntriesInDirectory( m_currentPath, allExtensionsHaveSingleSuffix );
|
|
else
|
|
allFileEntries = null;
|
|
}
|
|
|
|
selectedFileEntries.Clear();
|
|
|
|
if( !showHiddenFilesToggle.isOn )
|
|
ignoredFileAttributes |= FileAttributes.Hidden;
|
|
else
|
|
ignoredFileAttributes &= ~FileAttributes.Hidden;
|
|
|
|
validFileEntries.Clear();
|
|
|
|
if( allFileEntries != null )
|
|
{
|
|
if( sortFilesByName )
|
|
{
|
|
// Sort the files and folders in the following order:
|
|
// 1. Directories come before files
|
|
// 2. Directories and files are sorted by their names
|
|
Array.Sort( allFileEntries, ( entry1, entry2 ) =>
|
|
{
|
|
if( entry1.IsDirectory != entry2.IsDirectory )
|
|
return entry1.IsDirectory ? -1 : 1;
|
|
else
|
|
return entry1.Name.CompareTo( entry2.Name );
|
|
} );
|
|
}
|
|
|
|
for( int i = 0; i < allFileEntries.Length; i++ )
|
|
{
|
|
try
|
|
{
|
|
FileSystemEntry item = allFileEntries[i];
|
|
if( FileSystemEntryMatchesFilters( item, allExtensionsHaveSingleSuffix ) )
|
|
validFileEntries.Add( item );
|
|
}
|
|
catch( Exception e )
|
|
{
|
|
Debug.LogException( e );
|
|
}
|
|
}
|
|
}
|
|
|
|
// Restore the selection
|
|
if( pendingFileEntrySelection.Count > 0 )
|
|
{
|
|
for( int i = 0; i < pendingFileEntrySelection.Count; i++ )
|
|
{
|
|
string pendingFileEntry = pendingFileEntrySelection[i];
|
|
for( int j = 0; j < validFileEntries.Count; j++ )
|
|
{
|
|
if( validFileEntries[j].Name == pendingFileEntry )
|
|
{
|
|
selectedFileEntries.Add( j );
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
pendingFileEntrySelection.Clear();
|
|
}
|
|
|
|
if( !filenameInputField.interactable && selectedFileEntries.Count <= 1 )
|
|
{
|
|
filenameInputField.interactable = true;
|
|
|
|
if( selectedFileEntries.Count == 0 )
|
|
filenameInputField.text = string.Empty;
|
|
}
|
|
|
|
listView.UpdateList();
|
|
|
|
// Prevent the case where all the content stays offscreen after changing the search string
|
|
EnsureScrollViewIsWithinBounds();
|
|
}
|
|
|
|
// Returns whether or not the FileSystemEntry passes the file browser's filters and should be displayed in the files list
|
|
private bool FileSystemEntryMatchesFilters( FileSystemEntry item, bool allExtensionsHaveSingleSuffix )
|
|
{
|
|
if( !item.IsDirectory )
|
|
{
|
|
if( m_pickerMode == PickMode.Folders )
|
|
return false;
|
|
|
|
if( ( item.Attributes & ignoredFileAttributes ) != 0 )
|
|
return false;
|
|
|
|
string extension = item.Extension;
|
|
if( excludedExtensionsSet.Contains( extension ) )
|
|
return false;
|
|
else if( !allExtensionsHaveSingleSuffix )
|
|
{
|
|
for( int j = 0; j < excludedExtensions.Length; j++ )
|
|
{
|
|
if( extension.EndsWith( excludedExtensions[j], StringComparison.Ordinal ) )
|
|
{
|
|
excludedExtensionsSet.Add( extension );
|
|
continue;
|
|
}
|
|
}
|
|
}
|
|
|
|
if( !filters[filtersDropdown.value].MatchesExtension( extension, !allExtensionsHaveSingleSuffix ) )
|
|
return false;
|
|
}
|
|
else
|
|
{
|
|
if( ( item.Attributes & ignoredFileAttributes ) != 0 )
|
|
return false;
|
|
}
|
|
|
|
if( m_searchString.Length > 0 && textComparer.IndexOf( item.Name, m_searchString, textCompareOptions ) < 0 )
|
|
return false;
|
|
|
|
if( m_displayedEntriesFilter != null && !m_displayedEntriesFilter( item ) )
|
|
return false;
|
|
|
|
return true;
|
|
}
|
|
|
|
// Quickly selects all files and folders in the current directory
|
|
public void SelectAllFiles()
|
|
{
|
|
if( !m_allowMultiSelection || validFileEntries.Count == 0 )
|
|
return;
|
|
|
|
multiSelectionPivotFileEntry = 0;
|
|
|
|
selectedFileEntries.Clear();
|
|
|
|
if( m_pickerMode != PickMode.Files )
|
|
{
|
|
for( int i = 0; i < validFileEntries.Count; i++ )
|
|
selectedFileEntries.Add( i );
|
|
}
|
|
else
|
|
{
|
|
// Don't select folders in file picking mode if MultiSelectionToggleSelectionMode is enabled or about to be enabled
|
|
for( int i = 0; i < validFileEntries.Count; i++ )
|
|
{
|
|
#if UNITY_EDITOR || UNITY_STANDALONE || UNITY_WEBGL || UNITY_WSA || UNITY_WSA_10_0
|
|
if( !m_multiSelectionToggleSelectionMode || !validFileEntries[i].IsDirectory )
|
|
#else
|
|
if( !validFileEntries[i].IsDirectory )
|
|
#endif
|
|
selectedFileEntries.Add( i );
|
|
}
|
|
}
|
|
|
|
#if !UNITY_EDITOR && !UNITY_STANDALONE && !UNITY_WSA && !UNITY_WSA_10_0
|
|
MultiSelectionToggleSelectionMode = true;
|
|
#endif
|
|
|
|
UpdateFilenameInputFieldWithSelection();
|
|
listView.UpdateList();
|
|
}
|
|
|
|
// Quickly deselects all files and folders in the current directory
|
|
public void DeselectAllFiles()
|
|
{
|
|
if( selectedFileEntries.Count == 0 )
|
|
return;
|
|
|
|
selectedFileEntries.Clear();
|
|
MultiSelectionToggleSelectionMode = false;
|
|
|
|
filenameInputField.text = string.Empty;
|
|
filenameInputField.interactable = true;
|
|
|
|
listView.UpdateList();
|
|
}
|
|
|
|
// Prompts user to create a new folder in the current directory
|
|
public void CreateNewFolder()
|
|
{
|
|
StartCoroutine( CreateNewFolderCoroutine() );
|
|
}
|
|
|
|
private IEnumerator CreateNewFolderCoroutine()
|
|
{
|
|
filesScrollRect.verticalNormalizedPosition = 1f;
|
|
filesScrollRect.velocity = Vector2.zero;
|
|
|
|
if( selectedFileEntries.Count > 0 )
|
|
{
|
|
selectedFileEntries.Clear();
|
|
MultiSelectionToggleSelectionMode = false;
|
|
|
|
filenameInputField.text = string.Empty;
|
|
filenameInputField.interactable = true;
|
|
|
|
listView.UpdateList();
|
|
}
|
|
|
|
filesScrollRect.movementType = ScrollRect.MovementType.Unrestricted;
|
|
|
|
// The easiest way to insert a new item to the top of the list view is to just shift
|
|
// the list view downwards. However, it doesn't always work if we don't shift it twice
|
|
yield return null;
|
|
filesContainer.anchoredPosition = new Vector2( 0f, -m_skin.FileHeight );
|
|
yield return null;
|
|
filesContainer.anchoredPosition = new Vector2( 0f, -m_skin.FileHeight );
|
|
|
|
( (RectTransform) renameItem.transform ).anchoredPosition = new Vector2( 1f, m_skin.FileHeight );
|
|
renameItem.Show( string.Empty, m_skin.FileSelectedBackgroundColor, m_skin.FolderIcon, ( folderName ) =>
|
|
{
|
|
filesScrollRect.movementType = ScrollRect.MovementType.Clamped;
|
|
filesContainer.anchoredPosition = Vector2.zero;
|
|
|
|
if( string.IsNullOrEmpty( folderName ) )
|
|
return;
|
|
|
|
FileBrowserHelpers.CreateFolderInDirectory( CurrentPath, folderName );
|
|
|
|
pendingFileEntrySelection.Clear();
|
|
pendingFileEntrySelection.Add( folderName );
|
|
|
|
RefreshFiles( true );
|
|
|
|
if( m_pickerMode != PickMode.Files )
|
|
filenameInputField.text = folderName;
|
|
|
|
// Focus on the newly created folder
|
|
int fileEntryIndex = Mathf.Max( 0, FilenameToFileEntryIndex( folderName ) );
|
|
filesScrollRect.verticalNormalizedPosition = validFileEntries.Count > 1 ? ( 1f - (float) fileEntryIndex / ( validFileEntries.Count - 1 ) ) : 1f;
|
|
} );
|
|
}
|
|
|
|
// Prompts user to rename the selected file/folder
|
|
public void RenameSelectedFile()
|
|
{
|
|
if( selectedFileEntries.Count != 1 )
|
|
return;
|
|
|
|
MultiSelectionToggleSelectionMode = false;
|
|
|
|
int fileEntryIndex = selectedFileEntries[0];
|
|
FileSystemEntry fileInfo = validFileEntries[fileEntryIndex];
|
|
|
|
// Check if selected file is currently visible in ScrollRect
|
|
// We consider it visible if both the previous file entry and the next file entry are visible
|
|
bool prevFileEntryVisible = false, nextFileEntryVisible = false;
|
|
for( int i = 0; i < allItems.Count; i++ )
|
|
{
|
|
if( !allItems[i].gameObject.activeSelf )
|
|
continue;
|
|
|
|
if( allItems[i].Position == fileEntryIndex - 1 )
|
|
{
|
|
prevFileEntryVisible = true;
|
|
|
|
if( prevFileEntryVisible && nextFileEntryVisible )
|
|
break;
|
|
}
|
|
else if( allItems[i].Position == fileEntryIndex + 1 )
|
|
{
|
|
nextFileEntryVisible = true;
|
|
|
|
if( prevFileEntryVisible && nextFileEntryVisible )
|
|
break;
|
|
}
|
|
}
|
|
|
|
if( !prevFileEntryVisible || !nextFileEntryVisible )
|
|
filesScrollRect.verticalNormalizedPosition = validFileEntries.Count > 1 ? ( 1f - (float) fileEntryIndex / ( validFileEntries.Count - 1 ) ) : 1f;
|
|
|
|
filesScrollRect.velocity = Vector2.zero;
|
|
|
|
( (RectTransform) renameItem.transform ).anchoredPosition = new Vector2( 1f, -fileEntryIndex * m_skin.FileHeight );
|
|
renameItem.Show( fileInfo.Name, m_skin.FileSelectedBackgroundColor, GetIconForFileEntry( fileInfo ), ( newName ) =>
|
|
{
|
|
if( string.IsNullOrEmpty( newName ) || newName == fileInfo.Name )
|
|
return;
|
|
|
|
if( fileInfo.IsDirectory )
|
|
FileBrowserHelpers.RenameDirectory( fileInfo.Path, newName );
|
|
else
|
|
FileBrowserHelpers.RenameFile( fileInfo.Path, newName );
|
|
|
|
pendingFileEntrySelection.Clear();
|
|
pendingFileEntrySelection.Add( newName );
|
|
|
|
RefreshFiles( true );
|
|
|
|
if( ( fileInfo.IsDirectory && m_pickerMode != PickMode.Files ) || ( !fileInfo.IsDirectory && m_pickerMode != PickMode.Folders ) )
|
|
filenameInputField.text = newName;
|
|
} );
|
|
}
|
|
|
|
// Prompts user to delete the selected files & folders
|
|
public void DeleteSelectedFiles()
|
|
{
|
|
if( selectedFileEntries.Count == 0 )
|
|
return;
|
|
|
|
selectedFileEntries.Sort();
|
|
|
|
fileOperationConfirmationPanel.Show( this, validFileEntries, selectedFileEntries, FileBrowserFileOperationConfirmationPanel.OperationType.Delete, () =>
|
|
{
|
|
for( int i = selectedFileEntries.Count - 1; i >= 0; i-- )
|
|
{
|
|
FileSystemEntry fileInfo = validFileEntries[selectedFileEntries[i]];
|
|
if( fileInfo.IsDirectory )
|
|
FileBrowserHelpers.DeleteDirectory( fileInfo.Path );
|
|
else
|
|
FileBrowserHelpers.DeleteFile( fileInfo.Path );
|
|
}
|
|
|
|
selectedFileEntries.Clear();
|
|
|
|
MultiSelectionToggleSelectionMode = false;
|
|
RefreshFiles( true );
|
|
} );
|
|
}
|
|
|
|
// Makes sure that the selection persists after Refreshing the file entries
|
|
private void PersistFileEntrySelection()
|
|
{
|
|
pendingFileEntrySelection.Clear();
|
|
for( int i = 0; i < selectedFileEntries.Count; i++ )
|
|
pendingFileEntrySelection.Add( validFileEntries[selectedFileEntries[i]].Name );
|
|
}
|
|
|
|
private bool AddQuickLink( Sprite icon, string name, string path )
|
|
{
|
|
if( string.IsNullOrEmpty( path ) )
|
|
return false;
|
|
|
|
#if !UNITY_EDITOR && UNITY_ANDROID
|
|
if( !FileBrowserHelpers.ShouldUseSAFForPath( path ) )
|
|
#endif
|
|
{
|
|
#if !WIN_DIR_CHECK_WITHOUT_TIMEOUT && ( UNITY_EDITOR_WIN || UNITY_STANDALONE_WIN )
|
|
if( !CheckDirectoryExistsWithTimeout( path ) )
|
|
#else
|
|
if( !Directory.Exists( path ) )
|
|
#endif
|
|
return false;
|
|
|
|
path = GetPathWithoutTrailingDirectorySeparator( path.Trim() );
|
|
}
|
|
|
|
// Don't add quick link if it already exists
|
|
for( int i = 0; i < allQuickLinks.Count; i++ )
|
|
{
|
|
if( allQuickLinks[i].TargetPath == path )
|
|
return false;
|
|
}
|
|
|
|
FileBrowserQuickLink quickLink = (FileBrowserQuickLink) Instantiate( quickLinkPrefab, quickLinksContainer, false );
|
|
quickLink.SetFileBrowser( this, m_skin );
|
|
|
|
if( icon != null )
|
|
quickLink.SetQuickLink( icon, name, path );
|
|
else
|
|
quickLink.SetQuickLink( m_skin.FolderIcon, name, path );
|
|
|
|
Vector2 anchoredPos = new Vector2( 0f, -quickLinksContainer.sizeDelta.y );
|
|
|
|
quickLink.TransformComponent.anchoredPosition = anchoredPos;
|
|
anchoredPos.y -= m_skin.FileHeight;
|
|
|
|
quickLinksContainer.sizeDelta = new Vector2( 0f, -anchoredPos.y );
|
|
|
|
allQuickLinks.Add( quickLink );
|
|
|
|
return true;
|
|
}
|
|
|
|
private void ClearQuickLinksInternal()
|
|
{
|
|
Vector2 anchoredPos = Vector2.zero;
|
|
for( int i = 0; i < allQuickLinks.Count; i++ )
|
|
{
|
|
if( allQuickLinks[i].TargetPath == SAF_PICK_FOLDER_QUICK_LINK_PATH )
|
|
{
|
|
allQuickLinks[i].TransformComponent.anchoredPosition = anchoredPos;
|
|
anchoredPos.y -= m_skin.FileHeight;
|
|
}
|
|
else
|
|
{
|
|
Destroy( allQuickLinks[i].gameObject );
|
|
allQuickLinks.RemoveAt( i-- );
|
|
}
|
|
}
|
|
|
|
quickLinksContainer.sizeDelta = new Vector2( 0f, -anchoredPos.y );
|
|
|
|
quickLinksInitialized = true;
|
|
generateQuickLinksForDrives = false;
|
|
}
|
|
|
|
// Makes sure that scroll view's contents are within scroll view's bounds
|
|
private void EnsureScrollViewIsWithinBounds()
|
|
{
|
|
// When scrollbar is snapped to the very bottom of the scroll view, sometimes OnScroll alone doesn't work
|
|
if( filesScrollRect.verticalNormalizedPosition <= Mathf.Epsilon )
|
|
filesScrollRect.verticalNormalizedPosition = 0.0001f;
|
|
|
|
filesScrollRect.OnScroll( nullPointerEventData );
|
|
}
|
|
|
|
internal void EnsureWindowIsWithinBounds()
|
|
{
|
|
Vector2 canvasSize = rectTransform.sizeDelta;
|
|
Vector2 windowSize = windowTR.sizeDelta;
|
|
|
|
if( windowSize.x < minWidth )
|
|
windowSize.x = minWidth;
|
|
if( windowSize.y < minHeight )
|
|
windowSize.y = minHeight;
|
|
|
|
if( windowSize.x > canvasSize.x )
|
|
windowSize.x = canvasSize.x;
|
|
if( windowSize.y > canvasSize.y )
|
|
windowSize.y = canvasSize.y;
|
|
|
|
Vector2 windowPos = windowTR.anchoredPosition;
|
|
Vector2 canvasHalfSize = canvasSize * 0.5f;
|
|
Vector2 windowHalfSize = windowSize * 0.5f;
|
|
Vector2 windowBottomLeft = windowPos - windowHalfSize + canvasHalfSize;
|
|
Vector2 windowTopRight = windowPos + windowHalfSize + canvasHalfSize;
|
|
|
|
if( windowBottomLeft.x < 0f )
|
|
windowPos.x -= windowBottomLeft.x;
|
|
else if( windowTopRight.x > canvasSize.x )
|
|
windowPos.x -= windowTopRight.x - canvasSize.x;
|
|
|
|
if( windowBottomLeft.y < 0f )
|
|
windowPos.y -= windowBottomLeft.y;
|
|
else if( windowTopRight.y > canvasSize.y )
|
|
windowPos.y -= windowTopRight.y - canvasSize.y;
|
|
|
|
windowTR.anchoredPosition = windowPos;
|
|
windowTR.sizeDelta = windowSize;
|
|
}
|
|
|
|
// Handles responsive user interface
|
|
internal void OnWindowDimensionsChanged( Vector2 size )
|
|
{
|
|
float windowWidth = size.x;
|
|
float quickLinksWidth = Mathf.Min( middleViewQuickLinksOriginalSize.x, windowWidth * quickLinksMaxWidthPercentage );
|
|
|
|
if( middleViewQuickLinks.sizeDelta.x != quickLinksWidth )
|
|
{
|
|
middleViewQuickLinks.sizeDelta = new Vector2( quickLinksWidth, middleViewQuickLinksOriginalSize.y );
|
|
middleViewFiles.anchoredPosition = new Vector2( quickLinksWidth, 0f );
|
|
middleViewFiles.sizeDelta = new Vector2( -quickLinksWidth, middleViewQuickLinksOriginalSize.y );
|
|
middleViewSeparator.anchoredPosition = new Vector2( quickLinksWidth, 0f );
|
|
}
|
|
|
|
#if !UNITY_EDITOR && UNITY_ANDROID
|
|
// Responsive layout doesn't affect any other visible UI elements on Storage Access Framework
|
|
if( FileBrowserHelpers.ShouldUseSAF )
|
|
return;
|
|
#endif
|
|
|
|
if( windowWidth >= narrowScreenWidth )
|
|
{
|
|
if( pathInputField.transform.parent == pathInputFieldSlotBottom )
|
|
{
|
|
pathInputField.transform.SetParent( pathInputFieldSlotTop, false );
|
|
|
|
middleView.anchoredPosition = middleViewOriginalPosition;
|
|
middleView.sizeDelta = middleViewOriginalSize;
|
|
|
|
showHiddenFilesToggle.gameObject.SetActive( m_displayHiddenFilesToggle );
|
|
|
|
listView.OnViewportDimensionsChanged();
|
|
EnsureScrollViewIsWithinBounds();
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if( pathInputField.transform.parent == pathInputFieldSlotTop )
|
|
{
|
|
pathInputField.transform.SetParent( pathInputFieldSlotBottom, false );
|
|
|
|
float topViewAdditionalHeight = topViewNarrowScreen.sizeDelta.y;
|
|
middleView.anchoredPosition = middleViewOriginalPosition - new Vector2( 0f, topViewAdditionalHeight * 0.5f );
|
|
middleView.sizeDelta = middleViewOriginalSize - new Vector2( 0f, topViewAdditionalHeight );
|
|
|
|
// Responsive layout for narrow screens doesn't include "Show Hidden Files" toggle.
|
|
// We simply hide it because I think creating a new row for it would be an overkill
|
|
showHiddenFilesToggle.gameObject.SetActive( false );
|
|
|
|
listView.OnViewportDimensionsChanged();
|
|
EnsureScrollViewIsWithinBounds();
|
|
}
|
|
}
|
|
}
|
|
|
|
internal Sprite GetIconForFileEntry( FileSystemEntry fileInfo )
|
|
{
|
|
return m_skin.GetIconForFileEntry( fileInfo, !AllExtensionsHaveSingleSuffix );
|
|
}
|
|
|
|
internal static string GetExtensionFromFilename( string filename, bool extractOnlyLastSuffix )
|
|
{
|
|
int length = filename.Length;
|
|
|
|
if( extractOnlyLastSuffix )
|
|
{
|
|
// We are only interested in the last suffix of the extension
|
|
for( int i = length - 2; i >= 0; i-- )
|
|
{
|
|
if( filename[i] == '.' )
|
|
return filename.Substring( i, length - i ).ToLowerInvariant();
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// We are interested in all suffixes of the extension
|
|
for( int i = 0, upperLimit = length - 2; i <= upperLimit; i++ )
|
|
{
|
|
if( filename[i] == '.' )
|
|
return filename.Substring( i, length - i ).ToLowerInvariant();
|
|
}
|
|
}
|
|
|
|
return string.Empty;
|
|
}
|
|
|
|
private string GetPathWithoutTrailingDirectorySeparator( string path )
|
|
{
|
|
if( string.IsNullOrEmpty( path ) )
|
|
return null;
|
|
|
|
// Credit: http://stackoverflow.com/questions/6019227/remove-the-last-character-if-its-directoryseparatorchar-with-c-sharp
|
|
try
|
|
{
|
|
if( Path.GetDirectoryName( path ) != null )
|
|
{
|
|
char lastChar = path[path.Length - 1];
|
|
if( lastChar == Path.DirectorySeparatorChar || lastChar == Path.AltDirectorySeparatorChar )
|
|
path = path.Substring( 0, path.Length - 1 );
|
|
}
|
|
}
|
|
catch
|
|
{
|
|
}
|
|
|
|
return path;
|
|
}
|
|
|
|
private void UpdateFilenameInputFieldWithSelection()
|
|
{
|
|
// Refresh filenameInputField as follows:
|
|
// 0 files selected: *blank*
|
|
// 1 file selected: file.Name
|
|
// 2+ files selected: "file1.Name" "file2.Name" ... (up to FILENAME_INPUT_FIELD_MAX_FILE_COUNT filenames are displayed for performance reasons)
|
|
int filenameContributingFileCount = 0;
|
|
if( m_pickerMode != PickMode.Files )
|
|
filenameContributingFileCount = selectedFileEntries.Count;
|
|
else
|
|
{
|
|
for( int i = 0; i < selectedFileEntries.Count; i++ )
|
|
{
|
|
if( !validFileEntries[selectedFileEntries[i]].IsDirectory )
|
|
{
|
|
filenameContributingFileCount++;
|
|
|
|
if( filenameContributingFileCount >= FILENAME_INPUT_FIELD_MAX_FILE_COUNT )
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
filenameInputField.interactable = selectedFileEntries.Count <= 1;
|
|
|
|
if( filenameContributingFileCount == 0 )
|
|
{
|
|
// If multiple files were previously selected, clear the input field. If a single file was selected, preserve the filename
|
|
if( filenameInputField.text.StartsWith( "\"" ) )
|
|
filenameInputField.text = string.Empty;
|
|
}
|
|
else
|
|
{
|
|
if( filenameContributingFileCount > 1 )
|
|
{
|
|
if( multiSelectionFilenameBuilder == null )
|
|
multiSelectionFilenameBuilder = new StringBuilder( 75 );
|
|
else
|
|
multiSelectionFilenameBuilder.Length = 0;
|
|
}
|
|
|
|
for( int i = 0, fileCount = 0; i < selectedFileEntries.Count; i++ )
|
|
{
|
|
FileSystemEntry selectedFile = validFileEntries[selectedFileEntries[i]];
|
|
if( m_pickerMode != PickMode.Files || !selectedFile.IsDirectory )
|
|
{
|
|
if( filenameContributingFileCount == 1 )
|
|
{
|
|
filenameInputField.text = selectedFile.Name;
|
|
break;
|
|
}
|
|
else
|
|
{
|
|
multiSelectionFilenameBuilder.Append( "\"" ).Append( selectedFile.Name ).Append( "\" " );
|
|
|
|
if( ++fileCount >= FILENAME_INPUT_FIELD_MAX_FILE_COUNT )
|
|
{
|
|
multiSelectionFilenameBuilder.Append( "..." );
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if( filenameContributingFileCount > 1 )
|
|
filenameInputField.text = multiSelectionFilenameBuilder.ToString();
|
|
}
|
|
}
|
|
|
|
// Extracts filenames from input field. Input can be in 2 formats:
|
|
// 1 filename: file.Name
|
|
// 2+ filenames: "file1.Name" "file2.Name" ...
|
|
// Returns the length of the iterated filename
|
|
private int ExtractFilenameFromInput( string input, ref int startIndex, out int nextStartIndex )
|
|
{
|
|
if( !m_allowMultiSelection || input[startIndex] != '"' )
|
|
{
|
|
// Single file is selected, return it
|
|
nextStartIndex = input.Length;
|
|
return input.Length - startIndex;
|
|
}
|
|
|
|
// Seems like multiple files are selected
|
|
|
|
// Filename is " (a single quotation mark), very unlikely to happen but probably possible on some platforms
|
|
if( startIndex + 1 >= input.Length )
|
|
{
|
|
nextStartIndex = input.Length;
|
|
return 1;
|
|
}
|
|
|
|
int filenameEndIndex = input.IndexOf( '"', startIndex + 1 );
|
|
while( true )
|
|
{
|
|
// 1st iteration: filename is "abc
|
|
// 2nd iteration: filename is "abc"def
|
|
if( filenameEndIndex == -1 )
|
|
{
|
|
nextStartIndex = input.Length;
|
|
return input.Length - startIndex;
|
|
}
|
|
|
|
// 1st iteration: filename is abc (extracted from "abc")
|
|
// 2nd iteration: filename is abc"def (extracted from "abc"def")
|
|
if( filenameEndIndex == input.Length - 1 || input[filenameEndIndex + 1] == ' ' )
|
|
{
|
|
startIndex++;
|
|
|
|
nextStartIndex = filenameEndIndex + 1;
|
|
while( nextStartIndex < input.Length && input[nextStartIndex] == ' ' )
|
|
nextStartIndex++;
|
|
|
|
return filenameEndIndex - startIndex;
|
|
}
|
|
|
|
// Filename contains a " char
|
|
filenameEndIndex = input.IndexOf( '"', filenameEndIndex + 1 );
|
|
}
|
|
}
|
|
|
|
// Checks if a substring of the input field points to an existing file
|
|
private int FilenameToFileEntryIndex( string filename )
|
|
{
|
|
// Case-sensitive search result takes precedence, so case-insensitive search result is returned only if a case-sensitive match isn't found
|
|
int caseInsensitiveResult = -1;
|
|
for( int i = 0; i < validFileEntries.Count; i++ )
|
|
{
|
|
if( validFileEntries[i].Name.Length == filename.Length )
|
|
{
|
|
if( filename == validFileEntries[i].Name ) // Case-sensitive filename query
|
|
return i;
|
|
else if( textComparer.Compare( filename, validFileEntries[i].Name, textCompareOptions ) == 0 ) // Case-insensitive filename query
|
|
caseInsensitiveResult = i;
|
|
}
|
|
}
|
|
|
|
return caseInsensitiveResult;
|
|
}
|
|
|
|
// Verifies that filename doesn't contain any invalid characters
|
|
private bool VerifyFilename( string filename )
|
|
{
|
|
bool isWhitespace = true;
|
|
for( int i = 0; i < filename.Length; i++ )
|
|
{
|
|
char ch = filename[i];
|
|
if( invalidFilenameChars.Contains( ch ) )
|
|
return false;
|
|
|
|
if( isWhitespace && !char.IsWhiteSpace( ch ) )
|
|
isWhitespace = false;
|
|
}
|
|
|
|
return !isWhitespace;
|
|
}
|
|
|
|
// Credit: http://answers.unity3d.com/questions/898770/how-to-get-the-width-of-ui-text-with-horizontal-ov.html
|
|
private int CalculateLengthOfDropdownText( string str )
|
|
{
|
|
Font font = filterItemTemplate.font;
|
|
font.RequestCharactersInTexture( str, filterItemTemplate.fontSize, filterItemTemplate.fontStyle );
|
|
|
|
int totalLength = 0;
|
|
for( int i = 0; i < str.Length; i++ )
|
|
{
|
|
CharacterInfo characterInfo;
|
|
if( !font.GetCharacterInfo( str[i], out characterInfo, filterItemTemplate.fontSize ) )
|
|
totalLength += 5;
|
|
|
|
totalLength += characterInfo.advance;
|
|
}
|
|
|
|
return totalLength;
|
|
}
|
|
|
|
private string GetInitialPath( string initialPath )
|
|
{
|
|
if( !string.IsNullOrEmpty( initialPath ) && !FileBrowserHelpers.DirectoryExists( initialPath ) && FileBrowserHelpers.FileExists( initialPath ) )
|
|
{
|
|
// Path points to a file, use its parent directory's path instead
|
|
initialPath = FileBrowserHelpers.GetDirectoryName( initialPath );
|
|
}
|
|
|
|
if( string.IsNullOrEmpty( initialPath ) || !FileBrowserHelpers.DirectoryExists( initialPath ) )
|
|
{
|
|
if( CurrentPath.Length > 0 )
|
|
initialPath = CurrentPath;
|
|
else
|
|
{
|
|
string lastBrowsedFolder = LastBrowsedFolder;
|
|
if( !string.IsNullOrEmpty( lastBrowsedFolder ) && FileBrowserHelpers.DirectoryExists( lastBrowsedFolder ) )
|
|
initialPath = lastBrowsedFolder;
|
|
else
|
|
initialPath = defaultInitialPath;
|
|
}
|
|
}
|
|
|
|
m_currentPath = string.Empty; // Needed to correctly reset the pathsFollowed
|
|
|
|
return initialPath;
|
|
}
|
|
|
|
#if !WIN_DIR_CHECK_WITHOUT_TIMEOUT && ( UNITY_EDITOR_WIN || UNITY_STANDALONE_WIN )
|
|
private bool CheckDirectoryExistsWithTimeout( string path, int timeout = 750 )
|
|
{
|
|
if( timedOutDirectoryExistsRequests.Contains( path ) )
|
|
return false;
|
|
|
|
// Directory.Exists freezes for ~15 seconds for unreachable network drives on Windows, set a timeout using threads
|
|
bool directoryExists = false;
|
|
try
|
|
{
|
|
#if NET_STANDARD_2_0 || NET_4_6
|
|
// Credit: https://stackoverflow.com/a/52661569/2373034
|
|
System.Threading.Tasks.Task task = new System.Threading.Tasks.Task( () => directoryExists = Directory.Exists( path ) );
|
|
task.Start();
|
|
if( !task.Wait( timeout ) )
|
|
timedOutDirectoryExistsRequests.Add( path );
|
|
#else
|
|
// Credit: https://stackoverflow.com/q/1232953/2373034
|
|
System.Threading.Thread thread = new System.Threading.Thread( new System.Threading.ThreadStart( () => directoryExists = Directory.Exists( path ) ) );
|
|
thread.Start();
|
|
if( !thread.Join( timeout ) )
|
|
{
|
|
timedOutDirectoryExistsRequests.Add( path );
|
|
thread.Abort();
|
|
}
|
|
#endif
|
|
}
|
|
catch
|
|
{
|
|
directoryExists = Directory.Exists( path );
|
|
}
|
|
|
|
return directoryExists;
|
|
}
|
|
#endif
|
|
|
|
private bool CheckDirectoryWriteAccess( string path )
|
|
{
|
|
#if !UNITY_EDITOR && UNITY_ANDROID
|
|
if( FileBrowserHelpers.ShouldUseSAFForPath( path ) )
|
|
return true;
|
|
#endif
|
|
string tempFilePath = Path.Combine( path, "__fsWrite.tmp" );
|
|
try
|
|
{
|
|
File.Create( tempFilePath ).Close();
|
|
File.Delete( tempFilePath );
|
|
|
|
return true;
|
|
}
|
|
catch
|
|
{
|
|
return false;
|
|
}
|
|
finally
|
|
{
|
|
try
|
|
{
|
|
File.Delete( tempFilePath );
|
|
}
|
|
catch { }
|
|
}
|
|
}
|
|
|
|
// Check if Control/Command key is held
|
|
private bool IsCtrlKeyHeld()
|
|
{
|
|
#if ENABLE_INPUT_SYSTEM && !ENABLE_LEGACY_INPUT_MANAGER
|
|
#if UNITY_EDITOR_OSX || ( !UNITY_EDITOR && UNITY_STANDALONE_OSX )
|
|
return Keyboard.current != null && ( Keyboard.current.leftCommandKey.isPressed || Keyboard.current.rightCommandKey.isPressed );
|
|
#else
|
|
return Keyboard.current != null && Keyboard.current.ctrlKey.isPressed;
|
|
#endif
|
|
#else
|
|
#if UNITY_EDITOR_OSX || ( !UNITY_EDITOR && UNITY_STANDALONE_OSX )
|
|
return Input.GetKey( KeyCode.LeftCommand ) || Input.GetKey( KeyCode.RightCommand );
|
|
#else
|
|
return Input.GetKey( KeyCode.LeftControl ) || Input.GetKey( KeyCode.RightControl );
|
|
#endif
|
|
#endif
|
|
}
|
|
#endregion
|
|
|
|
#region File Browser Functions (static)
|
|
public static bool ShowSaveDialog( OnSuccess onSuccess, OnCancel onCancel,
|
|
PickMode pickMode, bool allowMultiSelection = false,
|
|
string initialPath = null, string initialFilename = null,
|
|
string title = "Save", string saveButtonText = "Save" )
|
|
{
|
|
return ShowDialogInternal( onSuccess, onCancel, pickMode, allowMultiSelection, pickMode != PickMode.Folders, initialPath, initialFilename, title, saveButtonText );
|
|
}
|
|
|
|
public static bool ShowLoadDialog( OnSuccess onSuccess, OnCancel onCancel,
|
|
PickMode pickMode, bool allowMultiSelection = false,
|
|
string initialPath = null, string initialFilename = null,
|
|
string title = "Load", string loadButtonText = "Select" )
|
|
{
|
|
return ShowDialogInternal( onSuccess, onCancel, pickMode, allowMultiSelection, false, initialPath, initialFilename, title, loadButtonText );
|
|
}
|
|
|
|
private static bool ShowDialogInternal( OnSuccess onSuccess, OnCancel onCancel,
|
|
PickMode pickMode, bool allowMultiSelection, bool acceptNonExistingFilename,
|
|
string initialPath, string initialFilename, string title, string submitButtonText )
|
|
{
|
|
// Instead of ignoring this dialog request, let's just override the currently visible dialog's properties
|
|
//if( Instance.gameObject.activeSelf )
|
|
//{
|
|
// Debug.LogError( "Error: Multiple dialogs are not allowed!" );
|
|
// return false;
|
|
//}
|
|
|
|
Instance.onSuccess = onSuccess;
|
|
Instance.onCancel = onCancel;
|
|
|
|
Instance.PickerMode = pickMode;
|
|
Instance.AllowMultiSelection = allowMultiSelection;
|
|
Instance.Title = title;
|
|
Instance.SubmitButtonText = submitButtonText;
|
|
Instance.AcceptNonExistingFilename = acceptNonExistingFilename;
|
|
|
|
Instance.Show( initialPath, initialFilename );
|
|
|
|
return true;
|
|
}
|
|
|
|
public static void HideDialog( bool invokeCancelCallback = false )
|
|
{
|
|
Instance.OnOperationCanceled( invokeCancelCallback );
|
|
}
|
|
|
|
public static IEnumerator WaitForSaveDialog( PickMode pickMode, bool allowMultiSelection = false,
|
|
string initialPath = null, string initialFilename = null,
|
|
string title = "Save", string saveButtonText = "Save" )
|
|
{
|
|
if( !ShowSaveDialog( null, null, pickMode, allowMultiSelection, initialPath, initialFilename, title, saveButtonText ) )
|
|
yield break;
|
|
|
|
while( Instance.gameObject.activeSelf )
|
|
yield return null;
|
|
}
|
|
|
|
public static IEnumerator WaitForLoadDialog( PickMode pickMode, bool allowMultiSelection = false,
|
|
string initialPath = null, string initialFilename = null,
|
|
string title = "Load", string loadButtonText = "Select" )
|
|
{
|
|
if( !ShowLoadDialog( null, null, pickMode, allowMultiSelection, initialPath, initialFilename, title, loadButtonText ) )
|
|
yield break;
|
|
|
|
while( Instance.gameObject.activeSelf )
|
|
yield return null;
|
|
}
|
|
|
|
public static bool AddQuickLink( string name, string path, Sprite icon = null )
|
|
{
|
|
if( string.IsNullOrEmpty( path ) || !FileBrowserHelpers.DirectoryExists( path ) )
|
|
return false;
|
|
|
|
if( !quickLinksInitialized )
|
|
{
|
|
// Fetching the list of external drives is only possible with the READ_EXTERNAL_STORAGE permission granted on Android
|
|
if( AskPermissions )
|
|
RequestPermission();
|
|
|
|
Instance.InitializeQuickLinks();
|
|
}
|
|
|
|
return Instance.AddQuickLink( icon, name, path );
|
|
}
|
|
|
|
public static void ClearQuickLinks()
|
|
{
|
|
Instance.ClearQuickLinksInternal();
|
|
}
|
|
|
|
public static void SetExcludedExtensions( params string[] excludedExtensions )
|
|
{
|
|
Instance.excludedExtensions = excludedExtensions ?? new string[0];
|
|
Instance.excludedExtensionsSet.Clear();
|
|
Instance.allExcludedExtensionsHaveSingleSuffix = true;
|
|
|
|
if( excludedExtensions != null )
|
|
{
|
|
for( int i = 0; i < excludedExtensions.Length; i++ )
|
|
{
|
|
excludedExtensions[i] = excludedExtensions[i].ToLowerInvariant();
|
|
if( excludedExtensions[i][0] != '.' )
|
|
excludedExtensions[i] = "." + excludedExtensions[i];
|
|
|
|
Instance.excludedExtensionsSet.Add( excludedExtensions[i] );
|
|
Instance.allExcludedExtensionsHaveSingleSuffix &= ( excludedExtensions[i].LastIndexOf( '.' ) == 0 );
|
|
}
|
|
}
|
|
}
|
|
|
|
public static void SetFilters( bool showAllFilesFilter )
|
|
{
|
|
SetFilters( showAllFilesFilter, (string[]) null );
|
|
}
|
|
|
|
public static void SetFilters( bool showAllFilesFilter, IEnumerable<string> filters )
|
|
{
|
|
SetFiltersPreProcessing( showAllFilesFilter );
|
|
|
|
if( filters != null )
|
|
{
|
|
foreach( string filter in filters )
|
|
{
|
|
if( !string.IsNullOrEmpty( filter ) )
|
|
Instance.filters.Add( new Filter( null, filter ) );
|
|
}
|
|
}
|
|
|
|
SetFiltersPostProcessing();
|
|
}
|
|
|
|
public static void SetFilters( bool showAllFilesFilter, params string[] filters )
|
|
{
|
|
SetFiltersPreProcessing( showAllFilesFilter );
|
|
|
|
if( filters != null )
|
|
{
|
|
for( int i = 0; i < filters.Length; i++ )
|
|
{
|
|
if( !string.IsNullOrEmpty( filters[i] ) )
|
|
Instance.filters.Add( new Filter( null, filters[i] ) );
|
|
}
|
|
}
|
|
|
|
SetFiltersPostProcessing();
|
|
}
|
|
|
|
public static void SetFilters( bool showAllFilesFilter, IEnumerable<Filter> filters )
|
|
{
|
|
SetFiltersPreProcessing( showAllFilesFilter );
|
|
|
|
if( filters != null )
|
|
{
|
|
foreach( Filter filter in filters )
|
|
{
|
|
if( filter != null && filter.defaultExtension.Length > 0 )
|
|
Instance.filters.Add( filter );
|
|
}
|
|
}
|
|
|
|
SetFiltersPostProcessing();
|
|
}
|
|
|
|
public static void SetFilters( bool showAllFilesFilter, params Filter[] filters )
|
|
{
|
|
SetFiltersPreProcessing( showAllFilesFilter );
|
|
|
|
if( filters != null )
|
|
{
|
|
for( int i = 0; i < filters.Length; i++ )
|
|
{
|
|
if( filters[i] != null && filters[i].defaultExtension.Length > 0 )
|
|
Instance.filters.Add( filters[i] );
|
|
}
|
|
}
|
|
|
|
SetFiltersPostProcessing();
|
|
}
|
|
|
|
private static void SetFiltersPreProcessing( bool showAllFilesFilter )
|
|
{
|
|
Instance.showAllFilesFilter = showAllFilesFilter;
|
|
|
|
Instance.filters.Clear();
|
|
|
|
if( showAllFilesFilter )
|
|
Instance.filters.Add( Instance.allFilesFilter );
|
|
}
|
|
|
|
private static void SetFiltersPostProcessing()
|
|
{
|
|
List<Filter> filters = Instance.filters;
|
|
|
|
if( filters.Count == 0 )
|
|
filters.Add( Instance.allFilesFilter );
|
|
|
|
int maxFilterStrLength = 100;
|
|
List<string> dropdownValues = new List<string>( filters.Count );
|
|
for( int i = 0; i < filters.Count; i++ )
|
|
{
|
|
string filterStr = filters[i].ToString();
|
|
dropdownValues.Add( filterStr );
|
|
|
|
maxFilterStrLength = Mathf.Max( maxFilterStrLength, Instance.CalculateLengthOfDropdownText( filterStr ) );
|
|
}
|
|
|
|
Vector2 size = Instance.filtersDropdownContainer.sizeDelta;
|
|
size.x = maxFilterStrLength + 28;
|
|
Instance.filtersDropdownContainer.sizeDelta = size;
|
|
|
|
Instance.filtersDropdown.ClearOptions();
|
|
Instance.filtersDropdown.AddOptions( dropdownValues );
|
|
Instance.filtersDropdown.value = 0;
|
|
|
|
Instance.allFiltersHaveSingleSuffix = filters[0].allExtensionsHaveSingleSuffix;
|
|
}
|
|
|
|
public static bool SetDefaultFilter( string defaultFilter )
|
|
{
|
|
if( string.IsNullOrEmpty( defaultFilter ) )
|
|
{
|
|
if( Instance.showAllFilesFilter )
|
|
{
|
|
Instance.filtersDropdown.value = 0;
|
|
Instance.filtersDropdown.RefreshShownValue();
|
|
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
defaultFilter = defaultFilter.ToLowerInvariant();
|
|
if( defaultFilter[0] != '.' )
|
|
defaultFilter = "." + defaultFilter;
|
|
|
|
for( int i = 0; i < Instance.filters.Count; i++ )
|
|
{
|
|
HashSet<string> extensions = Instance.filters[i].extensionsSet;
|
|
if( extensions != null && extensions.Contains( defaultFilter ) )
|
|
{
|
|
Instance.filtersDropdown.value = i;
|
|
Instance.filtersDropdown.RefreshShownValue();
|
|
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
public static Permission CheckPermission()
|
|
{
|
|
#if !UNITY_EDITOR && UNITY_ANDROID
|
|
Permission result = (Permission) FileBrowserHelpers.AJC.CallStatic<int>( "CheckPermission", FileBrowserHelpers.Context );
|
|
if( result == Permission.Denied && (Permission) PlayerPrefs.GetInt( "FileBrowserPermission", (int) Permission.ShouldAsk ) == Permission.ShouldAsk )
|
|
result = Permission.ShouldAsk;
|
|
|
|
return result;
|
|
#else
|
|
return Permission.Granted;
|
|
#endif
|
|
}
|
|
|
|
public static Permission RequestPermission()
|
|
{
|
|
#if !UNITY_EDITOR && UNITY_ANDROID
|
|
object threadLock = new object();
|
|
lock( threadLock )
|
|
{
|
|
FBPermissionCallbackAndroid nativeCallback = new FBPermissionCallbackAndroid( threadLock );
|
|
|
|
FileBrowserHelpers.AJC.CallStatic( "RequestPermission", FileBrowserHelpers.Context, nativeCallback, PlayerPrefs.GetInt( "FileBrowserPermission", (int) Permission.ShouldAsk ) );
|
|
|
|
if( nativeCallback.Result == -1 )
|
|
System.Threading.Monitor.Wait( threadLock );
|
|
|
|
if( (Permission) nativeCallback.Result != Permission.ShouldAsk && PlayerPrefs.GetInt( "FileBrowserPermission", -1 ) != nativeCallback.Result )
|
|
{
|
|
PlayerPrefs.SetInt( "FileBrowserPermission", nativeCallback.Result );
|
|
PlayerPrefs.Save();
|
|
}
|
|
|
|
return (Permission) nativeCallback.Result;
|
|
}
|
|
#else
|
|
return Permission.Granted;
|
|
#endif
|
|
}
|
|
#endregion
|
|
}
|
|
} |