Files
sbox-public/engine/Sandbox.Tools/Editor/EditorMainWindow.cs
s&box team 71f266059a Open source release
This commit imports the C# engine code and game files, excluding C++ source code.

[Source-Commit: ceb3d758046e50faa6258bc3b658a30c97743268]
2025-11-24 09:05:18 +00:00

558 lines
15 KiB
C#

using NativeEngine;
using System.Diagnostics;
namespace Editor;
public class EditorMainWindow : DockWindow
{
internal static EditorMainWindow Current;
Menu FileMenu { get; init; }
public Menu AppsMenu { get; init; }
public Menu ViewsMenu { get; init; }
public Menu GameMenu { get; init; }
Menu RecentScenesMenu { get; init; }
Menu EditMenu { get; init; }
internal ConsoleWidget Console => ConsoleWidget.Instance;
protected override bool OnClose()
{
// Check the editor scenes for one with unsaved changes
if ( GetUnsavedScenes().Any() || GetUnsavedResources().Any() )
{
ShowCloseDialog();
return false;
}
ProjectCookie?.Set( $"gizmo.settings", EditorScene.GizmoSettings );
return true;
}
private static List<SceneEditorSession> GetUnsavedScenes()
{
return SceneEditorSession.All.Where( x => x.HasUnsavedChanges ).ToList();
}
private static List<GameResource> GetUnsavedResources()
{
return ResourceLibrary.GetAll<GameResource>().Where( x =>
{
var asset = AssetSystem.FindByPath( x.ResourcePath );
var compiledFile = asset?.GetCompiledFile( true );
var isCloud = compiledFile != null && compiledFile.Contains( ".sbox/cloud/" );
return x.HasUnsavedChanges && !isCloud;
}
).ToList();
}
public void ShowCloseDialog()
{
var popup = new PopupDialogWidget( "💾" );
var saveableScenes = GetUnsavedScenes();
var saveableResources = GetUnsavedResources();
popup.FixedWidth = 512;
popup.WindowTitle = "Unsaved Changes";
popup.MessageLabel.Text = $"Do you want to save the changes you made?";
if ( saveableScenes.Any() )
{
popup.MessageLabel.Text += $"\n\nScenes:\n\n{string.Join( "\n", saveableScenes.Select( x => x.Scene.Name ) )}";
}
if ( saveableResources.Any() )
{
popup.MessageLabel.Text += $"\n\nResources:\n\n{string.Join( "\n", saveableResources.Select( x => x.ResourceName ) )}";
}
popup.ButtonLayout.Spacing = 4;
popup.ButtonLayout.AddStretchCell();
popup.ButtonLayout.Add( new Button( "Save" )
{
Clicked = () =>
{
// Save all the scenes
saveableScenes.ForEach( x => x.Save( false ) );
saveableResources.ForEach( x =>
{
var asset = AssetSystem.FindByPath( x.ResourcePath );
asset.SaveToDisk( x );
asset.Compile( false );
} );
popup.Destroy();
Destroy();
foreach ( var session in saveableScenes )
{
session.Destroy();
}
}
} );
popup.ButtonLayout.Add( new Button( "Don't Save" ) { Clicked = () => { Destroy(); popup.Destroy(); } } );
popup.ButtonLayout.Add( new Button( "Cancel" ) { Clicked = () => { EditorMainWindow.showLauncherOnExit = false; popup.Destroy(); } } );
popup.SetModal( true, true );
popup.Hide();
popup.Show();
}
static bool isEngineLoggingVerbose;
private Option save;
private Option saveAs;
private Option discard;
private Option undoOption;
private Option redoOption;
internal EditorMainWindow()
{
Current = this;
Visible = false;
Enabled = false;
WindowTitle = "s&box editor";
DeleteOnClose = true;
FullScreenManager = new();
DockManager.OnLayoutLoaded += OnDockLayoutLoaded;
{
FileMenu = MenuBar.AddMenu( "File" );
FileMenu.AddOption( "New Scene", "note_add", EditorScene.NewScene, "editor.new" );
FileMenu.AddOption( "Open", "file_open", EditorScene.Open, "editor.open" );
RecentScenesMenu = FileMenu.AddMenu( "Open Recent", "restore" );
RecentScenesMenu.AboutToShow += BuildRecentScenes;
FileMenu.AddSeparator();
save = FileMenu.AddOption( "Save", "save", EditorScene.SaveSession, "editor.save" );
saveAs = FileMenu.AddOption( "Save As..", "save_as", EditorScene.SaveSessionAs, "editor.save-as" );
FileMenu.AddOption( "Save All", null, EditorScene.SaveAllSessions, "editor.save-all" );
discard = FileMenu.AddOption( "Discard Changes", "auto_delete", EditorScene.Discard );
FileMenu.AddSeparator();
FileMenu.AddOption( "Close Project", "disabled_by_default", () => { showLauncherOnExit = true; Quit(); } );
FileMenu.AddOption( "Quit", "logout", Quit, "editor.quit" );
FileMenu.AboutToShow += OnFileMenuAboutToShow;
}
{
EditMenu = MenuBar.AddMenu( "Edit" );
undoOption = EditMenu.AddOption( "Undo", "undo", Undo, "editor.undo" );
redoOption = EditMenu.AddOption( "Redo", "redo", Redo, "editor.redo" );
EditMenu.AddSeparator();
EditMenu.AddOption( "Cut", "cut", EditorScene.Cut, "editor.cut" );
EditMenu.AddOption( "Copy", "copy", EditorScene.Copy, "editor.copy" );
EditMenu.AddOption( "Paste", "paste", EditorScene.Paste, "editor.paste" );
EditMenu.AddOption( "Paste As Child", null, EditorScene.PasteAsChild, "editor.paste-as-child" );
EditMenu.AboutToShow += OnEditMenuAboutToShow;
}
{
ViewsMenu = MenuBar.AddMenu( "View" );
ViewsMenu.AboutToShow += OnViewsMenuAboutToShow;
}
{
var projectMenu = MenuBar.AddMenu( "Project" );
projectMenu.AddOption( "Play", "play_arrow", EditorScene.TogglePlay, "editor.toggle-play" );
projectMenu.AddOption( new Option()
{
Checkable = true,
Checked = EditorScene.PlayMode,
Toggled = ( b ) => EditorScene.PlayMode = b,
Text = "Play in Game Mode",
Icon = "sports_esports"
} );
projectMenu.AddSeparator();
projectMenu.AddOption( "Open Project Folder", "folder", () => EditorUtility.OpenFolder( Project.Current.GetRootPath() ) );
projectMenu.AddOption( "Open Solution", "integration_instructions", OpenSolution, "editor.open-solution" );
}
{
MenuBar.AddMenu( "Scene" );
AppsMenu = MenuBar.AddMenu( "Tools" );
}
{
MenuBar.AddMenu( "Settings" );
var debug = MenuBar.AddMenu( "Debug" );
debug.AddOption( "Widget Debugger", null, () => g_pBindSystemGlobalHotkeys.Cmd_ShowWidgetDebugger() );
debug.AddOption( "Input Debugger", null, () => g_pBindSystemGlobalHotkeys.Cmd_ShowInputDebugger() );
debug.AddSeparator();
var help = MenuBar.AddMenu( "Help" );
help.AddOption( "Open Log Folder", "source", () => EditorUtility.OpenFolder( FileSystem.Root.GetFullPath( "/logs/" ) ) );
help.AddOption( "Developer Documentation", "article", () => EditorUtility.OpenFolder( "https://sbox.game/dev/" ) );
help.AddOption( "Report a Bug", "bug_report", () => EditorUtility.OpenFolder( "https://github.com/Facepunch/sbox-issues" ) );
help.AddSeparator();
help.AddOption( "About s&box editor", "info", () =>
{
var aboutWidget = new AboutWidget();
aboutWidget.SetModal( true, true );
aboutWidget.Show();
} );
help.AddSeparator();
var di = DisplayInfo.ForEnumValues<LogLevel>();
var rootRule = Logging.GetDefaultLevel();
{
var o = help.AddOption( "Trace Logging" );
o.Checkable = true;
o.FetchCheckedState = () => Logging.GetDefaultLevel() == LogLevel.Trace;
o.Toggled = ( b ) =>
{
Logging.SetRule( "*", b ? LogLevel.Trace : LogLevel.Info );
EditorCookie.Set( "DefaultLoggingLevel", b ? LogLevel.Trace : LogLevel.Info );
};
}
{
var o = help.AddOption( "Verbose Engine Logging" );
o.Checkable = true;
o.FetchCheckedState = () => isEngineLoggingVerbose;
o.Toggled = ( b ) =>
{
EngineGlue.SetEngineLoggingVerbose( b );
isEngineLoggingVerbose = b;
};
}
{
var o = help.AddOption( "Verbose Hotload Logging" );
o.Checkable = true;
o.FetchCheckedState = () => HotloadManager.hotload_log == 2;
o.Toggled = ( b ) =>
{
HotloadManager.hotload_log = b ? 2 : 0;
};
}
}
EditorWindow = this;
}
[Shortcut( "editor.open-solution", "CTRL+P", ShortcutType.Window )]
void OpenSolution()
{
CodeEditor.OpenSolution();
}
[Shortcut( "editor.undo", "CTRL+Z", ShortcutType.Window )]
static void Undo()
{
using ( SceneEditorSession.Scope() )
{
if ( SceneEditorSession.Active.IsUndoScopeOpen )
{
if ( EditorPreferences.UndoSounds )
{
EditorUtility.PlayRawSound( "sounds/editor/fail.wav" );
}
return;
}
SceneEditorSession.Active.UndoSystem.Undo();
}
}
[Shortcut( "editor.redo", "CTRL+Y", ShortcutType.Window )]
static void Redo()
{
using ( SceneEditorSession.Scope() )
{
if ( SceneEditorSession.Active.IsUndoScopeOpen )
{
if ( EditorPreferences.UndoSounds )
{
EditorUtility.PlayRawSound( "sounds/editor/fail.wav" );
}
return;
}
SceneEditorSession.Active.UndoSystem.Redo();
}
}
[Shortcut( "editor.quit", "CTRL+Q", ShortcutType.Window )]
static void Quit()
{
Current?.Close();
}
[Shortcut( "editor.video", "F6", ShortcutType.Window )]
static void ToggleVideo()
{
if ( Game.IsPlaying ) return;
ConVarSystem.Run( "video" );
}
protected override void OnPaint()
{
if ( Game.IsPlaying )
{
Paint.ClearPen();
Paint.SetBrush( Theme.Overlay );
Paint.DrawRect( LocalRect );
return;
}
base.OnPaint();
}
internal void OnStartupLoadingFinished()
{
// Load gizmo settings
EditorScene.RestoreState();
// Register our menu bar and dock options, doesn't open anything
MenuAttribute.RegisterMenuBar( "Editor", MenuBar );
DockAttribute.RegisterWindow( "Editor", this );
// This will attempt to restore the last used layout (or default layout if first time)
// Which means it will create dock widgets and move them around
// This also involves creating SceneDocks which open scenes
StateCookie = "SboxSceneEditor";
// fucking horrible
string geometryCookie = EditorCookie.GetString( $"Window.{StateCookie}.Geometry", null );
if ( geometryCookie is null )
{
// no saved geometry, so default to center
Center();
}
EditorEvent.Run( "editor.created", this );
RebuildApps();
SetVisible( true );
// Register the main editor window as an SDL window and tell the input system it's the main window
// We need this for focusing and relative mouse capture mode
NativeEngine.InputSystem.RegisterWindowWithSDL( _widget.winId() );
NativeEngine.InputSystem.SetEditorMainWindow( _widget.winId() );
}
record struct LayoutFile( string Name, string Json );
protected override void RestoreDefaultDockLayout()
{
var layout = FileSystem.Config.ReadJsonOrDefault<LayoutFile>( $"/editor/layout/default.json", default );
if ( layout.Name is null ) return;
if ( layout.Json is null ) return;
DockManager.State = layout.Json;
}
/// <summary>
/// Called when the layout is loaded. We want to force all the scene views to be visible!
/// </summary>
void OnDockLayoutLoaded()
{
SceneEditorSession.OnEditorWindowRestoreLayout();
}
/// <summary>
/// Called when the console key is pressed while the game is focused. Should
/// do everything possible to switch to the actual console.
/// </summary>
internal void ConsoleFocus()
{
// focus the editor window instead of the game window
EditorWindow.Blur();
EditorWindow.Focus( true );
// focus the console input if it's visible. The console key
// is used to switch between game and editor too, so don't be
// heavy handed by forcing the console to be visible etc.
if ( Console?.Visible ?? false )
{
Console.Input.Focus();
}
}
internal static bool showLauncherOnExit = false;
public override void OnDestroyed()
{
// Unsubscribe from events
if ( DockManager != null ) DockManager.OnLayoutLoaded -= OnDockLayoutLoaded;
if ( RecentScenesMenu != null ) RecentScenesMenu.AboutToShow -= BuildRecentScenes;
if ( FileMenu != null ) FileMenu.AboutToShow -= OnFileMenuAboutToShow;
if ( ViewsMenu != null ) ViewsMenu.AboutToShow -= OnViewsMenuAboutToShow;
if ( EditMenu != null ) EditMenu.AboutToShow -= OnEditMenuAboutToShow;
base.OnDestroyed();
if ( Sandbox.Internal.GlobalToolsNamespace.EditorWindow != this )
return;
Sandbox.Internal.GlobalToolsNamespace.EditorWindow = null;
EditorUtility.Quit( showLauncherOnExit );
}
/// <summary>
/// Called once to create the editor
/// </summary>
internal void Startup()
{
Size = new Vector2( 1920, 1080 );
g_pToolFramework2.SetStallMonitorMainThreadWindow( _widget );
OnStartupLoadingFinished();
NativeEngine.EngineGlobal.UpdateWindowSize();
}
[Event( "refresh" )]
void RebuildApps()
{
AppsMenu?.Clear();
foreach ( var tool in EngineTools.All )
{
var option = AppsMenu.AddOption( tool.Name, tool.Icon, () => EngineTools.ShowTool( tool.Name ) );
option.StatusTip = tool.Description;
option.ToolTip = $"{tool.Name} - {tool.Description}";
}
AppsMenu.AddSeparator();
foreach ( var tool in EditorTypeLibrary.GetAttributes<EditorAppAttribute>().OrderBy( x => x.Title ) )
{
var option = AppsMenu.AddOption( tool.Title, tool.Icon, () => tool.Open() );
option.StatusTip = tool.Description;
option.ToolTip = $"{tool.Title} - {tool.Description}";
}
// Force a repaint
Update();
}
FullScreenManager FullScreenManager { get; set; }
/// <summary>
/// Is a widget currently the fullscreen widget
/// </summary>
public bool IsFullscreen( Widget widget )
{
return FullScreenManager.Widget == widget;
}
/// <summary>
/// Sets a widget as the fullscreen widget
/// </summary>
/// <param name="widget"></param>
/// <returns>whether or not the widget is now fullscreen</returns>
public bool SetFullscreen( Widget widget )
{
if ( FullScreenManager.Widget == widget || !widget.IsValid() )
{
FullScreenManager.Clear();
return false;
}
FullScreenManager.SetWidget( widget );
return FullScreenManager.Widget == widget;
}
[Event( "asset.selected" )]
public void OnAssetSelected( Asset asset )
{
// maybe an option for this if people bitch about it
EditorUtility.PlayAssetSound( asset );
}
public void SetVisible( bool visible )
{
if ( visible )
{
EditorWindow.Enabled = true;
EditorWindow.Visible = true;
EditorWindow.Focus();
}
else
{
EditorWindow.Enabled = false;
EditorWindow.Visible = false;
}
Update();
}
private void OnFileMenuAboutToShow()
{
save.Enabled = SceneEditorSession.Active?.HasUnsavedChanges ?? true;
saveAs.Enabled = SceneEditorSession.Active?.HasUnsavedChanges ?? true;
discard.Enabled = SceneEditorSession.Active?.HasUnsavedChanges ?? false;
}
private void OnEditMenuAboutToShow()
{
UpdateEditMenu( undoOption, redoOption );
}
private void OnViewsMenuAboutToShow()
{
CreateDynamicViewMenu( ViewsMenu );
}
public void UpdateEditorTitle( string title )
{
var projectName = Project.Current?.Config.Title ?? "No Project";
Title = $"{title} - {projectName} - s&box editor{(Global.IsApiConnected ? "" : " - offline")}";
}
void BuildRecentScenes()
{
RecentScenesMenu.Clear();
var recentScenes = AssetSystem.All
.Where( x => x.LastOpened is not null )
.Where( x => x.AssetType.FileExtension == "scene" || x.AssetType.FileExtension == "prefab" )
.OrderByDescending( x => x.LastOpened )
.Take( 20 );
foreach ( var asset in recentScenes )
{
var attribute = EditorTypeLibrary.GetAttributes<AssetTypeAttribute>().Where( x => x.Extension == asset.AssetType.FileExtension ).FirstOrDefault();
RecentScenesMenu.AddOption( $"{asset.Name} ({asset.Path})", string.Empty, () => { asset.OpenInEditor(); } );
}
}
/// <summary>
/// Updates Undo/Redo states and text
/// </summary>
void UpdateEditMenu( Option undoOption, Option redoOption )
{
if ( SceneEditorSession.Active?.UndoSystem.Back.TryPeek( out var undoEntry ) ?? false )
{
undoOption.Enabled = true;
undoOption.Text = $"Undo {undoEntry.Name}";
}
else
{
undoOption.Enabled = false;
undoOption.Text = "Undo";
}
if ( SceneEditorSession.Active?.UndoSystem.Forward.TryPeek( out var redoEntry ) ?? false )
{
redoOption.Enabled = true;
redoOption.Text = $"Redo {redoEntry.Name}";
}
else
{
redoOption.Enabled = false;
redoOption.Text = "Redo";
}
}
}