Files
FlaxEngine/Source/Editor/Surface/ContextMenu/VisjectCM.cs
xxSeys1 3a1dde0081 rename VisualScriptingDescriptionPanel to NodeDescriptionPanel
The description panel no longer only shows in the visual scripting editor but also in the material editor.
There is one editor setting that shows/ hides both so I changed the name to something more general.
2024-09-11 22:19:44 +02:00

1105 lines
43 KiB
C#

// Copyright (c) 2012-2024 Wojciech Figat. All rights reserved.
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;
namespace FlaxEditor.Surface.ContextMenu
{
/// <summary>
/// The Visject Surface dedicated context menu for nodes spawning.
/// </summary>
/// <seealso cref="ContextMenuBase" />
[HideInEditor]
public class VisjectCM : ContextMenuBase
{
/// <summary>
/// Visject context menu item clicked delegate.
/// </summary>
/// <param name="clickedItem">The item that was clicked</param>
/// <param name="selectedBox">The currently user-selected box. Can be null.</param>
public delegate void ItemClickedDelegate(VisjectCMItem clickedItem, Elements.Box selectedBox);
/// <summary>
/// Visject Surface node archetype spawn ability checking delegate.
/// </summary>
/// <param name="groupArch">The nodes group archetype to check.</param>
/// <param name="arch">The node archetype to check.</param>
/// <returns>True if can use this node to spawn it on a surface, otherwise false..</returns>
public delegate bool NodeSpawnCheckDelegate(GroupArchetype groupArch, NodeArchetype arch);
/// <summary>
/// Visject Surface parameters getter delegate.
/// </summary>
/// <returns>TThe list of surface parameters or null if failed (readonly).</returns>
public delegate List<SurfaceParameter> ParameterGetterDelegate();
private const float DefaultWidth = 300;
private const float DefaultHeight = 400;
private readonly List<VisjectCMGroup> _groups = new List<VisjectCMGroup>(16);
private CheckBox _contextSensitiveToggle;
private bool _contextSensitiveSearchEnabled = true;
private readonly TextBox _searchBox;
private bool _waitingForInput;
private VisjectCMGroup _surfaceParametersGroup;
private Panel _panel1;
private VerticalPanel _groupsPanel;
private readonly ParameterGetterDelegate _parametersGetter;
private Elements.Box _selectedBox;
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;
/// <summary>
/// The selected item
/// </summary>
public VisjectCMItem SelectedItem
{
get => _selectedItem;
private set
{
_selectedItem = value;
_selectedItem?.OnSelect();
}
}
/// <summary>
/// Event fired when any item in this popup menu gets clicked.
/// </summary>
public event ItemClickedDelegate ItemClicked;
/// <summary>
/// Gets or sets a value indicating whether show groups expanded or collapsed.
/// </summary>
public bool ShowExpanded { get; set; }
/// <summary>
/// Gets the groups (read-only).
/// </summary>
public IList<VisjectCMGroup> Groups => _groups;
/// <summary>
/// The surface context menu initialization parameters.
/// </summary>
public struct InitInfo
{
/// <summary>
/// True if surface parameters are not read-only and can be modified via setter node.
/// </summary>
public bool CanSetParameters;
/// <summary>
/// True if the surface should make use of a description panel drawn at the bottom of the context menu
/// </summary>
public bool UseDescriptionPanel;
/// <summary>
/// The groups archetypes. Cannot be null.
/// </summary>
public List<GroupArchetype> Groups;
/// <summary>
/// The custom callback to handle node types validation for spawning. Cannot be null.
/// </summary>
public NodeSpawnCheckDelegate CanSpawnNode;
/// <summary>
/// The surface parameters getter. Can be null.
/// </summary>
public ParameterGetterDelegate ParametersGetter;
/// <summary>
/// The group with custom nodes group. Can be null.
/// </summary>
public GroupArchetype CustomNodesGroup;
/// <summary>
/// The parameter getter node archetype to spawn when adding the parameter getter. Can be null.
/// </summary>
public NodeArchetype ParameterGetNodeArchetype;
/// <summary>
/// The parameter setter node archetype to spawn when adding the parameter getter. Can be null.
/// </summary>
public NodeArchetype ParameterSetNodeArchetype;
/// <summary>
/// The surface style to use to draw images in the description panel
/// </summary>
public SurfaceStyle Style;
}
/// <summary>
/// Initializes a new instance of the <see cref="VisjectCM"/> class.
/// </summary>
/// <param name="info">The initialization info data.</param>
public VisjectCM(InitInfo info)
{
if (info.Groups == null)
throw new ArgumentNullException(nameof(info.Groups));
if (info.CanSpawnNode == null)
throw new ArgumentNullException(nameof(info.CanSpawnNode));
_parametersGetter = info.ParametersGetter;
_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(_useDescriptionPanel ? DefaultWidth + 50f : DefaultWidth, DefaultHeight);
var headerPanel = new Panel(ScrollBars.None)
{
Parent = this,
Height = 20,
Width = Width - 4,
X = 2,
Y = 1,
};
// Title bar
var titleFontReference = new FontReference(Style.Current.FontLarge.Asset, 10);
var titleLabel = new Label
{
Width = Width * 0.5f - 8f,
Height = 20,
X = 4,
Parent = headerPanel,
Text = "Select Node",
HorizontalAlignment = TextAlignment.Near,
Font = titleFontReference,
};
// Context sensitive toggle
var contextSensitiveLabel = new Label
{
Width = Width * 0.5f - 28,
Height = 20,
X = Width * 0.5f,
Parent = headerPanel,
Text = "Context Sensitive",
TooltipText = "Should the nodes be filtered to only show those that can be connected in the current context?",
HorizontalAlignment = TextAlignment.Far,
Font = titleFontReference,
};
_contextSensitiveToggle = new CheckBox
{
Width = 20,
Height = 20,
X = Width - 24,
Parent = headerPanel,
Checked = _contextSensitiveSearchEnabled,
};
_contextSensitiveToggle.StateChanged += OnContextSensitiveToggleStateChanged;
// Search box
_searchBox = new SearchBox(false, 2, 22)
{
Width = Width - 4,
Parent = this
};
_searchBox.TextChanged += OnSearchFilterChanged;
// Create first panel (for scrollbar)
var panel1 = new Panel(ScrollBars.Vertical)
{
Bounds = new Rectangle(0, _searchBox.Bottom + 1, Width, Height - _searchBox.Bottom - 2),
Parent = this
};
_panel1 = panel1;
// Create second panel (for groups arrangement)
var panel2 = new VerticalPanel
{
Parent = panel1,
AnchorPreset = AnchorPresets.HorizontalStretchTop,
IsScrollable = true,
};
_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<NodeArchetype>();
foreach (var groupArchetype in info.Groups)
{
// Get valid nodes
nodes.Clear();
foreach (var nodeArchetype in groupArchetype.Archetypes)
{
if ((nodeArchetype.Flags & NodeFlags.NoSpawnViaGUI) == 0 && info.CanSpawnNode(groupArchetype, nodeArchetype))
{
nodes.Add(nodeArchetype);
}
}
// Check if can create group for them
if (nodes.Count > 0)
{
var group = CreateGroup(groupArchetype);
group.Close(false);
for (int i = 0; i < nodes.Count; i++)
{
var item = new VisjectCMItem(group, groupArchetype, nodes[i])
{
Parent = group
};
}
group.SortChildren();
group.Parent = panel2;
_groups.Add(group);
}
}
// Add custom nodes (special handling)
if (info.CustomNodesGroup?.Archetypes != null)
{
foreach (var nodeArchetype in info.CustomNodesGroup.Archetypes)
{
if ((nodeArchetype.Flags & NodeFlags.NoSpawnViaGUI) != 0)
continue;
var groupName = Archetypes.Custom.GetNodeGroup(nodeArchetype);
// Find group to reuse
VisjectCMGroup group = null;
for (int j = 0; j < _groups.Count; j++)
{
if (string.Equals(_groups[j].Name, groupName, StringComparison.OrdinalIgnoreCase))
{
group = _groups[j];
break;
}
}
// Create new group if name is unique
if (group == null)
{
group = CreateGroup(info.CustomNodesGroup, true, groupName);
group.Close(false);
group.Parent = _groupsPanel;
_groups.Add(group);
}
// Add new item
var item = new VisjectCMItem(group, info.CustomNodesGroup, nodeArchetype)
{
Parent = group
};
// Order items
group.SortChildren();
}
}
}
/// <summary>
/// Adds the group archetype to add to the menu.
/// </summary>
/// <param name="groupArchetype">The group.</param>
/// <param name="withGroupMerge">True if merge group items into any existing group of the same name.</param>
public void AddGroup(GroupArchetype groupArchetype, bool withGroupMerge = true)
{
// Check if can create group for them to be spawned via GUI
if (groupArchetype.Archetypes.Any(x => (x.Flags & NodeFlags.NoSpawnViaGUI) == 0))
{
Profiler.BeginEvent("VisjectCM.AddGroup");
var group = CreateGroup(groupArchetype, withGroupMerge);
group.Close(false);
foreach (var nodeArchetype in groupArchetype.Archetypes)
{
var item = new VisjectCMItem(group, groupArchetype, nodeArchetype)
{
Parent = group
};
}
group.SortChildren();
group.Parent = _groupsPanel;
_groups.Add(group);
if (!IsLayoutLocked)
{
group.UnlockChildrenRecursive();
if (_contextSensitiveSearchEnabled && _selectedBox != null)
UpdateFilters();
else
SortGroups();
if (ShowExpanded)
group.Open(false);
group.PerformLayout();
if (_searchBox.TextLength != 0)
{
OnSearchFilterChanged();
}
}
else if (_contextSensitiveSearchEnabled)
{
group.EvaluateVisibilityWithBox(_selectedBox);
}
Profiler.EndEvent();
}
}
/// <summary>
/// Adds the group archetypes to add to the menu.
/// </summary>
/// <param name="groupArchetypes">The groups.</param>
/// <param name="withGroupMerge">True if merge group items into any existing group of the same name.</param>
public void AddGroups(IEnumerable<GroupArchetype> groupArchetypes, bool withGroupMerge = true)
{
// Check if can create group for them to be spawned via GUI
if (groupArchetypes.Any(g => g.Archetypes.Any(x => (x.Flags & NodeFlags.NoSpawnViaGUI) == 0)))
{
Profiler.BeginEvent("VisjectCM.AddGroups");
var isLayoutLocked = IsLayoutLocked;
LockChildrenRecursive();
Profiler.BeginEvent("Create Groups");
var groups = new List<VisjectCMGroup>();
foreach (var groupArchetype in groupArchetypes)
{
var group = CreateGroup(groupArchetype, withGroupMerge);
group.Close(false);
foreach (var nodeArchetype in groupArchetype.Archetypes)
{
var item = new VisjectCMItem(group, groupArchetype, nodeArchetype)
{
Parent = group
};
}
if (_contextSensitiveSearchEnabled)
group.EvaluateVisibilityWithBox(_selectedBox);
group.SortChildren();
if (ShowExpanded)
group.Open(false);
group.Parent = _groupsPanel;
_groups.Add(group);
groups.Add(group);
}
Profiler.EndEvent();
if (!isLayoutLocked)
{
if (_contextSensitiveSearchEnabled && _selectedBox != null)
UpdateFilters();
else
SortGroups();
Profiler.BeginEvent("Perform Layout");
UnlockChildrenRecursive();
foreach (var group in groups)
group.PerformLayout();
PerformLayout();
Profiler.EndEvent();
if (_searchBox.TextLength != 0)
{
OnSearchFilterChanged();
}
}
Profiler.EndEvent();
}
}
/// <summary>
/// Removes the group archetype to from to the menu.
/// </summary>
/// <param name="groupArchetype">The group.</param>
public void RemoveGroup(GroupArchetype groupArchetype)
{
for (int i = 0; i < _groups.Count; i++)
{
var group = _groups[i];
if (group.Archetypes.Remove(groupArchetype))
{
Profiler.BeginEvent("VisjectCM.RemoveGroup");
if (group.Archetypes.Count == 0)
{
_groups.RemoveAt(i);
group.Dispose();
}
else
{
var children = group.Children.ToArray();
foreach (var child in children)
{
if (child is VisjectCMItem item && item.GroupArchetype == groupArchetype)
item.Dispose();
}
}
Profiler.EndEvent();
break;
}
}
}
/// <summary>
/// Removes the group to from to the menu.
/// </summary>
/// <param name="group">The group.</param>
public void RemoveGroup(VisjectCMGroup group)
{
Profiler.BeginEvent("VisjectCM.RemoveGroup");
group.Dispose();
_groups.Remove(group);
Profiler.EndEvent();
}
private VisjectCMGroup CreateGroup(GroupArchetype groupArchetype, bool withGroupMerge = true, string name = null)
{
if (name == null)
name = groupArchetype.Name;
if (withGroupMerge)
{
for (int i = 0; i < _groups.Count; i++)
{
if (string.Equals(_groups[i].HeaderText, name, StringComparison.Ordinal))
return _groups[i];
}
}
return new VisjectCMGroup(this, groupArchetype)
{
HeaderText = name
};
}
private void OnSearchFilterChanged()
{
// Skip events during setup or init stuff
if (IsLayoutLocked)
return;
Profiler.BeginEvent("VisjectCM.OnSearchFilterChanged");
UpdateFilters();
_searchBox.Focus();
Profiler.EndEvent();
}
private void OnContextSensitiveToggleStateChanged(CheckBox checkBox)
{
// Skip events during setup or init stuff
if (IsLayoutLocked)
return;
Profiler.BeginEvent("VisjectCM.OnContextSensitiveToggleStateChanged");
_contextSensitiveSearchEnabled = checkBox.Checked;
UpdateFilters();
Profiler.EndEvent();
}
private void UpdateFilters()
{
if (string.IsNullOrEmpty(_searchBox.Text) && _selectedBox == null)
{
ResetView();
Profiler.EndEvent();
return;
}
// Update groups
LockChildrenRecursive();
var contextSensitiveSelectedBox = _contextSensitiveSearchEnabled ? _selectedBox : null;
for (int i = 0; i < _groups.Count; i++)
{
_groups[i].UpdateFilter(_searchBox.Text, contextSensitiveSelectedBox);
_groups[i].UpdateItemSort(contextSensitiveSelectedBox);
}
SortGroups();
UnlockChildrenRecursive();
// If no item is selected (or it's not visible anymore), select the top one
Profiler.BeginEvent("VisjectCM.Layout");
SelectedItem = _groups.Find(g => g.Visible)?.Children.Find(c => c.Visible && c is VisjectCMItem) as VisjectCMItem;
PerformLayout();
if (SelectedItem != null)
_panel1.ScrollViewTo(SelectedItem);
Profiler.EndEvent();
}
/// <summary>
/// Sort the groups and keeps <see cref="_groups"/> in sync
/// </summary>
private void SortGroups()
{
Profiler.BeginEvent("VisjectCM.SortGroups");
// Sort groups
_groupsPanel.SortChildren();
// Synchronize with _groups[]
for (int i = 0, groupsIndex = 0; i < _groupsPanel.ChildrenCount; i++)
{
if (_groupsPanel.Children[i] is VisjectCMGroup group)
{
_groups[groupsIndex] = group;
groupsIndex++;
}
}
Profiler.EndEvent();
}
/// <summary>
/// Called when user clicks on an item.
/// </summary>
/// <param name="item">The item.</param>
public void OnClickItem(VisjectCMItem item)
{
Hide();
ItemClicked?.Invoke(item, _selectedBox);
}
/// <summary>
/// Expands all the groups.
/// </summary>
/// <param name="animate">Enable/disable animation feature.</param>
public void ExpandAll(bool animate = false)
{
for (int i = 0; i < _groups.Count; i++)
_groups[i].Open(animate);
}
/// <summary>
/// Resets the view.
/// </summary>
public void ResetView()
{
Profiler.BeginEvent("VisjectCM.ResetView");
LockChildrenRecursive();
_searchBox.Clear();
SelectedItem = null;
for (int i = 0; i < _groups.Count; i++)
{
_groups[i].ResetView();
if (_contextSensitiveSearchEnabled)
_groups[i].EvaluateVisibilityWithBox(_selectedBox);
}
UnlockChildrenRecursive();
if (_contextSensitiveSearchEnabled && _selectedBox != null)
UpdateFilters();
else
SortGroups();
PerformLayout();
Profiler.EndEvent();
}
/// <summary>
/// Updates the surface parameters group.
/// </summary>
private void UpdateSurfaceParametersGroup()
{
Profiler.BeginEvent("VisjectCM.UpdateSurfaceParametersGroup");
// Remove the old one
if (_surfaceParametersGroup != null)
{
_groups.Remove(_surfaceParametersGroup);
_surfaceParametersGroup.Dispose();
_surfaceParametersGroup = null;
}
// Check if surface has any parameters
var parameters = _parametersGetter?.Invoke();
int count = parameters?.Count ?? 0;
if (count > 0)
{
// TODO: cache the allocated memory to reduce dynamic allocations
if (_parameterSetNodeArchetype != null)
count *= 2;
var archetypes = new NodeArchetype[count];
int archetypeIndex = 0;
var groupArchetype = new GroupArchetype
{
GroupID = 6,
Name = "Surface Parameters",
Color = new Color(52, 73, 94),
Archetypes = archetypes
};
var group = CreateGroup(groupArchetype, false);
group.ArrowImageOpened = new SpriteBrush(Style.Current.ArrowDown);
group.ArrowImageClosed = new SpriteBrush(Style.Current.ArrowRight);
group.Close(false);
// ReSharper disable once PossibleNullReferenceException
for (int i = 0; i < parameters.Count; i++)
{
var param = parameters[i];
// Define Getter node and create CM item
var node = (NodeArchetype)_parameterGetNodeArchetype.Clone();
node.Title = "Get " + param.Name;
node.DefaultValues[0] = param.ID;
archetypes[archetypeIndex++] = node;
var item = new VisjectCMItem(group, groupArchetype, node)
{
Parent = group
};
// Define Setter node and create CM item if parameter has a setter
if (_parameterSetNodeArchetype != null)
{
node = (NodeArchetype)_parameterSetNodeArchetype.Clone();
node.Title = "Set " + param.Name;
node.DefaultValues[0] = param.ID;
node.DefaultValues[1] = TypeUtils.GetDefaultValue(param.Type);
archetypes[archetypeIndex++] = node;
item = new VisjectCMItem(group, groupArchetype, node)
{
Parent = group
};
}
}
group.SortChildren();
group.UnlockChildrenRecursive();
group.Parent = _groupsPanel;
_groups.Add(group);
_surfaceParametersGroup = group;
}
Profiler.EndEvent();
}
/// <inheritdoc />
public override void Show(Control parent, Float2 location)
{
Show(parent, location, null);
}
/// <summary>
/// Show context menu over given control.
/// </summary>
/// <param name="parent">Parent control to attach to it.</param>
/// <param name="location">Popup menu origin location in parent control coordinates.</param>
/// <param name="startBox">The currently selected box that the new node will get connected to. Can be null</param>
public void Show(Control parent, Float2 location, Elements.Box startBox)
{
_selectedBox = startBox;
base.Show(parent, location);
}
/// <inheritdoc />
protected override void OnShow()
{
// Prepare
UpdateSurfaceParametersGroup();
ResetView();
_panel1.VScrollBar.TargetValue = 0;
Focus();
_waitingForInput = true;
base.OnShow();
}
/// <inheritdoc />
public override void Hide()
{
Focus(null);
if (_useDescriptionPanel)
HideDescriptionPanel();
base.Hide();
}
/// <summary>
/// Updates the description panel and shows information about the set archetype
/// </summary>
/// <param name="archetype">The node archetype</param>
public void SetDescriptionPanelArchetype(NodeArchetype archetype)
{
if (!_useDescriptionPanel)
return;
if (archetype == null || !Editor.Instance.Options.Options.Interface.NodeDescriptionPanel)
{
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 if (archetype.Elements != null) // Skip if no Elements (ex: Comment node)
{
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;
}
/// <summary>
/// Hides the description panel and resets the context menu to its original size
/// </summary>
private void HideDescriptionPanel()
{
if (!_descriptionPanelVisible)
return;
_descriptionInputPanel.RemoveChildren();
_descriptionOutputPanel.RemoveChildren();
Height = DefaultHeight;
UpdateWindowSize();
_descriptionPanelVisible = false;
}
/// <inheritdoc />
public override bool OnKeyDown(KeyboardKeys key)
{
if (key == KeyboardKeys.Escape)
{
Hide();
return true;
}
else if (key == KeyboardKeys.Return || key == KeyboardKeys.Tab)
{
if (SelectedItem != null)
OnClickItem(SelectedItem);
else
Hide();
return true;
}
else if (key == KeyboardKeys.ArrowUp)
{
if (SelectedItem == null)
return true;
var previousSelectedItem = GetPreviousSiblings<VisjectCMItem>(SelectedItem).FirstOrDefault(c => c.Visible) ??
(GetPreviousSiblings<VisjectCMGroup>(SelectedItem.Group).FirstOrDefault(c => c.Visible)?.Children
.FindLast(c => c.Visible && c is VisjectCMItem) as VisjectCMItem);
if (previousSelectedItem != null)
{
SelectedItem = previousSelectedItem;
// Scroll into view (without smoothing)
_panel1.ScrollViewTo(SelectedItem, true);
}
return true;
}
else if (key == KeyboardKeys.ArrowDown)
{
if (SelectedItem == null)
return true;
var nextSelectedItem = GetNextSiblings<VisjectCMItem>(SelectedItem).FirstOrDefault(c => c.Visible) ??
(GetNextSiblings<VisjectCMGroup>(SelectedItem.Group).FirstOrDefault(c => c.Visible)?.Children
.OfType<VisjectCMItem>().FirstOrDefault(c => c.Visible));
if (nextSelectedItem != null)
{
SelectedItem = nextSelectedItem;
_panel1.ScrollViewTo(SelectedItem);
}
return true;
}
if (_waitingForInput)
{
_waitingForInput = false;
_searchBox.Focus();
return _searchBox.OnKeyDown(key);
}
return base.OnKeyDown(key);
}
/// <summary>
/// Gets the next siblings of a control.
/// </summary>
/// <param name="item">A control that is attached to a parent</param>
/// <returns>An <see cref="IEnumerable{Control}"/> with the siblings that come after the current one.</returns>
private IEnumerable<Control> GetNextSiblings(Control item)
{
if (item?.Parent == null)
yield break;
var parent = item.Parent;
for (int i = item.IndexInParent + 1; i < parent.ChildrenCount; i++)
{
yield return parent.GetChild(i);
}
}
/// <summary>
/// Gets the next siblings of a control that have a specific type.
/// </summary>
/// <typeparam name="T">The type that the controls should have.</typeparam>
/// <param name="item">A control that is attached to a parent</param>
/// <returns>An <see cref="IEnumerable{T}"/> with the siblings that come after the current one.</returns>
private IEnumerable<T> GetNextSiblings<T>(Control item) where T : Control
{
return GetNextSiblings(item).OfType<T>();
}
/// <summary>
/// Gets the previous siblings of a control.
/// </summary>
/// <param name="item">A control that is attached to a parent</param>
/// <returns>An <see cref="IEnumerable{Control}"/> with the siblings that come before the current one.</returns>
private IEnumerable<Control> GetPreviousSiblings(Control item)
{
if (item?.Parent == null)
yield break;
var parent = item.Parent;
for (int i = item.IndexInParent - 1; i >= 0; i--)
{
yield return parent.GetChild(i);
}
}
/// <summary>
/// Gets the previous sibling of a control that have a specific type.
/// </summary>
/// <typeparam name="T">The type that the controls should have.</typeparam>
/// <param name="item">A control that is attached to a parent</param>
/// <returns>An <see cref="IEnumerable{T}"/> with the siblings that come before the current one.</returns>
private IEnumerable<T> GetPreviousSiblings<T>(Control item) where T : Control
{
return GetPreviousSiblings(item).OfType<T>();
}
/// <inheritdoc />
public override void OnDestroy()
{
_contextSensitiveToggle.StateChanged -= OnContextSensitiveToggleStateChanged;
base.OnDestroy();
}
}
}