diff --git a/game/addons/tools/Code/Editor/AssetBrowser/AssetBrowser.cs b/game/addons/tools/Code/Editor/AssetBrowser/AssetBrowser.cs index cdc9229d..9db1ad16 100644 --- a/game/addons/tools/Code/Editor/AssetBrowser/AssetBrowser.cs +++ b/game/addons/tools/Code/Editor/AssetBrowser/AssetBrowser.cs @@ -397,7 +397,7 @@ public partial class AssetBrowser : Widget, IBrowser, AssetSystem.IEventListener bool recursive = ShowRecursiveFiles || !Search.IsEmpty; AssetList.FullPathMode = recursive; - Path.UpdateSegments(); + Path.Rebuild(); RefreshTask = UpdateAssetListAsync( recursive, refreshToken.Token ); } diff --git a/game/addons/tools/Code/Editor/AssetBrowser/PathWidget.cs b/game/addons/tools/Code/Editor/AssetBrowser/PathWidget.cs index 5498a75f..401cb22d 100644 --- a/game/addons/tools/Code/Editor/AssetBrowser/PathWidget.cs +++ b/game/addons/tools/Code/Editor/AssetBrowser/PathWidget.cs @@ -14,6 +14,9 @@ public class PathWidget : Widget private Layout SegmentLayout => SegmentParent.Layout; private Layout EditLayout; + private List Segments = new(); + private PathElipses ElipsesWidget; + public PathWidget( AssetBrowser assetBrowser ) : base( assetBrowser ) { Browser = assetBrowser; @@ -49,23 +52,27 @@ public class PathWidget : Widget LineEdit.Focus(); LineEdit.SelectAll(); - SegmentLayout.Clear( true ); + SegmentParent.Visible = false; } protected override void OnResize() { base.OnResize(); LineEdit.Visible = false; + SegmentParent.Visible = true; UpdateSegments(); } - public void UpdateSegments() + /// + /// Rebuild the path segments based on current location + /// + public void Rebuild() { if ( SegmentLayout == null ) return; SegmentLayout.Clear( true ); - Enabled = true; + Segments.Clear(); var location = Browser.CurrentLocation; if ( location is null ) @@ -73,34 +80,77 @@ public class PathWidget : Widget LineEdit.Visible = false; LineEdit.Value = location.Path; - - float availableWidth = LocalRect.Width - 32 - 64; - float currentWidth = 0; + SegmentParent.Visible = true; if ( string.IsNullOrWhiteSpace( location.RelativePath ) && !location.IsRoot ) { // not a real path - SegmentLayout.Add( new PathSegment( Browser, location.Name, location.Path ) ); + var seg = new PathSegment( Browser, location.Name, location.Path ); + SegmentLayout.Add( seg ); + Segments.Add( seg ); Enabled = false; return; } - bool hasRoot = location.RootPath is not null; - var segments = location.RelativePath.NormalizeFilename( hasRoot, false ).Split( ['/'] ); + Enabled = true; - int truncIdx = -1; - bool hasVisible = false; + bool hasRoot = location.RootPath != null; + var segments = location.RelativePath.NormalizeFilename( hasRoot, false ).Split( '/' ); + string currentPath = ""; - // work out what segments should be visible - for ( int i = segments.Length - 1; i >= 0; i-- ) + // stuff that doesn't fit is tucked into an elipses menu + ElipsesWidget = new PathElipses( Browser ); + SegmentLayout.Add( ElipsesWidget ); + + for ( int i = 0; i < segments.Length; i++ ) { var segment = segments[i]; - if ( string.IsNullOrEmpty( segment ) && i > 0 ) continue; + if ( i > 0 && segment.Length == 0 ) + continue; - if ( segment == "." ) continue; + currentPath += (i > 0 ? "/" : "") + segment; + var absolutePath = location.RootPath == null ? currentPath : $"{location.RootPath}{currentPath}"; + if ( !absolutePath.EndsWith( "/" ) ) + absolutePath += "/"; string label = GetSegmentLabel( i, segment, location ); - float segmentWidth = MeasureTextWidth( label ); + + var seg = new PathSegment( Browser, label, absolutePath ); + SegmentLayout.Add( seg ); + Segments.Add( seg ); + + // add separators except after last segment + if ( i < segments.Length - 1 ) + { + seg.Separator = new PathSeparator( Browser, absolutePath ); + seg.Separator.Visible = false; + SegmentLayout.Add( seg.Separator ); + } + } + + SegmentLayout.AddStretchCell( 1 ); + + UpdateSegments(); + } + + /// + /// Update visibility of each segment, and elipses menu if needed + /// + private void UpdateSegments() + { + if ( Segments.Count == 0 ) + return; + + float currentWidth = 0; + bool hasVisible = false; + int truncIdx = -1; + + float availableWidth = LocalRect.Width - 32 - 64; + + // work out which segments fit + for ( int i = Segments.Count - 1; i >= 0; i-- ) + { + float segmentWidth = MeasureTextWidth( Segments[i].Label ); if ( currentWidth + segmentWidth + 32 > availableWidth && hasVisible ) { truncIdx = i; @@ -111,45 +161,20 @@ public class PathWidget : Widget hasVisible = true; } - string currentPath = ""; - PathElipses elipses = null; - for ( int i = 0; i < segments.Length; i++ ) + ElipsesWidget.Paths.Clear(); + + // what doesn't fit should be hidden, and added to the elipses menu + for ( int i = 0; i < Segments.Count; i++ ) { - var segment = segments[i]; - if ( i > 0 && segment.Length == 0 ) - continue; + Segments[i].Visible = i > truncIdx; - currentPath += (i > 0 ? "/" : "") + segments[i]; - - var absolutePath = location.RootPath is null ? currentPath : $"{location.RootPath}{currentPath}"; - if ( !absolutePath.EndsWith( "/" ) ) - absolutePath += "/"; - - string label = GetSegmentLabel( i, segment, location ); - - if ( truncIdx != -1 && i <= truncIdx ) + if ( !Segments[i].Visible ) { - elipses ??= SegmentLayout.Add( new PathElipses( Browser ) ); - elipses.Paths.Add( (label, absolutePath) ); - continue; - } - - SegmentLayout.Add( new PathSegment( Browser, label, absolutePath ) ); - - // Separator - bool hasSubdirectories = true; - if ( i == segments.Length - 1 ) - { - // current location's segment - if ( !location.IsValid() ) break; - hasSubdirectories = location.GetDirectories().Any(); - } - - if ( hasSubdirectories ) - { - SegmentLayout.Add( new PathSeparator( Browser, absolutePath ) ); + ElipsesWidget.Paths.Add( (Segments[i].Label, Segments[i].TargetPath) ); } } + + ElipsesWidget.Visible = truncIdx != -1; } string GetSegmentLabel( int index, string segment, AssetBrowser.Location location ) @@ -171,8 +196,7 @@ public class PathWidget : Widget { LineEdit.Visible = false; OnPathEdited?.Invoke( LineEdit.Value ); - - UpdateSegments(); + Rebuild(); Update(); } @@ -189,248 +213,258 @@ public class PathWidget : Widget Paint.SetBrush( Theme.ControlBackground ); Paint.DrawRect( rect, Theme.ControlRadius ); } -} -file class PathSegment : Widget -{ - private AssetBrowser Browser { get; init; } - private string Label; - private string TargetPath; - private string RelativePath + + class PathSegment : Widget { - get + public string Label { get; private set; } + public string TargetPath { get; private set; } + public string RelativePath { - var assetsPath = Project.Current.GetAssetsPath(); - var relativePath = System.IO.Path.GetRelativePath( assetsPath, TargetPath ); - relativePath = relativePath.Replace( '\\', '/' ); - if ( relativePath.StartsWith( ".." ) ) return null; - return relativePath.ToLower(); - } - } - - public PathSegment( AssetBrowser browser, string text, string path ) : base( null ) - { - Browser = browser; - Label = text; - - Paint.SetDefaultFont( 8 ); - FixedWidth = PathWidget.MeasureTextWidth( text ); - - AcceptDrops = true; - TargetPath = path; - - Cursor = CursorShape.Finger; - } - - protected override void OnMousePress( MouseEvent e ) - { - if ( e.LeftMouseButton ) - { - if ( !AssetBrowser.Location.TryParse( TargetPath, out var location ) ) - return; - - Browser.NavigateTo( location ); - } - else if ( e.RightMouseButton ) - { - var menu = new Menu(); - menu.AddOption( "Show in Explorer", "drive_file_move", action: () => EditorUtility.OpenFileFolder( TargetPath ) ); - menu.AddSeparator(); - var relativePath = RelativePath; - menu.AddOption( $"Copy Relative Path", "content_paste_go", action: () => EditorUtility.Clipboard.Copy( relativePath ) ).Enabled = relativePath is not null; - menu.AddOption( $"Copy Absolute Path", "content_paste", action: () => EditorUtility.Clipboard.Copy( TargetPath ) ); - menu.OpenAtCursor(); - } - - e.Accepted = false; - } - - protected override void OnPaint() - { - base.OnPaint(); - - Paint.ClearBrush(); - Paint.ClearPen(); - - if ( Paint.HasMouseOver ) - { - Paint.SetBrush( Color.White.WithAlpha( 0.1f ) ); - Paint.DrawRect( LocalRect.Shrink( 0, 2 ) ); - } - - Paint.SetPen( Theme.TextControl ); - Paint.SetDefaultFont( 8 ); - Paint.DrawText( LocalRect.Shrink( 8, 0 ), Label, TextFlag.LeftCenter ); - } - - public override void OnDragHover( DragEvent ev ) - { - if ( !ev.Data.Files.Any() ) - { - ev.Action = DropAction.Ignore; - return; - } - - ev.Action = ev.HasCtrl ? DropAction.Copy : DropAction.Move; - } - - public override void OnDragDrop( DragEvent ev ) - { - ev.Action = ev.HasCtrl ? DropAction.Copy : DropAction.Move; - - foreach ( var file in ev.Data.Files ) - { - var asset = AssetSystem.FindByPath( file ); - - if ( asset is null ) + get { - if ( !Path.Exists( file ) ) continue; + var assetsPath = Project.Current.GetAssetsPath(); + var relativePath = System.IO.Path.GetRelativePath( assetsPath, TargetPath ); + relativePath = relativePath.Replace( '\\', '/' ); + if ( relativePath.StartsWith( ".." ) ) return null; + return relativePath.ToLower(); + } + } - // This isn't an asset so just copy the file in directly - var destinationFile = Path.Combine( TargetPath, Path.GetFileName( file ) ); + public PathSeparator Separator { get; set; } - if ( Directory.Exists( file ) ) + private AssetBrowser Browser { get; init; } + + public PathSegment( AssetBrowser browser, string text, string path ) : base( null ) + { + Browser = browser; + Label = text; + + Paint.SetDefaultFont( 8 ); + FixedWidth = MeasureTextWidth( text ); + + AcceptDrops = true; + TargetPath = path; + + Cursor = CursorShape.Finger; + } + + protected override void OnVisibilityChanged( bool visible ) + { + base.OnVisibilityChanged( visible ); + Separator?.Visible = visible; + } + + protected override void OnMousePress( MouseEvent e ) + { + if ( e.LeftMouseButton ) + { + if ( !AssetBrowser.Location.TryParse( TargetPath, out var location ) ) + return; + + Browser.NavigateTo( location ); + } + else if ( e.RightMouseButton ) + { + var menu = new Menu(); + menu.AddOption( "Show in Explorer", "drive_file_move", action: () => EditorUtility.OpenFileFolder( TargetPath ) ); + menu.AddSeparator(); + var relativePath = RelativePath; + menu.AddOption( $"Copy Relative Path", "content_paste_go", action: () => EditorUtility.Clipboard.Copy( relativePath ) ).Enabled = relativePath is not null; + menu.AddOption( $"Copy Absolute Path", "content_paste", action: () => EditorUtility.Clipboard.Copy( TargetPath ) ); + menu.OpenAtCursor(); + } + + e.Accepted = false; + } + + protected override void OnPaint() + { + base.OnPaint(); + + Paint.ClearBrush(); + Paint.ClearPen(); + + if ( Paint.HasMouseOver ) + { + Paint.SetBrush( Color.White.WithAlpha( 0.1f ) ); + Paint.DrawRect( LocalRect.Shrink( 0, 2 ) ); + } + + Paint.SetPen( Theme.TextControl ); + Paint.SetDefaultFont( 8 ); + Paint.DrawText( LocalRect.Shrink( 8, 0 ), Label, TextFlag.LeftCenter ); + } + + public override void OnDragHover( DragEvent ev ) + { + if ( !ev.Data.Files.Any() ) + { + ev.Action = DropAction.Ignore; + return; + } + + ev.Action = ev.HasCtrl ? DropAction.Copy : DropAction.Move; + } + + public override void OnDragDrop( DragEvent ev ) + { + ev.Action = ev.HasCtrl ? DropAction.Copy : DropAction.Move; + + foreach ( var file in ev.Data.Files ) + { + var asset = AssetSystem.FindByPath( file ); + + if ( asset is null ) { - // Move Directory - EditorUtility.RenameDirectory( file, destinationFile ); - DirectoryEntry.RenameMetadata( file, destinationFile ); + if ( !Path.Exists( file ) ) continue; + + // This isn't an asset so just copy the file in directly + var destinationFile = Path.Combine( TargetPath, Path.GetFileName( file ) ); + + if ( Directory.Exists( file ) ) + { + // Move Directory + EditorUtility.RenameDirectory( file, destinationFile ); + DirectoryEntry.RenameMetadata( file, destinationFile ); + } + else + { + // Move File + if ( Path.GetFullPath( file ) == Path.GetFullPath( destinationFile ) ) + continue; + + if ( ev.Action == DropAction.Copy ) + File.Copy( file, destinationFile ); + else + File.Move( file, destinationFile ); + } } else { - // Move File - if ( Path.GetFullPath( file ) == Path.GetFullPath( destinationFile ) ) - continue; - + if ( asset.IsDeleted ) continue; if ( ev.Action == DropAction.Copy ) - File.Copy( file, destinationFile ); + EditorUtility.CopyAssetToDirectory( asset, TargetPath ); else - File.Move( file, destinationFile ); + EditorUtility.MoveAssetToDirectory( asset, TargetPath ); } } + } + } + + class PathSeparator : Widget + { + private ContextMenu menu; + + private AssetBrowser Browser { get; init; } + public string AbsolutePath { get; init; } + + public PathSeparator( AssetBrowser browser, string absolutePath ) : base( null ) + { + MinimumWidth = 16; + AbsolutePath = absolutePath; + Browser = browser; + } + + protected override void OnPaint() + { + base.OnPaint(); + + Paint.ClearBrush(); + Paint.ClearPen(); + + if ( Paint.HasMouseOver ) + { + Paint.SetBrush( Color.White.WithAlpha( 0.1f ) ); + Paint.DrawRect( LocalRect.Shrink( 0, 2 ) ); + } + + var rect = LocalRect; + + Paint.SetPen( Theme.TextControl ); + + if ( menu.IsValid() ) + { + Paint.Rotate( 90, rect.Position + new Vector2( 8, 8 ) ); + Paint.DrawIcon( rect + new Vector2( 4, -2 ), "arrow_forward_ios", 8f, TextFlag.Center ); + } else { - if ( asset.IsDeleted ) continue; - if ( ev.Action == DropAction.Copy ) - EditorUtility.CopyAssetToDirectory( asset, TargetPath ); - else - EditorUtility.MoveAssetToDirectory( asset, TargetPath ); + Paint.DrawIcon( rect, "arrow_forward_ios", 8f, TextFlag.Center ); } } - } -} - -file class PathSeparator : Widget -{ - private ContextMenu menu; - - private AssetBrowser Browser { get; init; } - public string AbsolutePath { get; init; } - - public PathSeparator( AssetBrowser browser, string absolutePath ) : base( null ) - { - MinimumWidth = 16; - AbsolutePath = absolutePath; - Browser = browser; - } - - protected override void OnPaint() - { - base.OnPaint(); - - Paint.ClearBrush(); - Paint.ClearPen(); - - if ( Paint.HasMouseOver ) - { - Paint.SetBrush( Color.White.WithAlpha( 0.1f ) ); - Paint.DrawRect( LocalRect.Shrink( 0, 2 ) ); - } - - var rect = LocalRect; - - Paint.SetPen( Theme.TextControl ); - - if ( menu.IsValid() ) - { - Paint.Rotate( 90, rect.Position + new Vector2( 8, 8 ) ); - Paint.DrawIcon( rect + new Vector2( 4, -2 ), "arrow_forward_ios", 8f, TextFlag.Center ); - } - else - { - Paint.DrawIcon( rect, "arrow_forward_ios", 8f, TextFlag.Center ); - } - } - - protected override void OnMouseClick( MouseEvent e ) - { - base.OnMouseClick( e ); - - menu?.Close(); - menu = new ContextMenu(); - - if ( !AssetBrowser.Location.TryParse( AbsolutePath, out var location ) ) - return; - - foreach ( var subDirectory in location.GetDirectories() ) - { - menu.AddOption( subDirectory.Name, action: () => Browser.NavigateTo( subDirectory ) ); - } - - menu.OpenAt( ScreenRect.BottomLeft ); - - e.Accepted = true; - } -} - -file class PathElipses : Widget -{ - private ContextMenu menu; - - private AssetBrowser Browser { get; init; } - public List<(string Label, string Path)> Paths { get; init; } - - public PathElipses( AssetBrowser browser ) : base( null ) - { - FixedWidth = 32; - Paths = new(); - Browser = browser; - } - - protected override void OnPaint() - { - base.OnPaint(); - - Paint.ClearBrush(); - Paint.ClearPen(); - - if ( Paint.HasMouseOver ) - { - Paint.SetBrush( Color.White.WithAlpha( 0.1f ) ); - Paint.DrawRect( LocalRect.Shrink( 0, 2 ) ); - } - - Paint.SetPen( Theme.TextControl ); - Paint.SetDefaultFont( 8 ); - Paint.DrawText( LocalRect.Shrink( 8, 0 ), "...", TextFlag.Center ); - } - - protected override void OnMouseClick( MouseEvent e ) - { - base.OnMouseClick( e ); - - menu?.Close(); - menu = new ContextMenu(); - - foreach ( var entry in Paths.Reverse<(string Label, string Path)>() ) - { - if ( !AssetBrowser.Location.TryParse( entry.Path, out var location ) ) - continue; - - menu.AddOption( entry.Label, action: () => Browser.NavigateTo( location ) ); - } - - menu.OpenAt( ScreenRect.BottomLeft ); - - e.Accepted = true; + + protected override void OnMouseClick( MouseEvent e ) + { + base.OnMouseClick( e ); + + menu?.Close(); + menu = new ContextMenu(); + + if ( !AssetBrowser.Location.TryParse( AbsolutePath, out var location ) ) + return; + + foreach ( var subDirectory in location.GetDirectories() ) + { + menu.AddOption( subDirectory.Name, action: () => Browser.NavigateTo( subDirectory ) ); + } + + menu.OpenAt( ScreenRect.BottomLeft ); + + e.Accepted = true; + } + } + + class PathElipses : Widget + { + private ContextMenu menu; + + private AssetBrowser Browser { get; init; } + public List<(string Label, string Path)> Paths { get; init; } + + public PathElipses( AssetBrowser browser ) : base( null ) + { + FixedWidth = 32; + Paths = new(); + Browser = browser; + } + + protected override void OnPaint() + { + base.OnPaint(); + + Paint.ClearBrush(); + Paint.ClearPen(); + + if ( Paint.HasMouseOver ) + { + Paint.SetBrush( Color.White.WithAlpha( 0.1f ) ); + Paint.DrawRect( LocalRect.Shrink( 0, 2 ) ); + } + + Paint.SetPen( Theme.TextControl ); + Paint.SetDefaultFont( 8 ); + Paint.DrawText( LocalRect.Shrink( 8, 0 ), "...", TextFlag.Center ); + } + + protected override void OnMouseClick( MouseEvent e ) + { + base.OnMouseClick( e ); + + menu?.Close(); + menu = new ContextMenu(); + + foreach ( var entry in Paths.Reverse<(string Label, string Path)>() ) + { + if ( !AssetBrowser.Location.TryParse( entry.Path, out var location ) ) + continue; + + menu.AddOption( entry.Label, action: () => Browser.NavigateTo( location ) ); + } + + menu.OpenAt( ScreenRect.BottomLeft ); + + e.Accepted = true; + } } }