Editor/AssetBrowserAddon/LocationEditor.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using Editor;
using Sandbox;

namespace Sandbox.AssetBrowserAddon;

/// <summary>
/// Dialog that captures the settings for a new custom Asset Browser location.
/// </summary>
public sealed class LocationEditor : Dialog
{
    private readonly Action<CustomLocationDefinition> _onConfirm;
    private readonly CustomLocationDefinition _initialDefinition;
    private readonly LineEdit _nameInput;
    private readonly LineEdit _iconInput;
    private readonly LineEdit _includeInput;
    private readonly LineEdit _excludeInput;
    private readonly AssetTypeSelector _assetTypeSelector;
    private readonly ToggleSwitch _projectOnlyToggle;

    public LocationEditor(Action<CustomLocationDefinition> onConfirm, CustomLocationDefinition initialDefinition = null)
    {
        _onConfirm = onConfirm;
        _initialDefinition = initialDefinition;

        Window.Title = initialDefinition is null ? "Add Bookmark" : "Edit Bookmark";
        Window.Size = new Vector2(500, 750);

        Layout = Layout.Column();
        Layout.Margin = 16f;
        Layout.Spacing = 12f;

        _nameInput = AddTextRow("Title", "My Bookmark");
        _iconInput = AddIconRow("Icon", "bookmark");

        Layout.Add( new Label( this ) { Text = "Asset Types" } );
        _assetTypeSelector = Layout.Add( new AssetTypeSelector( this, StyleInput ) );

        _projectOnlyToggle = Layout.Add( new ToggleSwitch( "Only include assets from this project", this ) );
        _projectOnlyToggle.MinimumHeight = Theme.RowHeight;
        _projectOnlyToggle.Value = true;

        _includeInput = AddTextArea("Include Folders", "Separate folders with ;");
        _excludeInput = AddTextArea("Exclude Folders", "Separate folders with ;");
        if ( _initialDefinition is not null )
        {
            _nameInput.Text = _initialDefinition.Name;
            _iconInput.Text = _initialDefinition.Icon;
            _assetTypeSelector.SetSelected( _initialDefinition.AssetTypes );
            _includeInput.Text = string.Join( ';', _initialDefinition.IncludeFolders ?? new List<string>() );
            _excludeInput.Text = string.Join( ';', _initialDefinition.ExcludeFolders ?? new List<string>() );
            _projectOnlyToggle.Value = _initialDefinition.ProjectAssetsOnly;

        }

        var buttonRow = Layout.AddRow();
        buttonRow.Spacing = 8f;
        buttonRow.AddStretchCell();
        buttonRow.Add( new Button( "Cancel", this ) { Clicked = Close } );
        var buttonLabel = _initialDefinition is null ? "Add Bookmark" : "Save Bookmark";
        var buttonIcon = _initialDefinition is null ? "add" : "save";
        buttonRow.Add( new Button.Primary( buttonLabel, buttonIcon, this ) { Clicked = Submit } );
    }

    private LineEdit AddTextRow(string label, string placeholder)
    {
        var row = Layout.AddRow();
        row.Spacing = 8f;
        row.Add( new Label( this ) { Text = label, FixedWidth = 130 } );

        var input = row.Add( new LineEdit( this ) );
        input.PlaceholderText = placeholder;
        StyleInput( input );
        return input;
    }

    private LineEdit AddIconRow(string label, string placeholder)
    {
        var row = Layout.AddRow();
        row.Spacing = 8f;
        row.Add( new Label( this ) { Text = label, FixedWidth = 130 } );

        var input = row.Add( new LineEdit( this ) );
        input.PlaceholderText = placeholder;
        StyleInput( input );

        var button = row.Add( new IconButton( "search", () => ShowIconPicker( input ), this ) );
        button.ToolTip = "Browse material icons";
        button.MinimumWidth = Theme.RowHeight;

        return input;
    }

    private LineEdit AddTextArea(string label, string placeholder)
    {
        var column = Layout.Add( Layout.Column() );
        column.Spacing = 4f;
        column.Add( new Label( this ) { Text = label } );

        var input = column.Add( new LineEdit( this ) );
        input.PlaceholderText = placeholder;
        StyleInput( input );
        return input;
    }

    private void Submit()
    {
        var name = _nameInput.Text?.Trim();
        if ( string.IsNullOrWhiteSpace( name ) )
        {
            EditorUtility.DisplayDialog( "Missing Title", "Please enter a title for the bookmark." );
            return;
        }

        var icon = string.IsNullOrWhiteSpace( _iconInput.Text ) ? "extension" : _iconInput.Text.Trim();

        var definition = _initialDefinition is null
            ? new CustomLocationDefinition()
            : new CustomLocationDefinition { Id = _initialDefinition.Id };

        definition.Name = name;
        definition.Icon = icon;
        definition.AssetTypes = _assetTypeSelector.SelectedTags.Select( NormalizeExtension ).Where( x => !string.IsNullOrWhiteSpace( x ) ).ToList();
        definition.IncludeFolders = SplitToList( _includeInput.Text );
        definition.ExcludeFolders = SplitToList( _excludeInput.Text );
        definition.ProjectAssetsOnly = _projectOnlyToggle.Value;

        _onConfirm?.Invoke( definition );
        Close();
    }

    private void ShowIconPicker( LineEdit target )
    {
        var pickerType = AppDomain.CurrentDomain.GetAssemblies()
            .Select( asm => asm.GetType( "Editor.IconPickerWidget", false ) )
            .FirstOrDefault( t => t is not null );

        var openPopup = pickerType?.GetMethod( "OpenPopup", BindingFlags.Public | BindingFlags.Static );
        if ( openPopup is null )
        {
            EditorUtility.DisplayDialog( "Icon Picker", "Unable to locate the icon picker widget." );
            return;
        }

        openPopup.Invoke( null, new object[]
        {
            this,
            target.Text ?? string.Empty,
            (Action<string>)(value => target.Text = value)
        } );
    }

    private static List<string> SplitToList(string raw)
    {
        if ( string.IsNullOrWhiteSpace( raw ) )
            return new List<string>();

        return raw
            .Split( new[] { ';', ',', '\n', '\r' }, StringSplitOptions.RemoveEmptyEntries )
            .Select( part => part.Trim() )
            .Where( part => part.Length > 0 )
            .ToList();
    }

    private static string NormalizeExtension( string value )
    {
        if ( string.IsNullOrWhiteSpace( value ) )
            return string.Empty;

        return value.Trim().TrimStart( '.' ).ToLowerInvariant();
    }

    private static void StyleInput( LineEdit input )
    {
        var background = Theme.ControlBackground.Darken( 0.25f ).Hex;
        var border = Theme.Border.Hex;
        input.SetStyles( $"background-color: {background}; border-color: {border};" );
    }
}