using System; using System.Collections.Generic; using System.Linq; using UnityEditor; using UnityEngine; namespace Unity.VisualScripting { [FuzzyOption(typeof(IUnit))] public class UnitOption : IUnitOption where TUnit : IUnit { public UnitOption() { sourceScriptGuids = new HashSet(); } public UnitOption(TUnit unit) : this() { this.unit = unit; FillFromUnit(); } [DoNotSerialize] protected bool filled { get; private set; } private TUnit _unit; protected UnitOptionRow source { get; private set; } public TUnit unit { get { // Load the node on demand to avoid deserialization overhead // Deserializing the entire database takes many seconds, // which is the reason why UnitOptionRow and SQLite are used // in the first place. if (_unit == null) { _unit = (TUnit)new SerializationData(source.unit).Deserialize(); } return _unit; } protected set { _unit = value; } } IUnit IUnitOption.unit => unit; public Type unitType { get; private set; } protected IUnitDescriptor descriptor => unit.Descriptor(); // Avoid using the descriptions for each option, because we don't need all fields described until the option is hovered protected UnitDescription description => unit.Description(); protected UnitPortDescription PortDescription(IUnitPort port) { return port.Description(); } public virtual IUnit InstantiateUnit() { var instance = unit.CloneViaSerialization(); instance.Define(); return instance; } void IUnitOption.PreconfigureUnit(IUnit unit) { PreconfigureUnit((TUnit)unit); } public virtual void PreconfigureUnit(TUnit unit) { } protected virtual void FillFromUnit() { unit.EnsureDefined(); unitType = unit.GetType(); labelHuman = Label(true); haystackHuman = Haystack(true); labelProgrammer = Label(false); haystackProgrammer = Haystack(false); category = Category(); order = Order(); favoriteKey = FavoriteKey(); UnityAPI.Async(() => icon = Icon()); showControlInputsInFooter = ShowControlInputsInFooter(); showControlOutputsInFooter = ShowControlOutputsInFooter(); showValueInputsInFooter = ShowValueInputsInFooter(); showValueOutputsInFooter = ShowValueOutputsInFooter(); controlInputCount = unit.controlInputs.Count; controlOutputCount = unit.controlOutputs.Count; valueInputTypes = unit.valueInputs.Select(vi => vi.type).ToHashSet(); valueOutputTypes = unit.valueOutputs.Select(vo => vo.type).ToHashSet(); filled = true; } protected virtual void FillFromData() { unit.EnsureDefined(); unitType = unit.GetType(); UnityAPI.Async(() => icon = Icon()); showControlInputsInFooter = ShowControlInputsInFooter(); showControlOutputsInFooter = ShowControlOutputsInFooter(); showValueInputsInFooter = ShowValueInputsInFooter(); showValueOutputsInFooter = ShowValueOutputsInFooter(); filled = true; } public virtual void Deserialize(UnitOptionRow row) { source = row; if (row.sourceScriptGuids != null) { sourceScriptGuids = row.sourceScriptGuids.Split(',').ToHashSet(); } unitType = Codebase.DeserializeType(row.unitType); category = row.category == null ? null : new UnitCategory(row.category); labelHuman = row.labelHuman; labelProgrammer = row.labelProgrammer; order = row.order; haystackHuman = row.haystackHuman; haystackProgrammer = row.haystackProgrammer; favoriteKey = row.favoriteKey; controlInputCount = row.controlInputCount; controlOutputCount = row.controlOutputCount; } public virtual UnitOptionRow Serialize() { var row = new UnitOptionRow(); if (sourceScriptGuids.Count == 0) { // Important to set to null here, because the code relies on // null checks, not empty string checks. row.sourceScriptGuids = null; } else { row.sourceScriptGuids = string.Join(",", sourceScriptGuids.ToArray()); } row.optionType = Codebase.SerializeType(GetType()); row.unitType = Codebase.SerializeType(unitType); row.unit = unit.Serialize().json; row.category = category?.fullName; row.labelHuman = labelHuman; row.labelProgrammer = labelProgrammer; row.order = order; row.haystackHuman = haystackHuman; row.haystackProgrammer = haystackProgrammer; row.favoriteKey = favoriteKey; row.controlInputCount = controlInputCount; row.controlOutputCount = controlOutputCount; row.valueInputTypes = valueInputTypes.Select(Codebase.SerializeType).ToSeparatedString("|").NullIfEmpty(); row.valueOutputTypes = valueOutputTypes.Select(Codebase.SerializeType).ToSeparatedString("|").NullIfEmpty(); return row; } public virtual void OnPopulate() { if (!filled) { FillFromData(); } } public virtual void Prewarm() { } #region Configuration public object value => this; public bool parentOnly => false; public virtual string headerLabel => label; public virtual bool showHeaderIcon => false; public virtual bool favoritable => true; #endregion #region Properties public HashSet sourceScriptGuids { get; protected set; } protected string labelHuman { get; set; } protected string labelProgrammer { get; set; } public string label => BoltCore.Configuration.humanNaming ? labelHuman : labelProgrammer; public UnitCategory category { get; private set; } public int order { get; private set; } public EditorTexture icon { get; private set; } protected string haystackHuman { get; set; } protected string haystackProgrammer { get; set; } public string haystack => BoltCore.Configuration.humanNaming ? haystackHuman : haystackProgrammer; public string favoriteKey { get; private set; } public virtual string formerHaystack => BoltFlowNameUtility.UnitPreviousTitle(unitType); GUIStyle IFuzzyOption.style => Style(); #endregion #region Contextual Filtering public int controlInputCount { get; private set; } public int controlOutputCount { get; private set; } private HashSet _valueInputTypes; private HashSet _valueOutputTypes; // On demand loading for initialization performance (type deserialization is expensive) public HashSet valueInputTypes { get { if (_valueInputTypes == null) { if (string.IsNullOrEmpty(source.valueInputTypes)) { _valueInputTypes = new HashSet(); } else { _valueInputTypes = source.valueInputTypes.Split('|').Select(Codebase.DeserializeType).ToHashSet(); } } return _valueInputTypes; } private set { _valueInputTypes = value; } } public HashSet valueOutputTypes { get { if (_valueOutputTypes == null) { if (string.IsNullOrEmpty(source.valueOutputTypes)) { _valueOutputTypes = new HashSet(); } else { _valueOutputTypes = source.valueOutputTypes.Split('|').Select(Codebase.DeserializeType).ToHashSet(); } } return _valueOutputTypes; } private set { _valueOutputTypes = value; } } #endregion #region Providers protected virtual string Label(bool human) { return BoltFlowNameUtility.UnitTitle(unitType, false, true); } protected virtual UnitCategory Category() { return unitType.GetAttribute(); } protected virtual int Order() { return unitType.GetAttribute()?.order ?? int.MaxValue; } protected virtual string Haystack(bool human) { return Label(human); } protected virtual EditorTexture Icon() { return descriptor.Icon(); } protected virtual GUIStyle Style() { return FuzzyWindow.defaultOptionStyle; } protected virtual string FavoriteKey() { return unit.GetType().FullName; } #endregion #region Search public virtual string SearchResultLabel(string query) { var label = SearchUtility.HighlightQuery(haystack, query); if (category != null) { label += $" (in {category.fullName})"; } return label; } #endregion #region Footer private string summary => description.summary; public bool hasFooter => !StringUtility.IsNullOrWhiteSpace(summary) || footerPorts.Any(); protected virtual bool ShowControlInputsInFooter() { return unitType.GetAttribute()?.ControlInputs ?? false; } protected virtual bool ShowControlOutputsInFooter() { return unitType.GetAttribute()?.ControlOutputs ?? false; } protected virtual bool ShowValueInputsInFooter() { return unitType.GetAttribute()?.ValueInputs ?? true; } protected virtual bool ShowValueOutputsInFooter() { return unitType.GetAttribute()?.ValueOutputs ?? true; } [DoNotSerialize] protected bool showControlInputsInFooter { get; private set; } [DoNotSerialize] protected bool showControlOutputsInFooter { get; private set; } [DoNotSerialize] protected bool showValueInputsInFooter { get; private set; } [DoNotSerialize] protected bool showValueOutputsInFooter { get; private set; } private IEnumerable footerPorts { get { if (showControlInputsInFooter) { foreach (var controlInput in unit.controlInputs) { yield return controlInput; } } if (showControlOutputsInFooter) { foreach (var controlOutput in unit.controlOutputs) { yield return controlOutput; } } if (showValueInputsInFooter) { foreach (var valueInput in unit.valueInputs) { yield return valueInput; } } if (showValueOutputsInFooter) { foreach (var valueOutput in unit.valueOutputs) { yield return valueOutput; } } } } public float GetFooterHeight(float width) { var hasSummary = !StringUtility.IsNullOrWhiteSpace(summary); var hasIcon = icon != null; var hasPorts = footerPorts.Any(); var height = 0f; width -= 2 * FooterStyles.padding; height += FooterStyles.padding; if (hasSummary) { if (hasIcon) { height += Mathf.Max(FooterStyles.unitIconSize, GetFooterSummaryHeight(width - FooterStyles.unitIconSize - FooterStyles.spaceAfterUnitIcon)); } else { height += GetFooterSummaryHeight(width); } } if (hasSummary && hasPorts) { height += FooterStyles.spaceBetweenDescriptionAndPorts; } foreach (var port in footerPorts) { height += GetFooterPortHeight(width, port); height += FooterStyles.spaceBetweenPorts; } if (hasPorts) { height -= FooterStyles.spaceBetweenPorts; } height += FooterStyles.padding; return height; } public void OnFooterGUI(Rect position) { var hasSummary = !StringUtility.IsNullOrWhiteSpace(summary); var hasIcon = icon != null; var hasPorts = footerPorts.Any(); var y = position.y; y += FooterStyles.padding; position.x += FooterStyles.padding; position.width -= FooterStyles.padding * 2; if (hasSummary) { if (hasIcon) { var iconPosition = new Rect ( position.x, y, FooterStyles.unitIconSize, FooterStyles.unitIconSize ); var summaryWidth = position.width - iconPosition.width - FooterStyles.spaceAfterUnitIcon; var summaryPosition = new Rect ( iconPosition.xMax + FooterStyles.spaceAfterUnitIcon, y, summaryWidth, GetFooterSummaryHeight(summaryWidth) ); GUI.DrawTexture(iconPosition, icon?[FooterStyles.unitIconSize]); OnFooterSummaryGUI(summaryPosition); y = Mathf.Max(iconPosition.yMax, summaryPosition.yMax); } else { OnFooterSummaryGUI(position.VerticalSection(ref y, GetFooterSummaryHeight(position.width))); } } if (hasSummary && hasPorts) { y += FooterStyles.spaceBetweenDescriptionAndPorts; } foreach (var port in footerPorts) { OnFooterPortGUI(position.VerticalSection(ref y, GetFooterPortHeight(position.width, port)), port); y += FooterStyles.spaceBetweenPorts; } if (hasPorts) { y -= FooterStyles.spaceBetweenPorts; } y += FooterStyles.padding; } private float GetFooterSummaryHeight(float width) { return FooterStyles.description.CalcHeight(new GUIContent(summary), width); } private void OnFooterSummaryGUI(Rect position) { EditorGUI.LabelField(position, summary, FooterStyles.description); } private string GetFooterPortLabel(IUnitPort port) { string type; if (port is ValueInput) { type = ((IUnitValuePort)port).type.DisplayName() + " Input"; } else if (port is ValueOutput) { type = ((IUnitValuePort)port).type.DisplayName() + " Output"; } else if (port is ControlInput) { type = "Trigger Input"; } else if (port is ControlOutput) { type = "Trigger Output"; } else { throw new NotSupportedException(); } var portDescription = PortDescription(port); if (!StringUtility.IsNullOrWhiteSpace(portDescription.summary)) { return $"{portDescription.label}: {portDescription.summary} {LudiqGUIUtility.DimString($"({type})")}"; } else { return $"{portDescription.label}: {LudiqGUIUtility.DimString($"({type})")}"; } } private float GetFooterPortDescriptionHeight(float width, IUnitPort port) { return FooterStyles.portDescription.CalcHeight(new GUIContent(GetFooterPortLabel(port)), width); } private void OnFooterPortDescriptionGUI(Rect position, IUnitPort port) { GUI.Label(position, GetFooterPortLabel(port), FooterStyles.portDescription); } private float GetFooterPortHeight(float width, IUnitPort port) { var descriptionWidth = width - FooterStyles.portIconSize - FooterStyles.spaceAfterPortIcon; return GetFooterPortDescriptionHeight(descriptionWidth, port); } private void OnFooterPortGUI(Rect position, IUnitPort port) { var iconPosition = new Rect ( position.x, position.y, FooterStyles.portIconSize, FooterStyles.portIconSize ); var descriptionWidth = position.width - FooterStyles.portIconSize - FooterStyles.spaceAfterPortIcon; var descriptionPosition = new Rect ( iconPosition.xMax + FooterStyles.spaceAfterPortIcon, position.y, descriptionWidth, GetFooterPortDescriptionHeight(descriptionWidth, port) ); var portDescription = PortDescription(port); var icon = portDescription.icon?[FooterStyles.portIconSize]; if (icon != null) { GUI.DrawTexture(iconPosition, icon); } OnFooterPortDescriptionGUI(descriptionPosition, port); } public static class FooterStyles { static FooterStyles() { description = new GUIStyle(EditorStyles.label); description.padding = new RectOffset(0, 0, 0, 0); description.wordWrap = true; description.richText = true; portDescription = new GUIStyle(EditorStyles.label); portDescription.padding = new RectOffset(0, 0, 0, 0); portDescription.wordWrap = true; portDescription.richText = true; portDescription.imagePosition = ImagePosition.TextOnly; } public static readonly GUIStyle description; public static readonly GUIStyle portDescription; public static readonly float spaceAfterUnitIcon = 7; public static readonly int unitIconSize = IconSize.Medium; public static readonly float spaceAfterPortIcon = 6; public static readonly int portIconSize = IconSize.Small; public static readonly float spaceBetweenDescriptionAndPorts = 8; public static readonly float spaceBetweenPorts = 8; public static readonly float padding = 8; } #endregion } }