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 GetUnsavedScenes() { return SceneEditorSession.All.Where( x => x.HasUnsavedChanges ).ToList(); } private static List GetUnsavedResources() { return ResourceLibrary.GetAll().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(); 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( $"/editor/layout/default.json", default ); if ( layout.Name is null ) return; if ( layout.Json is null ) return; DockManager.State = layout.Json; } /// /// Called when the layout is loaded. We want to force all the scene views to be visible! /// void OnDockLayoutLoaded() { SceneEditorSession.OnEditorWindowRestoreLayout(); } /// /// Called when the console key is pressed while the game is focused. Should /// do everything possible to switch to the actual console. /// 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 ); } /// /// Called once to create the editor /// 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().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; } /// /// Is a widget currently the fullscreen widget /// public bool IsFullscreen( Widget widget ) { return FullScreenManager.Widget == widget; } /// /// Sets a widget as the fullscreen widget /// /// /// whether or not the widget is now fullscreen 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().Where( x => x.Extension == asset.AssetType.FileExtension ).FirstOrDefault(); RecentScenesMenu.AddOption( $"{asset.Name} ({asset.Path})", string.Empty, () => { asset.OpenInEditor(); } ); } } /// /// Updates Undo/Redo states and text /// 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"; } } }