Editor/ShaderGraphPlus/ShaderTemplate/MainWindow.cs
using Editor;
using Sandbox.Helpers;
using Sandbox.Utility;
namespace ShaderGraphPlus;
[AttributeUsage( AttributeTargets.Property )]
internal sealed class TabPageAttribute : Attribute
{
public string Name { get; set; }
public TabPageAttribute( string name )
{
Name = name;
}
}
[EditorForAssetType( ShaderGraphPlusGlobals.ShaderTemplateAssetTypeExtension )]
[EditorApp( "Shader Template Editor", "gradient", "edit shader templates" )]
public sealed class ShaderTemplateEditorWindow : DockWindow, IAssetEditor
{
private ShaderTemplateResource _template;
private Asset _asset;
private bool _dirty = false;
private Widget _primaryDockCanvas;
private TabWidget _tabWidget;
private TextEditAreaWidget _textEditArea;
private UndoSystem _undoSystem;
private Option _undoMenuOption;
private Option _redoMenuOption;
private Option _undoOption;
private Option _redoOption;
private bool _undoRedoSet = false;
private ShaderDomain _lastShaderDomain;
public bool CanOpenMultipleAssets => false;
public ShaderTemplateEditorWindow() : base()
{
Size = new Vector2( 640, 480 );
InitUndo();
CreateToolBar();
CreateUI();
UpdateUndoRedoOptions();
Show();
StateCookie = ShaderGraphPlusGlobals.ShaderTemplateEditorStateCookieName;
CreateNew();
}
void IAssetEditor.SelectMember( string memberName )
{
throw new NotImplementedException();
}
public void AssetOpen( Asset asset )
{
if ( asset == null || string.IsNullOrWhiteSpace( asset.AbsolutePath ) )
return;
Open( asset.AbsolutePath );
}
private void InitUndo()
{
_undoSystem = new UndoSystem();
_undoSystem.Initialize();
_undoSystem.OnUndo += _ => UpdateUndoRedoOptions();
_undoSystem.OnRedo += _ => UpdateUndoRedoOptions();
}
private void CreateUI()
{
BuildMenuBar();
_primaryDockCanvas = new Widget( this );
_primaryDockCanvas.Layout = Layout.Column();
DockManager.SetCentralWidget( _primaryDockCanvas );
}
private void BuildMenuBar()
{
var file = MenuBar.AddMenu( "File" );
file.AddOption( "New", "common/new.png", New, "editor.new" ).StatusTip = "New Template";
file.AddOption( "Open", "common/open.png", Open, "editor.open" ).StatusTip = "Open Template";
file.AddOption( "Save", "common/save.png", Save, "editor.save" ).StatusTip = "Save Template";
file.AddOption( "Save As...", "common/save.png", SaveAs, "editor.save-as" ).StatusTip = "Save Template As...";
file.AddSeparator();
file.AddOption( "Reset Template Code To Default", "common/reset.png", () => ResetTemplateCodeToDefault() ).StatusTip = "Reset Template Code To Default";
file.AddSeparator();
file.AddOption( "Quit", null, Quit, "editor.quit" ).StatusTip = "Quit";
var edit = MenuBar.AddMenu( "Edit" );
_undoMenuOption = edit.AddOption( "Undo", "undo", () => Undo(), "editor.undo" );
_redoMenuOption = edit.AddOption( "Redo", "redo", () => Redo(), "editor.redo" );
var view = MenuBar.AddMenu( "View" );
view.AboutToShow += () => OnViewMenu( view );
}
private void CreateToolBar()
{
var toolBar = new ToolBar( this, ShaderGraphPlusGlobals.ShaderTemplateEditorToolbarName );
AddToolBar( toolBar, ToolbarPosition.Top );
toolBar.AddOption( "New", "common/new.png", New ).StatusTip = "New Template";
toolBar.AddOption( "Open", "common/open.png", Open ).StatusTip = "Open Template";
toolBar.AddOption( "Save", "common/save.png", () => Save() ).StatusTip = "Save Template";
toolBar.AddSeparator();
_undoOption = toolBar.AddOption( new Option( "Undo", "undo", () => Undo() ) );
_redoOption = toolBar.AddOption( new Option( "Redo", "redo", () => Redo() ) );
toolBar.AddSeparator();
}
private void OnViewMenu( Menu view )
{
view.Clear();
view.AddOption( "Restore To Default", "settings_backup_restore", ResetLayout );
view.AddSeparator();
foreach ( var dock in DockManager.DockTypes )
{
var o = view.AddOption( dock.Title, dock.Icon );
o.Checkable = true;
o.Checked = DockManager.IsDockOpen( dock.Title );
o.Toggled += ( b ) => DockManager.SetDockState( dock.Title, b );
}
}
protected override void BuildDefaultLayout()
{
}
private void Rebuild()
{
_primaryDockCanvas.Layout.Clear( true );
var so = _template.GetSerialized();
var oldestSerialized = _template.Serialize();
so.OnPropertyChanged += ( prop ) => OnPropertyUpdated( prop, oldestSerialized );
_tabWidget = new TabWidget( this );
_tabWidget.AddPage( "General", "settings", CreateTab( so, "General" ) );
if ( so.TryGetProperty( nameof( ShaderTemplateResource.Code ), out var codeProp ) )
{
_tabWidget.AddPage( "Code", "code", CreateCodeTab( codeProp ) );
}
else
{
throw new Exception( "Failed to get code SerilaizedProperty" );
}
_primaryDockCanvas.Layout.Add( _tabWidget );
}
private Widget CreateTab( SerializedObject serialized, string tabName )
{
var container = new Widget( null );
container.Layout = Layout.Column();
container.VerticalSizeMode = SizeMode.CanGrow;
var sheet = new ControlSheet();
sheet.AddObject( serialized, ( x ) =>
{
return x.TryGetAttribute<TabPageAttribute>( out var attrib ) && attrib.Name == tabName;
} );
container.Layout.Add( sheet );
container.Layout.AddStretchCell();
return container;
}
private Widget CreateCodeTab( SerializedProperty property )
{
var container = new Widget( null );
container.Layout = Layout.Column();
container.VerticalSizeMode = SizeMode.CanGrow;
_textEditArea = new TextEditAreaWidget( container );
_textEditArea.Value = property.GetValue<string>();
_textEditArea.ValueChanged = ( x ) =>
{
if ( !_undoRedoSet )
{
ExecuteUndoableAction( "Modify Code", () =>
{
_template.Code = x;
SetDirty();
} );
}
else
{
_template.Code = x;
SetDirty();
}
UpdateUndoRedoOptions();
};
container.Layout.Add( _textEditArea );
return container;
}
private void ResetTemplateCodeToDefault()
{
if ( _template == null || _textEditArea == null )
return;
ExecuteUndoableAction( "Reset Template Code To Default", () =>
{
switch ( _template.ShaderDomain )
{
case ShaderDomain.Surface:
_template.Code = ShaderTemplateSurface.Code;
break;
case ShaderDomain.Sky:
_template.Code = ShaderTemplateSky.Code;
break;
case ShaderDomain.PostProcess:
_template.Code = ShaderTemplatePostProcess.Code;
break;
}
_textEditArea.Value = _template.Code;
} );
SetDirty();
}
private void PromptSave( Action action )
{
if ( !_dirty )
{
action?.Invoke();
return;
}
var confirm = new PopupWindow(
$"Save Current {ShaderGraphPlusGlobals.ShaderTemplateAssetTypeName}", "The open template has unsaved changes. Would you like to save now?", "Cancel",
new Dictionary<string, Action>()
{
{ "No", () => action?.Invoke() },
{ "Yes", () => { if ( SaveInternal( false ) ) action?.Invoke(); } }
}
);
confirm.Show();
}
[Shortcut( "editor.new", "CTRL+N" )]
public void New()
{
PromptSave( CreateNew );
}
private void CreateNew()
{
_asset = null;
_template = new();
_dirty = false;
_lastShaderDomain = ShaderDomain.Surface;
WindowTitle = "untitled";
_undoSystem.Initialize();
Rebuild();
}
[Shortcut( "editor.quit", "CTRL+Q" )]
private void Quit()
{
Close();
}
[Shortcut( "editor.undo", "CTRL+Z", ShortcutType.Window )]
private bool Undo()
{
return _undoSystem.Undo();
}
[Shortcut( "editor.redo", "CTRL+Y", ShortcutType.Window )]
private bool Redo()
{
return _undoSystem.Redo();
}
private void Open()
{
var fd = new FileDialog( null )
{
Title = $"Open {ShaderGraphPlusGlobals.ShaderTemplateAssetTypeName}",
DefaultSuffix = $".{ShaderGraphPlusGlobals.ShaderTemplateAssetTypeExtension}"
};
fd.SetNameFilter( $"{ShaderGraphPlusGlobals.ShaderTemplateAssetTypeName} (*.{ShaderGraphPlusGlobals.ShaderTemplateAssetTypeExtension})" );
if ( !fd.Execute() )
return;
PromptSave( () => Open( fd.SelectedFile ) );
}
public void Open( string path, bool addToPath = true )
{
var asset = AssetSystem.FindByPath( path );
if ( asset == null )
return;
if ( asset == _asset )
{
Focus();
return;
}
var template = new ShaderTemplateResource();
template.Deserialize( System.IO.File.ReadAllText( path ), Path.GetFileName( path ) );
_asset = asset;
_template = template;
_dirty = false;
_lastShaderDomain = template.ShaderDomain;
WindowTitle = _asset?.Name;
_undoSystem.Initialize();
Rebuild();
}
[Shortcut( "editor.save-as", "CTRL+SHIFT+S" )]
public void SaveAs()
{
SaveInternal( true );
}
[Shortcut( "editor.save", "CTRL+S" )]
public void Save()
{
SaveInternal( false );
}
private string GetSavePath()
{
var fd = new FileDialog( null )
{
Title = $"Save {ShaderGraphPlusGlobals.ShaderTemplateAssetTypeName}",
DefaultSuffix = $".{ShaderGraphPlusGlobals.ShaderTemplateAssetTypeExtension}"
};
fd.SelectFile( $"untitled.{ShaderGraphPlusGlobals.ShaderTemplateAssetTypeExtension}" );
fd.SetFindFile();
fd.SetModeSave();
fd.SetNameFilter( $"S{ShaderGraphPlusGlobals.ShaderTemplateAssetTypeName} (*.{ShaderGraphPlusGlobals.ShaderTemplateAssetTypeExtension})" );
if ( !fd.Execute() )
return null;
return fd.SelectedFile;
}
private bool SaveInternal( bool saveAs )
{
var savePath = _asset == null || saveAs ? GetSavePath() : _asset.AbsolutePath;
if ( string.IsNullOrWhiteSpace( savePath ) )
return false;
// Write serialized file to asset file
System.IO.File.WriteAllText( savePath, _template.Serialize() );
if ( saveAs )
{
// If we're saving as, we want to register the new asset
_asset = null;
}
// Register asset if we haven't already
_asset ??= AssetSystem.RegisterFile( savePath );
if ( _asset == null )
{
SGPLogger.Warning( $"Unable to register asset {savePath}" );
return false;
}
MainAssetBrowser.Instance?.Local.UpdateAssetList();
Update();
_dirty = false;
WindowTitle = _asset?.Name;
EditorEvent.Run( ShaderGraphPlusGlobals.EditorEvents.ShaderTemplateUpdate, _asset?.RelativePath );
return true;
}
public void SetDirty()
{
Update();
UpdateUndoRedoOptions();
_dirty = true;
WindowTitle = $"{_asset?.Name ?? "untitled"}*";
}
private void UpdateUndoRedoOptions()
{
var undoOptionText = $"Undo {(_undoSystem.Back.Count > 0 ? $"\"{_undoSystem.Back.Peek().Name}\"" : "")}";
var redoOptionText = $"Redo {(_undoSystem.Forward.Count > 0 ? $"\"{_undoSystem.Forward.Peek().Name}\"" : "")}";
_undoMenuOption.Enabled = _undoSystem.Back.Count > 0;
_redoMenuOption.Enabled = _undoSystem.Forward.Count > 0;
_undoMenuOption.Text = undoOptionText;
_redoMenuOption.Text = redoOptionText;
_undoOption.Enabled = _undoSystem.Back.Count > 0;
_redoOption.Enabled = _undoSystem.Forward.Count > 0;
_undoOption.ToolTip = undoOptionText;
_redoOption.ToolTip = redoOptionText;
_undoOption.StatusTip = undoOptionText;
_redoOption.StatusTip = redoOptionText;
}
private DisposeAction UndoRedoScope()
{
bool prev = _undoRedoSet;
_undoRedoSet = true;
return new DisposeAction( () =>
{
_undoRedoSet = prev;
} );
}
public void ExecuteUndoableAction( string undoName, Action action )
{
var preState = _template.Serialize();
action.Invoke();
var postState = _template.Serialize();
_undoSystem.Insert( undoName,
() =>
{
using ( UndoRedoScope() )
{
_template.Deserialize( preState );
_textEditArea.Value = _template.Code;
SetDirty();
}
},
() =>
{
using ( UndoRedoScope() )
{
_template.Deserialize( postState );
_textEditArea.Value = _template.Code;
SetDirty();
}
} );
}
private void OnPropertyUpdated( SerializedProperty serializedProperty, string oldestSerialized )
{
if ( serializedProperty is null ) return;
var undoName = $"Modify {serializedProperty.Name}";
if ( serializedProperty.Name == nameof( ShaderTemplateResource.ShaderDomain ) )
{
var currentShaderDomain = serializedProperty.GetValue<ShaderDomain>();
if ( _lastShaderDomain != currentShaderDomain )
{
_lastShaderDomain = currentShaderDomain;
var popup = new PopupWindow(
"Shader Domian Changed", "Would you like to reset the template code to the selected shader domain default?", "No",
new Dictionary<string, Action>()
{
{ "Yes", () => ResetTemplateCodeToDefault() }
}
);
popup.Show();
}
}
var serializedTemplate = _template.Serialize();
if ( _undoSystem.Back.Count > 0 )
{
var lastUndo = _undoSystem.Back.Peek();
if ( lastUndo?.Name == undoName )
{
lastUndo = _undoSystem.Back.Pop();
_undoSystem.Insert( undoName, lastUndo.Undo, () =>
{
using ( UndoRedoScope() )
{
_template.Deserialize( serializedTemplate );
_textEditArea.Value = _template.Code;
SetDirty();
}
} );
}
else
{
_undoSystem.Insert( undoName, lastUndo.Redo, () =>
{
using ( UndoRedoScope() )
{
_template.Deserialize( serializedTemplate );
_textEditArea.Value = _template.Code;
SetDirty();
}
} );
}
}
else
{
_undoSystem.Insert( undoName, () =>
{
using ( UndoRedoScope() )
{
_template.Deserialize( oldestSerialized );
_textEditArea.Value = _template.Code;
SetDirty();
}
}, () =>
{
using ( UndoRedoScope() )
{
_template.Deserialize( serializedTemplate );
_textEditArea.Value = _template.Code;
SetDirty();
}
} );
}
SetDirty();
}
protected override bool OnClose()
{
if ( !_dirty )
{
return true;
}
var confirm = new PopupWindow(
$"Save Current {ShaderGraphPlusGlobals.ShaderTemplateAssetTypeName}", "The open template has unsaved changes. Would you like to save now?", "Cancel",
new Dictionary<string, Action>()
{
{ "No", () => { _dirty = false; Close(); } },
{ "Yes", () => { if ( SaveInternal( false ) ) Close(); } }
}
);
confirm.Show();
return false;
}
}