// Copyright (c) Wojciech Figat. All rights reserved. using System; using System.Collections.Generic; using System.Globalization; using System.IO; using System.Reflection; using System.Runtime.InteropServices; using System.Text; using System.Xml; using FlaxEditor.Content; using FlaxEditor.GUI.Dialogs; using FlaxEditor.GUI.Docking; using FlaxEditor.Windows; using FlaxEditor.Windows.Assets; using FlaxEditor.Windows.Profiler; using FlaxEngine; using FlaxEngine.Assertions; using FlaxEngine.GUI; using DockPanel = FlaxEditor.GUI.Docking.DockPanel; using DockState = FlaxEditor.GUI.Docking.DockState; using FloatWindowDockPanel = FlaxEditor.GUI.Docking.FloatWindowDockPanel; using Window = FlaxEngine.Window; namespace FlaxEditor.Modules { /// /// Manages Editor windows and popups. /// /// public sealed class WindowsModule : EditorModule { private DateTime _lastLayoutSaveTime; private float _projectIconScreenshotTimeout = -1; private string _windowsLayoutPath; private struct WindowRestoreData { public string AssemblyName; public string TypeName; public DockState DockState; public DockPanel DockedTo; public int DockedTabIndex; public float? SplitterValue = null; public bool SelectOnShow = false; public bool Maximize; public bool Minimize; public Float2 FloatSize; public Float2 FloatPosition; public Guid AssetItemID; // Constructor, to allow for default values public WindowRestoreData() { } } private readonly List _restoreWindows = new List(); /// /// The main editor window. /// public Window MainWindow { get; private set; } /// /// Occurs when main editor window is being closed. /// public event Action MainWindowClosing; /// /// The content window. /// public ContentWindow ContentWin; /// /// The edit game window. /// public EditGameWindow EditWin; /// /// The game window. /// public GameWindow GameWin; /// /// The properties window. /// public PropertiesWindow PropertiesWin; /// /// The scene tree window. /// public SceneTreeWindow SceneWin; /// /// The debug log window. /// public DebugLogWindow DebugLogWin; /// /// The output log window. /// public OutputLogWindow OutputLogWin; /// /// The toolbox window. /// public ToolboxWindow ToolboxWin; /// /// The graphics quality window. /// public GraphicsQualityWindow GraphicsQualityWin; /// /// The game cooker window. /// public GameCookerWindow GameCookerWin; /// /// The profiler window. /// public ProfilerWindow ProfilerWin; /// /// The editor options window. /// public EditorOptionsWindow EditorOptionsWin; /// /// The plugins manager window. /// public PluginsWindow PluginsWin; /// /// The Visual Script debugger window. /// public VisualScriptDebuggerWindow VisualScriptDebuggerWin; /// /// List with all created editor windows. /// public readonly List Windows = new List(32); /// /// Occurs when new window gets opened and added to the editor windows list. /// public event Action WindowAdded; /// /// Occurs when new window gets closed and removed from the editor windows list. /// public event Action WindowRemoved; internal WindowsModule(Editor editor) : base(editor) { InitOrder = -75; } /// /// Takes the screenshot of the current viewport. /// public void TakeScreenshot() { // Select task SceneRenderTask target = null; if (Editor.Windows.EditWin.IsSelected) { // Use editor window target = EditWin.Viewport.Task; } else { // Use game window GameWin.FocusOrShow(); } // Fire screenshot taking Screenshot.Capture(target); } /// /// Updates the main window title. /// public void UpdateWindowTitle() { var mainWindow = MainWindow; if (mainWindow) { var projectPath = Globals.ProjectFolder; #if PLATFORM_WINDOWS projectPath = projectPath.Replace('/', '\\'); #endif var engineVersion = Editor.EngineProject.Version; var engineVersionText = engineVersion.Revision > 0 ? $"{engineVersion.Major}.{engineVersion.Minor}.{engineVersion.Revision}" : $"{engineVersion.Major}.{engineVersion.Minor}"; var title = $"Flax Editor {engineVersionText} - \'{projectPath}\'"; mainWindow.Title = title; } } /// /// Flash main editor window to catch user attention /// public void FlashMainWindow() { MainWindow?.FlashWindow(); } /// /// Finds the first window that is using given element to view/edit it. /// /// The item. /// Editor window or null if cannot find any window. public EditorWindow FindEditor(ContentItem item) { if (item == null) return null; for (int i = 0; i < Windows.Count; i++) { var win = Windows[i]; if (win.IsEditingItem(item)) { return win; } } return null; } /// /// Closes all windows that are using given element to view/edit it. /// /// The item. public void CloseAllEditors(ContentItem item) { for (int i = 0; i < Windows.Count; i++) { var win = Windows[i]; if (win.IsEditingItem(item)) { win.Close(); i--; } } } /// /// Saves the current workspace layout. /// public void SaveCurrentLayout() { _lastLayoutSaveTime = DateTime.UtcNow; SaveLayout(_windowsLayoutPath); } /// /// Loads the default workspace layout for the current editor version. /// public void LoadDefaultLayout() { var path = StringUtils.CombinePaths(Globals.EngineContentFolder, "Editor/LayoutDefault.xml"); if (File.Exists(path)) { LoadLayout(path); } } /// /// Loads the layout from the file. /// /// The layout file path. /// True if layout has been loaded otherwise if failed (e.g. missing file). public bool LoadLayout(string path) { if (Editor.IsHeadlessMode) return false; Editor.Log(string.Format("Loading editor windows layout from \'{0}\'", path)); if (!File.Exists(path)) { Editor.LogWarning("Cannot load windows layout. File is missing."); return false; } XmlDocument doc = new XmlDocument(); var masterPanel = Editor.UI.MasterPanel; try { doc.Load(path); var root = doc["DockPanelLayout"]; if (root == null) { Editor.LogWarning("Invalid windows layout file."); return false; } // Reset existing layout masterPanel.ResetLayout(); // Get metadata int version = int.Parse(root.Attributes["Version"].Value, CultureInfo.InvariantCulture); switch (version) { case 4: { // Main window info if (MainWindow) { var mainWindowNode = root["MainWindow"]; bool isMaximized = true, isMinimized = false; Rectangle bounds = LoadBounds(mainWindowNode, ref isMaximized, ref isMinimized); LoadWindow(MainWindow, ref bounds, isMaximized, false); } // Load master panel structure var masterPanelNode = root["MasterPanel"]; if (masterPanelNode != null) { LoadPanel(masterPanelNode, masterPanel); } // Load all floating windows structure var floating = root.SelectNodes("Float"); if (floating != null) { foreach (XmlElement child in floating) { if (child == null) continue; // Get window properties bool isMaximized = false, isMinimized = false; Rectangle bounds = LoadBounds(child, ref isMaximized, ref isMinimized); // Create window and floating dock panel var window = FloatWindowDockPanel.CreateFloatWindow(MainWindow.GUI, bounds.Location, bounds.Size, WindowStartPosition.Manual, string.Empty); var panel = new FloatWindowDockPanel(masterPanel, window.GUI); LoadWindow(panel.Window.Window, ref bounds, isMaximized, isMinimized); // Load structure LoadPanel(child, panel); // Check if no child windows loaded (due to file errors or loading problems) if (panel.TabsCount == 0 && panel.ChildPanelsCount == 0) { // Close empty window Editor.LogWarning("Empty floating window inside layout."); window.Close(); } else { // Perform layout var windowGUI = window.GUI; windowGUI.IsLayoutLocked = false; windowGUI.PerformLayout(); // Show window.Show(); window.Focus(); // Perform layout again windowGUI.PerformLayout(); } } } break; } default: { Editor.LogWarning("Unsupported windows layout version"); return false; } } } catch (Exception ex) { Editor.LogWarning("Failed to load windows layout."); Editor.LogWarning(ex); return false; } finally { masterPanel.PerformLayout(); } return true; } private void SavePanel(XmlWriter writer, DockPanel panel) { writer.WriteAttributeString("SelectedTab", panel.SelectedTabIndex.ToString()); for (int i = 0; i < panel.TabsCount; i++) { var win = panel.Tabs[i]; writer.WriteStartElement("Window"); writer.WriteAttributeString("Typename", win.SerializationTypename); if (win.UseLayoutData) { writer.WriteStartElement("Data"); win.OnLayoutSerialize(writer); writer.WriteEndElement(); } writer.WriteEndElement(); } for (int i = 0; i < panel.ChildPanelsCount; i++) { var p = panel.ChildPanels[i]; // Skip empty panels if (p.TabsCount == 0) continue; writer.WriteStartElement("Panel"); DockState state = p.TryGetDockState(out float splitterValue); writer.WriteAttributeString("DockState", ((int)state).ToString()); writer.WriteAttributeString("SplitterValue", splitterValue.ToString(CultureInfo.InvariantCulture)); SavePanel(writer, p); writer.WriteEndElement(); } } private void LoadPanel(XmlElement node, DockPanel panel) { int selectedTab = int.Parse(node.GetAttribute("SelectedTab"), CultureInfo.InvariantCulture); // Load docked windows var windows = node.SelectNodes("Window"); if (windows != null) { foreach (XmlElement child in windows) { if (child == null) continue; var typename = child.GetAttribute("Typename"); var window = GetWindow(typename); if (window != null) { if (child.SelectSingleNode("Data") is XmlElement data) { window.OnLayoutDeserialize(data); } else { window.OnLayoutDeserialize(); } window.Show(DockState.DockFill, panel); } } } // Load child panels var panels = node.SelectNodes("Panel"); if (panels != null) { foreach (XmlElement child in panels) { if (child == null) continue; // Create child panel DockState state = (DockState)int.Parse(child.GetAttribute("DockState"), CultureInfo.InvariantCulture); float splitterValue = float.Parse(child.GetAttribute("SplitterValue"), CultureInfo.InvariantCulture); var p = panel.CreateChildPanel(state, splitterValue); LoadPanel(child, p); // Check if panel has no docked window (due to loading problems or sth) if (p.TabsCount == 0 && p.ChildPanelsCount == 0) { // Remove empty panel Editor.LogWarning("Empty panel inside layout."); p.RemoveIt(); } } } panel.SelectTab(selectedTab); } private static void SaveBounds(XmlWriter writer, Window win) { writer.WriteStartElement("Bounds"); { var bounds = win.ClientBounds; writer.WriteAttributeString("X", bounds.X.ToString(CultureInfo.InvariantCulture)); writer.WriteAttributeString("Y", bounds.Y.ToString(CultureInfo.InvariantCulture)); writer.WriteAttributeString("Width", bounds.Width.ToString(CultureInfo.InvariantCulture)); writer.WriteAttributeString("Height", bounds.Height.ToString(CultureInfo.InvariantCulture)); writer.WriteAttributeString("IsMaximized", win.IsMaximized.ToString()); writer.WriteAttributeString("IsMinimized", win.IsMinimized.ToString()); } writer.WriteEndElement(); } private static Rectangle LoadBounds(XmlElement node, ref bool isMaximized, ref bool isMinimized) { var bounds = node["Bounds"]; var isMaximizedText = bounds.GetAttribute("IsMaximized"); if (!string.IsNullOrEmpty(isMaximizedText) && bool.TryParse(isMaximizedText, out var tmpBool)) isMaximized = tmpBool; var isMinimizedText = bounds.GetAttribute("IsMinimized"); if (!string.IsNullOrEmpty(isMinimizedText) && bool.TryParse(isMinimizedText, out tmpBool)) isMinimized = tmpBool; float x = float.Parse(bounds.GetAttribute("X"), CultureInfo.InvariantCulture); float y = float.Parse(bounds.GetAttribute("Y"), CultureInfo.InvariantCulture); float width = float.Parse(bounds.GetAttribute("Width"), CultureInfo.InvariantCulture); float height = float.Parse(bounds.GetAttribute("Height"), CultureInfo.InvariantCulture); return new Rectangle(x, y, width, height); } private static void LoadWindow(Window win, ref Rectangle bounds, bool isMaximized, bool isMinimized) { var virtualDesktopBounds = Platform.VirtualDesktopBounds; var virtualDesktopSafeLeftCorner = virtualDesktopBounds.Location; var virtualDesktopSafeRightCorner = virtualDesktopBounds.BottomRight; // Clamp position to match current desktop dimensions (if window was on desktop that is now inactive) if (bounds.X < virtualDesktopSafeLeftCorner.X || bounds.Y < virtualDesktopSafeLeftCorner.Y || bounds.X > virtualDesktopSafeRightCorner.X || bounds.Y > virtualDesktopSafeRightCorner.Y) bounds.Location = virtualDesktopSafeLeftCorner; if (isMaximized) { if (win.IsMaximized) win.Restore(); win.ClientPosition = bounds.Location; win.Maximize(); } else { if (Mathf.Min(bounds.Size.X, bounds.Size.Y) >= 1) { win.ClientBounds = bounds; } else { win.ClientPosition = bounds.Location; } if (isMinimized) win.Minimize(); } } private class LayoutNameDialog : Dialog { private TextBox _textbox; public LayoutNameDialog() : base("Enter Layout Name") { var name = new TextBox(false, 8, 8, 200) { WatermarkText = "Enter layout slot name", Parent = this, }; _textbox = name; var okButton = new Button(name.Right - 50, name.Bottom + 4, 50) { Text = "OK", Parent = this, }; okButton.Clicked += OnSubmit; var cancelButton = new Button(okButton.Left - 54, okButton.Y, 50) { Text = "Cancel", Parent = this, }; cancelButton.Clicked += OnCancel; _dialogSize = okButton.BottomRight + new Float2(8); } /// public override void OnSubmit() { var name = _textbox.Text; if (name.Length == 0) { MessageBox.Show("Cannot use the empty name."); return; } if (Utilities.Utils.HasInvalidPathChar(name)) { MessageBox.Show("Cannot use this name. It contains one or more invalid characters."); return; } base.OnSubmit(); var path = StringUtils.CombinePaths(Editor.LocalCachePath, "LayoutsCache", "Layout_" + name + ".xml"); Editor.Instance.Windows.SaveLayout(path); } } /// /// Asks user for the layout name and saves the current windows layout in the current project cache folder. /// public void SaveLayout() { if (Editor.IsHeadlessMode) return; new LayoutNameDialog().Show(); } /// /// Saves the layout to the file. /// /// The layout file path. public void SaveLayout(string path) { if (Editor.IsHeadlessMode) return; //Editor.Log(string.Format("Saving editor windows layout to \'{0}\'", path)); var settings = new XmlWriterSettings { Indent = true, IndentChars = "\t", Encoding = Encoding.UTF8, OmitXmlDeclaration = true, }; var masterPanel = Editor.UI.MasterPanel; if (masterPanel == null) return; using (XmlWriter writer = XmlWriter.Create(path, settings)) { writer.WriteStartDocument(); writer.WriteStartElement("DockPanelLayout"); // Metadata writer.WriteAttributeString("Version", "4"); // Main window info if (MainWindow) { writer.WriteStartElement("MainWindow"); SaveBounds(writer, MainWindow); writer.WriteEndElement(); } // Master panel structure writer.WriteStartElement("MasterPanel"); SavePanel(writer, masterPanel); writer.WriteEndElement(); // Save all floating windows structure for (int i = 0; i < masterPanel.FloatingPanels.Count; i++) { var panel = masterPanel.FloatingPanels[i]; var window = panel.Window; if (window == null) continue; writer.WriteStartElement("Float"); SavePanel(writer, panel); SaveBounds(writer, window.Window); writer.WriteEndElement(); } writer.WriteEndElement(); writer.WriteEndDocument(); } } /// /// Opens the specified editor window (shows it with editor options handling for new windows). /// /// The window. public void Open(EditorWindow window) { var newLocation = (DockState)Editor.Options.Options.Interface.NewWindowLocation; if (newLocation == DockState.Float) { // Check if there is a floating window that has the same size var dpi = (float)Platform.Dpi / 96.0f; var dpiScale = Platform.CustomDpiScale; var defaultSize = window.DefaultSize * dpi; for (var i = 0; i < Editor.UI.MasterPanel.FloatingPanels.Count; i++) { var win = Editor.UI.MasterPanel.FloatingPanels[i]; if (Float2.Abs(win.Size - defaultSize).LengthSquared < 100) { window.Show(DockState.DockFill, win); window.Focus(); return; } } window.ShowFloating(defaultSize * dpiScale); } else { window.Show(newLocation); } } /// /// Gets that is represented by the given serialized typename. Used to restore workspace layout. /// /// The typename. /// The window or null if failed. private EditorWindow GetWindow(string typename) { // Try use already opened window for (int i = 0; i < Windows.Count; i++) { if (string.Equals(Windows[i].SerializationTypename, typename, StringComparison.OrdinalIgnoreCase)) return Windows[i]; } // Check if it's an asset ID if (Guid.TryParse(typename, out Guid id)) { var el = Editor.ContentDatabase.Find(id); if (el != null) { // Open asset return Editor.ContentEditing.Open(el, true); } } return null; } /// public override void OnInit() { Assert.IsNull(MainWindow); _windowsLayoutPath = StringUtils.CombinePaths(Globals.ProjectCacheFolder, "WindowsLayout.xml"); // Create main window var settings = CreateWindowSettings.Default; settings.Title = "Flax Editor"; settings.Size = Platform.DesktopSize * 0.75f; settings.StartPosition = WindowStartPosition.CenterScreen; settings.ShowAfterFirstPaint = true; #if PLATFORM_WINDOWS if (!Editor.Instance.Options.Options.Interface.UseNativeWindowSystem) { settings.HasBorder = false; // Skip OS sizing frame and implement it using LeftButtonHit settings.HasSizingFrame = false; } #elif PLATFORM_LINUX settings.HasBorder = false; #endif MainWindow = Platform.CreateWindow(ref settings); if (MainWindow == null) { Editor.LogError("Failed to create editor main window!"); return; } UpdateWindowTitle(); // Link for main window events MainWindow.Closing += MainWindow_OnClosing; MainWindow.Closed += MainWindow_OnClosed; // Create default editor windows ContentWin = new ContentWindow(Editor); EditWin = new EditGameWindow(Editor); GameWin = new GameWindow(Editor); PropertiesWin = new PropertiesWindow(Editor); SceneWin = new SceneTreeWindow(Editor); DebugLogWin = new DebugLogWindow(Editor); OutputLogWin = new OutputLogWindow(Editor); ToolboxWin = new ToolboxWindow(Editor); GraphicsQualityWin = new GraphicsQualityWindow(Editor); GameCookerWin = new GameCookerWindow(Editor); ProfilerWin = new ProfilerWindow(Editor); EditorOptionsWin = new EditorOptionsWindow(Editor); PluginsWin = new PluginsWindow(Editor); VisualScriptDebuggerWin = new VisualScriptDebuggerWindow(Editor); // Bind events Level.SceneSaveError += OnSceneSaveError; Level.SceneLoaded += OnSceneLoaded; Level.SceneLoadError += OnSceneLoadError; Level.SceneLoading += OnSceneLoading; Level.SceneSaved += OnSceneSaved; Level.SceneSaving += OnSceneSaving; Level.SceneUnloaded += OnSceneUnloaded; Level.SceneUnloading += OnSceneUnloading; Editor.ContentDatabase.WorkspaceRebuilt += OnWorkspaceRebuilt; Editor.StateMachine.StateChanged += OnEditorStateChanged; } internal void AddToRestore(AssetEditorWindow win) { AddToRestore(win, win.GetType(), new WindowRestoreData { AssetItemID = win.Item.ID, }); } internal void AddToRestore(CustomEditorWindow win) { // Validate if can restore type var type = win.GetType(); var constructor = type.GetConstructor(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic, null, Type.EmptyTypes, null); if (constructor == null || type.IsGenericType) return; AddToRestore(win.Window, type, new WindowRestoreData()); } private void AddToRestore(EditorWindow win, Type type, WindowRestoreData winData) { // Ensure that this window is only selected following recompilation // if it was the active tab in its dock panel. Otherwise, there is a // risk of interrupting the user's workflow by potentially selecting // background tabs. var window = win.RootWindow?.Window; var panel = win.ParentDockPanel; winData.SelectOnShow = panel.SelectedTab == win; winData.DockedTabIndex = 0; if (panel is FloatWindowDockPanel && window != null && panel.TabsCount == 1) { winData.DockState = DockState.Float; winData.FloatPosition = window.Position; winData.FloatSize = window.ClientSize; winData.Maximize = window.IsMaximized; winData.Minimize = window.IsMinimized; winData.DockedTo = panel; } else { for (int i = 0; i < panel.Tabs.Count; i++) { if (panel.Tabs[i] == win) { winData.DockedTabIndex = i; break; } } if (panel.TabsCount > 1) { winData.DockState = DockState.DockFill; winData.DockedTo = panel; } else { winData.DockState = panel.TryGetDockState(out var splitterValue); winData.DockedTo = panel.ParentDockPanel; winData.SplitterValue = splitterValue; } } winData.AssemblyName = type.Assembly.GetName().Name; winData.TypeName = type.FullName; _restoreWindows.Add(winData); } private void OnWorkspaceRebuilt() { // Go in reverse order to create floating Prefab windows first before docked windows for (int i = _restoreWindows.Count - 1; i >= 0; i--) { var winData = _restoreWindows[i]; try { var assembly = Utils.GetAssemblyByName(winData.AssemblyName); if (assembly == null) continue; var type = assembly.GetType(winData.TypeName); if (type == null) continue; if (type.IsAssignableTo(typeof(AssetEditorWindow))) { var ctor = type.GetConstructor(new Type[] { typeof(Editor), typeof(AssetItem) }); var assetItem = Editor.ContentDatabase.FindAsset(winData.AssetItemID); var win = (AssetEditorWindow)ctor.Invoke(new object[] { Editor.Instance, assetItem }); win.Show(winData.DockState, winData.DockState != DockState.Float ? winData.DockedTo : null, winData.SelectOnShow, winData.SplitterValue); if (winData.DockState == DockState.Float) { var window = win.RootWindow.Window; window.Position = winData.FloatPosition; if (winData.Maximize) { window.Maximize(); } else if (winData.Minimize) { window.Minimize(); } else { window.ClientSize = winData.FloatSize; } // Update panel reference in other windows docked to this panel foreach (ref var otherData in CollectionsMarshal.AsSpan(_restoreWindows)) { if (otherData.DockedTo == winData.DockedTo) otherData.DockedTo = win.ParentDockPanel; } } var panel = win.ParentDockPanel; int currentTabIndex = 0; for (int pi = 0; pi < panel.TabsCount; pi++) { if (panel.Tabs[pi] == win) { currentTabIndex = pi; break; } } while (currentTabIndex > winData.DockedTabIndex) { win.ParentDockPanel.MoveTabLeft(currentTabIndex); currentTabIndex--; } while (currentTabIndex < winData.DockedTabIndex) { win.ParentDockPanel.MoveTabRight(currentTabIndex); currentTabIndex++; } panel.PerformLayout(true); } else { var win = (CustomEditorWindow)Activator.CreateInstance(type); win.Show(winData.DockState, winData.DockedTo, winData.SelectOnShow, winData.SplitterValue); if (winData.DockState == DockState.Float) { var window = win.Window.RootWindow.Window; window.Position = winData.FloatPosition; if (winData.Maximize) { window.Maximize(); } else if (winData.Minimize) { window.Minimize(); } else { window.ClientSize = winData.FloatSize; } } } } catch (Exception ex) { Editor.LogWarning(ex); Editor.LogWarning(string.Format("Failed to restore window {0} (assembly: {1})", winData.TypeName, winData.AssemblyName)); } } // Restored windows stole the focus from Editor if (_restoreWindows.Count > 0) Editor.Instance.Windows.MainWindow.Focus(); _restoreWindows.Clear(); } private void MainWindow_OnClosing(ClosingReason reason, ref bool cancel) { Editor.Log("Main window is closing, reason: " + reason); if (Editor.StateMachine.IsPlayMode) { // Cancel closing but leave the play mode cancel = true; Editor.Log("Skip closing editor and leave the play mode"); Editor.Simulation.RequestStopPlay(); return; } SaveCurrentLayout(); // Block closing only on user events if (reason == ClosingReason.User) { // Check if cancel action or save scene before exit if (Editor.Scene.CheckSaveBeforeClose()) { // Cancel cancel = true; return; } // Close all asset editor windows for (int i = 0; i < Windows.Count; i++) { if (Windows[i] is AssetEditorWindow assetEditorWindow) { if (assetEditorWindow.Close(ClosingReason.User)) { // Cancel cancel = true; return; } // Remove it OnWindowRemove(assetEditorWindow); i--; } } } MainWindowClosing?.Invoke(); } private void MainWindow_OnClosed() { Editor.Log("Main window is closed"); MainWindow = null; // Capture project icon screenshot (not in play mode and if editor was used for some time) if (!Editor.StateMachine.IsPlayMode && Time.TimeSinceStartup >= 5.0f && !Editor.IsHeadlessMode && EditWin.Viewport.Task != null && EditWin.Viewport.Task.LastUsedFrame > 100 && GPUDevice.Instance?.RendererType != RendererType.Null) { Editor.Log("Capture project icon screenshot"); _projectIconScreenshotTimeout = Time.TimeSinceStartup + 0.8f; // wait 800ms for a screenshot task EditWin.Viewport.SaveProjectIcon(); } else { // Close editor Engine.RequestExit(); } } internal void OnWindowAdd(EditorWindow window) { Windows.Add(window); WindowAdded?.Invoke(window); } internal void OnWindowRemove(EditorWindow window) { Windows.Remove(window); WindowRemoved?.Invoke(window); } /// public override void OnEndInit() { UpdateWindowTitle(); // Initialize windows for (int i = 0; i < Windows.Count; i++) { try { Windows[i].OnInit(); } catch (Exception ex) { Editor.LogWarning(ex); Editor.LogError("Failed to init window " + Windows[i]); } } // Load current workspace layout if (!LoadLayout(_windowsLayoutPath)) LoadDefaultLayout(); // Clear timer flag _lastLayoutSaveTime = DateTime.UtcNow; } /// public override void OnUpdate() { Profiler.BeginEvent("WindowsModule.Update"); // Auto save workspace layout every few seconds var now = DateTime.UtcNow; if (_lastLayoutSaveTime.Ticks > 10 && now - _lastLayoutSaveTime >= TimeSpan.FromSeconds(10)) { Profiler.BeginEvent("Save Layout"); SaveCurrentLayout(); Profiler.EndEvent(); } // Auto close on project icon saving end if (_projectIconScreenshotTimeout > 0 && Time.TimeSinceStartup > _projectIconScreenshotTimeout) { Editor.Log("Closing Editor after project icon screenshot"); EditWin.Viewport.SaveProjectIconEnd(); Engine.RequestExit(); } // Update editor windows for (int i = 0; i < Windows.Count; i++) { Windows[i].OnUpdate(); } Profiler.EndEvent(); } /// public override void OnExit() { // Unbind events Level.SceneSaveError -= OnSceneSaveError; Level.SceneLoaded -= OnSceneLoaded; Level.SceneLoadError -= OnSceneLoadError; Level.SceneLoading -= OnSceneLoading; Level.SceneSaved -= OnSceneSaved; Level.SceneSaving -= OnSceneSaving; Level.SceneUnloaded -= OnSceneUnloaded; Level.SceneUnloading -= OnSceneUnloading; Editor.ContentDatabase.WorkspaceRebuilt -= OnWorkspaceRebuilt; Editor.StateMachine.StateChanged -= OnEditorStateChanged; // Close main window MainWindow?.Close(ClosingReason.EngineExit); MainWindow = null; // Close all windows var windows = Windows.ToArray(); for (int i = 0; i < windows.Length; i++) { if (windows[i] != null) windows[i].Close(ClosingReason.EngineExit); } } #region Window Events private void OnEditorStateChanged() { for (int i = 0; i < Windows.Count; i++) Windows[i].OnEditorStateChanged(); } private void OnSceneSaveError(Scene scene, Guid sceneId) { for (int i = 0; i < Windows.Count; i++) Windows[i].OnSceneSaveError(scene, sceneId); } private void OnSceneLoaded(Scene scene, Guid sceneId) { for (int i = 0; i < Windows.Count; i++) Windows[i].OnSceneLoaded(scene, sceneId); } private void OnSceneLoadError(Scene scene, Guid sceneId) { for (int i = 0; i < Windows.Count; i++) Windows[i].OnSceneLoadError(scene, sceneId); } private void OnSceneLoading(Scene scene, Guid sceneId) { for (int i = 0; i < Windows.Count; i++) Windows[i].OnSceneLoading(scene, sceneId); } private void OnSceneSaved(Scene scene, Guid sceneId) { for (int i = 0; i < Windows.Count; i++) Windows[i].OnSceneSaved(scene, sceneId); } private void OnSceneSaving(Scene scene, Guid sceneId) { for (int i = 0; i < Windows.Count; i++) Windows[i].OnSceneSaving(scene, sceneId); } private void OnSceneUnloaded(Scene scene, Guid sceneId) { for (int i = 0; i < Windows.Count; i++) Windows[i].OnSceneUnloaded(scene, sceneId); } private void OnSceneUnloading(Scene scene, Guid sceneId) { for (int i = 0; i < Windows.Count; i++) Windows[i].OnSceneUnloading(scene, sceneId); } /// public override void OnPlayBeginning() { for (int i = 0; i < Windows.Count; i++) Windows[i].OnPlayBeginning(); } /// public override void OnPlayBegin() { for (int i = 0; i < Windows.Count; i++) Windows[i].OnPlayBegin(); } /// public override void OnPlayEnd() { for (int i = 0; i < Windows.Count; i++) Windows[i].OnPlayEnd(); } #endregion } }