using Sandbox; namespace Editor; public enum ComponentViewMode { Default, Events } /// /// The component sheet that is used to edit a component's properties in the GameObjectInspector /// public partial class ComponentSheet : Widget { public ComponentSheetHeader Header { get; private set; } SerializedObject TargetObject; Layout Content; Guid GameObjectId; internal ComponentViewMode ViewMode; bool draggingComponentAbove; bool draggingComponentBelow; string ExpandedCookieString => $"expand.{GameObjectId}.{TargetObject.TypeName}"; /// /// The user's local preference to having this component expanded or not. /// bool ExpandedCookie { get => ProjectCookie.Get( ExpandedCookieString, true ); set { // Don't bother storing the cookie if it's an expanded component if ( value ) { ProjectCookie.Remove( ExpandedCookieString ); } else { ProjectCookie.Set( ExpandedCookieString, value ); } } } /// /// Is this component currently expanded? /// internal bool Expanded { get; set; } = true; /// /// Expands/shrinks the component in the component list. /// /// internal void SetExpanded( bool expanded ) { Expanded = expanded; RebuildContent(); ExpandedCookie = expanded; } public ComponentSheet( Guid gameObjectId, SerializedObject target ) : base( null ) { GameObjectId = gameObjectId; Name = "ComponentSheet"; TargetObject = target; Layout = Layout.Column(); Layout.Margin = new( 0, 1 ); SetSizeMode( SizeMode.Flexible, SizeMode.CanShrink ); ViewMode = ComponentViewMode.Default; // Check to see if we have a cookie to say if the component isn't expanded Expanded = ExpandedCookie; AcceptDrops = true; Header = new ComponentSheetHeader( TargetObject, this ); Header.BuildUI(); Layout.Add( Header ); Content = Layout.AddColumn(); Content.Margin = new Sandbox.UI.Margin( 0, 0, 0, 0 ); Frame(); } protected override void OnPaint() { base.OnPaint(); if ( !IsBeingDroppedOn ) return; var rect = LocalRect; if ( draggingComponentAbove ) { Paint.SetPen( Theme.Blue.Lighten( 0.2f ), 2f, PenStyle.Dot ); Paint.DrawLine( rect.TopLeft, rect.TopRight ); } if ( draggingComponentBelow ) { Paint.SetPen( Theme.Blue.Lighten( 0.2f ), 2f, PenStyle.Dot ); Paint.DrawLine( rect.BottomLeft, rect.BottomRight ); } } public override void OnDragHover( DragEvent ev ) { base.OnDragHover( ev ); if ( !TryDragComponent( ev, out _, out var moveDelta ) ) { draggingComponentAbove = false; draggingComponentBelow = false; return; } draggingComponentAbove = moveDelta < 0; draggingComponentBelow = moveDelta > 0; } public override void OnDragDrop( DragEvent ev ) { base.OnDragDrop( ev ); if ( !TryDragComponent( ev, out var component, out var moveDelta ) ) return; using var scene = SceneEditorSession.Scope(); using ( SceneEditorSession.Active.UndoScope( "Change Component Order" ).WithGameObjectChanges( component.GameObject, GameObjectUndoFlags.Components ).Push() ) { component.Components.Move( component, moveDelta ); } } /// /// We collect a list of conditional properties (properties that hide or show depending on the value of a property). /// If the value of any of these properties changes, we need to rebuild the content. /// List> hideShowConditions = new(); int lastHash; [EditorEvent.Frame] public void Frame() { var hash = BuildHashCode(); if ( lastHash != hash ) { lastHash = hash; RebuildContent(); } } public int BuildHashCode() { HashCode hc = new HashCode(); hc.Add( TargetObject ); hc.Add( ViewMode ); foreach ( var condition in hideShowConditions ) { hc.Add( condition() ); } return hc.ToHashCode(); } [EditorEvent.Hotload] public void RebuildContent() { using var _ = SuspendUpdates.For( this ); Content.Clear( true ); hideShowConditions.Clear(); BuildInstanceContent(); lastHash = BuildHashCode(); } static int GetOrderValue( SerializedProperty prop ) { if ( prop.TryGetAttribute( out var order ) ) return order.Value; return int.MaxValue; } void BuildInstanceContent() { if ( !Expanded ) return; using var sup = SuspendUpdates.For( this ); if ( ViewMode == ComponentViewMode.Default ) { var componentEditor = ComponentEditorWidget.Create( TargetObject ); if ( componentEditor.IsValid() ) { Content.Add( componentEditor ); Header.ContextMenu += componentEditor.OnHeaderContextMenu; return; } } hideShowConditions.Clear(); HashSet handledGroups = new( StringComparer.OrdinalIgnoreCase ); var cs = new ControlSheet(); cs.IncludePropertyNames = true; Content.Add( cs ); cs.AddObject( TargetObject, FilterProperties ); } bool FilterProperties( SerializedProperty o ) { if ( o.PropertyType is null ) return false; // // We're only going to hide the base OnComponent stuff in the event tab now. // All other events can show inline in the property sheet, like they should. // We want to remove the OnComponent // var hideInEventTab = o.PropertyType.IsAssignableTo( typeof( Delegate ) ) && o.Name.StartsWith( "OnComponent" ); if ( ViewMode == ComponentViewMode.Events && !hideInEventTab ) return false; if ( ViewMode != ComponentViewMode.Events && hideInEventTab ) return false; if ( o.IsMethod ) return true; if ( !o.HasAttribute() ) return false; return true; } bool TryDragComponent( DragEvent ev, out Component component, out int delta ) { delta = 0; component = ev.Data.OfType().FirstOrDefault(); var myComponent = TargetObject.Targets.OfType().FirstOrDefault(); if ( !component.IsValid() || !myComponent.IsValid() || myComponent == component ) { return false; } var componentList = myComponent.Components; var components = componentList.GetAll().ToList(); var myComponentIndex = components.IndexOf( myComponent ); var draggedComponentIndex = components.IndexOf( component ); if ( myComponentIndex == -1 || draggedComponentIndex == -1 ) { return false; } delta = myComponentIndex - draggedComponentIndex; return true; } }