s&box Package Code Search
Search C# source code, UI razor templates, shaders, and configs across s&box packages.
link Raw Facepunch API Link: https://public.facepunch.com/sbox/code/search/1/?ident=yugi.hierarchychanges&take=20
Showing code results for query:
*
(2 total matches found)
Editor
library
using System;
using System.IO;
using System.Text.Json;
using Editor;
using Sandbox;
using System.Collections.Generic;
using System.Linq;
namespace HierarchyChanges;
/// <summary>Prototype palette for editor-only annotations on the stock hierarchy.</summary>
[Dock( "Editor", "Hierarchy Changes", "palette", DockArea.Right )]
public sealed class HierarchyColoursDock : Widget
{
private readonly Label status;
public HierarchyColoursDock( Widget parent ) : base( parent )
{
Layout = Layout.Column();
Layout.Margin = 8;
Layout.Spacing = 6;
Layout.Add( new Label( "Select objects in the hierarchy, then choose a colour.", this ) );
Layout.Add( new Label( "Middle-click a hierarchy row to expand or collapse it.", this ) );
for ( var i = 0; i < HierarchyColourPreview.Colours.Length; i++ )
{
var index = i;
var button = Layout.Add( new Button( HierarchyColourPreview.Names[i], this ) );
button.SetStyles( $"background-color: {HierarchyColourPreview.Css[i]}; color: white;" );
button.Clicked += () => HierarchyColourPreview.Assign( index );
}
Layout.Add( new Button( "Clear selected colours", this ) ).Clicked += () => HierarchyColourPreview.Assign( -1 );
Layout.Add( new Button( "Make selected names bold", this ) ).Clicked += () => HierarchyColourPreview.SetBold( true );
Layout.Add( new Button( "Use regular names", this ) ).Clicked += () => HierarchyColourPreview.SetBold( false );
Layout.Add( new Button( "Toggle colour preview", this ) ).Clicked += HierarchyColourPreview.Toggle;
status = Layout.Add( new Label( "", this ) );
Layout.AddStretchCell();
}
[EditorEvent.Frame]
private void RefreshStatus()
{
status.Text = HierarchyColourPreview.Status;
}
}
/// <summary>
/// Adds colour underlays and dispatches bold rows while retaining stock interaction.
/// This remains independent of game code so it can move into a library's Editor folder.
/// </summary>
internal static class HierarchyColourPreview
{
internal static readonly string[] Names = ["Red", "Orange", "Yellow", "Green", "Blue", "Purple"];
internal static readonly string[] Css = ["#873b42", "#875331", "#756725", "#346849", "#365f87", "#694482"];
internal static readonly Color[] Colours =
[
new( 0.85f, 0.25f, 0.30f ), new( 0.95f, 0.48f, 0.18f ), new( 0.90f, 0.78f, 0.20f ),
new( 0.25f, 0.75f, 0.40f ), new( 0.25f, 0.55f, 0.95f ), new( 0.68f, 0.35f, 0.90f )
];
private static Dictionary<string, int> assignments = new();
// Preserve the original 0..5 colour values. Bits 0..2 hold colour (7 = none);
// bit 3 stores bold independently, so clearing colour never clears emphasis.
private const int NoColour = 7;
private const int BoldFlag = 8;
private static TreeView tree;
private static TreeView inputTree;
private static Func<bool> previousPaint;
private static bool enabled = true;
private static string settingsPath;
private static bool storageAvailable;
private static string error;
internal static string Status => error ?? (enabled ? "Preview on · colours and bold save automatically." : "Preview off · saved styles retained.");
// The hook also runs when the palette is hidden. It reconnects when the hierarchy is rebuilt.
[EditorEvent.Frame]
private static void Frame()
{
LoadProject();
var current = SceneTreeWidget.Current?.TreeView;
AttachMiddleClick( current is not null && current.IsValid() ? current : null );
if ( !enabled || current is null || !current.IsValid() )
{
Detach();
return;
}
if ( ReferenceEquals( tree, current ) ) return;
Detach();
tree = current;
previousPaint = tree.OnPaintOverride;
tree.OnPaintOverride = PaintUnderlay;
tree.Update();
}
[EditorEvent.Hotload]
private static void Hotload()
{
Detach();
AttachMiddleClick( null );
}
private static void AttachMiddleClick( TreeView current )
{
if ( ReferenceEquals( inputTree, current ) ) return;
// Input stays active with colour preview off; remove only our own subscription.
if ( inputTree is not null && inputTree.IsValid() ) inputTree.MouseMiddlePress -= MiddleClick;
inputTree = current;
if ( inputTree is not null ) inputTree.MouseMiddlePress += MiddleClick;
}
private static void MiddleClick()
{
if ( inputTree is null || !inputTree.IsValid() ) return;
var item = inputTree.GetItemAt( inputTree.FromScreen( Editor.Application.CursorPosition ) );
if ( item is null || !item.HasChildren || item.Object is not TreeNode node
|| node.ExpanderHidden || node.Value is not GameObject go || !go.IsValid() ) return;
// Toggle only this branch, preserving descendant expansion state and selection.
inputTree.Toggle( go );
inputTree.Update();
}
private static void Detach()
{
// Restore only our own callback; don't erase another extension's later replacement.
if ( tree is not null && tree.IsValid() && tree.OnPaintOverride == PaintUnderlay )
{
tree.OnPaintOverride = previousPaint;
tree.Update();
}
tree = null;
previousPaint = null;
}
internal static void Toggle()
{
enabled = !enabled;
Frame();
}
private static string Key( GameObject go ) => $"{go.Scene.Id:N}/{go.Id:N}";
internal static void Assign( int colour )
=> EditSelected( value => (value & BoldFlag) | (colour < 0 ? NoColour : colour) );
internal static void SetBold( bool bold )
=> EditSelected( value => bold ? value | BoldFlag : value & ~BoldFlag );
private static void EditSelected( Func<int, int> edit )
{
LoadProject();
if ( !storageAvailable ) return;
var scene = SceneEditorSession.Active?.Scene;
if ( scene is null || !scene.IsEditor ) return;
var selected = SceneEditorSession.Active.Selection.OfType<GameObject>().Where( go => go.IsValid() && go.Scene == scene ).ToArray();
if ( selected.Length == 0 ) return;
var next = new Dictionary<string, int>( assignments );
foreach ( var go in selected )
{
var key = Key( go );
var value = edit( next.GetValueOrDefault( key, NoColour ) );
if ( value == NoColour ) next.Remove( key );
else next[key] = value;
}
try
{
Directory.CreateDirectory( Path.GetDirectoryName( settingsPath ) );
var temporary = settingsPath + ".tmp";
File.WriteAllText( temporary, JsonSerializer.Serialize( next, new JsonSerializerOptions { WriteIndented = true } ) );
File.Move( temporary, settingsPath, true );
assignments = next;
error = null;
tree?.Update();
}
catch ( Exception e ) { error = $"Could not save colours: {e.Message}"; }
}
private static void LoadProject()
{
var root = Project.Current?.RootDirectory?.FullName;
var path = root is null ? null : Path.Combine( root, "Settings", "hierarchy-colours.json" );
if ( settingsPath == path ) return;
settingsPath = path;
assignments = new();
storageAvailable = path is not null;
error = null;
try
{
if ( path is not null && File.Exists( path ) )
assignments = JsonSerializer.Deserialize<Dictionary<string, int>>( File.ReadAllText( path ) ) ?? new();
}
catch ( Exception e )
{
// Never overwrite unreadable settings with an empty palette.
storageAvailable = false;
error = $"Could not load colours: {e.Message}";
}
}
private static bool PaintUnderlay()
{
if ( previousPaint?.Invoke() == true ) return true;
var scene = SceneEditorSession.Active?.Scene;
if ( scene is null || !scene.IsEditor || assignments.Count == 0 ) return false;
// ItemLayouts is protected. Public hit-testing discovers only visible rows, then
// jumps to each row's bottom; it does not traverse every object in a large scene.
var x = Math.Max( tree.Margin.Left + 1, tree.Width * 0.5f );
var rows = new List<VirtualWidget>();
var hasBold = false;
for ( float y = 0; y < tree.Height; )
{
var item = tree.GetItemAt( new Vector2( x, y ) );
if ( item is null ) { y += 1; continue; }
y = Math.Max( y + 1, item.Rect.Bottom + 0.5f );
rows.Add( item );
if ( item.Object is not TreeNode node || node.Value is not GameObject go || !go.IsValid() ) continue;
if ( !assignments.TryGetValue( Key( go ), out var style ) ) continue;
hasBold |= (style & BoldFlag) != 0;
var index = style & NoColour;
if ( index >= Colours.Length ) continue;
var rect = item.Rect;
rect.Left = 0;
rect.Right = tree.Width;
Paint.ClearPen();
Paint.SetBrush( Colours[index].WithAlpha( go.Active ? 0.24f : 0.10f ) );
Paint.DrawRect( rect );
}
if ( !hasBold ) return false;
// Stock GameObjectNode hardcodes font weight. When bold is visible, dispatch
// rows ourselves, preserving the actual nodes for selection, drag and rename.
Paint.Antialiasing = true;
Paint.TextAntialiasing = true;
foreach ( var item in rows ) PaintRow( item );
return true;
}
private static void PaintRow( VirtualWidget item )
{
if ( item.Object is not TreeNode node ) return;
item.Selected = tree.IsSelected( item.Object );
Paint.SetFlags( item.Selected, item.Hovered, item.Pressed, false, true );
var rect = item.Rect;
var childrenRect = item.ChildrenRect;
var oldIndent = item.Indent;
var indent = tree.IndentWidth * item.Column + tree.ExpandWidth;
try
{
item.Indent = indent;
item.Rect.Left += indent;
item.ChildrenRect.Left += indent + tree.IndentWidth;
// Specialized prefab/scene root nodes keep their own painter.
if ( node.GetType().Name == "GameObjectNode" && node.Value is GameObject go
&& go.IsValid() && assignments.TryGetValue( Key( go ), out var style ) && (style & BoldFlag) != 0 )
BoldHierarchyRow.PaintBold( item, go, tree );
else node.OnPaint( item );
}
finally
{
// Hit-testing shares these layouts; never leave paint indentation behind.
item.Rect = rect;
item.ChildrenRect = childrenRect;
item.Indent = oldIndent;
}
if ( !item.HasChildren || node.ExpanderHidden ) return;
var expander = rect;
expander.Left += indent - tree.ExpandWidth;
expander.Width = tree.ExpandWidth;
Paint.SetPen( Theme.Text.WithAlpha( item.IsOpen ? 1 : 0.6f ) );
Paint.DrawIcon( expander, item.IsOpen ? "arrow_drop_down" : "arrow_right", 26, TextFlag.Center );
}
}
Editor
library
using Editor;
using Sandbox;
using System;
using System.Linq;
using static Editor.BaseItemWidget;
namespace HierarchyChanges;
// Adapted from Facepunch's installed GameObjectNode.OnPaint (2026-09-06).
// Source: sbox-public/game/addons/tools/Code/Scene/SceneTree/GameObjectNode.cs.
// Keep this compatibility copy aligned with upstream hierarchy indicators.
// Only the object name uses weight 700; following status text returns to normal.
internal static class BoldHierarchyRow
{
internal static void PaintBold( VirtualWidget item, GameObject Value, TreeView TreeView )
{
if ( !Value.Scene.IsValid() )
return;
var isEven = item.Row % 2 == 0;
var isHovered = item.Hovered;
var selected = item.Selected || item.Pressed || item.Dragging;
var isBone = Value.Flags.Contains( GameObjectFlags.Bone );
var isProceduralBone = Value.Flags.Contains( GameObjectFlags.ProceduralBone );
var isAttachment = Value.Flags.Contains( GameObjectFlags.Attachment );
var isNetworked = Value.Scene.IsEditor ? Value.NetworkMode == NetworkMode.Object : Value.Network.Active;
var isNetworkRoot = isNetworked && (Value.Scene.IsEditor ? Value.NetworkMode == NetworkMode.Object : Value.IsNetworkRoot);
bool isErrored = Value.Flags.Contains( GameObjectFlags.Error );
bool isLoading = Value.Flags.Contains( GameObjectFlags.Loading );
bool isTemporary = Value.Flags.Contains( GameObjectFlags.NotSaved );
bool isEditorOnly = Value.Flags.Contains( GameObjectFlags.EditorOnly );
var fullSpanRect = item.Rect;
fullSpanRect.Left = 0;
fullSpanRect.Right = TreeView.Width;
float opacity = 0.9f;
if ( !Value.Active ) opacity *= 0.5f;
Color pen = Theme.TextControl;
string icon = "layers";
Color iconColor = Theme.TextControl.WithAlpha( 0.6f );
Color overlayIconColor = iconColor;
string overlayIcon = null;
if ( Value.IsPrefabInstance )
{
pen = Theme.Blue;
overlayIconColor = Theme.Blue;
if ( Value.IsPrefabInstanceRoot )
{
icon = "dataset";
iconColor = Theme.Blue;
if ( EditorUtility.Prefabs.IsInstanceModified( Value ) )
{
Paint.ClearPen();
Paint.SetBrush( Theme.Blue.Darken( 0.25f ) );
var modifiedRect = fullSpanRect;
modifiedRect.Width = 2;
Paint.DrawRect( modifiedRect );
}
}
if ( EditorUtility.Prefabs.IsGameObjectAddedToInstance( Value ) )
{
overlayIcon = "add_circle";
}
}
if ( isBone )
{
icon = "polyline";
iconColor = Theme.Pink.WithAlpha( 0.8f );
if ( isProceduralBone )
{
iconColor = Theme.Blue.WithAlpha( 0.8f );
}
}
if ( isAttachment )
{
icon = "push_pin";
iconColor = Theme.Pink.WithAlpha( 0.8f );
if ( isProceduralBone )
{
iconColor = Theme.Blue.WithAlpha( 0.8f );
}
}
if ( isTemporary )
{
iconColor = Color.White.WithAlpha( 0.5f );
pen = iconColor.Lighten( 0.2f ).WithAlpha( 0.8f );
icon = "no_sim";
}
if ( isNetworked )
{
icon = "rss_feed";
iconColor = Theme.Blue.WithAlpha( 0.8f );
if ( Value.Network.IsOwner )
{
iconColor = Theme.Green.WithAlpha( 0.8f );
}
if ( Value.IsProxy )
{
iconColor = Theme.TextControl.WithAlpha( 0.6f );
}
if ( !isNetworkRoot ) iconColor = iconColor.WithAlphaMultiplied( 0.4f );
}
if ( isErrored )
{
icon = "report";
iconColor = Theme.Red.WithAlpha( 0.8f );
}
if ( isEditorOnly )
{
icon = "highlight_alt";
iconColor = Theme.Yellow.WithAlpha( 0.8f );
pen = Theme.Yellow.WithAlpha( 0.6f );
}
//
// If there's a drag and drop happening, fade out nodes that aren't possible
//
if ( TreeView.IsBeingDroppedOn )
{
if ( TreeView.CurrentItemDragEvent.Data.Object is GameObject[] gos && gos.Any( go => Value.IsAncestor( go ) ) )
{
opacity *= 0.23f;
}
else if ( TreeView.CurrentItemDragEvent.Data.Object is GameObject go && Value.IsAncestor( go ) )
{
opacity *= 0.23f;
}
}
if ( item.Dropping )
{
Paint.ClearPen();
Paint.SetBrush( Theme.Blue );
if ( TreeView.CurrentItemDragEvent.DropEdge.HasFlag( ItemEdge.Top ) )
{
var droprect = item.Rect;
droprect.Top -= 1;
droprect.Height = 2;
Paint.DrawRect( droprect, 2 );
}
else if ( TreeView.CurrentItemDragEvent.DropEdge.HasFlag( ItemEdge.Bottom ) )
{
var droprect = item.Rect;
droprect.Top = droprect.Bottom - 1;
droprect.Height = 2;
Paint.DrawRect( droprect, 2 );
}
else
{
Paint.SetBrushAndPen( Theme.Blue.WithAlpha( 0.2f ), Theme.Blue );
Paint.PenSize = 2;
Paint.DrawRect( item.Rect, 4 );
}
}
if ( selected )
{
//item.PaintBackground( Color.Transparent, 3 );
Paint.ClearPen();
Paint.SetBrush( Theme.SelectedBackground.WithAlpha( opacity ) );
Paint.DrawRect( fullSpanRect );
}
else if ( isHovered )
{
Paint.ClearPen();
Paint.SetBrush( Theme.SelectedBackground.WithAlpha( 0.25f ) );
Paint.DrawRect( fullSpanRect );
}
else if ( isEven )
{
Paint.ClearPen();
Paint.SetBrush( Theme.SurfaceLightBackground.WithAlpha( 0.1f ) );
Paint.DrawRect( fullSpanRect );
}
var name = Value.Name;
if ( string.IsNullOrWhiteSpace( name ) ) name = "Untitled GameObject";
var r = item.Rect;
r.Left += 4;
var iconSize = 16;
Paint.Pen = iconColor.WithAlphaMultiplied( opacity );
Paint.DrawIcon( r, icon, iconSize, TextFlag.LeftCenter );
if ( !string.IsNullOrEmpty( overlayIcon ) )
{
var overlayIconRect = r;
overlayIconRect.Left += 8;
overlayIconRect.Top += 8;
overlayIconRect.Width = 13;
overlayIconRect.Height = 13;
Paint.Pen = Theme.WidgetBackground;
Paint.SetBrush( Theme.WidgetBackground );
Paint.DrawRect( overlayIconRect, 12 );
overlayIconRect.Left += 1;
Paint.Pen = overlayIconColor;
Paint.DrawIcon( overlayIconRect, overlayIcon, 13, TextFlag.Center );
}
r.Left += 22;
Paint.Pen = pen.WithAlphaMultiplied( opacity );
Paint.SetDefaultFont( weight: 700 );
r.Left += Paint.DrawText( r, name, TextFlag.LeftCenter ).Width + 4;
Paint.SetDefaultFont();
if ( isLoading )
{
Paint.Pen = Theme.Blue;
Paint.DrawIcon( r, "access_time_filled", iconSize, TextFlag.LeftCenter );
r.Left += 22;
}
if ( isNetworkRoot && Value.Network.OwnerId != Guid.Empty )
{
var connection = Connection.Find( Value.Network.OwnerId );
if ( connection is null )
{
Paint.Pen = Theme.Blue;
Paint.DrawText( r, $"Unknown Owner - {Value.Network.OwnerId}", TextFlag.LeftCenter );
r.Left += 22;
}
else
{
Paint.Pen = Theme.Blue;
Paint.DrawText( r, $"{connection.DisplayName}", TextFlag.LeftCenter );
r.Left += 22;
}
}
if ( Value.Tags.Has( "hidden" ) )
{
var eyeRect = item.Rect;
eyeRect.Right -= 4;
eyeRect.Left = eyeRect.Right - 18;
Paint.Pen = Theme.TextControl;
Paint.DrawIcon( eyeRect, "visibility_off", 14, TextFlag.Center );
}
}
}
Debug: View Raw JSON Response
{
"TotalCount": 2,
"Files": [
{
"Ident": "yugi.hierarchychanges",
"Path": "Editor/HierarchyColoursDock.cs",
"FileName": "HierarchyColoursDock.cs",
"PackageType": "library",
"CodeKind": "Editor",
"AssetVersionId": 381874,
"Code": "using System;\nusing System.IO;\nusing System.Text.Json;\nusing Editor;\nusing Sandbox;\nusing System.Collections.Generic;\nusing System.Linq;\n\nnamespace HierarchyChanges;\n\n\n/// <summary>Prototype palette for editor-only annotations on the stock hierarchy.</summary>\n[Dock( \"Editor\", \"Hierarchy Changes\", \"palette\", DockArea.Right )]\npublic sealed class HierarchyColoursDock : Widget\n{\n\tprivate readonly Label status;\n\n\tpublic HierarchyColoursDock( Widget parent ) : base( parent )\n\t{\n\t\tLayout = Layout.Column();\n\t\tLayout.Margin = 8;\n\t\tLayout.Spacing = 6;\n\t\tLayout.Add( new Label( \"Select objects in the hierarchy, then choose a colour.\", this ) );\n\t\tLayout.Add( new Label( \"Middle-click a hierarchy row to expand or collapse it.\", this ) );\n\t\tfor ( var i = 0; i < HierarchyColourPreview.Colours.Length; i++ )\n\t\t{\n\t\t\tvar index = i;\n\t\t\tvar button = Layout.Add( new Button( HierarchyColourPreview.Names[i], this ) );\n\t\t\tbutton.SetStyles( $\"background-color: {HierarchyColourPreview.Css[i]}; color: white;\" );\n\t\t\tbutton.Clicked += () => HierarchyColourPreview.Assign( index );\n\t\t}\n\t\tLayout.Add( new Button( \"Clear selected colours\", this ) ).Clicked += () => HierarchyColourPreview.Assign( -1 );\n\t\tLayout.Add( new Button( \"Make selected names bold\", this ) ).Clicked += () => HierarchyColourPreview.SetBold( true );\n\t\tLayout.Add( new Button( \"Use regular names\", this ) ).Clicked += () => HierarchyColourPreview.SetBold( false );\n\t\tLayout.Add( new Button( \"Toggle colour preview\", this ) ).Clicked += HierarchyColourPreview.Toggle;\n\t\tstatus = Layout.Add( new Label( \"\", this ) );\n\t\tLayout.AddStretchCell();\n\t}\n\n\t[EditorEvent.Frame]\n\tprivate void RefreshStatus()\n\t{\n\t\tstatus.Text = HierarchyColourPreview.Status;\n\t}\n}\n\n/// <summary>\n/// Adds colour underlays and dispatches bold rows while retaining stock interaction.\n/// This remains independent of game code so it can move into a library's Editor folder.\n/// </summary>\ninternal static class HierarchyColourPreview\n{\n\tinternal static readonly string[] Names = [\"Red\", \"Orange\", \"Yellow\", \"Green\", \"Blue\", \"Purple\"];\n\tinternal static readonly string[] Css = [\"#873b42\", \"#875331\", \"#756725\", \"#346849\", \"#365f87\", \"#694482\"];\n\tinternal static readonly Color[] Colours =\n\t[\n\t\tnew( 0.85f, 0.25f, 0.30f ), new( 0.95f, 0.48f, 0.18f ), new( 0.90f, 0.78f, 0.20f ),\n\t\tnew( 0.25f, 0.75f, 0.40f ), new( 0.25f, 0.55f, 0.95f ), new( 0.68f, 0.35f, 0.90f )\n\t];\n\tprivate static Dictionary<string, int> assignments = new();\n\t// Preserve the original 0..5 colour values. Bits 0..2 hold colour (7 = none);\n\t// bit 3 stores bold independently, so clearing colour never clears emphasis.\n\tprivate const int NoColour = 7;\n\tprivate const int BoldFlag = 8;\n\tprivate static TreeView tree;\n\tprivate static TreeView inputTree;\n\tprivate static Func<bool> previousPaint;\n\tprivate static bool enabled = true;\n\tprivate static string settingsPath;\n\tprivate static bool storageAvailable;\n\tprivate static string error;\n\tinternal static string Status => error ?? (enabled ? \"Preview on \u00b7 colours and bold save automatically.\" : \"Preview off \u00b7 saved styles retained.\");\n\n\t// The hook also runs when the palette is hidden. It reconnects when the hierarchy is rebuilt.\n\t[EditorEvent.Frame]\n\tprivate static void Frame()\n\t{\n\t\tLoadProject();\n\t\tvar current = SceneTreeWidget.Current?.TreeView;\n\t\tAttachMiddleClick( current is not null && current.IsValid() ? current : null );\n\t\tif ( !enabled || current is null || !current.IsValid() )\n\t\t{\n\t\t\tDetach();\n\t\t\treturn;\n\t\t}\n\t\tif ( ReferenceEquals( tree, current ) ) return;\n\t\tDetach();\n\t\ttree = current;\n\t\tpreviousPaint = tree.OnPaintOverride;\n\t\ttree.OnPaintOverride = PaintUnderlay;\n\t\ttree.Update();\n\t}\n\n\t[EditorEvent.Hotload]\n\tprivate static void Hotload()\n\t{\n\t\tDetach();\n\t\tAttachMiddleClick( null );\n\t}\n\n\tprivate static void AttachMiddleClick( TreeView current )\n\t{\n\t\tif ( ReferenceEquals( inputTree, current ) ) return;\n\t\t// Input stays active with colour preview off; remove only our own subscription.\n\t\tif ( inputTree is not null && inputTree.IsValid() ) inputTree.MouseMiddlePress -= MiddleClick;\n\t\tinputTree = current;\n\t\tif ( inputTree is not null ) inputTree.MouseMiddlePress += MiddleClick;\n\t}\n\n\tprivate static void MiddleClick()\n\t{\n\t\tif ( inputTree is null || !inputTree.IsValid() ) return;\n\t\tvar item = inputTree.GetItemAt( inputTree.FromScreen( Editor.Application.CursorPosition ) );\n\t\tif ( item is null || !item.HasChildren || item.Object is not TreeNode node\n\t\t\t|| node.ExpanderHidden || node.Value is not GameObject go || !go.IsValid() ) return;\n\t\t// Toggle only this branch, preserving descendant expansion state and selection.\n\t\tinputTree.Toggle( go );\n\t\tinputTree.Update();\n\t}\n\n\tprivate static void Detach()\n\t{\n\t\t// Restore only our own callback; don't erase another extension's later replacement.\n\t\tif ( tree is not null && tree.IsValid() && tree.OnPaintOverride == PaintUnderlay )\n\t\t{\n\t\t\ttree.OnPaintOverride = previousPaint;\n\t\t\ttree.Update();\n\t\t}\n\t\ttree = null;\n\t\tpreviousPaint = null;\n\t}\n\n\tinternal static void Toggle()\n\t{\n\t\tenabled = !enabled;\n\t\tFrame();\n\t}\n\n\tprivate static string Key( GameObject go ) => $\"{go.Scene.Id:N}/{go.Id:N}\";\n\n\tinternal static void Assign( int colour )\n\t\t=> EditSelected( value => (value & BoldFlag) | (colour < 0 ? NoColour : colour) );\n\n\tinternal static void SetBold( bool bold )\n\t\t=> EditSelected( value => bold ? value | BoldFlag : value & ~BoldFlag );\n\n\tprivate static void EditSelected( Func<int, int> edit )\n\t{\n\t\tLoadProject();\n\t\tif ( !storageAvailable ) return;\n\t\tvar scene = SceneEditorSession.Active?.Scene;\n\t\tif ( scene is null || !scene.IsEditor ) return;\n\t\tvar selected = SceneEditorSession.Active.Selection.OfType<GameObject>().Where( go => go.IsValid() && go.Scene == scene ).ToArray();\n\t\tif ( selected.Length == 0 ) return;\n\t\tvar next = new Dictionary<string, int>( assignments );\n\t\tforeach ( var go in selected )\n\t\t{\n\t\t\tvar key = Key( go );\n\t\t\tvar value = edit( next.GetValueOrDefault( key, NoColour ) );\n\t\t\tif ( value == NoColour ) next.Remove( key );\n\t\t\telse next[key] = value;\n\t\t}\n\t\ttry\n\t\t{\n\t\t\tDirectory.CreateDirectory( Path.GetDirectoryName( settingsPath ) );\n\t\t\tvar temporary = settingsPath + \".tmp\";\n\t\t\tFile.WriteAllText( temporary, JsonSerializer.Serialize( next, new JsonSerializerOptions { WriteIndented = true } ) );\n\t\t\tFile.Move( temporary, settingsPath, true );\n\t\t\tassignments = next;\n\t\t\terror = null;\n\t\t\ttree?.Update();\n\t\t}\n\t\tcatch ( Exception e ) { error = $\"Could not save colours: {e.Message}\"; }\n\t}\n\n\tprivate static void LoadProject()\n\t{\n\t\tvar root = Project.Current?.RootDirectory?.FullName;\n\t\tvar path = root is null ? null : Path.Combine( root, \"Settings\", \"hierarchy-colours.json\" );\n\t\tif ( settingsPath == path ) return;\n\t\tsettingsPath = path;\n\t\tassignments = new();\n\t\tstorageAvailable = path is not null;\n\t\terror = null;\n\t\ttry\n\t\t{\n\t\t\tif ( path is not null && File.Exists( path ) )\n\t\t\t\tassignments = JsonSerializer.Deserialize<Dictionary<string, int>>( File.ReadAllText( path ) ) ?? new();\n\t\t}\n\t\tcatch ( Exception e )\n\t\t{\n\t\t\t// Never overwrite unreadable settings with an empty palette.\n\t\t\tstorageAvailable = false;\n\t\t\terror = $\"Could not load colours: {e.Message}\";\n\t\t}\n\t}\n\n\tprivate static bool PaintUnderlay()\n\t{\n\t\tif ( previousPaint?.Invoke() == true ) return true;\n\t\tvar scene = SceneEditorSession.Active?.Scene;\n\t\tif ( scene is null || !scene.IsEditor || assignments.Count == 0 ) return false;\n\t\t// ItemLayouts is protected. Public hit-testing discovers only visible rows, then\n\t\t// jumps to each row's bottom; it does not traverse every object in a large scene.\n\t\tvar x = Math.Max( tree.Margin.Left + 1, tree.Width * 0.5f );\n\t\tvar rows = new List<VirtualWidget>();\n\t\tvar hasBold = false;\n\t\tfor ( float y = 0; y < tree.Height; )\n\t\t{\n\t\t\tvar item = tree.GetItemAt( new Vector2( x, y ) );\n\t\t\tif ( item is null ) { y += 1; continue; }\n\t\t\ty = Math.Max( y + 1, item.Rect.Bottom + 0.5f );\n\t\t\trows.Add( item );\n\t\t\tif ( item.Object is not TreeNode node || node.Value is not GameObject go || !go.IsValid() ) continue;\n\t\t\tif ( !assignments.TryGetValue( Key( go ), out var style ) ) continue;\n\t\t\thasBold |= (style & BoldFlag) != 0;\n\t\t\tvar index = style & NoColour;\n\t\t\tif ( index >= Colours.Length ) continue;\n\t\t\tvar rect = item.Rect;\n\t\t\trect.Left = 0;\n\t\t\trect.Right = tree.Width;\n\t\t\tPaint.ClearPen();\n\t\t\tPaint.SetBrush( Colours[index].WithAlpha( go.Active ? 0.24f : 0.10f ) );\n\t\t\tPaint.DrawRect( rect );\n\t\t}\n\t\tif ( !hasBold ) return false;\n\t\t// Stock GameObjectNode hardcodes font weight. When bold is visible, dispatch\n\t\t// rows ourselves, preserving the actual nodes for selection, drag and rename.\n\t\tPaint.Antialiasing = true;\n\t\tPaint.TextAntialiasing = true;\n\t\tforeach ( var item in rows ) PaintRow( item );\n\t\treturn true;\n\t}\n\n\tprivate static void PaintRow( VirtualWidget item )\n\t{\n\t\tif ( item.Object is not TreeNode node ) return;\n\t\titem.Selected = tree.IsSelected( item.Object );\n\t\tPaint.SetFlags( item.Selected, item.Hovered, item.Pressed, false, true );\n\t\tvar rect = item.Rect;\n\t\tvar childrenRect = item.ChildrenRect;\n\t\tvar oldIndent = item.Indent;\n\t\tvar indent = tree.IndentWidth * item.Column + tree.ExpandWidth;\n\t\ttry\n\t\t{\n\t\t\titem.Indent = indent;\n\t\t\titem.Rect.Left += indent;\n\t\t\titem.ChildrenRect.Left += indent + tree.IndentWidth;\n\t\t\t// Specialized prefab/scene root nodes keep their own painter.\n\t\t\tif ( node.GetType().Name == \"GameObjectNode\" && node.Value is GameObject go\n\t\t\t\t&& go.IsValid() && assignments.TryGetValue( Key( go ), out var style ) && (style & BoldFlag) != 0 )\n\t\t\t\tBoldHierarchyRow.PaintBold( item, go, tree );\n\t\t\telse node.OnPaint( item );\n\t\t}\n\t\tfinally\n\t\t{\n\t\t\t// Hit-testing shares these layouts; never leave paint indentation behind.\n\t\t\titem.Rect = rect;\n\t\t\titem.ChildrenRect = childrenRect;\n\t\t\titem.Indent = oldIndent;\n\t\t}\n\t\tif ( !item.HasChildren || node.ExpanderHidden ) return;\n\t\tvar expander = rect;\n\t\texpander.Left += indent - tree.ExpandWidth;\n\t\texpander.Width = tree.ExpandWidth;\n\t\tPaint.SetPen( Theme.Text.WithAlpha( item.IsOpen ? 1 : 0.6f ) );\n\t\tPaint.DrawIcon( expander, item.IsOpen ? \"arrow_drop_down\" : \"arrow_right\", 26, TextFlag.Center );\n\t}\n}\n"
},
{
"Ident": "yugi.hierarchychanges",
"Path": "Editor/BoldHierarchyRow.cs",
"FileName": "BoldHierarchyRow.cs",
"PackageType": "library",
"CodeKind": "Editor",
"AssetVersionId": 381874,
"Code": "using Editor;\nusing Sandbox;\nusing System;\nusing System.Linq;\nusing static Editor.BaseItemWidget;\n\nnamespace HierarchyChanges;\n\n// Adapted from Facepunch's installed GameObjectNode.OnPaint (2026-09-06).\n// Source: sbox-public/game/addons/tools/Code/Scene/SceneTree/GameObjectNode.cs.\n// Keep this compatibility copy aligned with upstream hierarchy indicators.\n// Only the object name uses weight 700; following status text returns to normal.\ninternal static class BoldHierarchyRow\n{\n\tinternal static void PaintBold( VirtualWidget item, GameObject Value, TreeView TreeView )\n\t{\n\t\tif ( !Value.Scene.IsValid() )\n\t\t\treturn;\n\n\t\tvar isEven = item.Row % 2 == 0;\n\t\tvar isHovered = item.Hovered;\n\t\tvar selected = item.Selected || item.Pressed || item.Dragging;\n\t\tvar isBone = Value.Flags.Contains( GameObjectFlags.Bone );\n\t\tvar isProceduralBone = Value.Flags.Contains( GameObjectFlags.ProceduralBone );\n\t\tvar isAttachment = Value.Flags.Contains( GameObjectFlags.Attachment );\n\t\tvar isNetworked = Value.Scene.IsEditor ? Value.NetworkMode == NetworkMode.Object : Value.Network.Active;\n\t\tvar isNetworkRoot = isNetworked && (Value.Scene.IsEditor ? Value.NetworkMode == NetworkMode.Object : Value.IsNetworkRoot);\n\n\t\tbool isErrored = Value.Flags.Contains( GameObjectFlags.Error );\n\t\tbool isLoading = Value.Flags.Contains( GameObjectFlags.Loading );\n\t\tbool isTemporary = Value.Flags.Contains( GameObjectFlags.NotSaved );\n\t\tbool isEditorOnly = Value.Flags.Contains( GameObjectFlags.EditorOnly );\n\n\t\tvar fullSpanRect = item.Rect;\n\t\tfullSpanRect.Left = 0;\n\t\tfullSpanRect.Right = TreeView.Width;\n\n\t\tfloat opacity = 0.9f;\n\n\t\tif ( !Value.Active ) opacity *= 0.5f;\n\n\t\tColor pen = Theme.TextControl;\n\t\tstring icon = \"layers\";\n\t\tColor iconColor = Theme.TextControl.WithAlpha( 0.6f );\n\t\tColor overlayIconColor = iconColor;\n\n\t\tstring overlayIcon = null;\n\n\t\tif ( Value.IsPrefabInstance )\n\t\t{\n\t\t\tpen = Theme.Blue;\n\t\t\toverlayIconColor = Theme.Blue;\n\n\t\t\tif ( Value.IsPrefabInstanceRoot )\n\t\t\t{\n\t\t\t\ticon = \"dataset\";\n\t\t\t\ticonColor = Theme.Blue;\n\n\t\t\t\tif ( EditorUtility.Prefabs.IsInstanceModified( Value ) )\n\t\t\t\t{\n\t\t\t\t\tPaint.ClearPen();\n\t\t\t\t\tPaint.SetBrush( Theme.Blue.Darken( 0.25f ) );\n\t\t\t\t\tvar modifiedRect = fullSpanRect;\n\t\t\t\t\tmodifiedRect.Width = 2;\n\t\t\t\t\tPaint.DrawRect( modifiedRect );\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif ( EditorUtility.Prefabs.IsGameObjectAddedToInstance( Value ) )\n\t\t\t{\n\t\t\t\toverlayIcon = \"add_circle\";\n\t\t\t}\n\t\t}\n\n\t\tif ( isBone )\n\t\t{\n\t\t\ticon = \"polyline\";\n\t\t\ticonColor = Theme.Pink.WithAlpha( 0.8f );\n\n\t\t\tif ( isProceduralBone )\n\t\t\t{\n\t\t\t\ticonColor = Theme.Blue.WithAlpha( 0.8f );\n\t\t\t}\n\t\t}\n\n\t\tif ( isAttachment )\n\t\t{\n\t\t\ticon = \"push_pin\";\n\t\t\ticonColor = Theme.Pink.WithAlpha( 0.8f );\n\n\t\t\tif ( isProceduralBone )\n\t\t\t{\n\t\t\t\ticonColor = Theme.Blue.WithAlpha( 0.8f );\n\t\t\t}\n\t\t}\n\n\t\tif ( isTemporary )\n\t\t{\n\t\t\ticonColor = Color.White.WithAlpha( 0.5f );\n\t\t\tpen = iconColor.Lighten( 0.2f ).WithAlpha( 0.8f );\n\t\t\ticon = \"no_sim\";\n\t\t}\n\n\t\tif ( isNetworked )\n\t\t{\n\t\t\ticon = \"rss_feed\";\n\t\t\ticonColor = Theme.Blue.WithAlpha( 0.8f );\n\n\t\t\tif ( Value.Network.IsOwner )\n\t\t\t{\n\t\t\t\ticonColor = Theme.Green.WithAlpha( 0.8f );\n\t\t\t}\n\n\t\t\tif ( Value.IsProxy )\n\t\t\t{\n\t\t\t\ticonColor = Theme.TextControl.WithAlpha( 0.6f );\n\t\t\t}\n\n\t\t\tif ( !isNetworkRoot ) iconColor = iconColor.WithAlphaMultiplied( 0.4f );\n\t\t}\n\n\t\tif ( isErrored )\n\t\t{\n\t\t\ticon = \"report\";\n\t\t\ticonColor = Theme.Red.WithAlpha( 0.8f );\n\t\t}\n\n\t\tif ( isEditorOnly )\n\t\t{\n\t\t\ticon = \"highlight_alt\";\n\t\t\ticonColor = Theme.Yellow.WithAlpha( 0.8f );\n\t\t\tpen = Theme.Yellow.WithAlpha( 0.6f );\n\t\t}\n\n\n\t\t//\n\t\t// If there's a drag and drop happening, fade out nodes that aren't possible\n\t\t//\n\t\tif ( TreeView.IsBeingDroppedOn )\n\t\t{\n\t\t\tif ( TreeView.CurrentItemDragEvent.Data.Object is GameObject[] gos && gos.Any( go => Value.IsAncestor( go ) ) )\n\t\t\t{\n\t\t\t\topacity *= 0.23f;\n\t\t\t}\n\t\t\telse if ( TreeView.CurrentItemDragEvent.Data.Object is GameObject go && Value.IsAncestor( go ) )\n\t\t\t{\n\t\t\t\topacity *= 0.23f;\n\t\t\t}\n\t\t}\n\n\t\tif ( item.Dropping )\n\t\t{\n\t\t\tPaint.ClearPen();\n\t\t\tPaint.SetBrush( Theme.Blue );\n\n\t\t\tif ( TreeView.CurrentItemDragEvent.DropEdge.HasFlag( ItemEdge.Top ) )\n\t\t\t{\n\t\t\t\tvar droprect = item.Rect;\n\t\t\t\tdroprect.Top -= 1;\n\t\t\t\tdroprect.Height = 2;\n\t\t\t\tPaint.DrawRect( droprect, 2 );\n\t\t\t}\n\t\t\telse if ( TreeView.CurrentItemDragEvent.DropEdge.HasFlag( ItemEdge.Bottom ) )\n\t\t\t{\n\t\t\t\tvar droprect = item.Rect;\n\t\t\t\tdroprect.Top = droprect.Bottom - 1;\n\t\t\t\tdroprect.Height = 2;\n\t\t\t\tPaint.DrawRect( droprect, 2 );\n\t\t\t}\n\t\t\telse\n\t\t\t{\n\t\t\t\tPaint.SetBrushAndPen( Theme.Blue.WithAlpha( 0.2f ), Theme.Blue );\n\t\t\t\tPaint.PenSize = 2;\n\t\t\t\tPaint.DrawRect( item.Rect, 4 );\n\t\t\t}\n\t\t}\n\n\t\tif ( selected )\n\t\t{\n\t\t\t//item.PaintBackground( Color.Transparent, 3 );\n\t\t\tPaint.ClearPen();\n\t\t\tPaint.SetBrush( Theme.SelectedBackground.WithAlpha( opacity ) );\n\t\t\tPaint.DrawRect( fullSpanRect );\n\t\t}\n\t\telse if ( isHovered )\n\t\t{\n\t\t\tPaint.ClearPen();\n\t\t\tPaint.SetBrush( Theme.SelectedBackground.WithAlpha( 0.25f ) );\n\t\t\tPaint.DrawRect( fullSpanRect );\n\t\t}\n\t\telse if ( isEven )\n\t\t{\n\t\t\tPaint.ClearPen();\n\t\t\tPaint.SetBrush( Theme.SurfaceLightBackground.WithAlpha( 0.1f ) );\n\t\t\tPaint.DrawRect( fullSpanRect );\n\t\t}\n\n\t\tvar name = Value.Name;\n\t\tif ( string.IsNullOrWhiteSpace( name ) ) name = \"Untitled GameObject\";\n\n\t\tvar r = item.Rect;\n\t\tr.Left += 4;\n\n\t\tvar iconSize = 16;\n\n\t\tPaint.Pen = iconColor.WithAlphaMultiplied( opacity );\n\t\tPaint.DrawIcon( r, icon, iconSize, TextFlag.LeftCenter );\n\t\tif ( !string.IsNullOrEmpty( overlayIcon ) )\n\t\t{\n\t\t\tvar overlayIconRect = r;\n\t\t\toverlayIconRect.Left += 8;\n\t\t\toverlayIconRect.Top += 8;\n\t\t\toverlayIconRect.Width = 13;\n\t\t\toverlayIconRect.Height = 13;\n\t\t\tPaint.Pen = Theme.WidgetBackground;\n\t\t\tPaint.SetBrush( Theme.WidgetBackground );\n\t\t\tPaint.DrawRect( overlayIconRect, 12 );\n\t\t\toverlayIconRect.Left += 1;\n\t\t\tPaint.Pen = overlayIconColor;\n\t\t\tPaint.DrawIcon( overlayIconRect, overlayIcon, 13, TextFlag.Center );\n\t\t}\n\t\tr.Left += 22;\n\n\t\tPaint.Pen = pen.WithAlphaMultiplied( opacity );\n\t\tPaint.SetDefaultFont( weight: 700 );\n\t\tr.Left += Paint.DrawText( r, name, TextFlag.LeftCenter ).Width + 4;\n\n\t\tPaint.SetDefaultFont();\n\n\t\tif ( isLoading )\n\t\t{\n\t\t\tPaint.Pen = Theme.Blue;\n\t\t\tPaint.DrawIcon( r, \"access_time_filled\", iconSize, TextFlag.LeftCenter );\n\t\t\tr.Left += 22;\n\t\t}\n\n\t\tif ( isNetworkRoot && Value.Network.OwnerId != Guid.Empty )\n\t\t{\n\t\t\tvar connection = Connection.Find( Value.Network.OwnerId );\n\t\t\tif ( connection is null )\n\t\t\t{\n\t\t\t\tPaint.Pen = Theme.Blue;\n\t\t\t\tPaint.DrawText( r, $\"Unknown Owner - {Value.Network.OwnerId}\", TextFlag.LeftCenter );\n\t\t\t\tr.Left += 22;\n\t\t\t}\n\t\t\telse\n\t\t\t{\n\t\t\t\tPaint.Pen = Theme.Blue;\n\t\t\t\tPaint.DrawText( r, $\"{connection.DisplayName}\", TextFlag.LeftCenter );\n\t\t\t\tr.Left += 22;\n\t\t\t}\n\n\n\t\t}\n\n\t\tif ( Value.Tags.Has( \"hidden\" ) )\n\t\t{\n\t\t\tvar eyeRect = item.Rect;\n\t\t\teyeRect.Right -= 4;\n\t\t\teyeRect.Left = eyeRect.Right - 18;\n\n\t\t\tPaint.Pen = Theme.TextControl;\n\t\t\tPaint.DrawIcon( eyeRect, \"visibility_off\", 14, TextFlag.Center );\n\t\t}\n\t}\n\n}\n\n"
}
]
}