Files
sbox-public/game/addons/tools/Code/Scene/GameObjectInspector/GameObjectControlWidget.cs
Matt Stevens 18da35110c Qt Advanced Docking System (#4041)
* Add Qt-Advanced-Docking-System 5.0
* Bind new CDockManager, dock areas
* Use Valve's CSS & images for ADS as a baseline, load stuff from disk instead of embedded in c++
* dockmanager.css in tool styles, auto loads and hotloads
* Rewrite DockManager
* delete toolwindowmanager
* Delete the 50 different ways to create/add/register a dock, you just add a dock, that's all u need
* More dockmanager rejigging
* restore all editor dock layouts and asset browser on the new docking system
* remove last toolwindowmanager remnants, add ADS to third party legal notice
* build classic default layout in code for main editor reset/first-run, add relative area AddDock overload
* relativeTo placement API, restore correct default layouts for all tool windows, register [Dock] types closed instead of eagerly docking
* styling pass
* disable ADS built-in stylesheet load, styling comes from editor stylesheet
* lock picker browser tabs against closing/dragging, make ActionGraph reset layout actually reposition docks
* add DockManager.OpenDock, dedupe layout code in EditorMainWindow/ActionGraph
* CloseDockWidget internal
* add "Close Tabs to the Left/Right" to dock tab context menu
* expose dock MinimumSizeFromContent
* only offer close left/right tab options when open tabs exist on that side
* move "Close Tabs to the Left/Right" out of the ADS lib into a components factory
* guard dock tab context menu against pinned docks with no dock area
* fix crash when pinned tab is destroyed before delayed auto hide timer fires
* recreate scene widget swapchains when Qt recreates their native window on dock reparent
*  wrap all sbox changes to the dock lib in #ifdef SBOX
* save a plain window rect as fallback for when Qt rejects saved geometry on mixed-DPI monitors
* expose DockWidget Float/IsFloating
* always restore window geometry from a plain saved rect instead of Qt's DPI fragile geometry blob
* route all window centering through one screen aware helper that handles mixed DPI monitors
* restore window geometry onto the screen it was saved on
* don't close live docks when a dock type unregisters, hotload substitutes them in place

---------

Co-authored-by: Layla <1667289+aylaylay@users.noreply.github.com>
2026-07-12 12:41:08 +01:00

300 lines
7.3 KiB
C#

using Editor.AssetPickers;
namespace Editor;
[CustomEditor( typeof( GameObject ) )]
public class GameObjectControlWidget : ControlWidget
{
public override bool SupportsMultiEdit => true;
bool IsInList => Parent?.Parent is ListControlWidget;
IconButton PickerButton;
public bool ShowFullName { get; set; } = true;
public GameObjectControlWidget( SerializedProperty property ) : base( property )
{
if ( !property.IsEditable )
ReadOnly = true;
IsDraggable = true;
SetSizeMode( SizeMode.Default, SizeMode.Default );
Layout = Layout.Column();
Layout.Spacing = 2;
var inner = Layout.Add( Layout.Row() );
inner.Margin = 2;
inner.AddStretchCell( 1 );
PickerButton = inner.Add( new IconButton( "colorize", () =>
{
PropertyStartEdit();
EyeDropperTool.SetTargetProperty( property );
EyeDropperTool.OnBackToLastTool = () => PropertyFinishEdit();
}, this ) );
PickerButton.FixedWidth = Theme.RowHeight - 4;
PickerButton.FixedHeight = Theme.RowHeight - 4;
PickerButton.Visible = false;
Cursor = CursorShape.Finger;
AcceptDrops = true;
}
protected override void OnContextMenu( ContextMenuEvent e )
{
var m = new ContextMenu( this );
var go = SerializedProperty.GetValue<GameObject>();
if ( go is PrefabScene prefab )
{
var asset = AssetSystem.FindByPath( prefab.Source.ResourcePath );
m.AddOption( "Open in Editor", "edit", () => asset?.OpenInEditor() ).Enabled = asset != null && !asset.IsProcedural;
m.AddOption( "Find in Asset Browser", "search", () => LocalAssetBrowser.OpenTo( asset, true ) ).Enabled = asset is not null;
m.AddSeparator();
}
else if ( go is not null )
{
m.AddOption( "Find in Scene", "search", () => EditorUtility.FindInScene( go ) );
m.AddSeparator();
}
//m.AddOption( "Copy", "file_copy" );
//m.AddOption( "Paste", "content_paste" );
//m.AddSeparator();
if ( !ReadOnly )
{
m.AddOption( "Clear", "backspace", action: Clear );
}
if ( m.OptionCount > 0 )
{
m.OpenAtCursor( false );
}
e.Accepted = true;
}
protected override void PaintControl()
{
var rect = LocalRect.Shrink( 6, 0 );
var go = SerializedProperty.GetValue<GameObject>();
Paint.SetDefaultFont();
if ( SerializedProperty.IsMultipleDifferentValues )
{
Paint.SetPen( Theme.MultipleValues );
Paint.DrawIcon( rect, "panorama_wide_angle_select", 14, TextFlag.LeftCenter );
rect.Left += 22;
Paint.DrawText( rect, "Multiple Values", TextFlag.LeftCenter );
}
else if ( !go.IsValid() )
{
Paint.SetPen( Theme.TextControl.WithAlpha( 0.3f ) );
Paint.DrawIcon( rect, "radio_button_unchecked", 14, TextFlag.LeftCenter );
rect.Left += 22;
Paint.DrawText( rect, "None (GameObject)", TextFlag.LeftCenter );
}
else if ( go is PrefabScene )
{
Paint.SetPen( Theme.Blue );
Paint.DrawIcon( rect, "panorama_wide_angle_select", 14, TextFlag.LeftCenter );
rect.Left += 22;
Paint.DrawText( rect, go.Name, TextFlag.LeftCenter );
}
else
{
Paint.SetPen( Theme.Green );
Paint.DrawIcon( rect, "panorama_wide_angle_select", 14, TextFlag.LeftCenter );
rect.Left += 22;
Paint.DrawText( rect, BuildName( go ), TextFlag.LeftCenter );
}
if ( EyeDropperTool.TargetProperty == SerializedProperty )
{
Paint.SetPen( ControlHighlightSecondary.WithAlpha( 0.8f ), 2, PenStyle.Dot );
Paint.SetBrush( ControlHighlightSecondary.WithAlpha( 0.2f ) );
Paint.DrawRect( LocalRect.Shrink( 2 ), Theme.ControlRadius );
}
PickerButton.Visible = IsControlHovered;
}
private string BuildName( GameObject go )
{
if ( !ShowFullName )
{
return go.Name;
}
string str = "";
if ( go.Parent.IsValid() && go.Parent is not Scene )
{
str += $"{BuildName( go.Parent )} > ";
}
str += $"{go.Name}";
return str;
}
void UpdateFromAsset( Asset asset )
{
if ( asset.TryLoadResource( out PrefabFile prefabFile ) )
{
PropertyStartEdit();
SerializedProperty.SetValue( SceneUtility.GetPrefabScene( prefabFile ) );
PropertyFinishEdit();
}
}
void UpdateFromGameObject( GameObject go )
{
PropertyStartEdit();
SerializedProperty.SetValue( go );
PropertyFinishEdit();
}
void UpdateFromAssets( Asset[] assets )
{
if ( assets.Length == 0 ) return;
if ( assets[0].TryLoadResource( out PrefabFile prefabFile ) )
{
PropertyStartEdit();
SerializedProperty.SetValue( SceneUtility.GetPrefabScene( prefabFile ) );
PropertyFinishEdit();
}
if ( IsInList )
{
var list = Parent.Parent as ListControlWidget;
for ( int i = 1; i < assets.Length; i++ )
{
if ( assets[i].TryLoadResource( out PrefabFile newPrefabFile ) )
{
list.Collection.Add( SceneUtility.GetPrefabScene( newPrefabFile ) );
}
}
}
}
protected override void OnMouseClick( MouseEvent e )
{
if ( e.LeftMouseButton )
{
e.Accepted = true;
var go = SerializedProperty.GetValue<GameObject>();
if ( ReadOnly ) return;
var resource = (go as PrefabScene)?.Source ?? null;
var asset = resource != null ? AssetSystem.FindByPath( resource.ResourcePath ) : null;
// Open Prefab Asset Picker
var prefabAssetType = AssetType.Find( "prefab", false );
var picker = AssetPicker.Create( null, prefabAssetType, new AssetPicker.PickerOptions()
{
EnableMultiselect = IsInList
} );
picker.Window.Title = $"Select GameObject/Prefab";
picker.OnAssetHighlighted = ( o ) => UpdateFromAsset( o.FirstOrDefault() );
picker.OnAssetPicked = ( o ) => UpdateFromAssets( o );
picker.Show();
var sceneBrowser = new GameObjectSceneBrowser( null );
sceneBrowser.OnGameObjectSelect = ( o ) => UpdateFromGameObject( o );
if ( picker is GenericPicker genericPicker )
{
genericPicker.DockManager.AddDock( "Scene Browser", null, sceneBrowser, DockArea.Center );
sceneBrowser.ConfirmButton = genericPicker.ConfirmButton;
sceneBrowser.OnConfirm = () => picker.Close();
if ( go.IsValid() && go is not PrefabScene )
{
sceneBrowser.Focus();
}
}
picker.SetSelection( asset );
}
}
void Clear()
{
PropertyStartEdit();
SerializedProperty.SetValue<GameObject>( null );
SignalValuesChanged();
PropertyFinishEdit();
}
protected override void OnDragStart()
{
var drag = new Drag( this );
drag.Data.Object = SerializedProperty.GetValue<GameObject>();
drag.Execute();
}
public override void OnDragHover( DragEvent ev )
{
ev.Action = DropAction.Ignore;
if ( ev.Data.OfType<GameObject>().Any() )
{
ev.Action = DropAction.Link;
return;
}
if ( ev.Data.HasFileOrFolder )
{
var asset = AssetSystem.FindByPath( ev.Data.Files.First() );
if ( asset is null ) return;
if ( asset.AssetType.FileExtension != "prefab" ) return;
if ( asset.TryLoadResource( out PrefabFile prefabFile ) )
{
ev.Action = DropAction.Link;
return;
}
}
}
public override void OnDragDrop( DragEvent ev )
{
if ( ev.Data.OfType<GameObject>().FirstOrDefault() is { } go )
{
PropertyStartEdit();
SerializedProperty.SetValue( go );
PropertyFinishEdit();
return;
}
if ( ev.Data.HasFileOrFolder )
{
var asset = AssetSystem.FindByPath( ev.Data.Files.First() );
if ( asset is null ) return;
if ( asset.AssetType.FileExtension != "prefab" ) return;
if ( asset.TryLoadResource( out PrefabFile prefabFile ) )
{
PropertyStartEdit();
SerializedProperty.SetValue( SceneUtility.GetPrefabScene( prefabFile ) );
PropertyFinishEdit();
return;
}
}
}
}