Files
sbox-public/game/addons/tools/Code/Editor/AssetBrowser/AssetBrowser.cs

847 lines
20 KiB
C#

using System.IO;
using System.Text;
using System.Threading;
namespace Editor;
/// <summary>
/// A list of assets with filtering options.
/// </summary>
public partial class AssetBrowser : Widget, IBrowser, AssetSystem.IEventListener
{
/// <summary>
/// Directory, contents of which are currently being displayed.
/// </summary>
public Location CurrentLocation { get; protected set; }
/// <summary>
/// Only show these asset types.
/// </summary>
public List<AssetType> FilterAssetTypes { get; protected set; }
/// <summary>
/// Internal asset list panel.
/// </summary>
protected AssetList AssetList;
/// <inheritdoc cref="BaseItemWidget.MultiSelect"/>
public bool MultiSelect
{
get => AssetList.MultiSelect;
set => AssetList.MultiSelect = value;
}
public int SelectedCount => AssetList.Selection.Count;
public List<T> GetSelected<T>() => AssetList.Selection.OfType<T>().ToList();
public PathWidget Path;
public SearchWidget Search;
ToolButton ViewMode;
ChipsWidget Chips;
ListHeader ListHeader;
string lastSortColumn = "";
bool lastSortAscending = true;
/// <summary>
/// Internal left side panel that lists all local projects and other asset locations.
/// </summary>
protected AssetLocations AssetLocations;
/// <summary>
/// Asset has been clicked
/// </summary>
public Action<Asset> OnAssetHighlight;
/// <summary>
/// Asset has been clicked
/// </summary>
public Action<Asset[]> OnAssetsHighlight;
/// <inheritdoc cref="AssetList.OnHighlight"/>
public Action<IEnumerable<IAssetListEntry>> OnHighlight;
/// <summary>
/// Asset has been selected (double click, or enter)
/// </summary>
public Action<Asset> OnAssetSelected;
/// <summary>
/// Package has been selected (double click, or enter)
/// </summary>
public Action<Package> OnPackageSelected;
/// <summary>
/// Asset has been selected (double click, or enter)
/// </summary>
public Action<string> OnFileSelected;
public Action OnFolderOpened;
/// <summary>
/// Show files generated by asset compilation and other usually unuseful files.
/// </summary>
public bool ShowJunkFiles { get; set; }
public bool HideNonAssets { get; set; }
bool _showRecursive = false;
private HistoryStack<string> History = new();
protected virtual string CookieKey => "AssetBrowser";
/// <summary>
/// If true, show all files in a flat view, if false, show files and folders like they would appear in Windows Explorer.
/// </summary>
public bool ShowRecursiveFiles
{
get => _showRecursive;
set
{
if ( _showRecursive == value ) return;
_showRecursive = value;
watcher.IncludeSubdirectories = value;
UpdateAssetList();
SaveSettings();
}
}
/// <summary>
/// View mode, i.e. thumbs, list or whatever else.
/// </summary>
public AssetListViewMode ViewModeType
{
get;
set
{
field = value;
AssetList.ViewMode = field;
UpdateViewModeIcon();
SaveSettings();
}
}
private FileSystemWatcher watcher;
public AssetBrowser( Widget parent ) : this( parent, null )
{
}
public AssetBrowser( Widget parent, List<AssetType> assetTypeFilters ) : base( parent )
{
MinimumSize = new( 100, 100 );
Layout = Layout.Row();
FilterAssetTypes = assetTypeFilters;
Path = new PathWidget( this );
Path.MinimumHeight = Theme.RowHeight;
Path.OnPathEdited += ( path ) => NavigateTo( path );
Search = new SearchWidget( this );
Search.MinimumHeight = Theme.RowHeight;
Search.ValueChanged += () => { UpdateAssetList(); SaveSettings(); };
ListHeader = new ListHeader( this, ["Name", "Date", "Type", "Size", "Path"] );
ListHeader.SetColumnVisible( "Path", false );
ListHeader.OnColumnSelect += SortAssetList;
ListHeader.OnColumnResize += () => AssetList?.Update();
AssetList = new AssetList( this );
AssetList.Browser = this;
AssetList.OnViewModeChanged += () => { UpdateViewModeIcon(); SaveSettings(); };
AssetList.OnHighlight = ( entries ) =>
{
var assets = entries.OfType<AssetEntry>().ToList();
if ( assets.Count == entries.Count() )
{
if ( assets.Count > 1 )
{
OnAssetsHighlight?.Invoke( assets.Select( x => x.Asset ).ToArray() );
}
else
{
OnAssetHighlight?.Invoke( assets.First().Asset );
}
}
OnHighlight?.Invoke( entries );
};
Chips = new ChipsWidget();
Chips.OnActiveChanged = UpdateAssetList;
var body = new Widget( this );
body.Layout = Layout.Column();
var toolbar = body.Layout.AddRow();
body.Layout.AddSpacingCell( 2 );
body.Layout.Add( Chips );
body.Layout.AddSpacingCell( 2 );
body.Layout.Add( ListHeader );
body.Layout.Add( AssetList, 1 );
CreateLocations();
var splitter = new Splitter( this );
splitter.IsHorizontal = true;
splitter.AddWidget( AssetLocations );
splitter.SetStretch( 0, 1 );
splitter.AddWidget( body );
splitter.SetStretch( 1, 5 );
Layout.Add( splitter );
BuildToolbar( toolbar );
UpdateToolbarOptions();
if ( FilterAssetTypes is not null )
{
Search.AssetTypes.ActiveTags = FilterAssetTypes.SelectMany( x => x.FileExtensions ).ToHashSet();
Search.AssetTypes.Rebuild();
Search.AssetTypes.Enabled = false;
}
watcher = new FileSystemWatcher();
watcher.Changed += OnExternalChanges;
watcher.Created += OnExternalChanges;
watcher.Deleted += OnExternalChanges;
watcher.Renamed += OnExternalChanges;
SetInitialLocation();
RefreshCookies();
if ( History.Count == 0 && CurrentLocation != null )
{
History.Add( CurrentLocation.Path );
}
}
protected virtual void SetInitialLocation()
{
}
protected virtual void CreateLocations()
{
}
public override void OnDestroyed()
{
base.OnDestroyed();
watcher?.Dispose();
}
private void OnExternalChanges( object sender, FileSystemEventArgs e )
{
MainThread.Queue( UpdateAssetList );
}
private void GoBack()
{
if ( !History.CanGoBack() )
return;
NavigateTo( History.GoBack(), false );
}
private void GoForward()
{
if ( !History.CanGoForward() )
return;
NavigateTo( History.GoForward(), false );
}
protected override void OnMousePress( MouseEvent e )
{
base.OnMousePress( e );
if ( e.Button == MouseButtons.Back && History.CanGoBack() )
{
GoBack();
return;
}
if ( e.Button == MouseButtons.Forward && History.CanGoForward() )
{
GoForward();
return;
}
}
private void RefreshCookies()
{
SettingsCookie = GetCookieKey();
History.StateCookie = SettingsCookie;
GlobalSettingsCookie = "AssetBrowser.Global";
if ( History.Count > 0 )
{
NavigateTo( History.Current, false );
}
LoadSettings();
UpdateAssetList();
}
private string GetCookieKey()
{
StringBuilder stringBuilder = new( CookieKey );
if ( FilterAssetTypes != null )
stringBuilder.Append( $".{string.Join( ",", FilterAssetTypes )}" );
return stringBuilder.ToString();
}
/// <summary>
/// Go up once in the folder structure.
/// </summary>
public void OpenParentFolder()
{
if ( CurrentLocation == null ) return;
if ( !CurrentLocation.CanGoUp() ) return;
string path = CurrentLocation.Path.NormalizeFilename( false, false );
string parentPath = path.TrimEnd( '/' ); // remove trailing slash if any
int i = path.LastIndexOf( '/' );
parentPath = (i > 0) ? path[..(i + 1)] : null;
NavigateTo( parentPath );
}
public void NavigateTo( string absolutePath, bool addToHistory = true )
{
if ( string.IsNullOrEmpty( absolutePath ) )
return;
if ( !Location.TryParse( absolutePath.NormalizeFilename( false, false ), out var location ) )
return;
NavigateTo( location, addToHistory );
}
public void NavigateTo( Location location, bool addToHistory = true )
{
if ( !location.IsValid() )
return;
CurrentLocation = location;
UpdateAssetList();
if ( location is DiskLocation )
{
watcher.Path = CurrentLocation.Path;
watcher.EnableRaisingEvents = true;
}
else
{
watcher.EnableRaisingEvents = false;
}
if ( addToHistory )
{
History.Add( CurrentLocation.Path );
}
OnFolderOpened?.Invoke();
AssetLocations.SelectFolder( CurrentLocation.Path );
}
void AssetSystem.IEventListener.OnAssetSystemChanges()
{
UpdateAssetList();
}
[Event( "content.changed" )]
void OnContentChanged( string file )
{
UpdateAssetList();
}
CancellationTokenSource refreshToken;
Task<bool> RefreshTask;
/// <summary>
/// Update the list of displayed assets.
/// </summary>
public virtual void UpdateAssetList()
{
if ( CurrentLocation is null )
return;
refreshToken?.Cancel();
refreshToken = new CancellationTokenSource();
if ( !CurrentLocation.IsValid() )
{
// folder must've been deleted, try go somewhere safe
var location = new DiskLocation( Sandbox.Project.Current.GetAssetsPath() );
if ( location.IsValid() )
{
NavigateTo( location, false );
}
return;
}
bool recursive = ShowRecursiveFiles || !Search.IsEmpty;
AssetList.FullPathMode = recursive;
Path.UpdateSegments();
RefreshTask = UpdateAssetListAsync( recursive, refreshToken.Token );
}
private async Task<bool> UpdateAssetListAsync( bool recursive, CancellationToken token )
{
List<object> items = new List<object>();
var tagCounts = new Dictionary<string, int>();
AssetList.Clear();
await Task.Run( () =>
{
//
// Collect directories
//
if ( !recursive )
{
string path = CurrentLocation.Path;
foreach ( var directory in CurrentLocation.GetDirectories() )
{
if ( token.IsCancellationRequested )
return;
var di = new DirectoryEntry( directory.Path );
// Hide hidden directories
if ( di.DirectoryInfo.Exists && di.DirectoryInfo.Attributes.HasFlag( FileAttributes.Hidden ) )
continue;
// Hide obj/ folder in code directories
if ( EditorUtility.IsCodeFolder( path ) && di.Name.Equals( "obj", StringComparison.OrdinalIgnoreCase ) )
continue;
items.Add( di );
}
}
//
// Collect files
//
var files = Search.Filter( CurrentLocation, token, recursive );
if ( files is null ) return;
foreach ( var file in files )
{
if ( token.IsCancellationRequested )
return;
if ( Chips.ShouldFilter( file ) )
continue;
if ( !ShowJunkFiles )
{
//
// Filter out unwanted crap
//
if ( file.Name.Contains( ".generated", StringComparison.OrdinalIgnoreCase ) ||
file.Name.EndsWith( ".meta", StringComparison.OrdinalIgnoreCase ) )
{
continue;
}
}
if ( file.Exists && file.Attributes.HasFlag( FileAttributes.Hidden ) )
continue;
var asset = AssetSystem.FindByPath( file.ToString() );
//
// Filter out compiled assets if we have a source asset
//
string sourcePath = asset?.GetSourceFile();
if ( file.Name.EndsWith( "_c" ) && !string.IsNullOrEmpty( sourcePath ) )
{
// ( but only if the extensions are similar and would both be shown in this filter, eg so we don't hide .sounds because we have a .wav etc )
if ( Search.AssetTypes.ActiveTags.Count == 0 || file.Extension.Contains( System.IO.Path.GetExtension( sourcePath ) ) )
continue;
}
if ( asset == null && HideNonAssets )
continue;
items.Add( new AssetEntry( file, asset ) );
if ( asset != null )
{
foreach ( var tag in asset.Tags )
{
tagCounts[tag] = tagCounts.GetValueOrDefault( tag ) + 1;
}
}
}
}, token );
if ( token.IsCancellationRequested )
return false;
AssetList.SetItems( items );
if ( !string.IsNullOrEmpty( lastSortColumn ) )
{
SortAssetList( lastSortColumn, lastSortAscending );
}
Chips.ClearButKeepActive();
foreach ( var tagPair in tagCounts )
{
if ( token.IsCancellationRequested )
return false;
var tag = FindTag( tagPair.Key );
Chips.AddOption( tag );
}
return true;
}
private void SortAssetList( string sortBy, bool ascending )
{
List<object> items;
switch ( sortBy )
{
case "Name":
items = AssetList.Items.OrderBy( x =>
{
string sort = "";
if ( x is AssetEntry ae )
{
sort = ae.Name.ToLower();
}
else if ( x is DirectoryEntry de )
{
// Directories always come first
sort = (ListHeader.SortAscending ? "! " : "z ") + de.Name.ToLower();
}
return sort;
} ).ToList();
break;
case "Date":
items = AssetList.Items.OrderBy( x =>
{
var date = DateTimeOffset.Now;
if ( x is AssetEntry ae )
{
date = ae.FileInfo?.LastWriteTime ?? date;
}
var sort = date.UtcTicks.ToString();
if ( x is DirectoryEntry de )
{
// Directories always come first
sort = (ListHeader.SortAscending ? "! " : "z ") + de.Name.ToLower();
}
return sort;
} ).ToList();
break;
case "Type":
items = AssetList.Items.OrderBy( x =>
{
var type = (x is AssetEntry ae) ? ae.TypeName : "";
var sort = type.ToLower();
if ( x is DirectoryEntry de )
{
// Directories always come first
sort = (ListHeader.SortAscending ? "! " : "z ") + de.Name.ToLower();
}
return sort;
} ).ToList();
break;
case "Size":
items = AssetList.Items.OrderBy( x =>
{
long size = (x is AssetEntry ae) ? (ae.FileInfo?.Length ?? 0) : 0;
if ( x is DirectoryEntry de )
{
// Directories always come first/last
size = -long.MaxValue + de.Name.ToLong();
}
return size;
} ).ToList();
break;
default:
items = AssetList.Items.ToList();
break;
}
if ( !ascending )
{
items.Reverse();
}
AssetList.SetItems( items );
lastSortColumn = sortBy;
lastSortAscending = ascending;
}
private static AssetTagSystem.TagDefinition FindTag( string tagName )
{
foreach ( var tag in AssetTagSystem.All )
{
if ( tag.Tag.Equals( tagName ) )
{
return tag;
}
}
return default;
}
protected override void OnKeyPress( KeyEvent e )
{
if ( e.Key == KeyCode.F5 )
{
UpdateAssetList();
return;
}
base.OnKeyPress( e );
}
void OpenSettingsMenu( Rect source )
{
var menu = new ContextMenu( this );
{
var o = menu.AddOption( new Option( this, "Show Bullshit Files" ) );
o.StatusTip = "Show All Files, even those that are useless for us to see and will make a big mess of the UI.";
o.Checkable = true;
o.Checked = ShowJunkFiles;
o.Toggled = ( b ) =>
{
ShowJunkFiles = b;
UpdateAssetList();
SaveSettings();
};
}
{
var o = menu.AddOption( new Option( this, "Show Assets Only" ) );
o.StatusTip = "Filters out file types that aren't part of our asset system";
o.Checkable = true;
o.Checked = HideNonAssets;
o.Toggled = ( b ) =>
{
HideNonAssets = b;
UpdateAssetList();
SaveSettings();
};
}
{
var o = new Option( "Include Path in Search" );
o.Checkable = true;
o.Checked = AssetLocations.IncludePathNames;
o.Toggled = ( b ) =>
{
AssetLocations.SetIncludePathNames( b );
UpdateAssetList();
};
menu.AddOption( o );
}
menu.AddSeparator();
{
var o = menu.AddOption( new Option( this, "Split Left", "first_page" ) );
o.Triggered = () =>
{
var ab = EditorWindow.DockManager.Create<MainAssetBrowser>();
ab.Local.NavigateTo( CurrentLocation );
ab.Local.ViewModeType = ViewModeType;
EditorWindow.DockManager.AddDock( this, ab, DockArea.Left );
};
}
{
var o = menu.AddOption( new Option( this, "Split Right", "last_page" ) );
o.Triggered = () =>
{
var ab = EditorWindow.DockManager.Create<MainAssetBrowser>();
ab.Local.NavigateTo( CurrentLocation );
ab.Local.ViewModeType = ViewModeType;
EditorWindow.DockManager.AddDock( this, ab, DockArea.Right );
};
}
menu.OpenAt( source.BottomLeft, false );
}
string SettingsCookie { get; set; }
string GlobalSettingsCookie { get; set; }
bool SuppressSaveSettings { get; set; }
/// <summary>
/// Load settings saved by <see cref="SaveSettings"/>.
/// </summary>
public virtual void LoadSettings()
{
if ( string.IsNullOrEmpty( SettingsCookie ) )
return;
SuppressSaveSettings = true;
ViewModeType = (AssetListViewMode)ProjectCookie.Get( $"{SettingsCookie}.ViewMode", (int)AssetList.ViewMode );
ShowJunkFiles = ProjectCookie.Get( $"{SettingsCookie}.ShowJunkFiles", ShowJunkFiles );
HideNonAssets = ProjectCookie.Get( $"{SettingsCookie}.HideNonAssets", HideNonAssets );
ShowRecursiveFiles = ProjectCookie.Get( $"{SettingsCookie}.ShowRecursiveFiles", ShowRecursiveFiles );
if ( FilterAssetTypes is null )
{
Search.Value = ProjectCookie.Get( $"{SettingsCookie}.Search", Search.Value );
if ( ProjectCookie.TryGet( $"{SettingsCookie}.ActiveTags", out string activeTags ) )
{
Search.AssetTypes.ActiveTags = activeTags.Split( ';' ).Where( x => x.Length > 0 ).ToHashSet();
}
if ( ProjectCookie.TryGet( $"{SettingsCookie}.ExcludedTags", out string excludedTags ) )
{
Search.AssetTypes.ExcludedTags = excludedTags.Split( ';' ).Where( x => x.Length > 0 ).ToHashSet();
}
Search.AssetTypes.Rebuild();
}
SuppressSaveSettings = false;
}
/// <summary>
/// Save asset browser user settings, such as view mode, <see cref="ShowJunkFiles"/> and <see cref="ShowRecursiveFiles"/>.
/// The save slot is separate for each asset browser filter combination set in the constructor.
/// </summary>
public virtual void SaveSettings()
{
if ( string.IsNullOrEmpty( SettingsCookie ) )
return;
if ( SuppressSaveSettings )
return;
ProjectCookie.Set( $"{SettingsCookie}.ViewMode", (int)AssetList.ViewMode );
ProjectCookie.Set( $"{SettingsCookie}.ShowJunkFiles", ShowJunkFiles );
ProjectCookie.Set( $"{SettingsCookie}.HideNonAssets", HideNonAssets );
ProjectCookie.Set( $"{SettingsCookie}.ShowRecursiveFiles", ShowRecursiveFiles );
if ( FilterAssetTypes is null )
{
ProjectCookie.Set( $"{SettingsCookie}.Search", Search.Value );
ProjectCookie.Set( $"{SettingsCookie}.ActiveTags", string.Join( ';', Search.AssetTypes.ActiveTags ) );
ProjectCookie.Set( $"{SettingsCookie}.ExcludedTags", string.Join( ';', Search.AssetTypes.ExcludedTags ) );
}
}
/// <summary>
/// Focus on this asset, make it selected
/// </summary>
public async void FocusOnAsset( Asset asset, bool skipEvents = false )
{
if ( asset is null ) return;
var folder = System.IO.Path.GetDirectoryName( asset.AbsolutePath );
EditorWindow.DockManager.RaiseDock( this );
NavigateTo( folder );
// wait for the list to (successfully) populate before selecting the item
bool success = await RefreshTask;
if ( !success )
return;
AssetEntry entry = AssetList.Items.OfType<AssetEntry>().Where( x => x.Asset?.RelativePath == asset.RelativePath ).FirstOrDefault();
if ( entry is null )
return;
AssetList.SelectItem( entry, skipEvents: skipEvents );
AssetList.ScrollTo( entry );
}
public void OnAssetCreated( Asset asset, string path )
{
var entry = AssetList.AddItem( new AssetEntry( new FileInfo( path ), asset ) );
AssetList.SelectItem( entry, skipEvents: true );
AssetList.OpenRenameFlyout( entry );
}
void AssetSystem.IEventListener.OnAssetTagsChanged()
{
UpdateToolbarOptions();
}
[Event( "tools.package.loaded" ), Event( "localaddons.changed", Priority = 9999 )]
void UpdateToolbarOptions()
{
if ( Search.AssetTypes == null ) return;
Search.AssetTypes.Options.Clear();
TagPicker.Option SelectAssetTypes( AssetType x ) => new( x.FileExtension )
{
PixmapIcon = x.Icon16,
Title = x.FriendlyName,
Group = (string.IsNullOrEmpty( x.Category ) ? "Other" : x.Category),
Column = 0,
Count = () => AssetSystem.All.Where( y => y.AssetType == x ).Count(),
Color = x.Color
};
TagPicker.Option SelectAssetTags( AssetTagSystem.TagDefinition x ) => new( $"tag:{x.Tag}" )
{
PixmapIcon = x.IconPixmap,
Title = x.Title,
Subtitle = x.Description,
Group = "Tags",
Column = 1,
Count = () => AssetSystem.All.Where( y => y.Tags.Contains( x.Tag ) ).Count()
};
Search.AssetTypes.Options.AddRange( AssetType.All.Where( x => !x.HiddenByDefault && !x.IsGameResource ).Select( SelectAssetTypes ).OrderBy( x => x.Title ) );
Search.AssetTypes.Options.AddRange( AssetType.All.Where( x => !x.HiddenByDefault && x.IsGameResource ).Select( SelectAssetTypes ).OrderBy( x => x.Title ) );
Search.AssetTypes.Options.AddRange( AssetTagSystem.All.Where( x => x.AutoTag ).Select( SelectAssetTags ).OrderBy( x => x.Title ) );
Search.AssetTypes.Options.AddRange( AssetTagSystem.All.Where( x => !x.AutoTag ).Select( SelectAssetTags ).OrderBy( x => x.Title ) );
}
[Event( "assetsystem.highlight" )]
public void Highlight( string path )
{
var folder = System.IO.Path.GetDirectoryName( path );
var absolutePath = FileSystem.Mounted.GetFullPath( folder );
NavigateTo( absolutePath );
foreach ( var item in AssetList.Items )
{
if ( item is AssetEntry entry && entry.FileInfo.FullName == path )
{
AssetList.SelectItem( item );
}
}
}
public virtual void AddPin( string filter )
{
}
}