Material palette groups (#3596)

https://files.facepunch.com/louie/1b1511b1/sbox-dev_yO6MOKHQIG.mp4
This commit is contained in:
bakscratch
2025-12-16 14:09:07 +00:00
committed by GitHub
parent b2122ed23e
commit 10c74fa037
3 changed files with 305 additions and 112 deletions

View File

@@ -1,4 +1,6 @@
namespace Editor.MeshEditor; using Sandbox.UI;
namespace Editor.MeshEditor;
class ActiveMaterialWidget : ControlWidget class ActiveMaterialWidget : ControlWidget
{ {
@@ -10,29 +12,26 @@ class ActiveMaterialWidget : ControlWidget
public ActiveMaterialWidget( SerializedProperty property ) : base( property ) public ActiveMaterialWidget( SerializedProperty property ) : base( property )
{ {
FixedHeight = 220; FixedHeight = 220;
Layout = Layout.Row(); Layout = Layout.Row();
Layout.Margin = 8;
ToolTip = ""; ToolTip = "";
_materialWidget = Layout.Add( new MaterialWidget() );
_materialWidget.ToolTip = "Active Material";
_materialWidget.FixedSize = FixedHeight - 26;
_materialWidget.Cursor = CursorShape.Finger;
Layout.AddStretchCell( 1 );
_paletteStrip = Layout.Add( new MaterialPaletteWidget() ); _paletteStrip = Layout.Add( new MaterialPaletteWidget() );
_paletteStrip.MaterialClicked += OnPaletteMaterialClicked; _paletteStrip.MaterialClicked += OnPaletteMaterialClicked;
_paletteStrip.FixedHeight = FixedHeight - 26; _paletteStrip.FixedHeight = FixedHeight - 8;
_paletteStrip.FixedWidth = 64;
_paletteStrip.GetActiveMaterial = () => _materialWidget.Material; _paletteStrip.GetActiveMaterial = () => _materialWidget.Material;
Layout.AddStretchCell( 1 ); Layout.AddSpacingCell( 1 );
_materialWidget = Layout.Add( new MaterialWidget() );
_materialWidget.ToolTip = "Active Material";
_materialWidget.FixedSize = FixedHeight - 22;
_materialWidget.Cursor = CursorShape.Finger;
Frame(); Frame();
} }
protected override void OnPaint() protected override void OnPaint()
{ {
// nothing // nothing
@@ -99,7 +98,7 @@ class ActiveMaterialWidget : ControlWidget
base.OnMouseClick( e ); base.OnMouseClick( e );
// If we are selecting the Material Widget continue. (Probably better way of doing this) // If we are selecting the Material Widget continue. (Probably better way of doing this)
if ( !_materialWidget.LocalRect.IsInside( e.LocalPosition ) ) if ( _materialWidget.ContentRect.IsInside( e.LocalPosition ) )
return; return;
if ( ReadOnly ) return; if ( ReadOnly ) return;

View File

@@ -33,13 +33,13 @@ public class MaterialWidget : Widget
if ( icon is not null ) if ( icon is not null )
{ {
Paint.Draw( LocalRect.Shrink( 2 ), icon ); Paint.Draw( LocalRect, icon );
} }
if ( ShowFilename && asset is not null ) if ( ShowFilename && asset is not null )
{ {
Paint.SetDefaultFont( 7 ); Paint.SetDefaultFont( 7 );
Theme.DrawFilename( LocalRect.Shrink( 4 ), asset.RelativePath, TextFlag.LeftBottom, Color.White ); Theme.DrawFilename( LocalRect, asset.RelativePath, TextFlag.LeftBottom, Color.White );
} }
} }

View File

@@ -2,42 +2,281 @@
public class MaterialPaletteWidget : Widget public class MaterialPaletteWidget : Widget
{ {
const int MaxRecentMaterials = 12; const int MaxCells = 12;
const int RecentColumns = 6; const int PaletteColumns = 6;
readonly List<Material> _recentMaterials = new(); readonly List<Material> _recentMaterials = new();
readonly RecentMaterialSlotWidget[] _slots; readonly PaletteMaterialSlotWidget[] _slots;
readonly List<string> _paletteNames = new();
string _paletteId = "Default";
public event Action<Material> MaterialClicked; public event Action<Material> MaterialClicked;
public Func<Material> GetActiveMaterial { get; set; } public Func<Material> GetActiveMaterial { get; set; }
public string PaletteId
{
get => _paletteId;
set
{
if ( string.IsNullOrEmpty( value ) || _paletteId == value )
return;
_paletteId = value;
SaveActivePalette();
LoadPaletteFromCookie();
Update();
}
}
public MaterialPaletteWidget() public MaterialPaletteWidget()
{ {
Layout = Layout.Column(); Layout = Layout.Column();
Layout.Margin = 0; Layout.Alignment = TextFlag.Center;
var grid = Layout.Grid(); var grid = Layout.Grid();
grid.Spacing = 2; grid.Spacing = 2;
Layout.Add( grid ); Layout.Add( grid );
_slots = new RecentMaterialSlotWidget[MaxRecentMaterials]; _slots = new PaletteMaterialSlotWidget[MaxCells];
for ( int i = 0; i < MaxRecentMaterials; i++ ) for ( int i = 0; i < MaxCells; i++ )
{ {
var col = i / RecentColumns; var col = i / PaletteColumns;
var row = i % RecentColumns; var row = i % PaletteColumns;
var slot = new RecentMaterialSlotWidget( this ) var slot = new PaletteMaterialSlotWidget( this )
{ {
ShowFilename = false, ShowFilename = false,
FixedSize = 32 FixedSize = 32
}; };
_slots[i] = slot; _slots[i] = slot;
grid.AddCell( col, row, slot ); grid.AddCell( col, row, slot );
} }
LoadPalettes();
LoadPaletteFromCookie();
}
void LoadPalettes()
{
_paletteNames.Clear();
string rawNames;
try { rawNames = ProjectCookie.Get( "MeshEditor.MaterialPalettes.Names", string.Empty ); }
catch { rawNames = string.Empty; }
if ( string.IsNullOrWhiteSpace( rawNames ) )
{
_paletteNames.Add( "Default" );
}
else
{
foreach ( var name in rawNames.Split( ';', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries ) )
{
if ( !_paletteNames.Contains( name ) )
_paletteNames.Add( name );
}
if ( _paletteNames.Count == 0 )
_paletteNames.Add( "Default" );
}
try { _paletteId = ProjectCookie.Get( "MeshEditor.MaterialPalettes.Active", _paletteNames[0] ); }
catch { _paletteId = _paletteNames[0]; }
if ( !_paletteNames.Contains( _paletteId ) )
_paletteId = _paletteNames[0];
}
void SavePalettes()
{
ProjectCookie.Set( "MeshEditor.MaterialPalettes.Names", string.Join( ";", _paletteNames ) );
SaveActivePalette();
}
void SaveActivePalette()
{
ProjectCookie.Set( "MeshEditor.MaterialPalettes.Active", _paletteId ?? string.Empty );
}
protected override void OnContextMenu( ContextMenuEvent e )
{
base.OnContextMenu( e );
var m = new ContextMenu();
AddPaletteMenu( m );
m.OpenAtCursor( false );
e.Accepted = true;
}
internal void AddPaletteMenu( ContextMenu m )
{
LoadPalettes();
var p = m.AddMenu( "Palettes", "palette" );
foreach ( var name in _paletteNames )
{
var localName = name;
var icon = (localName == _paletteId) ? "check" : "palette";
p.AddOption( localName, icon, () => PaletteId = localName );
}
p.AddSeparator();
p.AddOption( "New Palette…", "add", ShowCreatePalettePopup );
p.AddOption( "Rename Palette…", "edit", () => ShowRenamePalettePopup( _paletteId ) ).Enabled = _paletteNames.Count > 0;
p.AddOption( "Duplicate Palette", "content_copy", () => DuplicatePalette( _paletteId ) ).Enabled = _paletteNames.Count > 0;
var del = p.AddOption( "Delete Palette", "delete", () => DeletePalette( _paletteId ) );
del.Enabled = _paletteNames.Count > 1;
}
void ShowCreatePalettePopup()
{
var popup = new PopupWidget( this );
popup.FixedWidth = 220;
popup.Layout = Layout.Column();
popup.Layout.Margin = 8;
popup.Layout.Spacing = 4;
_ = popup.Layout.Add( new Label.Small( "New palette" ) );
var entry = popup.Layout.Add( new LineEdit( popup ) );
entry.FixedHeight = Theme.RowHeight;
entry.PlaceholderText = "Palette name…";
void Commit()
{
var name = entry.Value?.Trim();
if ( string.IsNullOrEmpty( name ) ) { popup.Destroy(); return; }
if ( _paletteNames.Contains( name ) ) { popup.Destroy(); return; }
_paletteNames.Add( name );
_paletteId = name;
SavePalettes();
LoadPaletteFromCookie();
popup.Destroy();
}
entry.ReturnPressed += Commit;
popup.OpenAtCursor();
entry.Focus();
}
void ShowRenamePalettePopup( string oldName )
{
if ( string.IsNullOrEmpty( oldName ) )
return;
var popup = new PopupWidget( this );
popup.FixedWidth = 220;
popup.Layout = Layout.Column();
popup.Layout.Margin = 8;
popup.Layout.Spacing = 4;
_ = popup.Layout.Add( new Label.Small( "Rename palette" ) );
var entry = popup.Layout.Add( new LineEdit( popup ) );
entry.FixedHeight = Theme.RowHeight;
entry.Value = oldName;
void Commit()
{
var newName = entry.Value?.Trim();
if ( string.IsNullOrEmpty( newName ) || newName == oldName ) { popup.Destroy(); return; }
if ( _paletteNames.Contains( newName ) ) { popup.Destroy(); return; }
RenamePalette( oldName, newName );
popup.Destroy();
}
entry.ReturnPressed += Commit;
popup.OpenAtCursor();
entry.Focus();
}
void RenamePalette( string oldName, string newName )
{
var idx = _paletteNames.IndexOf( oldName );
if ( idx < 0 ) return;
_paletteNames[idx] = newName;
var oldKey = $"MeshEditor.MaterialPalette.{oldName}";
var newKey = $"MeshEditor.MaterialPalette.{newName}";
try
{
var data = ProjectCookie.Get( oldKey, string.Empty );
ProjectCookie.Set( newKey, data );
ProjectCookie.Set( oldKey, string.Empty );
}
catch { }
if ( _paletteId == oldName )
_paletteId = newName;
SavePalettes();
LoadPaletteFromCookie();
}
void DuplicatePalette( string sourceName )
{
if ( string.IsNullOrEmpty( sourceName ) )
return;
var baseName = $"{sourceName} Copy";
var newName = baseName;
int counter = 2;
while ( _paletteNames.Contains( newName ) )
newName = $"{baseName} {counter++}";
_paletteNames.Add( newName );
var srcKey = $"MeshEditor.MaterialPalette.{sourceName}";
var dstKey = $"MeshEditor.MaterialPalette.{newName}";
try
{
var data = ProjectCookie.Get( srcKey, string.Empty );
ProjectCookie.Set( dstKey, data );
}
catch { }
_paletteId = newName;
SavePalettes();
LoadPaletteFromCookie();
}
void DeletePalette( string name )
{
if ( _paletteNames.Count <= 1 )
return;
var idx = _paletteNames.IndexOf( name );
if ( idx < 0 )
return;
_paletteNames.RemoveAt( idx );
var key = $"MeshEditor.MaterialPalette.{name}";
try { ProjectCookie.Set( key, string.Empty ); }
catch { }
_paletteId = _paletteNames[Math.Clamp( idx - 1, 0, _paletteNames.Count - 1 )];
SavePalettes();
LoadPaletteFromCookie(); LoadPaletteFromCookie();
} }
@@ -48,20 +287,14 @@ public class MaterialPaletteWidget : Widget
var path = material.ResourcePath; var path = material.ResourcePath;
if ( !string.IsNullOrEmpty( path ) ) if ( !string.IsNullOrEmpty( path ) )
{
_recentMaterials.RemoveAll( m => m is not null && m.ResourcePath == path ); _recentMaterials.RemoveAll( m => m is not null && m.ResourcePath == path );
}
else else
{
_recentMaterials.RemoveAll( m => m == material ); _recentMaterials.RemoveAll( m => m == material );
}
_recentMaterials.Insert( 0, material ); _recentMaterials.Insert( 0, material );
if ( _recentMaterials.Count > MaxRecentMaterials ) if ( _recentMaterials.Count > _slots.Length )
{
_recentMaterials.RemoveAt( _recentMaterials.Count - 1 ); _recentMaterials.RemoveAt( _recentMaterials.Count - 1 );
}
UpdateSlots(); UpdateSlots();
SavePaletteToCookie(); SavePaletteToCookie();
@@ -70,16 +303,7 @@ public class MaterialPaletteWidget : Widget
void UpdateSlots() void UpdateSlots()
{ {
for ( int i = 0; i < _slots.Length; i++ ) for ( int i = 0; i < _slots.Length; i++ )
{ _slots[i].Material = i < _recentMaterials.Count ? _recentMaterials[i] : null;
if ( i < _recentMaterials.Count )
{
_slots[i].Material = _recentMaterials[i];
}
else
{
_slots[i].Material = null;
}
}
} }
internal void SlotClickedApply( Material material ) internal void SlotClickedApply( Material material )
@@ -88,7 +312,7 @@ public class MaterialPaletteWidget : Widget
MaterialClicked?.Invoke( material ); MaterialClicked?.Invoke( material );
} }
private void SlotSetMaterial( RecentMaterialSlotWidget slot, Material mat ) private void SlotSetMaterial( PaletteMaterialSlotWidget slot, Material mat )
{ {
if ( slot is null ) return; if ( slot is null ) return;
@@ -106,21 +330,18 @@ public class MaterialPaletteWidget : Widget
SavePaletteToCookie(); SavePaletteToCookie();
} }
private void SlotAssignFromActive( RecentMaterialSlotWidget slot ) private void SlotAssignFromActive( PaletteMaterialSlotWidget slot )
{ {
if ( GetActiveMaterial is null ) if ( GetActiveMaterial is null ) return;
return;
var mat = GetActiveMaterial(); var mat = GetActiveMaterial();
if ( mat is null ) if ( mat is null ) return;
return;
SlotSetMaterial( slot, mat ); SlotSetMaterial( slot, mat );
} }
private void SlotAssignMaterial( RecentMaterialSlotWidget slot ) private void SlotAssignMaterial( PaletteMaterialSlotWidget slot )
{ {
// Open a picker just for materials and assign the result to this slot.
var picker = AssetPicker.Create( null, AssetType.Material, new AssetPicker.PickerOptions() var picker = AssetPicker.Create( null, AssetType.Material, new AssetPicker.PickerOptions()
{ {
EnableMultiselect = false EnableMultiselect = false
@@ -142,10 +363,7 @@ public class MaterialPaletteWidget : Widget
picker.Show(); picker.Show();
} }
private void SlotClear( RecentMaterialSlotWidget slot ) private void SlotClear( PaletteMaterialSlotWidget slot ) => SlotSetMaterial( slot, null );
{
SlotSetMaterial( slot, null );
}
void SavePaletteToCookie() void SavePaletteToCookie()
{ {
@@ -159,24 +377,14 @@ public class MaterialPaletteWidget : Widget
.Take( _slots.Length ) .Take( _slots.Length )
.Select( m => m is not null ? m.ResourcePath ?? string.Empty : string.Empty ); .Select( m => m is not null ? m.ResourcePath ?? string.Empty : string.Empty );
var data = string.Join( ";", parts ); ProjectCookie.Set( $"MeshEditor.MaterialPalette.{_paletteId}", string.Join( ";", parts ) );
// Maybe this should be scene specific?
ProjectCookie.Set( "MeshEditor.MaterialPalette", data );
} }
void LoadPaletteFromCookie() void LoadPaletteFromCookie()
{ {
string data; string data;
try { data = ProjectCookie.Get( $"MeshEditor.MaterialPalette.{_paletteId}", string.Empty ); }
try catch { data = string.Empty; }
{
data = ProjectCookie.Get( "MeshEditor.MaterialPalette", string.Empty );
}
catch
{
data = string.Empty;
}
_recentMaterials.Clear(); _recentMaterials.Clear();
@@ -188,7 +396,7 @@ public class MaterialPaletteWidget : Widget
var parts = data.Split( ';' ); var parts = data.Split( ';' );
for ( int i = 0; i < MaxRecentMaterials; i++ ) for ( int i = 0; i < _slots.Length; i++ )
{ {
if ( i >= parts.Length || string.IsNullOrWhiteSpace( parts[i] ) ) if ( i >= parts.Length || string.IsNullOrWhiteSpace( parts[i] ) )
{ {
@@ -204,23 +412,22 @@ public class MaterialPaletteWidget : Widget
continue; continue;
} }
var mat = asset.LoadResource( typeof( Material ) ) as Material; _recentMaterials.Add( asset.LoadResource( typeof( Material ) ) as Material );
_recentMaterials.Add( mat );
} }
UpdateSlots(); UpdateSlots();
} }
class RecentMaterialSlotWidget : MaterialWidget class PaletteMaterialSlotWidget : MaterialWidget
{ {
readonly MaterialPaletteWidget _strip; readonly MaterialPaletteWidget _strip;
bool _isDownloading; bool _isDownloading;
bool _isValidDropHover; bool _isValidDropHover;
public RecentMaterialSlotWidget( MaterialPaletteWidget strip )
public PaletteMaterialSlotWidget( MaterialPaletteWidget strip )
{ {
_strip = strip; _strip = strip;
ToolTip = ""; ToolTip = "";
AcceptDrops = true; AcceptDrops = true;
Cursor = CursorShape.Finger; Cursor = CursorShape.Finger;
} }
@@ -229,26 +436,17 @@ public class MaterialPaletteWidget : Widget
{ {
base.OnMouseClick( e ); base.OnMouseClick( e );
if ( Material is not null ) if ( Material.IsValid() ) _strip.SlotClickedApply( Material );
{ else _strip.SlotAssignFromActive( this );
_strip.SlotClickedApply( Material );
}
else
{
_strip.SlotAssignFromActive( this );
}
} }
protected override void OnContextMenu( ContextMenuEvent e ) protected override void OnContextMenu( ContextMenuEvent e )
{ {
var m = new ContextMenu(); var m = new ContextMenu();
bool hasMaterial = Material is not null; bool hasMaterial = Material.IsValid();
var text = hasMaterial ? "Change Material" : "Set Material"; var text = hasMaterial ? "Change Material" : "Set Material";
m.AddOption( text, "format_color_fill", () => m.AddOption( text, "format_color_fill", () => _strip.SlotAssignMaterial( this ) );
{
_strip.SlotAssignMaterial( this );
} );
m.AddSeparator(); m.AddSeparator();
@@ -263,10 +461,10 @@ public class MaterialPaletteWidget : Widget
} }
} }
m.AddOption( "Clear", "backspace", () => _strip.AddPaletteMenu( m );
{ m.AddSeparator();
_strip.SlotClear( this );
} ).Enabled = hasMaterial; m.AddOption( "Clear", "backspace", () => _strip.SlotClear( this ) ).Enabled = hasMaterial;
m.OpenAtCursor( false ); m.OpenAtCursor( false );
e.Accepted = true; e.Accepted = true;
@@ -300,18 +498,25 @@ public class MaterialPaletteWidget : Widget
Paint.SetBrushAndPen( Color.Transparent, Color.White ); Paint.SetBrushAndPen( Color.Transparent, Color.White );
Paint.DrawRect( controlRect, 0 ); Paint.DrawRect( controlRect, 0 );
} }
} }
else else
{ {
var baseFill = Theme.Text.WithAlpha( 0.01f ); var baseFill = Theme.ControlBackground;
var baseLine = Theme.Text.WithAlpha( 0.1f ); var baseLine = Color.Transparent;
var iconColor = Theme.Text.WithAlpha( 0.1f ); var iconColor = Theme.TextLight;
if ( Paint.HasMouseOver ) if ( Paint.HasMouseOver )
{ {
baseFill = Theme.Text.WithAlpha( 0.04f ); baseFill = Theme.ControlBackground;
baseLine = Theme.Text.WithAlpha( 0.2f ); baseLine = Color.Transparent;
iconColor = Theme.Text.WithAlpha( 0.2f ); iconColor = Theme.TextLight.Lighten( 0.8f );
}
else
{
baseFill = Theme.ControlBackground;
baseLine = Color.Transparent;
iconColor = Theme.TextLight;
} }
if ( _isValidDropHover ) if ( _isValidDropHover )
@@ -435,6 +640,9 @@ public class MaterialPaletteWidget : Widget
_strip.SlotSetMaterial( this, droppedMaterial ); _strip.SlotSetMaterial( this, droppedMaterial );
ev.Action = DropAction.Link; ev.Action = DropAction.Link;
_isValidDropHover = false;
Update();
} }
async Task AssignFromUrlAsync( string identUrl ) async Task AssignFromUrlAsync( string identUrl )
@@ -452,20 +660,7 @@ public class MaterialPaletteWidget : Widget
if ( mat is null ) if ( mat is null )
return; return;
Material = mat; _strip.SlotSetMaterial( this, mat );
var index = Array.IndexOf( _strip._slots, this );
if ( index >= 0 )
{
if ( index >= _strip._recentMaterials.Count )
{
while ( _strip._recentMaterials.Count <= index )
_strip._recentMaterials.Add( null );
}
_strip._recentMaterials[index] = mat;
_strip.SavePaletteToCookie();
}
} }
finally finally
{ {
@@ -491,7 +686,6 @@ public class MaterialPaletteWidget : Widget
} }
} }
} }
file class TextureTooltip : Widget file class TextureTooltip : Widget
{ {
Widget target; Widget target;