diff --git a/Source/Editor/Modules/SourceCodeEditing/CodeDocsModule.cs b/Source/Editor/Modules/SourceCodeEditing/CodeDocsModule.cs index 610368a82..a26fe9924 100644 --- a/Source/Editor/Modules/SourceCodeEditing/CodeDocsModule.cs +++ b/Source/Editor/Modules/SourceCodeEditing/CodeDocsModule.cs @@ -316,7 +316,9 @@ namespace FlaxEditor.Modules.SourceCodeEditing var memberReader = xmlReader.ReadSubtree(); if (memberReader.ReadToDescendant("summary")) { - result[rawName] = memberReader.ReadInnerXml().Replace('\n', ' ').Trim(); + // Remove and replace them with the captured group (the content of the cref). Additionally, getting rid of prefixes + const string crefPattern = @""; + result[rawName] = Regex.Replace(memberReader.ReadInnerXml(), crefPattern, "$1").Replace('\n', ' ').Trim(); } } } diff --git a/Source/Editor/Options/InterfaceOptions.cs b/Source/Editor/Options/InterfaceOptions.cs index 7dec0f5cc..8f786362d 100644 --- a/Source/Editor/Options/InterfaceOptions.cs +++ b/Source/Editor/Options/InterfaceOptions.cs @@ -375,6 +375,13 @@ namespace FlaxEditor.Options [EditorDisplay("Visject"), EditorOrder(550)] public float ConnectionCurvature { get; set; } = 1.0f; + /// + /// Gets or sets the visject connection curvature. + /// + [DefaultValue(true)] + [EditorDisplay("Visject"), EditorOrder(550), Tooltip("Shows/hides the description panel in the visual scripting context menu.")] + public bool VisualScriptingDescriptionPanel { get; set; } = true; + private static FontAsset DefaultFont => FlaxEngine.Content.LoadAsyncInternal(EditorAssets.PrimaryFont); private static FontAsset ConsoleFont => FlaxEngine.Content.LoadAsyncInternal(EditorAssets.InconsolataRegularFont); diff --git a/Source/Editor/Surface/Archetypes/Bitwise.cs b/Source/Editor/Surface/Archetypes/Bitwise.cs index 5b96627f0..92616bcbc 100644 --- a/Source/Editor/Surface/Archetypes/Bitwise.cs +++ b/Source/Editor/Surface/Archetypes/Bitwise.cs @@ -59,8 +59,8 @@ namespace FlaxEditor.Surface.Archetypes { Op1(1, "Bitwise NOT", "Negates the value using bitwise operation", new[] { "!", "~" }), Op2(2, "Bitwise AND", "Performs a bitwise conjunction on two values", new[] { "&" }), - Op2(3, "Bitwise OR", "", new[] { "|" }), - Op2(4, "Bitwise XOR", "", new[] { "^" }), + Op2(3, "Bitwise OR", "Performs a bitwise disjunction on two values", new[] { "|" }), + Op2(4, "Bitwise XOR", "Performs a bitwise exclusive disjunction on two values", new[] { "^" }), }; } } diff --git a/Source/Editor/Surface/Archetypes/Boolean.cs b/Source/Editor/Surface/Archetypes/Boolean.cs index 268d7a360..73db196d6 100644 --- a/Source/Editor/Surface/Archetypes/Boolean.cs +++ b/Source/Editor/Surface/Archetypes/Boolean.cs @@ -58,11 +58,11 @@ namespace FlaxEditor.Surface.Archetypes public static NodeArchetype[] Nodes = { Op1(1, "Boolean NOT", "Negates the boolean value", new[] { "!", "~" }), - Op2(2, "Boolean AND", "Performs a logical conjunction on two values", new[] { "&&" }), - Op2(3, "Boolean OR", "Returns true if either (or both) of its operands is true", new[] { "||" }), - Op2(4, "Boolean XOR", "", new[] { "^" }), - Op2(5, "Boolean NOR", ""), - Op2(6, "Boolean NAND", ""), + Op2(2, "Boolean AND", "Performs a logical conjunction on two values. Returns true if both of its operands are true", new[] { "&&" }), + Op2(3, "Boolean OR", "Performs a logical disjunction on two values. Returns true if either (or both) of its operands is true", new[] { "||" }), + Op2(4, "Boolean XOR", "Performs a logical exclusive disjunction on two values. Returns true if both of its operands are different", new[] { "^" }), + Op2(5, "Boolean NOR", "Performs a logical disjunction on two values and negates it. Returns true if both of its operands are false"), + Op2(6, "Boolean NAND", "Performs a logical conjunction on two values and negates it. Returns true if either (or both) of its operands are false"), }; } } diff --git a/Source/Editor/Surface/Archetypes/Constants.cs b/Source/Editor/Surface/Archetypes/Constants.cs index b94bc34cb..59b554e6b 100644 --- a/Source/Editor/Surface/Archetypes/Constants.cs +++ b/Source/Editor/Surface/Archetypes/Constants.cs @@ -182,6 +182,13 @@ namespace FlaxEditor.Surface.Archetypes base.OnDestroy(); } + + internal static void GetInputOutputDescription(NodeArchetype nodeArch, out (string, ScriptType)[] inputs, out (string, ScriptType)[] outputs) + { + var type = new ScriptType(nodeArch.DefaultValues[0].GetType()); + inputs = null; + outputs = [(type.Name, type)]; + } } private class ArrayNode : SurfaceNode @@ -321,6 +328,12 @@ namespace FlaxEditor.Surface.Archetypes array.SetValue(value, box.ID - 1); SetValue(0, array); } + + internal static void GetInputOutputDescription(NodeArchetype nodeArch, out (string, ScriptType)[] inputs, out (string, ScriptType)[] outputs) + { + inputs = null; + outputs = [("", new ScriptType(typeof(Array)))]; + } } private class DictionaryNode : SurfaceNode @@ -449,6 +462,12 @@ namespace FlaxEditor.Surface.Archetypes array.SetValue(value, box.ID - 1); SetValue(0, array); } + + internal static void GetInputOutputDescription(NodeArchetype nodeArch, out (string, ScriptType)[] inputs, out (string, ScriptType)[] outputs) + { + inputs = null; + outputs = [("", new ScriptType(typeof(System.Collections.Generic.Dictionary)))]; + } } /// @@ -743,6 +762,7 @@ namespace FlaxEditor.Surface.Archetypes Title = "Enum", Create = (id, context, arch, groupArch) => new EnumNode(id, context, arch, groupArch), Description = "Enum constant value.", + GetInputOutputDescription = EnumNode.GetInputOutputDescription, Flags = NodeFlags.VisualScriptGraph | NodeFlags.AnimGraph | NodeFlags.NoSpawnViaGUI, Size = new Float2(180, 20), DefaultValues = new object[] @@ -779,6 +799,7 @@ namespace FlaxEditor.Surface.Archetypes Title = "Array", Create = (id, context, arch, groupArch) => new ArrayNode(id, context, arch, groupArch), Description = "Constant array value.", + GetInputOutputDescription = ArrayNode.GetInputOutputDescription, Flags = NodeFlags.VisualScriptGraph | NodeFlags.AnimGraph, Size = new Float2(150, 20), DefaultValues = new object[] { new int[] { 0, 1, 2 } }, @@ -790,6 +811,7 @@ namespace FlaxEditor.Surface.Archetypes Title = "Dictionary", Create = (id, context, arch, groupArch) => new DictionaryNode(id, context, arch, groupArch), Description = "Creates an empty dictionary.", + GetInputOutputDescription = DictionaryNode.GetInputOutputDescription, Flags = NodeFlags.VisualScriptGraph | NodeFlags.AnimGraph, Size = new Float2(150, 40), DefaultValues = new object[] { typeof(int).FullName, typeof(string).FullName }, diff --git a/Source/Editor/Surface/Archetypes/Packing.cs b/Source/Editor/Surface/Archetypes/Packing.cs index 40244c2ca..99899e033 100644 --- a/Source/Editor/Surface/Archetypes/Packing.cs +++ b/Source/Editor/Surface/Archetypes/Packing.cs @@ -245,6 +245,29 @@ namespace FlaxEditor.Surface.Archetypes } return false; } + + internal static void GetInputOutputDescription(NodeArchetype nodeArch, out (string, ScriptType)[] inputs, out (string, ScriptType)[] outputs) + { + var typeName = (string)nodeArch.DefaultValues[0]; + var type = TypeUtils.GetType(typeName); + if (type) + { + var fields = type.GetMembers(BindingFlags.Public | BindingFlags.Instance).Where(x => x.IsField).ToArray(); + var fieldsLength = fields.Length; + inputs = new (string, ScriptType)[fieldsLength]; + for (var i = 0; i < fieldsLength; i++) + { + var field = fields[i]; + inputs[i] = (field.Name, field.ValueType); + } + + outputs = [(type.Name, type)]; + return; + } + + inputs = null; + outputs = null; + } } private sealed class UnpackStructureNode : StructureNode @@ -283,6 +306,29 @@ namespace FlaxEditor.Surface.Archetypes } return false; } + + internal static void GetInputOutputDescription(NodeArchetype nodeArch, out (string, ScriptType)[] inputs, out (string, ScriptType)[] outputs) + { + var typeName = (string)nodeArch.DefaultValues[0]; + var type = TypeUtils.GetType(typeName); + if (type) + { + inputs = [(type.Name, type)]; + + var fields = type.GetMembers(BindingFlags.Public | BindingFlags.Instance).Where(x => x.IsField).ToArray(); + var fieldsLength = fields.Length; + outputs = new (string, ScriptType)[fieldsLength]; + for (var i = 0; i < fieldsLength; i++) + { + var field = fields[i]; + outputs[i] = (field.Name, field.ValueType); + } + return; + } + + inputs = null; + outputs = null; + } } /// @@ -411,6 +457,7 @@ namespace FlaxEditor.Surface.Archetypes Create = (id, context, arch, groupArch) => new PackStructureNode(id, context, arch, groupArch), IsInputCompatible = PackStructureNode.IsInputCompatible, IsOutputCompatible = PackStructureNode.IsOutputCompatible, + GetInputOutputDescription = PackStructureNode.GetInputOutputDescription, Description = "Makes the structure data to from the components.", Flags = NodeFlags.VisualScriptGraph | NodeFlags.AnimGraph | NodeFlags.NoSpawnViaGUI, Size = new Float2(180, 20), @@ -523,6 +570,7 @@ namespace FlaxEditor.Surface.Archetypes Create = (id, context, arch, groupArch) => new UnpackStructureNode(id, context, arch, groupArch), IsInputCompatible = UnpackStructureNode.IsInputCompatible, IsOutputCompatible = UnpackStructureNode.IsOutputCompatible, + GetInputOutputDescription = UnpackStructureNode.GetInputOutputDescription, Description = "Breaks the structure data to allow extracting components from it.", Flags = NodeFlags.VisualScriptGraph | NodeFlags.AnimGraph | NodeFlags.NoSpawnViaGUI, Size = new Float2(180, 20), diff --git a/Source/Editor/Surface/Archetypes/Particles.cs b/Source/Editor/Surface/Archetypes/Particles.cs index 081f53e30..03df9a0a9 100644 --- a/Source/Editor/Surface/Archetypes/Particles.cs +++ b/Source/Editor/Surface/Archetypes/Particles.cs @@ -122,7 +122,7 @@ namespace FlaxEditor.Surface.Archetypes { Name = module.Title, Tag = module.TypeID, - TooltipText = module.Description, + TooltipText = $"{(string.IsNullOrEmpty(module.Signature) ? module.Title : module.Signature)}\n{module.Description}", }); } cm.ItemClicked += item => AddModule((ushort)item.Tag); diff --git a/Source/Editor/Surface/ContextMenu/VisjectCM.cs b/Source/Editor/Surface/ContextMenu/VisjectCM.cs index ae4cef3dc..526c24fae 100644 --- a/Source/Editor/Surface/ContextMenu/VisjectCM.cs +++ b/Source/Editor/Surface/ContextMenu/VisjectCM.cs @@ -3,8 +3,10 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Text.RegularExpressions; using FlaxEditor.GUI.ContextMenu; using FlaxEditor.GUI.Input; +using FlaxEditor.Scripting; using FlaxEngine; using FlaxEngine.GUI; using FlaxEngine.Utilities; @@ -39,6 +41,9 @@ namespace FlaxEditor.Surface.ContextMenu /// TThe list of surface parameters or null if failed (readonly). public delegate List ParameterGetterDelegate(); + private const float DefaultWidth = 300; + private const float DefaultHeight = 400; + private readonly List _groups = new List(16); private CheckBox _contextSensitiveToggle; private bool _contextSensitiveSearchEnabled = true; @@ -52,10 +57,31 @@ namespace FlaxEditor.Surface.ContextMenu private NodeArchetype _parameterGetNodeArchetype; private NodeArchetype _parameterSetNodeArchetype; + // Description panel elements + private readonly bool _useDescriptionPanel; + private bool _descriptionPanelVisible; + private readonly Panel _descriptionPanel; + private readonly Panel _descriptionPanelSeparator; + private readonly Image _descriptionDeclaringClassImage; + private readonly Label _descriptionSignatureLabel; + private readonly Label _descriptionLabel; + private readonly VerticalPanel _descriptionInputPanel; + private readonly VerticalPanel _descriptionOutputPanel; + private readonly SurfaceStyle _surfaceStyle; + private VisjectCMItem _selectedItem; + /// /// The selected item /// - public VisjectCMItem SelectedItem; + public VisjectCMItem SelectedItem + { + get => _selectedItem; + private set + { + _selectedItem = value; + _selectedItem?.OnSelect(); + } + } /// /// Event fired when any item in this popup menu gets clicked. @@ -82,6 +108,11 @@ namespace FlaxEditor.Surface.ContextMenu /// public bool CanSetParameters; + /// + /// True if the surface should make use of a description panel drawn at the bottom of the context menu + /// + public bool UseDescriptionPanel; + /// /// The groups archetypes. Cannot be null. /// @@ -111,6 +142,11 @@ namespace FlaxEditor.Surface.ContextMenu /// The parameter setter node archetype to spawn when adding the parameter getter. Can be null. /// public NodeArchetype ParameterSetNodeArchetype; + + /// + /// The surface style to use to draw images in the description panel + /// + public SurfaceStyle Style; } /// @@ -127,9 +163,11 @@ namespace FlaxEditor.Surface.ContextMenu _parameterGetNodeArchetype = info.ParameterGetNodeArchetype ?? Archetypes.Parameters.Nodes[0]; if (info.CanSetParameters) _parameterSetNodeArchetype = info.ParameterSetNodeArchetype ?? Archetypes.Parameters.Nodes[3]; + _useDescriptionPanel = info.UseDescriptionPanel; + _surfaceStyle = info.Style; // Context menu dimensions - Size = new Float2(300, 400); + Size = new Float2(_useDescriptionPanel ? DefaultWidth + 50f : DefaultWidth, DefaultHeight); var headerPanel = new Panel(ScrollBars.None) { @@ -190,7 +228,6 @@ namespace FlaxEditor.Surface.ContextMenu Bounds = new Rectangle(0, _searchBox.Bottom + 1, Width, Height - _searchBox.Bottom - 2), Parent = this }; - _panel1 = panel1; // Create second panel (for groups arrangement) @@ -202,6 +239,70 @@ namespace FlaxEditor.Surface.ContextMenu }; _groupsPanel = panel2; + // Create description panel elements only when description panel is about to be used + if (_useDescriptionPanel) + { + _descriptionPanel = new Panel(ScrollBars.None) + { + Parent = this, + Bounds = new Rectangle(0, Height, Width, 0), + BackgroundColor = Style.Current.BackgroundNormal, + }; + + _descriptionDeclaringClassImage = new Image(8, 12, 20, 20) + { + Parent = _descriptionPanel, + Brush = new SpriteBrush(info.Style.Icons.BoxClose), + }; + + var descriptionFontReference = new FontReference(Style.Current.FontMedium.Asset, 9f); + _descriptionSignatureLabel = new Label(32, 8, Width - 40, 0) + { + Parent = _descriptionPanel, + HorizontalAlignment = TextAlignment.Near, + VerticalAlignment = TextAlignment.Near, + Wrapping = TextWrapping.WrapWords, + Font = descriptionFontReference, + Bold = true, + AutoHeight = true, + }; + _descriptionSignatureLabel.SetAnchorPreset(AnchorPresets.TopLeft, true); + + _descriptionLabel = new Label(32, 0, Width - 40, 0) + { + Parent = _descriptionPanel, + HorizontalAlignment = TextAlignment.Near, + VerticalAlignment = TextAlignment.Near, + Wrapping = TextWrapping.WrapWords, + Font = descriptionFontReference, + AutoHeight = true, + }; + _descriptionLabel.SetAnchorPreset(AnchorPresets.TopLeft, true); + + _descriptionPanelSeparator = new Panel(ScrollBars.None) + { + Parent = _descriptionPanel, + Bounds = new Rectangle(8, Height, Width - 16, 2), + BackgroundColor = Style.Current.BackgroundHighlighted, + }; + + _descriptionInputPanel = new VerticalPanel() + { + Parent = _descriptionPanel, + X = 8, + Width = Width * 0.5f - 16, + AutoSize = true, + }; + + _descriptionOutputPanel = new VerticalPanel() + { + Parent = _descriptionPanel, + X = Width * 0.5f + 8, + Width = Width * 0.5f - 16, + AutoSize = true, + }; + } + // Init groups var nodes = new List(); foreach (var groupArchetype in info.Groups) @@ -693,9 +794,189 @@ namespace FlaxEditor.Surface.ContextMenu { Focus(null); + if (_useDescriptionPanel) + HideDescriptionPanel(); + base.Hide(); } + /// + /// Updates the description panel and shows information about the set archetype + /// + /// The node archetype + public void SetDescriptionPanelArchetype(NodeArchetype archetype) + { + if (!_useDescriptionPanel) + return; + + if (archetype == null || !Editor.Instance.Options.Options.Interface.VisualScriptingDescriptionPanel) + { + HideDescriptionPanel(); + return; + } + + Profiler.BeginEvent("VisjectCM.SetDescriptionPanelArchetype"); + + _descriptionInputPanel.RemoveChildren(); + _descriptionOutputPanel.RemoveChildren(); + + // Fetch description and information from the memberInfo - mainly from nodes that got fetched asynchronously by the visual scripting editor + ScriptType declaringType; + if (archetype.Tag is ScriptMemberInfo memberInfo) + { + var name = memberInfo.Name; + if (memberInfo.IsMethod && memberInfo.Name.StartsWith("get_") || memberInfo.Name.StartsWith("set_")) + { + name = memberInfo.Name.Substring(4); + } + + declaringType = memberInfo.DeclaringType; + _descriptionSignatureLabel.Text = memberInfo.DeclaringType + "." + name; + + // We have to add the Instance information manually for members that aren't static + if (!memberInfo.IsStatic) + AddInputOutputElement(archetype, declaringType, false, $"Instance ({memberInfo.DeclaringType.Name})"); + + // We also have to manually add the Return information as well. + if (memberInfo.ValueType != ScriptType.Null && memberInfo.ValueType != ScriptType.Void) + { + // When a field has a setter we don't want it to show the input as a return output. + if (memberInfo.IsField && archetype.Title.StartsWith("Set ")) + AddInputOutputElement(archetype, memberInfo.ValueType, false, $"({memberInfo.ValueType.Name})"); + else + AddInputOutputElement(archetype, memberInfo.ValueType, true, $"Return ({memberInfo.ValueType.Name})"); + } + + for (int i = 0; i < memberInfo.ParametersCount; i++) + { + var param = memberInfo.GetParameters()[i]; + AddInputOutputElement(archetype, param.Type, param.IsOut, $"{param.Name} ({param.Type.Name})"); + } + } + else + { + // Try to fetch as many informations as possible from the predefined hardcoded nodes + _descriptionSignatureLabel.Text = string.IsNullOrEmpty(archetype.Signature) ? archetype.Title : archetype.Signature; + declaringType = archetype.DefaultType; + + // Some nodes are more delicate. Like Arrays or Dictionaries. In this case we let them fetch the inputs/outputs for us. + // Otherwise, it is not possible to show a proper return type on some nodes. + if (archetype.GetInputOutputDescription != null) + { + archetype.GetInputOutputDescription.Invoke(archetype, out var inputs, out var outputs); + + foreach (var input in inputs ?? []) + { + AddInputOutputElement(archetype, input.Type, false, $"{input.Name} ({input.Type.Name})"); + } + + foreach (var output in outputs ?? []) + { + AddInputOutputElement(archetype, output.Type, true, $"{output.Name} ({output.Type.Name})"); + } + } + else + { + foreach (var element in archetype.Elements) + { + if (element.Type is not (NodeElementType.Input or NodeElementType.Output)) + continue; + + var typeText = element.ConnectionsType ? element.ConnectionsType.Name : archetype.ConnectionsHints.ToString(); + AddInputOutputElement(archetype, element.ConnectionsType, element.Type == NodeElementType.Output, $"{element.Text} ({typeText})"); + } + } + } + + // Set declaring type icon color + _surfaceStyle.GetConnectionColor(declaringType, archetype.ConnectionsHints, out var declaringTypeColor); + _descriptionDeclaringClassImage.Color = declaringTypeColor; + _descriptionDeclaringClassImage.MouseOverColor = declaringTypeColor; + + // Calculate the description panel height. (I am doing this manually since working with autoSize on horizontal/vertical panels didn't work, especially with nesting - Nils) + float panelHeight = _descriptionSignatureLabel.Height; + + // If thee is no description we move the signature label down a bit to align it with the icon. Just a cosmetic check + if (string.IsNullOrEmpty(archetype.Description)) + { + _descriptionSignatureLabel.Y = 15; + _descriptionLabel.Text = ""; + } + else + { + _descriptionSignatureLabel.Y = 8; + _descriptionLabel.Y = _descriptionSignatureLabel.Bounds.Bottom + 6f; + // Replacing multiple whitespaces with a linebreak for better readability. (Mainly doing this because the ToolTip fetching system replaces linebreaks with whitespaces - Nils) + _descriptionLabel.Text = Regex.Replace(archetype.Description, @"\s{2,}", "\n"); + } + + // Some padding and moving elements around + _descriptionPanelSeparator.Y = _descriptionLabel.Bounds.Bottom + 8f; + panelHeight += _descriptionLabel.Height + 32f; + _descriptionInputPanel.Y = panelHeight; + _descriptionOutputPanel.Y = panelHeight; + panelHeight += Mathf.Max(_descriptionInputPanel.Height, _descriptionOutputPanel.Height); + + // Forcing the description panel to at least have a minimum height to not make the window size change too much in order to reduce jittering + // TODO: Remove the Mathf.Max and just set the height to panelHeight once the window jitter issue is fixed - Nils + _descriptionPanel.Height = Mathf.Max(140f, panelHeight); + Height = DefaultHeight + _descriptionPanel.Height; + UpdateWindowSize(); + _descriptionPanelVisible = true; + + Profiler.EndEvent(); + } + + private void AddInputOutputElement(NodeArchetype nodeArchetype, ScriptType type, bool isOutput, string text) + { + // Using a panel instead of a vertical panel because auto sizing is unreliable - so we are doing this manually here as well + var elementPanel = new Panel() + { + Parent = isOutput ? _descriptionOutputPanel : _descriptionInputPanel, + Width = Width * 0.5f, + Height = 16, + AnchorPreset = AnchorPresets.TopLeft + }; + + _surfaceStyle.GetConnectionColor(type, nodeArchetype.ConnectionsHints, out var typeColor); + elementPanel.AddChild(new Image(2, 0, 12, 12) + { + Brush = new SpriteBrush(_surfaceStyle.Icons.BoxOpen), + Color = typeColor, + MouseOverColor = typeColor, + AutoFocus = false, + }).SetAnchorPreset(AnchorPresets.TopLeft, true); + + // Forcing the first letter to be capital and removing the '&' char from pointer-references + text = (char.ToUpper(text[0]) + text.Substring(1)).Replace("&", ""); + var elementText = new Label(16, 0, Width * 0.5f - 32, 16) + { + Text = text, + HorizontalAlignment = TextAlignment.Near, + VerticalAlignment = TextAlignment.Near, + Wrapping = TextWrapping.WrapWords, + AutoHeight = true, + }; + elementText.SetAnchorPreset(AnchorPresets.TopLeft, true); + elementPanel.AddChild(elementText); + elementPanel.Height = elementText.Height; + } + + /// + /// Hides the description panel and resets the context menu to its original size + /// + private void HideDescriptionPanel() + { + if (!_descriptionPanelVisible) + return; + + _descriptionInputPanel.RemoveChildren(); + _descriptionOutputPanel.RemoveChildren(); + Height = DefaultHeight; + UpdateWindowSize(); + _descriptionPanelVisible = false; + } + /// public override bool OnKeyDown(KeyboardKeys key) { diff --git a/Source/Editor/Surface/ContextMenu/VisjectCMItem.cs b/Source/Editor/Surface/ContextMenu/VisjectCMItem.cs index 9d582c491..792579b67 100644 --- a/Source/Editor/Surface/ContextMenu/VisjectCMItem.cs +++ b/Source/Editor/Surface/ContextMenu/VisjectCMItem.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Text; using FlaxEditor.Scripting; using FlaxEditor.Surface.Elements; using FlaxEditor.Utilities; @@ -62,7 +63,7 @@ namespace FlaxEditor.Surface.ContextMenu Group = group; _groupArchetype = groupArchetype; _archetype = archetype; - TooltipText = _archetype.Description; + TooltipText = GetTooltip(); } /// @@ -315,6 +316,23 @@ namespace FlaxEditor.Surface.ContextMenu } } + /// + /// Callback when selected by the visject CM + /// + public void OnSelect() + { + Group.ContextMenu.SetDescriptionPanelArchetype(_archetype); + } + + private string GetTooltip() + { + StringBuilder sb = new StringBuilder(); + sb.Append(string.IsNullOrEmpty(_archetype.Signature) ? _archetype.Title : _archetype.Signature); + if (!string.IsNullOrEmpty(_archetype.Description)) + sb.Append("\n" + _archetype.Description); + return sb.ToString(); + } + /// public override bool OnMouseDown(Float2 location, MouseButton button) { @@ -338,6 +356,14 @@ namespace FlaxEditor.Surface.ContextMenu return base.OnMouseUp(location, button); } + /// + public override void OnMouseEnter(Float2 location) + { + Group.ContextMenu.SetDescriptionPanelArchetype(_archetype); + + base.OnMouseEnter(location); + } + /// public override void OnMouseLeave() { diff --git a/Source/Editor/Surface/NodeArchetype.cs b/Source/Editor/Surface/NodeArchetype.cs index d0e3f2291..609b7b5f7 100644 --- a/Source/Editor/Surface/NodeArchetype.cs +++ b/Source/Editor/Surface/NodeArchetype.cs @@ -90,10 +90,15 @@ namespace FlaxEditor.Surface public delegate SurfaceNode CreateCustomNodeFunc(uint id, VisjectSurfaceContext context, NodeArchetype nodeArch, GroupArchetype groupArch); /// - /// Checks if the given type is compatible with the given node archetype. Used for custom nodes + /// Checks if the given type is compatible with the given node archetype. Used for custom nodes. /// public delegate bool IsCompatible(NodeArchetype nodeArch, ScriptType portType, ConnectionsHint hint, VisjectSurfaceContext context); + /// + /// Gets description of inputs and outputs of the archetype. Used for special cases for the description panel. + /// + public delegate void GetElementsDescriptionFunc(NodeArchetype nodeArch, out (string Name, ScriptType Type)[] inputs, out (string Name, ScriptType Type)[] outputs); + /// /// Unique node type ID within a single group. /// @@ -134,11 +139,21 @@ namespace FlaxEditor.Surface /// public string SubTitle; + /// + /// Node signature for tooltip and description purposes + /// + public string Signature; + /// /// Short node description. /// public string Description; + /// + /// Custom function to get descriptions of input and output elements. Used for description panel (optional). + /// + public GetElementsDescriptionFunc GetInputOutputDescription; + /// /// Alternative node titles. /// @@ -210,7 +225,9 @@ namespace FlaxEditor.Surface Flags = Flags, Title = Title, SubTitle = SubTitle, + Signature = Signature, Description = Description, + GetInputOutputDescription = GetInputOutputDescription, AlternativeTitles = (string[])AlternativeTitles?.Clone(), Tag = Tag, SortScore = SortScore, diff --git a/Source/Editor/Surface/SurfaceNode.cs b/Source/Editor/Surface/SurfaceNode.cs index 0eb739d72..10f079579 100644 --- a/Source/Editor/Surface/SurfaceNode.cs +++ b/Source/Editor/Surface/SurfaceNode.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; +using System.Text; using FlaxEditor.Scripting; using FlaxEditor.Surface.Elements; using FlaxEditor.Surface.Undo; @@ -124,7 +125,7 @@ namespace FlaxEditor.Surface Archetype = nodeArch; GroupArchetype = groupArch; AutoFocus = false; - TooltipText = nodeArch.Description; + TooltipText = GetTooltip(); CullChildren = false; BackgroundColor = Style.Current.BackgroundNormal; @@ -851,6 +852,15 @@ namespace FlaxEditor.Surface } } + private string GetTooltip() + { + StringBuilder sb = new StringBuilder(); + sb.Append(string.IsNullOrEmpty(Archetype.Signature) ? Archetype.Title : Archetype.Signature); + if (!string.IsNullOrEmpty(Archetype.Description)) + sb.Append("\n" + Archetype.Description); + return sb.ToString(); + } + /// protected override bool ShowTooltip => base.ShowTooltip && _headerRect.Contains(ref _mousePosition) && !Surface.IsLeftMouseButtonDown && !Surface.IsRightMouseButtonDown && !Surface.IsPrimaryMenuOpened; diff --git a/Source/Editor/Surface/SurfaceUtils.cs b/Source/Editor/Surface/SurfaceUtils.cs index 9bec6c179..ad77aa510 100644 --- a/Source/Editor/Surface/SurfaceUtils.cs +++ b/Source/Editor/Surface/SurfaceUtils.cs @@ -440,12 +440,23 @@ namespace FlaxEditor.Surface return sb.ToString(); } - internal static string GetVisualScriptMemberInfoDescription(ScriptMemberInfo member) + internal static string GetVisualScriptMemberInfoSignature(ScriptMemberInfo member) { var name = member.Name; var declaringType = member.DeclaringType; var valueType = member.ValueType; + // Getter/setter method of the property - we can return early here + if (member.IsMethod && (name.StartsWith("get_") || name.StartsWith("set_"))) + { + var flags = member.IsStatic ? BindingFlags.Public | BindingFlags.Static | BindingFlags.DeclaredOnly : BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly; + var property = declaringType.GetMembers(name.Substring(4), MemberTypes.Property, flags); + if (property != null && property.Length != 0) + { + return GetVisualScriptMemberInfoSignature(property[0]); + } + } + var sb = new StringBuilder(); if (member.IsStatic) sb.Append("static "); @@ -456,32 +467,19 @@ namespace FlaxEditor.Surface sb.Append(declaringType.Name); sb.Append('.'); sb.Append(name); + if (member.IsMethod) { - // Getter/setter method of the property - if (name.StartsWith("get_") || name.StartsWith("set_")) + sb.Append('('); + var parameters = member.GetParameters(); + for (int i = 0; i < parameters.Length; i++) { - var flags = member.IsStatic ? BindingFlags.Public | BindingFlags.Static | BindingFlags.DeclaredOnly : BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly; - var property = declaringType.GetMembers(name.Substring(4), MemberTypes.Property, flags); - if (property != null && property.Length != 0) - { - return GetVisualScriptMemberInfoDescription(property[0]); - } - } - // Method - else - { - sb.Append('('); - var parameters = member.GetParameters(); - for (int i = 0; i < parameters.Length; i++) - { - if (i != 0) - sb.Append(", "); - ref var param = ref parameters[i]; - param.ToString(sb); - } - sb.Append(')'); + if (i != 0) + sb.Append(", "); + ref var param = ref parameters[i]; + param.ToString(sb); } + sb.Append(')'); } else if (member.IsProperty) { @@ -495,8 +493,35 @@ namespace FlaxEditor.Surface sb.Append('}'); } + return sb.ToString(); + } + + internal static string GetVisualScriptMemberShortDescription(ScriptMemberInfo member) + { + var name = member.Name; + var declaringType = member.DeclaringType; + + // Getter/setter method of the property - we can return early here + if (member.IsMethod && (name.StartsWith("get_") || name.StartsWith("set_"))) + { + var flags = member.IsStatic ? BindingFlags.Public | BindingFlags.Static | BindingFlags.DeclaredOnly : BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly; + var property = declaringType.GetMembers(name.Substring(4), MemberTypes.Property, flags); + if (property != null && property.Length != 0) + { + return GetVisualScriptMemberShortDescription(property[0]); + } + } + + return Editor.Instance.CodeDocs.GetTooltip(member); + } + + internal static string GetVisualScriptMemberInfoDescription(ScriptMemberInfo member) + { + var signature = GetVisualScriptMemberInfoSignature(member); + var sb = new StringBuilder(signature); + // Tooltip - var tooltip = Editor.Instance.CodeDocs.GetTooltip(member); + var tooltip = GetVisualScriptMemberShortDescription(member); if (!string.IsNullOrEmpty(tooltip)) sb.Append("\n").Append(tooltip); diff --git a/Source/Editor/Surface/VisjectSurface.ContextMenu.cs b/Source/Editor/Surface/VisjectSurface.ContextMenu.cs index 7f284420d..604394769 100644 --- a/Source/Editor/Surface/VisjectSurface.ContextMenu.cs +++ b/Source/Editor/Surface/VisjectSurface.ContextMenu.cs @@ -237,11 +237,13 @@ namespace FlaxEditor.Surface return new VisjectCM(new VisjectCM.InitInfo { CanSetParameters = CanSetParameters, + UseDescriptionPanel = UseContextMenuDescriptionPanel, Groups = NodeArchetypes, CanSpawnNode = CanUseNodeType, ParametersGetter = () => Parameters, CustomNodesGroup = GetCustomNodes(), ParameterGetNodeArchetype = GetParameterGetterNodeArchetype(out _), + Style = Style, }); } diff --git a/Source/Editor/Surface/VisjectSurface.cs b/Source/Editor/Surface/VisjectSurface.cs index 78277dbe2..b3869f304 100644 --- a/Source/Editor/Surface/VisjectSurface.cs +++ b/Source/Editor/Surface/VisjectSurface.cs @@ -535,6 +535,11 @@ namespace FlaxEditor.Surface /// public virtual bool CanSetParameters => false; + /// + /// True of the context menu should make use of a description panel drawn at the bottom of the menu + /// + public virtual bool UseContextMenuDescriptionPanel => false; + /// /// Gets a value indicating whether surface supports/allows live previewing graph modifications due to value sliders and color pickers. True by default but disabled for shader surfaces that generate and compile shader source at flight. /// diff --git a/Source/Editor/Surface/VisjectSurfaceContext.Serialization.cs b/Source/Editor/Surface/VisjectSurfaceContext.Serialization.cs index 09147efac..7ddbc72db 100644 --- a/Source/Editor/Surface/VisjectSurfaceContext.Serialization.cs +++ b/Source/Editor/Surface/VisjectSurfaceContext.Serialization.cs @@ -51,6 +51,7 @@ namespace FlaxEditor.Surface { TypeID = originalNodeId, Title = "Missing Node :(", + Signature = "Missing Node :(", Description = ":(", Flags = NodeFlags.AllGraphs, Size = new Float2(200, 70), diff --git a/Source/Editor/Surface/VisualScriptSurface.cs b/Source/Editor/Surface/VisualScriptSurface.cs index ee0a3c6d6..e17453b0a 100644 --- a/Source/Editor/Surface/VisualScriptSurface.cs +++ b/Source/Editor/Surface/VisualScriptSurface.cs @@ -174,6 +174,9 @@ namespace FlaxEditor.Surface /// public override bool CanSetParameters => true; + /// + public override bool UseContextMenuDescriptionPanel => true; + /// public override bool CanUseNodeType(GroupArchetype groupArchetype, NodeArchetype nodeArchetype) { @@ -235,10 +238,12 @@ namespace FlaxEditor.Surface var node = (NodeArchetype)Archetypes.Function.Nodes[2].Clone(); node.Flags &= ~NodeFlags.NoSpawnViaGUI; - node.Description = Editor.Instance.CodeDocs.GetTooltip(member); + node.Signature = SurfaceUtils.GetVisualScriptMemberInfoSignature(member); + node.Description = SurfaceUtils.GetVisualScriptMemberShortDescription(member); node.DefaultValues[0] = name; node.DefaultValues[1] = parameters.Length; node.Title = "Override " + name; + node.Tag = member; nodes.Add(node); } } @@ -276,6 +281,7 @@ namespace FlaxEditor.Surface node.DefaultValues[0] = Activator.CreateInstance(scriptType.Type); node.Flags &= ~NodeFlags.NoSpawnViaGUI; node.Title = scriptTypeName; + node.Signature = scriptTypeName; node.Description = Editor.Instance.CodeDocs.GetTooltip(scriptType); // Create group archetype @@ -326,6 +332,7 @@ namespace FlaxEditor.Surface node.DefaultValues[0] = scriptTypeTypeName; node.Flags &= ~NodeFlags.NoSpawnViaGUI; node.Title = "Pack " + scriptTypeName; + node.Signature = "Pack " + scriptTypeName; node.Description = tooltip; ((IList)group.Archetypes).Add(node); @@ -334,6 +341,7 @@ namespace FlaxEditor.Surface node.DefaultValues[0] = scriptTypeTypeName; node.Flags &= ~NodeFlags.NoSpawnViaGUI; node.Title = "Unpack " + scriptTypeName; + node.Signature = "Unpack " + scriptTypeName; node.Description = tooltip; ((IList)group.Archetypes).Add(node); } @@ -367,7 +375,8 @@ namespace FlaxEditor.Surface node.DefaultValues[2] = parameters.Length; node.Flags &= ~NodeFlags.NoSpawnViaGUI; node.Title = SurfaceUtils.GetMethodDisplayName((string)node.DefaultValues[1]); - node.Description = SurfaceUtils.GetVisualScriptMemberInfoDescription(member); + node.Signature = SurfaceUtils.GetVisualScriptMemberInfoSignature(member); + node.Description = SurfaceUtils.GetVisualScriptMemberShortDescription(member); node.SubTitle = string.Format(" (in {0})", scriptTypeName); node.Tag = member; @@ -415,8 +424,10 @@ namespace FlaxEditor.Surface node.DefaultValues[3] = member.IsStatic; node.Flags &= ~NodeFlags.NoSpawnViaGUI; node.Title = "Get " + name; - node.Description = SurfaceUtils.GetVisualScriptMemberInfoDescription(member); + node.Signature = SurfaceUtils.GetVisualScriptMemberInfoSignature(member); + node.Description = SurfaceUtils.GetVisualScriptMemberShortDescription(member); node.SubTitle = string.Format(" (in {0})", scriptTypeName); + node.Tag = member; // Create group archetype var groupKey = new KeyValuePair(scriptTypeName, 16); @@ -449,8 +460,10 @@ namespace FlaxEditor.Surface node.DefaultValues[3] = member.IsStatic; node.Flags &= ~NodeFlags.NoSpawnViaGUI; node.Title = "Set " + name; - node.Description = SurfaceUtils.GetVisualScriptMemberInfoDescription(member); + node.Signature = SurfaceUtils.GetVisualScriptMemberInfoSignature(member); + node.Description = SurfaceUtils.GetVisualScriptMemberShortDescription(member); node.SubTitle = string.Format(" (in {0})", scriptTypeName); + node.Tag = member; // Create group archetype var groupKey = new KeyValuePair(scriptTypeName, 16); @@ -507,8 +520,10 @@ namespace FlaxEditor.Surface bindNode.DefaultValues[1] = name; bindNode.Flags &= ~NodeFlags.NoSpawnViaGUI; bindNode.Title = "Bind " + name; - bindNode.Description = SurfaceUtils.GetVisualScriptMemberInfoDescription(member); + bindNode.Signature = SurfaceUtils.GetVisualScriptMemberInfoSignature(member); + bindNode.Description = SurfaceUtils.GetVisualScriptMemberShortDescription(member); bindNode.SubTitle = string.Format(" (in {0})", scriptTypeName); + bindNode.Tag = member; ((IList)group.Archetypes).Add(bindNode); // Add Unbind event node @@ -517,8 +532,10 @@ namespace FlaxEditor.Surface unbindNode.DefaultValues[1] = name; unbindNode.Flags &= ~NodeFlags.NoSpawnViaGUI; unbindNode.Title = "Unbind " + name; + unbindNode.Signature = bindNode.Signature; unbindNode.Description = bindNode.Description; unbindNode.SubTitle = bindNode.SubTitle; + unbindNode.Tag = member; ((IList)group.Archetypes).Add(unbindNode); #if DEBUG_EVENTS_SEARCHING