{"TotalCount":1,"Files":[{"Ident":"yugi.propertyfinder","Path":"Editor/PropertyFinderWindow.cs","FileName":"PropertyFinderWindow.cs","PackageType":"library","CodeKind":"Editor","AssetVersionId":381869,"Code":"using System;\nusing Editor;\nusing Sandbox;\nusing System.Collections.Generic;\nusing System.Linq;\n\nnamespace PropertyFinder;\n\n/// \u003Csummary\u003E\n/// Searches the selected objects\u0027 component properties by name and shows only the matches,\n/// editable in place. The stock inspector\u0027s filter is private, so this is its own window:\n/// frameless, opened with CTRL\u002BF, hidden when focus or a click goes elsewhere in the editor.\n/// \u003C/summary\u003E\npublic sealed class PropertyFinderWindow : Widget\n{\n\tprivate const int MaxObjects = 16;\n\tprivate const float Grip = 14;\n\tprivate const string PositionCookie = \u0022PropertyFinder.Position\u0022;\n\tprivate const string SizeCookie = \u0022PropertyFinder.Size\u0022;\n\n\tprivate static PropertyFinderWindow instance;\n\n\t// Frames left in which to (re)claim keyboard focus after opening.\n\tprivate static int pendingFocus;\n\n\tprivate readonly LineEdit search;\n\tprivate readonly Label status;\n\tprivate readonly ScrollArea scroll;\n\tprivate int lastHash;\n\n\t// Hiding on focus loss only arms once focus has actually reached the window, otherwise\n\t// the viewport still holding focus in the frames after CTRL\u002BF would hide it at once.\n\tprivate bool armed;\n\tprivate bool mouseWasDown;\n\n\tprivate bool dragging;\n\tprivate bool resizing;\n\tprivate Vector2 grabOffset;\n\n\t// Stored whole-component, like the stock inspector, so undo survives a later delete.\n\tprivate IDisposable undoScope;\n\n\tprivate PropertyFinderWindow( Widget parent ) : base( parent )\n\t{\n\t\tWindowFlags = WindowFlags.Tool | WindowFlags.FramelessWindowHint;\n\t\tNoSystemBackground = true;\n\t\tTranslucentBackground = true;\n\t\tMinimumSize = new Vector2( 260, 120 );\n\n\t\tLayout = Layout.Column();\n\t\tLayout.Margin = 10;\n\t\tLayout.Spacing = 6;\n\n\t\tsearch = Layout.Add( new LineEdit( this ) );\n\t\tsearch.PlaceholderText = \u0022Search properties on the selection\u2026\u0022;\n\t\tsearch.TextEdited \u002B= _ =\u003E Rebuild();\n\n\t\tstatus = Layout.Add( new Label( \u0022\u0022, this ) );\n\t\t// Let presses fall through so the status line doubles as a drag handle.\n\t\tstatus.TransparentForMouseEvents = true;\n\n\t\tscroll = Layout.Add( new ScrollArea( this ), 1 );\n\t\tscroll.Canvas = new Widget( scroll );\n\t\tscroll.Canvas.Layout = Layout.Column();\n\t\tscroll.Canvas.Layout.Spacing = 4;\n\n\t\tSize = EditorCookie.Get( SizeCookie, new Vector2( 380, 460 ) );\n\t\tvar fallback = EditorWindow.ScreenRect.Center - Size * 0.5f;\n\t\tPosition = EditorCookie.Get( PositionCookie, fallback );\n\n\t\tRebuild();\n\t}\n\n\t// The mesh Edge/Vertex tools also bind CTRL\u002BF (weld UVs) inside the scene view.\n\t[Menu( \u0022Editor\u0022, \u0022Edit/Property Finder\u0022 )]\n\t[Shortcut( \u0022propertyfinder.open\u0022, \u0022CTRL\u002BF\u0022 )]\n\tpublic static void Open()\n\t{\n\t\tif ( instance is null || !instance.IsValid() )\n\t\t\tinstance = new PropertyFinderWindow( EditorWindow );\n\n\t\tinstance.ApplyStyle();\n\t\tinstance.armed = false;\n\t\tinstance.mouseWasDown = true; // ignore the click that may have opened it (menu)\n\t\tinstance.Show();\n\t\tinstance.Raise();\n\t\t// Focusing inside the key press doesn\u0027t stick: activation lands afterwards and takes\n\t\t// focus back. Claim it over the next few frames instead.\n\t\tpendingFocus = 5;\n\t\tinstance.Rebuild();\n\t}\n\n\tprivate void Dismiss()\n\t{\n\t\tdragging = resizing = false;\n\t\tStoreGeometry();\n\t\tHide();\n\t}\n\n\tprivate void StoreGeometry()\n\t{\n\t\tEditorCookie.Set( PositionCookie, Position );\n\t\tEditorCookie.Set( SizeCookie, Size );\n\t}\n\n\t[EditorEvent.Frame]\n\tprivate void Frame()\n\t{\n\t\tif ( !ReferenceEquals( instance, this ) || !Visible ) return;\n\n\t\tif ( pendingFocus \u003E 0 )\n\t\t{\n\t\t\tpendingFocus--;\n\t\t\t// Select only when (re)gaining focus, so text typed during the retries is never selected.\n\t\t\tif ( !search.IsFocused )\n\t\t\t{\n\t\t\t\tsearch.Focus( true );\n\t\t\t\tsearch.SelectAll();\n\t\t\t}\n\t\t}\n\n\t\tvar focus = Editor.Application.FocusWidget;\n\t\tvar focusInside = Contains( focus );\n\t\tif ( focusInside ) armed = true;\n\n\t\t// Focus moved to another part of the editor (inspector, hierarchy, 3D view).\n\t\t// Focus in a separate window (a colour picker or asset picker opened from a row) is ignored.\n\t\tif ( armed \u0026\u0026 focus.IsValid() \u0026\u0026 !focusInside \u0026\u0026 IsInMainWindow( focus ) )\n\t\t{\n\t\t\tDismiss();\n\t\t\treturn;\n\t\t}\n\n\t\t// Clicking the 3D view doesn\u0027t always take keyboard focus, so a fresh press outside\n\t\t// the window counts too, unless one of the editor\u0027s sticky popups is open.\n\t\tvar mouseDown = Editor.Application.MouseButtons != MouseButtons.None;\n\t\tvar pressed = mouseDown \u0026\u0026 !mouseWasDown;\n\t\tmouseWasDown = mouseDown;\n\t\t// A row\u0027s right-click menu can hang past the window edge; clicking it is not \u0022outside\u0022.\n\t\tif ( pressed \u0026\u0026 !ScreenRect.IsInside( Editor.Application.CursorPosition ) \u0026\u0026 StickyPopup.All.Count == 0 \u0026\u0026 !OverOwnPopup() )\n\t\t{\n\t\t\tDismiss();\n\t\t\treturn;\n\t\t}\n\n\t\tExtendContextMenus();\n\n\t\t// Rebuild only when the query, selection or component lists change. Rebuilding every\n\t\t// frame would destroy the control being edited and drop its focus mid-drag.\n\t\tif ( BuildHash() != lastHash ) Rebuild();\n\t}\n\n\t// The window lives for the whole editor session (hidden, never destroyed), so anything set\n\t// only in the constructor misses hot-reloaded changes. Styling is re-applied here instead.\n\tprivate void ApplyStyle()\n\t{\n\t\tsearch.SetStyles( $\u0022background-color: {Theme.ControlBackground.Darken( 0.4f ).Hex};\u0022 );\n\t}\n\n\t[EditorEvent.Hotload]\n\tprivate void Hotload()\n\t{\n\t\tApplyStyle();\n\t\tRebuild();\n\t}\n\n\tprivate bool Contains( Widget widget )\n\t{\n\t\t// Walk parents by hand: popups parented to a row are separate windows, and still ours.\n\t\tfor ( var w = widget; w is not null; w = w.Parent )\n\t\t\tif ( ReferenceEquals( w, this ) ) return true;\n\t\treturn false;\n\t}\n\n\tprivate static bool IsInMainWindow( Widget widget )\n\t{\n\t\tvar root = widget;\n\t\twhile ( root.Parent is not null ) root = root.Parent;\n\t\treturn ReferenceEquals( root, EditorWindow );\n\t}\n\n\tprotected override void OnKeyPress( KeyEvent e )\n\t{\n\t\tif ( e.Key == KeyCode.Escape )\n\t\t{\n\t\t\tDismiss();\n\t\t\te.Accepted = true;\n\t\t\treturn;\n\t\t}\n\t\tbase.OnKeyPress( e );\n\t}\n\n\t// No title bar: drag from the margins or status line, resize from the bottom-right corner.\n\tprivate bool InGrip( Vector2 local ) =\u003E local.x \u003E Width - Grip \u0026\u0026 local.y \u003E Height - Grip;\n\n\t// Controls like dropdowns let their press fall through to this window. Only the bare\n\t// margins and the status line may start a drag, never the search box or results.\n\tprivate bool OnFreeArea( Vector2 local )\n\t{\n\t\tforeach ( var child in new Widget[] { search, scroll } )\n\t\t\tif ( new Rect( child.Position, child.Size ).IsInside( local ) ) return false;\n\t\treturn true;\n\t}\n\n\tprivate static bool LeftHeld =\u003E Editor.Application.MouseButtons.HasFlag( MouseButtons.Left );\n\n\tprotected override void OnMousePress( MouseEvent e )\n\t{\n\t\tif ( !e.LeftMouseButton ) return;\n\t\tresizing = InGrip( e.LocalPosition );\n\t\tdragging = !resizing \u0026\u0026 OnFreeArea( e.LocalPosition );\n\t\tif ( !dragging \u0026\u0026 !resizing ) return;\n\t\tgrabOffset = resizing ? Size - e.LocalPosition : e.LocalPosition;\n\t\te.Accepted = true;\n\t}\n\n\tprotected override void OnMouseMove( MouseEvent e )\n\t{\n\t\t// A popup that grabs the mouse swallows the release; never keep following a button\n\t\t// that is no longer held.\n\t\tif ( (dragging || resizing) \u0026\u0026 !LeftHeld )\n\t\t{\n\t\t\tStoreGeometry();\n\t\t\tdragging = resizing = false;\n\t\t\treturn;\n\t\t}\n\n\t\tif ( resizing )\n\t\t\tSize = new Vector2( MathF.Max( MinimumSize.x, e.LocalPosition.x \u002B grabOffset.x ), MathF.Max( MinimumSize.y, e.LocalPosition.y \u002B grabOffset.y ) );\n\t\telse if ( dragging )\n\t\t\tPosition = e.ScreenPosition - grabOffset;\n\t}\n\n\tprotected override void OnMouseReleased( MouseEvent e )\n\t{\n\t\tif ( dragging || resizing ) StoreGeometry();\n\t\tdragging = resizing = false;\n\t}\n\n\tprotected override void OnPaint()\n\t{\n\t\tPaint.Antialiasing = true;\n\t\tPaint.SetPen( Theme.ControlBackground.Lighten( 0.3f ) );\n\t\tPaint.SetBrush( Theme.WindowBackground );\n\t\tPaint.DrawRect( LocalRect.Shrink( 0.5f ), 6 );\n\n\t\t// Resize grip: two short diagonals in the corner.\n\t\tPaint.SetPen( Theme.Text.WithAlpha( 0.3f ), 1 );\n\t\tvar c = LocalRect.BottomRight - 4;\n\t\tPaint.DrawLine( c - new Vector2( 8, 0 ), c - new Vector2( 0, 8 ) );\n\t\tPaint.DrawLine( c - new Vector2( 4, 0 ), c - new Vector2( 0, 4 ) );\n\t}\n\n\tprivate static GameObject[] Selected()\n\t{\n\t\tvar session = SceneEditorSession.Active;\n\t\tif ( session is null ) return [];\n\t\treturn session.Selection.OfType\u003CGameObject\u003E().Where( go =\u003E go.IsValid() ).Take( MaxObjects ).ToArray();\n\t}\n\n\tprivate int BuildHash()\n\t{\n\t\tvar hc = new HashCode();\n\t\thc.Add( search.Text );\n\t\tforeach ( var go in Selected() )\n\t\t{\n\t\t\thc.Add( go.Id );\n\t\t\tforeach ( var component in go.Components.GetAll() )\n\t\t\t\tif ( component.IsValid() ) hc.Add( component.Id );\n\t\t}\n\t\treturn hc.ToHashCode();\n\t}\n\n\tprivate void Rebuild()\n\t{\n\t\tlastHash = BuildHash();\n\t\tvar canvas = scroll.Canvas;\n\t\tusing var _ = SuspendUpdates.For( canvas );\n\t\tcanvas.Layout.Clear( true );\n\t\trows.Clear();\n\n\t\tvar terms = (search.Text ?? \u0022\u0022).Split( \u0027 \u0027, StringSplitOptions.RemoveEmptyEntries );\n\t\tvar objects = Selected();\n\n\t\tif ( objects.Length == 0 )\n\t\t{\n\t\t\tstatus.Text = \u0022Select an object in the hierarchy.\u0022;\n\t\t\tcanvas.Layout.AddStretchCell();\n\t\t\treturn;\n\t\t}\n\t\tif ( terms.Length == 0 )\n\t\t{\n\t\t\tstatus.Text = \u0022Type part of a property name. Spaces narrow the search.\u0022;\n\t\t\tcanvas.Layout.AddStretchCell();\n\t\t\treturn;\n\t\t}\n\n\t\tvar matches = 0;\n\t\tforeach ( var go in objects )\n\t\t{\n\t\t\tforeach ( var component in go.Components.GetAll() )\n\t\t\t{\n\t\t\t\tif ( !component.IsValid() || component.Flags.HasFlag( ComponentFlags.Hidden ) ) continue;\n\n\t\t\t\tvar so = component.GetSerialized();\n\t\t\t\tvar hits = so.Where( p =\u003E Matches( p, terms ) ).ToArray();\n\t\t\t\tif ( hits.Length == 0 ) continue;\n\t\t\t\tmatches \u002B= hits.Length;\n\n\t\t\t\tvar header = canvas.Layout.Add( new Label( $\u0022{go.Name}  \u203A  {so.TypeTitle}\u0022, canvas ) );\n\t\t\t\theader.SetStyles( \u0022font-weight: 600; padding-top: 6px;\u0022 );\n\n\t\t\t\tso.OnPropertyStartEdit \u002B= p =\u003E StartEdit( p, component );\n\t\t\t\tso.OnPropertyChanged \u002B= p =\u003E Changed( p, component );\n\t\t\t\tso.OnPropertyFinishEdit \u002B= p =\u003E FinishEdit( p, component );\n\n\t\t\t\t// Rows are added one by one instead of via AddObject, which would rebuild [Feature]\n\t\t\t\t// tabs and hide matches behind unselected tabs. Tab and group names become sections.\n\t\t\t\tforeach ( var section in hits.GroupBy( SectionName ) )\n\t\t\t\t{\n\t\t\t\t\tif ( !string.IsNullOrEmpty( section.Key ) )\n\t\t\t\t\t{\n\t\t\t\t\t\tvar sub = canvas.Layout.Add( new Label( section.Key, canvas ) );\n\t\t\t\t\t\tsub.SetStyles( $\u0022color: {Theme.Text.WithAlpha( 0.55f ).Hex}; padding-top: 2px;\u0022 );\n\t\t\t\t\t}\n\n\t\t\t\t\tvar sheet = new ControlSheet();\n\t\t\t\t\tsheet.IncludePropertyNames = true;\n\t\t\t\t\tforeach ( var property in section )\n\t\t\t\t\t{\n\t\t\t\t\t\tvar control = sheet.AddRow( property );\n\t\t\t\t\t\tif ( control.IsValid() ) rows[control] = (component, property.Name);\n\t\t\t\t\t}\n\t\t\t\t\tcanvas.Layout.Add( sheet );\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tstatus.Text = matches == 0\n\t\t\t? \u0022No matching properties.\u0022\n\t\t\t: $\u0022{matches} match{(matches == 1 ? \u0022\u0022 : \u0022es\u0022)}\u0022 \u002B (objects.Length == MaxObjects ? $\u0022 (first {MaxObjects} objects)\u0022 : \u0022\u0022);\n\t\tcanvas.Layout.AddStretchCell();\n\t}\n\n\t// Same visibility rules as the stock ComponentSheet, minus the Advanced toggle:\n\t// finding an advanced property is a reason to search.\n\tprivate static bool Matches( SerializedProperty p, string[] terms )\n\t{\n\t\tif ( p.PropertyType is null ) return false;\n\t\tif ( p.PropertyType.IsAssignableTo( typeof( Delegate ) ) \u0026\u0026 p.Name.StartsWith( \u0022OnComponent\u0022 ) ) return false;\n\t\tif ( !p.IsMethod \u0026\u0026 !p.HasAttribute\u003CPropertyAttribute\u003E() ) return false;\n\n\t\tvar haystack = $\u0022{p.Name} {p.DisplayName} {SectionName( p )}\u0022;\n\t\treturn terms.All( t =\u003E haystack.Contains( t, StringComparison.OrdinalIgnoreCase ) );\n\t}\n\n\t// \u0022Feature tab / Group\u0022, either part optional.\n\tprivate static string SectionName( SerializedProperty p )\n\t{\n\t\tvar feature = p.TryGetAttribute\u003CFeatureAttribute\u003E( out var f ) ? f.Title : null;\n\t\tvar parts = new[] { feature, p.GroupName }.Where( s =\u003E !string.IsNullOrWhiteSpace( s ) ).Distinct();\n\t\treturn string.Join( \u0022 / \u0022, parts );\n\t}\n\n\t// --- \u0022Show in Inspector\u0022 -----------------------------------------------------------------\n\t// The stock row builds its context menu internally with no extension hook, so the option is\n\t// appended to that menu once it has opened: same Copy/Paste/Reset/Jump to code, plus ours.\n\n\tprivate readonly Dictionary\u003CControlWidget, (Component Component, string Property)\u003E rows = new();\n\tprivate readonly HashSet\u003CContextMenu\u003E extendedMenus = new();\n\tprivate int menuScanFrames;\n\tprivate bool rightWasDown;\n\n\tprivate bool OverOwnPopup()\n\t{\n\t\t// Open menus aren\u0027t reachable by walking the widget tree (measured), so ask what\u0027s hovered.\n\t\tfor ( var w = Editor.Application.HoveredWidget; w is not null; w = w.Parent )\n\t\t\tif ( w is Menu ) return true;\n\t\treturn false;\n\t}\n\n\tprivate void ExtendContextMenus()\n\t{\n\t\tvar rightDown = Editor.Application.MouseButtons.HasFlag( MouseButtons.Right );\n\t\tif ( rightDown \u0026\u0026 !rightWasDown )\n\t\t{\n\t\t\t// Remember which row was right-clicked. On Windows the menu opens on RELEASE,\n\t\t\t// so keep looking for it for about a second.\n\t\t\tpressedRow = RowUnder( Editor.Application.HoveredWidget );\n\t\t\tmenuScanFrames = 60;\n\t\t}\n\t\trightWasDown = rightDown;\n\t\tif ( menuScanFrames \u003C= 0 ) return;\n\t\tmenuScanFrames--;\n\n\t\textendedMenus.RemoveWhere( m =\u003E !m.IsValid() );\n\n\t\t// The open menu isn\u0027t reachable by walking the widget tree (measured: 0 found), but\n\t\t// it pops up under the cursor, so the hovered widget leads to it.\n\t\tContextMenu menu = null;\n\t\tfor ( var w = Editor.Application.HoveredWidget; w is not null \u0026\u0026 menu is null; w = w.Parent )\n\t\t\tmenu = w as ContextMenu;\n\n\t\tif ( menu is null || pressedRow is null || extendedMenus.Contains( menu ) ) return;\n\n\t\textendedMenus.Add( menu );\n\t\tmenuScanFrames = 0;\n\t\tvar target = pressedRow.Value;\n\t\tmenu.AddSeparator();\n\t\tmenu.AddOption( \u0022Show in Inspector\u0022, \u0022manage_search\u0022, () =\u003E ShowInInspector( target.Component, target.Property ) );\n\t}\n\n\tprivate (Component Component, string Property)? pressedRow;\n\n\t// Climb from the widget under the cursor to the nearest ancestor holding a tracked control,\n\t// never past the results canvas (which holds every row).\n\tprivate (Component Component, string Property)? RowUnder( Widget w )\n\t{\n\t\tfor ( ; w is not null \u0026\u0026 !ReferenceEquals( w, scroll.Canvas ) \u0026\u0026 !ReferenceEquals( w, this ); w = w.Parent )\n\t\t{\n\t\t\tif ( w is ControlWidget c \u0026\u0026 Lookup( c ) is { } direct ) return direct;\n\t\t\tvar controls = w.GetDescendants\u003CControlWidget\u003E().Select( Lookup ).Where( x =\u003E x is not null ).Distinct().ToArray();\n\t\t\tif ( controls.Length == 1 ) return controls[0];\n\t\t\tif ( controls.Length \u003E 1 ) return null;\n\t\t}\n\t\treturn null;\n\t}\n\n\t// Match by the control\u0027s property, not by widget identity: a widget found by walking the\n\t// tree may be a different managed wrapper than the one AddRow returned.\n\tprivate (Component Component, string Property)? Lookup( ControlWidget control )\n\t{\n\t\tif ( control is null ) return null;\n\t\tif ( rows.TryGetValue( control, out var direct ) ) return direct;\n\t\tvar p = control.SerializedProperty;\n\t\tif ( p is null ) return null;\n\t\tforeach ( var entry in rows.Values )\n\t\t\tif ( Targets( p, entry.Component, entry.Property ) ) return entry;\n\t\treturn null;\n\t}\n\n\tprivate static (Component Component, string Property) reveal;\n\tprivate static int revealFrames;\n\n\tprivate void ShowInInspector( Component component, string property )\n\t{\n\t\tif ( !component.IsValid() ) return;\n\t\tvar session = SceneEditorSession.Resolve( component );\n\t\tsession?.Selection.Set( component.GameObject );\n\t\tDismiss();\n\t\tEditorWindow.DockManager.RaiseDock( \u0022Inspector\u0022 );\n\t\t// The inspector rebuilds for the new selection over the next frames; keep looking.\n\t\treveal = (component, property);\n\t\trevealFrames = 60;\n\t}\n\n\tprivate static bool Targets( SerializedProperty p, Component component, string name )\n\t\t=\u003E p is not null \u0026\u0026 p.Name == name \u0026\u0026 (p.Parent?.Targets?.Contains( component ) ?? false);\n\n\t[EditorEvent.Frame]\n\tprivate static void RevealFrame()\n\t{\n\t\tif ( revealFrames \u003C= 0 ) return;\n\t\trevealFrames--;\n\n\t\tvar (component, name) = reveal;\n\t\tvar inspector = EditorWindow.GetDescendants\u003CInspector\u003E().FirstOrDefault( i =\u003E i.IsValid() \u0026\u0026 i.Visible );\n\t\tif ( !component.IsValid() || inspector is null ) return;\n\n\t\t// A collapsed component builds no rows. Expanding goes through the stock SetExpanded, which\n\t\t// is internal (the header\u0027s own OnExpandChanged is protected), so reflection it is; if a\n\t\t// future editor renames it, the reveal just stops at the component.\n\t\tvar header = inspector.GetDescendants\u003CComponentSheetHeader\u003E().FirstOrDefault( h =\u003E ReferenceEquals( h.GetComponent(), component ) );\n\t\tif ( header is not null \u0026\u0026 !header.IsExpanded )\n\t\t{\n\t\t\tvar sheet = header.Parent as ComponentSheet;\n\t\t\tvar setExpanded = typeof( ComponentSheet ).GetMethod( \u0022SetExpanded\u0022, System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Public );\n\t\t\tif ( sheet is not null \u0026\u0026 setExpanded is not null )\n\t\t\t{\n\t\t\t\theader.IsExpanded = true;\n\t\t\t\tsetExpanded.Invoke( sheet, [true] );\n\t\t\t\theader.Update();\n\t\t\t\treturn; // rows build this frame; look for them next frame\n\t\t\t}\n\t\t}\n\n\t\t// A property on an unselected [Feature] tab has no row yet: select its tab first.\n\t\tforeach ( var tab in inspector.GetDescendants\u003CFeatureTabOption\u003E() )\n\t\t{\n\t\t\tif ( tab.IsSelected || tab.Feature.Properties is null ) continue;\n\t\t\tif ( !tab.Feature.Properties.Any( p =\u003E Targets( p, component, name ) ) ) continue;\n\t\t\tinspector.GetDescendants\u003CFeatureTabWidget\u003E().FirstOrDefault( w =\u003E w.GetDescendants\u003CFeatureTabOption\u003E().Contains( tab ) )?.Select( tab );\n\t\t\treturn; // the page builds this frame; find the row next frame\n\t\t}\n\n\t\tvar control = inspector.GetDescendants\u003CControlWidget\u003E()\n\t\t\t.FirstOrDefault( c =\u003E c.IsValid() \u0026\u0026 c.Visible \u0026\u0026 Targets( c.SerializedProperty, component, name ) );\n\t\tif ( control is null ) return;\n\n\t\trevealFrames = 0;\n\t\tvar row = control.Parent ?? control;\n\t\tfor ( var w = row.Parent; w is not null; w = w.Parent )\n\t\t{\n\t\t\tif ( w is ScrollArea area )\n\t\t\t{\n\t\t\t\tarea.MakeVisible( row );\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\t\t_ = new FlashOverlay( row );\n\t}\n\n\tprivate void StartEdit( SerializedProperty property, Component component )\n\t{\n\t\tvar session = SceneEditorSession.Resolve( component );\n\t\tif ( session is null ) return;\n\t\tusing var scene = session.Scene.Push();\n\t\tundoScope?.Dispose();\n\t\tundoScope = session.UndoScope( $\u0022Edit {property.Name} on {component.GetType().Name}\u0022 ).WithComponentChanges( component ).Push();\n\t\tproperty.DispatchPreEdited();\n\t}\n\n\tprivate static void Changed( SerializedProperty property, Component component )\n\t{\n\t\tusing var scene = component.Scene?.Push();\n\t\tproperty.DispatchEdited();\n\t}\n\n\tprivate void FinishEdit( SerializedProperty property, Component component )\n\t{\n\t\tusing var scene = component.Scene?.Push();\n\t\tproperty.DispatchEdited();\n\t\tundoScope?.Dispose();\n\t\tundoScope = null;\n\t}\n}\n\n/// \u003Csummary\u003EBriefly tints a widget so the eye lands on it after a jump.\u003C/summary\u003E\nfile sealed class FlashOverlay : Widget\n{\n\tprivate const float Duration = 1.2f;\n\tprivate readonly RealTimeSince age = 0;\n\n\tpublic FlashOverlay( Widget target ) : base( target )\n\t{\n\t\tTransparentForMouseEvents = true;\n\t\tPosition = 0;\n\t\tSize = target.Size;\n\t\tShow();\n\t}\n\n\t[EditorEvent.Frame]\n\tprivate void Frame()\n\t{\n\t\tif ( age \u003E Duration ) { Destroy(); return; }\n\t\tif ( Parent is not null ) Size = Parent.Size;\n\t\tUpdate();\n\t}\n\n\tprotected override void OnPaint()\n\t{\n\t\tvar fade = 1 - MathF.Min( 1, age / Duration );\n\t\tPaint.ClearPen();\n\t\tPaint.SetBrush( Theme.Primary.WithAlpha( 0.35f * fade ) );\n\t\tPaint.DrawRect( LocalRect, 3 );\n\t}\n}\n"}]}