using UnityEngine; using UnityEditor; using System; using System.Collections.Generic; using System.IO; using System.Reflection; using UnityEngine.UIElements; using UnityEditor.UIElements; namespace Unity.Cinemachine.Editor { /// /// Collection of tools and helpers for drawing inspectors /// [InitializeOnLoad] static class InspectorUtility { /// /// Callback that happens whenever something undoable happens, either with /// objects or with selection. This is a good way to track user activity. /// public static EditorApplication.CallbackFunction UserDidSomething; static InspectorUtility() { ObjectChangeEvents.changesPublished -= OnUserDidSomethingStream; ObjectChangeEvents.changesPublished += OnUserDidSomethingStream; Selection.selectionChanged -= OnUserDidSomething; Selection.selectionChanged += OnUserDidSomething; static void OnUserDidSomething() => UserDidSomething?.Invoke(); static void OnUserDidSomethingStream(ref ObjectChangeEventStream stream) => UserDidSomething?.Invoke(); } #if !CINEMACHINE_NO_CM2_SUPPORT /// Put multiple properties on a single inspector line, with /// optional label overrides. Passing null as a label (or sublabel) override will /// cause the property's displayName to be used as a label. For no label at all, /// pass GUIContent.none. /// Rect in which to draw /// Main label /// Properties to place on the line /// Sublabels for the properties public static void MultiPropertyOnLine( Rect rect, GUIContent label, SerializedProperty[] props, GUIContent[] subLabels) { if (props == null || props.Length == 0) return; const int hSpace = 2; int indentLevel = EditorGUI.indentLevel; float labelWidth = EditorGUIUtility.labelWidth; float totalSubLabelWidth = 0; int numBoolColumns = 0; List actualLabels = new List(); for (int i = 0; i < props.Length; ++i) { GUIContent sublabel = new GUIContent(props[i].displayName, props[i].tooltip); if (subLabels != null && subLabels.Length > i && subLabels[i] != null) sublabel = subLabels[i]; actualLabels.Add(sublabel); totalSubLabelWidth += GUI.skin.label.CalcSize(sublabel).x; if (i > 0) totalSubLabelWidth += hSpace; // Special handling for toggles, or it looks stupid if (props[i].propertyType == SerializedPropertyType.Boolean) { totalSubLabelWidth += rect.height + hSpace; ++numBoolColumns; } } float subFieldWidth = rect.width - labelWidth - totalSubLabelWidth; float numCols = props.Length - numBoolColumns; float colWidth = numCols == 0 ? 0 : subFieldWidth / numCols; // Main label. If no first sublabel, then main label must take on that // role, for mouse dragging value-scrolling support int subfieldStartIndex = 0; if (label == null) label = new GUIContent(props[0].displayName, props[0].tooltip); if (actualLabels[0] != GUIContent.none) rect = EditorGUI.PrefixLabel(rect, label); else { rect.width = labelWidth + colWidth; EditorGUI.PropertyField(rect, props[0], label); rect.x += rect.width + hSpace; subfieldStartIndex = 1; } for (int i = subfieldStartIndex; i < props.Length; ++i) { EditorGUI.indentLevel = 0; EditorGUIUtility.labelWidth = GUI.skin.label.CalcSize(actualLabels[i]).x; if (props[i].propertyType == SerializedPropertyType.Boolean) { rect.x += hSpace; rect.width = EditorGUIUtility.labelWidth + rect.height; EditorGUI.BeginProperty(rect, actualLabels[i], props[i]); props[i].boolValue = EditorGUI.ToggleLeft(rect, actualLabels[i], props[i].boolValue); } else { rect.width = EditorGUIUtility.labelWidth + colWidth; EditorGUI.BeginProperty(rect, actualLabels[i], props[i]); EditorGUI.PropertyField(rect, props[i], actualLabels[i]); } EditorGUI.EndProperty(); rect.x += rect.width + hSpace; } EditorGUIUtility.labelWidth = labelWidth; EditorGUI.indentLevel = indentLevel; } public static float PropertyHeightOfChidren(SerializedProperty property) { float height = 0; var childProperty = property.Copy(); var endProperty = childProperty.GetEndProperty(); childProperty.NextVisible(true); while (!SerializedProperty.EqualContents(childProperty, endProperty)) { height += EditorGUI.GetPropertyHeight(childProperty) + EditorGUIUtility.standardVerticalSpacing; childProperty.NextVisible(false); } return height - EditorGUIUtility.standardVerticalSpacing; } public static void DrawChildProperties(Rect position, SerializedProperty property) { var childProperty = property.Copy(); var endProperty = childProperty.GetEndProperty(); childProperty.NextVisible(true); while (!SerializedProperty.EqualContents(childProperty, endProperty)) { position.height = EditorGUI.GetPropertyHeight(childProperty); EditorGUI.PropertyField(position, childProperty, true); position.y += position.height + EditorGUIUtility.standardVerticalSpacing; childProperty.NextVisible(false); } } public static void HelpBoxWithButton( string message, MessageType messageType, GUIContent buttonContent, Action onClicked) { float lineHeight = EditorGUIUtility.singleLineHeight; var buttonSize = GUI.skin.label.CalcSize(buttonContent); buttonSize.x += lineHeight; var rect = EditorGUI.IndentedRect(EditorGUILayout.GetControlRect(false, 2)); var boxContent = new GUIContent(message + "\n"); // to make room for the button var boxWidth = rect.width; var boxHeight = GUI.skin.GetStyle("helpbox").CalcHeight(boxContent, rect.width - 3 * lineHeight) + buttonSize.y; rect = EditorGUILayout.GetControlRect(false, boxHeight); rect = EditorGUI.IndentedRect(rect); rect.width = boxWidth; rect.height = boxHeight; EditorGUI.HelpBox(rect, boxContent.text, messageType); rect.x += rect.width - buttonSize.x - 6; rect.width = buttonSize.x; rect.y += rect.height - buttonSize.y - 6; rect.height = buttonSize.y; if (GUI.Button(rect, buttonContent, EditorStyles.miniButton)) onClicked(); } public static float EnabledFoldoutHeight(SerializedProperty property, string enabledPropertyName) { var enabledProp = property.FindPropertyRelative(enabledPropertyName); if (enabledProp == null) return EditorGUI.GetPropertyHeight(property); if (!enabledProp.boolValue) return EditorGUIUtility.singleLineHeight; return PropertyHeightOfChidren(property); } public static bool EnabledFoldout( Rect rect, SerializedProperty property, string enabledPropertyName, GUIContent label = null) { var enabledProp = property.FindPropertyRelative(enabledPropertyName); if (enabledProp == null) { EditorGUI.PropertyField(rect, property, true); rect.x += EditorGUIUtility.labelWidth; EditorGUI.LabelField(rect, new GUIContent($"unknown field `{enabledPropertyName}`")); return property.isExpanded; } rect.height = EditorGUIUtility.singleLineHeight; label ??= new GUIContent(property.displayName, enabledProp.tooltip); EditorGUI.PropertyField(rect, enabledProp, label); if (enabledProp.boolValue) { ++EditorGUI.indentLevel; var childProperty = property.Copy(); var endProperty = childProperty.GetEndProperty(); childProperty.NextVisible(true); while (!SerializedProperty.EqualContents(childProperty, endProperty)) { if (!SerializedProperty.EqualContents(childProperty, enabledProp)) { rect.y += rect.height + EditorGUIUtility.standardVerticalSpacing; rect.height = EditorGUI.GetPropertyHeight(childProperty); EditorGUI.PropertyField(rect, childProperty, true); } childProperty.NextVisible(false); } --EditorGUI.indentLevel; } return enabledProp.boolValue; } public static bool EnabledFoldoutSingleLine( Rect rect, SerializedProperty property, string enabledPropertyName, string disabledToggleLabel, GUIContent label = null) { var enabledProp = property.FindPropertyRelative(enabledPropertyName); if (enabledProp == null) { EditorGUI.PropertyField(rect, property, true); rect.x += EditorGUIUtility.labelWidth; EditorGUI.LabelField(rect, new GUIContent($"unknown field `{enabledPropertyName}`")); return property.isExpanded; } rect.height = EditorGUIUtility.singleLineHeight; label ??= new GUIContent(property.displayName, enabledProp.tooltip); EditorGUI.PropertyField(rect, enabledProp, label); if (!enabledProp.boolValue) { if (!string.IsNullOrEmpty(disabledToggleLabel)) { var w = EditorGUIUtility.labelWidth + EditorGUIUtility.singleLineHeight + 3; var r = rect; r.x += w; r.width -= w; var oldColor = GUI.color; GUI.color = new (oldColor.r, oldColor.g, oldColor.g, 0.5f); EditorGUI.LabelField(r, disabledToggleLabel); GUI.color = oldColor; } } else { rect.width -= EditorGUIUtility.labelWidth + EditorGUIUtility.singleLineHeight; rect.x += EditorGUIUtility.labelWidth + EditorGUIUtility.singleLineHeight; var childProperty = property.Copy(); var endProperty = childProperty.GetEndProperty(); childProperty.NextVisible(true); while (!SerializedProperty.EqualContents(childProperty, endProperty)) { if (!SerializedProperty.EqualContents(childProperty, enabledProp)) { var oldWidth = EditorGUIUtility.labelWidth; EditorGUIUtility.labelWidth = 6; // for dragging EditorGUI.PropertyField(rect, childProperty, new GUIContent(" ")); EditorGUIUtility.labelWidth = oldWidth; break; // Draw only the first property } childProperty.NextVisible(false); } } return enabledProp.boolValue; } #endif /// /// Add to a list all assets of a given type found in a given location /// /// The asset type to look for /// The list to add found assets to /// The location in which to look. Path is relative to package root. public static void AddAssetsFromPackageSubDirectory( Type type, List assets, string path) { try { path = CinemachineCore.kPackageRoot + "/" + path; var info = new DirectoryInfo(path); path += "/"; var fileInfo = info.GetFiles(); for (int i = 0; i < fileInfo.Length; ++i) { var file = fileInfo[i]; if (file.Extension != ".asset") continue; var name = path + file.Name; var a = AssetDatabase.LoadAssetAtPath(name, type) as ScriptableObject; if (a != null) assets.Add(a); } } catch { } } /// /// Normalize a curve so that each of X and Y axes ranges from 0 to 1 /// /// Curve to normalize /// The normalized curve public static AnimationCurve NormalizeCurve(AnimationCurve curve) { return RuntimeUtility.NormalizeCurve(curve, true, true); } /// /// Remove the "Cinemachine" prefix, then call the standard Unity Nicify. /// /// The name to nicify /// The nicified name public static string NicifyClassName(string name) { if (name.StartsWith("Cinemachine")) name = name.Substring(11); // Trim the prefix return ObjectNames.NicifyVariableName(name); } /// /// Remove the "Cinemachine" prefix, then call the standard Unity Nicify, /// and add (Deprecated) to types with Obsolete attributes. /// /// The type to nicify as a string /// The nicified name public static string NicifyClassName(Type type) { var name = type.Name; if (name.StartsWith("Cinemachine")) name = name.Substring(11); // Trim the prefix name = ObjectNames.NicifyVariableName(name); if (type.GetCustomAttribute() != null) name += " (Deprecated)"; return name; } // Temporarily here /// /// Creates a new GameObject. /// /// Name to give the object. /// Optional components to add. /// The GameObject that was created. [Obsolete("Use ObjectFactory.CreateGameObject(string name, params Type[] types) instead.")] public static GameObject CreateGameObject(string name, params Type[] types) { return ObjectFactory.CreateGameObject(name, types); } private static int m_lastRepaintFrame; /// /// Force a repaint of the Game View /// /// Like it says public static void RepaintGameView(UnityEngine.Object unused = null) { if (m_lastRepaintFrame == Time.frameCount) return; m_lastRepaintFrame = Time.frameCount; EditorApplication.QueuePlayerLoopUpdate(); UnityEditorInternal.InternalEditorUtility.RepaintAllViews(); } static Dictionary s_AssignableTypes = new Dictionary(); public const string s_NoneString = "(none)"; public static string GetAssignableBehaviourNames(Type inputType) { if (inputType == null) return "(none)"; if (!s_AssignableTypes.ContainsKey(inputType)) { var allSources = ReflectionHelpers.GetTypesInAllDependentAssemblies( (Type t) => inputType.IsAssignableFrom(t) && !t.IsAbstract && typeof(MonoBehaviour).IsAssignableFrom(t) && t.GetCustomAttribute() == null); var s = string.Empty; var iter = allSources.GetEnumerator(); while (iter.MoveNext()) { var sep = (s.Length == 0) ? string.Empty : ", "; s += sep + iter.Current.Name; } if (s.Length == 0) s = s_NoneString; s_AssignableTypes[inputType] = s; } return s_AssignableTypes[inputType]; } ///============================================================================================== ///============================================================================================== /// UI Elements utilities ///============================================================================================== ///============================================================================================== /// Aligns fields created by UI toolkit the unity inspector standard way. public static string kAlignFieldClass => BaseField.alignedFieldUssClassName; // this is a hack to get around some vertical alignment issues in UITK public static float SingleLineHeight => EditorGUIUtility.singleLineHeight - EditorGUIUtility.standardVerticalSpacing; /// /// Convenience extension for UserDidSomething callbacks, making it easier to use lambdas. /// Cleans itself up when the owner is undisplayed. Works in inspectors and PropertyDrawers. /// public static void TrackAnyUserActivity( this VisualElement owner, EditorApplication.CallbackFunction callback) { owner.RegisterCallback(_ => { UserDidSomething += callback; owner.OnInitialGeometry(callback); owner.RegisterCallback(_ => UserDidSomething -= callback); }); } /// /// Convenience extension for EditorApplication.update callbacks, making it easier to use lambdas. /// Cleans itself up when the owner is undisplayed. Works in inspectors and PropertyDrawers. /// public static void ContinuousUpdate( this VisualElement owner, EditorApplication.CallbackFunction callback) { owner.RegisterCallback(_ => { owner.OnInitialGeometry(callback); EditorApplication.update += callback; owner.RegisterCallback(_ => EditorApplication.update -= callback); }); } /// /// Convenience extension to get a callback after initial geometry creation, making it easier to use lambdas. /// Callback will only be called once. Works in inspectors and PropertyDrawers. /// public static void OnInitialGeometry( this VisualElement owner, EditorApplication.CallbackFunction callback) { owner.RegisterCallback(OnGeometryChanged); void OnGeometryChanged(GeometryChangedEvent _) { owner.UnregisterCallback(OnGeometryChanged); // call only once callback(); } } /// /// Convenience extension to track a property value change plus an initial callback at creation time. /// This simplifies logic for the caller, allowing use of lambda callback. /// public static void TrackPropertyWithInitialCallback( this VisualElement owner, SerializedProperty property, Action callback) { owner.OnInitialGeometry(() => callback(property)); owner.TrackPropertyValue(property, callback); } /// Control the visibility of a widget /// The widget /// Whether it should be visible public static void SetVisible(this VisualElement e, bool show) => e.style.display = show ? StyleKeyword.Null : DisplayStyle.None; /// Is the widgte visible? /// The widget /// True if visible public static bool IsVisible(this VisualElement e) => e.style.display != DisplayStyle.None; /// Convenience method: calls e.Add(child) and returns child./// public static T AddChild(this VisualElement e, T child) where T : VisualElement { e.Add(child); return child; } /// /// Tries to set isDelayed of a FloatField, IntField, or TextField child, if it exists. /// /// Parent widget /// name of child (or null) public static void SafeSetIsDelayed(this VisualElement e, string name = null) { var f = e.Q(name); if (f != null) f.isDelayed = true; var i = e.Q(name); if (i != null) i.isDelayed = true; var t = e.Q(name); if (t != null) t.isDelayed = true; } /// /// Draw a bold header in the inspector - hack to get around missing UITK functionality /// /// Container in which to put the header /// The text of the header /// optional tooltip for the header public static void AddHeader(this VisualElement ux, string text, string tooltip = "") { var verticalPad = SingleLineHeight / 2; var row = ux.AddChild(new LabeledRow($"{text}", tooltip, new VisualElement { style = { flexBasis = 0} })); row.focusable = false; row.Label.style.flexGrow = 1; row.Label.style.paddingTop = verticalPad; row.Label.style.paddingBottom = EditorGUIUtility.standardVerticalSpacing; } /// /// Create a space between inspector sections /// /// Container in which to add the space public static void AddSpace(this VisualElement ux) { ux.Add(new VisualElement { style = { height = SingleLineHeight / 2 }}); } /// /// Add a property dragger to a float or int label, so that dragging it changes the property value. /// public static void AddPropertyDragger(this Label label, SerializedProperty p, VisualElement field) { if (p.propertyType == SerializedPropertyType.Float || p.propertyType == SerializedPropertyType.Integer) { label.AddToClassList("unity-base-field__label--with-dragger"); label.OnInitialGeometry(() => { if (p.propertyType == SerializedPropertyType.Float) new FieldMouseDragger(field.Q()).SetDragZone(label); else if (p.propertyType == SerializedPropertyType.Integer) new FieldMouseDragger(field.Q()).SetDragZone(label); }); } } /// A small warning sybmol, suitable for embedding in an inspector row /// The tooltip text /// The little picture: error, warning, or info public static Label MiniHelpIcon(string tooltip, HelpBoxMessageType iconType = HelpBoxMessageType.Warning) { string icon = iconType switch { HelpBoxMessageType.Warning => "console.warnicon.sml", HelpBoxMessageType.Error => "console.erroricon.sml", _ => "console.infoicon.sml", }; return new Label { tooltip = tooltip, style = { flexGrow = 0, flexBasis = SingleLineHeight, backgroundImage = (StyleBackground)EditorGUIUtility.IconContent(icon).image, width = SingleLineHeight, height = SingleLineHeight, alignSelf = Align.Center } }; } /// A small popup context menu, suitable for embedding in an inspector row /// The tooltip text /// The context menu to show when the button is pressed public static Button MiniPopupButton(string tooltip = null, ContextualMenuManipulator contextMenu = null) { var button = new Button { tooltip = tooltip, style = { flexGrow = 0, flexBasis = SingleLineHeight, backgroundImage = (StyleBackground)EditorGUIUtility.IconContent("_Popup").image, width = SingleLineHeight, height = SingleLineHeight, alignSelf = Align.Center, paddingRight = 0, borderRightWidth = 0, marginRight = 0 }}; if (contextMenu != null) { contextMenu.activators.Clear(); contextMenu.activators.Add(new ManipulatorActivationFilter { button = MouseButton.LeftMouse }); button.AddManipulator(contextMenu); } return button; } /// A small dropdown context menu, suitable for embedding in an inspector row /// The tooltip text /// The context menu to show when the button is pressed public static Button MiniDropdownButton(string tooltip = null, ContextualMenuManipulator contextMenu = null) { var button = new Button { tooltip = tooltip, style = { flexGrow = 0, flexBasis = SingleLineHeight, backgroundImage = (StyleBackground)EditorGUIUtility.IconContent("dropdown").image, width = SingleLineHeight, height = SingleLineHeight, alignSelf = Align.Center, paddingRight = 0, borderRightWidth = 0, marginRight = 0 }}; if (contextMenu != null) { contextMenu.activators.Clear(); contextMenu.activators.Add(new ManipulatorActivationFilter { button = MouseButton.LeftMouse }); button.AddManipulator(contextMenu); } return button; } /// /// This is a hack to get proper layout within th inspector. /// There seems to be no sanctioned way to get the current inspector label width. /// This creates a row with a properly-sized label in front of it. /// public class LabeledRow : BaseField // bool is just a dummy because it has to be something { public Label Label => labelElement; public VisualElement Contents { get; } public LabeledRow(string label, string tooltip = "") : this (label, tooltip, new VisualElement()) { style.flexDirection = FlexDirection.Row; style.flexGrow = 1; Contents.style.flexDirection = FlexDirection.Row; Contents.style.flexGrow = 1; } public LabeledRow(string label, string tooltip, VisualElement contents) : base(label, contents) { Contents = contents; AddToClassList(alignedFieldUssClassName); this.tooltip = tooltip; Label.tooltip = tooltip; } } /// /// This is an inspector container with 2 side-by-side rows. The Left row's width is /// locked to the inspector field label size, for proper alignment. /// public class LeftRightRow : VisualElement { public VisualElement Left; public VisualElement Right; /// /// Set this to offset the Left/Right division from the inspector's Label/Content line /// public float DivisionOffset = 0; /// /// Set this to zero the left margin, useful for foldouts that control the margin themselves. /// public bool KillLeftMargin; public LeftRightRow() { // This is to peek at the resolved label width var hack = AddChild(this, new LabeledRow(" ") { style = { height = 1, marginTop = -2 }}); var row = AddChild(this, new VisualElement { style = { flexDirection = FlexDirection.Row }}); Left = row.AddChild(new VisualElement { style = { flexDirection = FlexDirection.Row, flexGrow = 0 }}); Right = row.AddChild(new VisualElement { style = { flexDirection = FlexDirection.Row, flexGrow = 1 }}); hack.Label.RegisterCallback((_) => { if (KillLeftMargin) hack.style.marginLeft = 0; Left.style.width = hack.Label.resolvedStyle.width + DivisionOffset; row.style.marginLeft = hack.resolvedStyle.marginLeft; }); } } /// A foldout that displays an overlay in the right-hand column when closed. /// The overlay can optionally have a label of its own (use with caution). public class FoldoutWithOverlay : VisualElement { public readonly Foldout OpenFoldout; public readonly Foldout ClosedFoldout; public readonly VisualElement Overlay; public readonly Label OverlayLabel; public FoldoutWithOverlay(Foldout foldout, VisualElement overlay, Label overlayLabel) { OpenFoldout = foldout; Overlay = overlay; OverlayLabel = overlayLabel; Add(foldout); // There are 2 modes for this element: foldout closed and foldout open. // When closed, we cheat the layout system, and to implement this we do a switcheroo var closedContainer = AddChild(this, new LeftRightRow() { KillLeftMargin = true, style = { flexGrow = 1 }}); var closedFoldout = new Foldout { text = foldout.text, tooltip = foldout.tooltip, value = false }; ClosedFoldout = closedFoldout; ClosedFoldout = closedContainer.Left.AddChild(ClosedFoldout); if (overlayLabel != null) closedContainer.Right.Add(overlayLabel); closedContainer.Right.Add(overlay); // Outdent the label if (overlayLabel != null) closedContainer.Right.OnInitialGeometry(() => closedContainer.Right.style.marginLeft = -overlayLabel.resolvedStyle.width); // Swap the open and closed foldouts when the foldout is opened or closed foldout.SetVisible(foldout.value); closedFoldout.RegisterValueChangedCallback((evt) => { if (evt.target == closedFoldout) { if (evt.newValue && evt.target == closedFoldout) { closedContainer.SetVisible(false); foldout.SetVisible(true); foldout.value = true; closedFoldout.SetValueWithoutNotify(false); //foldout.Focus(); // GML why doesn't this work? } evt.StopPropagation(); } }); closedContainer.SetVisible(!foldout.value); foldout.RegisterValueChangedCallback((evt) => { if (evt.target == foldout) { if (!evt.newValue) { closedContainer.SetVisible(true); foldout.SetVisible(false); closedFoldout.SetValueWithoutNotify(false); foldout.value = false; //closedFoldout.Focus(); // GML why doesn't this work? } evt.StopPropagation(); } }); } } /// /// A property field with a minimally-sized label that does not respect inspector sizing. /// Suitable for embedding in a row within the right-hand side of the inspector. /// public class CompactPropertyField : VisualElement { public Label Label; public PropertyField Field; public CompactPropertyField(SerializedProperty property) : this(property, property.displayName) {} public CompactPropertyField(SerializedProperty property, string label, float minLabelWidth = 0) { style.flexDirection = FlexDirection.Row; if (!string.IsNullOrEmpty(label)) Label = AddChild(this, new Label(label) { tooltip = property?.tooltip, style = { alignSelf = Align.Center, minWidth = minLabelWidth }}); Field = AddChild(this, new PropertyField(property, "") { style = { flexGrow = 1, flexBasis = 10 } }); if (Label != null) AddPropertyDragger(Label, property, Field); } } /// /// A row containing a property field. Suitable for adding widgets nest to the property field. /// public static LabeledRow PropertyRow( SerializedProperty property, out PropertyField propertyField, string label = null) { var row = new LabeledRow(label ?? property.displayName, property.tooltip); var field = propertyField = row.Contents.AddChild(new PropertyField(property, "") { style = { flexGrow = 1, flexBasis = SingleLineHeight * 5 }}); AddPropertyDragger(row.Label, property, propertyField); // Kill any left margin that gets inserted into the property field field.OnInitialGeometry(() => { var children = field.Children().GetEnumerator(); if (children.MoveNext()) children.Current.style.marginLeft = 0; children.Dispose(); }); return row; } public static VisualElement HelpBoxWithButton( string message, HelpBoxMessageType messageType, string buttonText, Action onClicked, ContextualMenuManipulator contextMenu = null) { var box = new VisualElement { style = { flexDirection = FlexDirection.Column, paddingTop = 8, paddingBottom = 8, paddingLeft = 8, paddingRight = 8 }}; box.AddToClassList("unity-help-box"); var row = box.AddChild(new VisualElement { style = { flexDirection = FlexDirection.Row, flexGrow = 1 }}); var icon = row.AddChild(MiniHelpIcon("", messageType)); icon.style.alignSelf = Align.Auto; icon.style.marginRight = 6; var text = row.AddChild(new Label(message) { style = { flexGrow = 1, flexBasis = 100, alignSelf = Align.Center, whiteSpace = WhiteSpace.Normal }}); var buttons = box.AddChild(new VisualElement { style = { flexDirection = FlexDirection.Row, flexGrow = 1, marginTop = 6 }}); buttons.Add(new VisualElement { style = { flexGrow = 1 }}); var button = buttons.AddChild(new Button(onClicked) { text = buttonText }); if (contextMenu != null) { contextMenu.activators.Clear(); contextMenu.activators.Add(new ManipulatorActivationFilter { button = MouseButton.LeftMouse }); button.AddManipulator(contextMenu); } return box; } public static void AddRemainingProperties(VisualElement ux, SerializedProperty property) { if (property != null) { var p = property.Copy(); do { if (p.name != "m_Script") ux.Add(new PropertyField(p)); } while (p.NextVisible(false)); } } } }