Files
FlaxEngine/Source/Editor/Modules/WindowsModule.cs

1236 lines
43 KiB
C#

// 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
{
/// <summary>
/// Manages Editor windows and popups.
/// </summary>
/// <seealso cref="FlaxEditor.Modules.EditorModule" />
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<WindowRestoreData> _restoreWindows = new List<WindowRestoreData>();
/// <summary>
/// The main editor window.
/// </summary>
public Window MainWindow { get; private set; }
/// <summary>
/// Occurs when main editor window is being closed.
/// </summary>
public event Action MainWindowClosing;
/// <summary>
/// The content window.
/// </summary>
public ContentWindow ContentWin;
/// <summary>
/// The edit game window.
/// </summary>
public EditGameWindow EditWin;
/// <summary>
/// The game window.
/// </summary>
public GameWindow GameWin;
/// <summary>
/// The properties window.
/// </summary>
public PropertiesWindow PropertiesWin;
/// <summary>
/// The scene tree window.
/// </summary>
public SceneTreeWindow SceneWin;
/// <summary>
/// The debug log window.
/// </summary>
public DebugLogWindow DebugLogWin;
/// <summary>
/// The output log window.
/// </summary>
public OutputLogWindow OutputLogWin;
/// <summary>
/// The toolbox window.
/// </summary>
public ToolboxWindow ToolboxWin;
/// <summary>
/// The graphics quality window.
/// </summary>
public GraphicsQualityWindow GraphicsQualityWin;
/// <summary>
/// The game cooker window.
/// </summary>
public GameCookerWindow GameCookerWin;
/// <summary>
/// The profiler window.
/// </summary>
public ProfilerWindow ProfilerWin;
/// <summary>
/// The editor options window.
/// </summary>
public EditorOptionsWindow EditorOptionsWin;
/// <summary>
/// The plugins manager window.
/// </summary>
public PluginsWindow PluginsWin;
/// <summary>
/// The Visual Script debugger window.
/// </summary>
public VisualScriptDebuggerWindow VisualScriptDebuggerWin;
/// <summary>
/// List with all created editor windows.
/// </summary>
public readonly List<EditorWindow> Windows = new List<EditorWindow>(32);
/// <summary>
/// Occurs when new window gets opened and added to the editor windows list.
/// </summary>
public event Action<EditorWindow> WindowAdded;
/// <summary>
/// Occurs when new window gets closed and removed from the editor windows list.
/// </summary>
public event Action<EditorWindow> WindowRemoved;
internal WindowsModule(Editor editor)
: base(editor)
{
InitOrder = -75;
}
/// <summary>
/// Takes the screenshot of the current viewport.
/// </summary>
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);
}
/// <summary>
/// Updates the main window title.
/// </summary>
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;
}
}
/// <summary>
/// Flash main editor window to catch user attention
/// </summary>
public void FlashMainWindow()
{
MainWindow?.FlashWindow();
}
/// <summary>
/// Finds the first window that is using given element to view/edit it.
/// </summary>
/// <param name="item">The item.</param>
/// <returns>Editor window or null if cannot find any window.</returns>
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;
}
/// <summary>
/// Closes all windows that are using given element to view/edit it.
/// </summary>
/// <param name="item">The item.</param>
public void CloseAllEditors(ContentItem item)
{
for (int i = 0; i < Windows.Count; i++)
{
var win = Windows[i];
if (win.IsEditingItem(item))
{
win.Close();
i--;
}
}
}
/// <summary>
/// Saves the current workspace layout.
/// </summary>
public void SaveCurrentLayout()
{
_lastLayoutSaveTime = DateTime.UtcNow;
SaveLayout(_windowsLayoutPath);
}
/// <summary>
/// Loads the default workspace layout for the current editor version.
/// </summary>
public void LoadDefaultLayout()
{
var path = StringUtils.CombinePaths(Globals.EngineContentFolder, "Editor/LayoutDefault.xml");
if (File.Exists(path))
{
LoadLayout(path);
}
}
/// <summary>
/// Loads the layout from the file.
/// </summary>
/// <param name="path">The layout file path.</param>
/// <returns>True if layout has been loaded otherwise if failed (e.g. missing file).</returns>
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);
}
/// <inheritdoc />
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);
}
}
/// <summary>
/// Asks user for the layout name and saves the current windows layout in the current project cache folder.
/// </summary>
public void SaveLayout()
{
if (Editor.IsHeadlessMode)
return;
new LayoutNameDialog().Show();
}
/// <summary>
/// Saves the layout to the file.
/// </summary>
/// <param name="path">The layout file path.</param>
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();
}
}
/// <summary>
/// Opens the specified editor window (shows it with editor options handling for new windows).
/// </summary>
/// <param name="window">The window.</param>
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);
}
}
/// <summary>
/// Gets <see cref="EditorWindow"/> that is represented by the given serialized typename. Used to restore workspace layout.
/// </summary>
/// <param name="typename">The typename.</param>
/// <returns>The window or null if failed.</returns>
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;
}
/// <inheritdoc />
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;
if (window == null)
return;
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 &&
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);
}
/// <inheritdoc />
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;
}
/// <inheritdoc />
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();
}
/// <inheritdoc />
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);
}
/// <inheritdoc />
public override void OnPlayBeginning()
{
for (int i = 0; i < Windows.Count; i++)
Windows[i].OnPlayBeginning();
}
/// <inheritdoc />
public override void OnPlayBegin()
{
for (int i = 0; i < Windows.Count; i++)
Windows[i].OnPlayBegin();
}
/// <inheritdoc />
public override void OnPlayEnd()
{
for (int i = 0; i < Windows.Count; i++)
Windows[i].OnPlayEnd();
}
#endregion
}
}