From 373ce6b212b459f27c0ea0943ed9027bf4fb396e Mon Sep 17 00:00:00 2001 From: Ari Vuollet Date: Tue, 18 Aug 2026 00:49:05 +0300 Subject: [PATCH] Keep asset windows alive during scripts reload Recreated asset editor windows are now reattached to the native window after scripts reload to keep the current state and Z-order of the windows untouched on platforms where window state can't be changed without user interaction (Wayland). --- Source/Editor/CustomEditorWindow.cs | 18 +- Source/Editor/GUI/Docking/DockWindow.cs | 48 ++++ .../GUI/Docking/FloatWindowDockPanel.cs | 12 +- Source/Editor/Modules/WindowsModule.cs | 214 +++++++++--------- .../Windows/Assets/AssetEditorWindow.cs | 2 +- .../Windows/Assets/BehaviorTreeWindow.cs | 2 +- .../Editor/Windows/Assets/JsonAssetWindow.cs | 2 +- Source/Editor/Windows/Assets/PrefabWindow.cs | 2 +- Source/Engine/Platform/Base/Enums.h | 5 + 9 files changed, 183 insertions(+), 122 deletions(-) diff --git a/Source/Editor/CustomEditorWindow.cs b/Source/Editor/CustomEditorWindow.cs index 3df453ef8..4a94e0a34 100644 --- a/Source/Editor/CustomEditorWindow.cs +++ b/Source/Editor/CustomEditorWindow.cs @@ -1,8 +1,10 @@ // Copyright (c) Wojciech Figat. All rights reserved. +using System; using FlaxEditor.CustomEditors; using FlaxEditor.GUI.Docking; using FlaxEditor.Windows; +using FlaxEngine; using FlaxEngine.GUI; namespace FlaxEditor @@ -89,8 +91,20 @@ namespace FlaxEditor { Editor.Instance.Windows.AddToRestore(this); } - Window.Close(); - Window.Dispose(); + Window.Close(ClosingReason.ScriptsReload); + } + + /// + /// Reattaches the window control to existing floating window. + /// + /// The window handle. + /// Initial window state. + /// The panel to dock to, if any. + /// Only used if is set. If true the window will be selected after docking it. + /// The splitter value to use if toDock is not null. If not specified, a default value will be used. + public void Restore(IntPtr windowHandle, DockState state = DockState.Float, DockPanel toDock = null, bool autoSelect = true, float? splitterValue = null) + { + _win.Restore(windowHandle, state, toDock, autoSelect, splitterValue); } /// diff --git a/Source/Editor/GUI/Docking/DockWindow.cs b/Source/Editor/GUI/Docking/DockWindow.cs index 8cde79ce8..20ce85b0a 100644 --- a/Source/Editor/GUI/Docking/DockWindow.cs +++ b/Source/Editor/GUI/Docking/DockWindow.cs @@ -1,7 +1,9 @@ // Copyright (c) Wojciech Figat. All rights reserved. +using System; using System.Xml; using System.Globalization; +using System.Linq; using FlaxEngine; using FlaxEngine.Assertions; using FlaxEngine.GUI; @@ -232,6 +234,46 @@ namespace FlaxEditor.GUI.Docking } } + /// + /// Reattaches the window control to existing floating window. + /// + /// The window handle. + /// Initial window state. + /// Panel to dock to it. + /// Only used if is set. If true the window will be selected after docking it. + /// Only used if is set. The splitter value to use. If not specified, a default value will be used. + public void Restore(IntPtr windowHandle, DockState state = DockState.Float, DockPanel toDock = null, bool autoSelect = true, float? splitterValue = null) + { + if (state != DockState.Float) + { + Show(state, toDock, autoSelect, splitterValue); + return; + } + + Undock(); + + // Find the existing window and remove all controls from it + var window = Editor.GetWindows().First(x => x.NativePtr == windowHandle); + var windowGUI = window.GUI; + while (windowGUI.Children.Count > 0) + windowGUI.Children[^1].Dispose(); + windowGUI.EndTrackingMouse(); + + // Create dock panel for the window + var dockPanel = new FloatWindowDockPanel(_masterPanel, windowGUI); + dockPanel.DockWindowInternal(DockState.DockFill, this); + + // Perform layout + Visible = true; + windowGUI.UnlockChildrenRecursive(); + windowGUI.PerformLayout(); + + OnShow(); + + // Perform layout again + windowGUI.PerformLayout(); + } + /// /// Shows the window. /// @@ -332,6 +374,12 @@ namespace FlaxEditor.GUI.Docking } else { + if (reason == ClosingReason.ScriptsReload && _dockedTo is FloatWindowDockPanel floatPanel) + { + // Unlink the window to keep it alive during scripts reload + floatPanel.UnlinkWindow(); + } + // Undock Undock(); diff --git a/Source/Editor/GUI/Docking/FloatWindowDockPanel.cs b/Source/Editor/GUI/Docking/FloatWindowDockPanel.cs index b47d76bdd..c7481d338 100644 --- a/Source/Editor/GUI/Docking/FloatWindowDockPanel.cs +++ b/Source/Editor/GUI/Docking/FloatWindowDockPanel.cs @@ -89,6 +89,13 @@ namespace FlaxEditor.GUI.Docking } } + internal void UnlinkWindow() + { + _window?.Window.Closing -= OnClosing; + _window?.Window.LeftButtonHit -= OnLeftButtonHit; + _window = null; + } + /// protected override void PerformLayoutBeforeChildren() { @@ -191,9 +198,7 @@ namespace FlaxEditor.GUI.Docking } // Unlink - _window.Window.Closing -= OnClosing; - _window.Window.LeftButtonHit = null; - _window = null; + UnlinkWindow(); // Remove object FlaxEngine.Assertions.Assert.IsTrue(TabsCount == 0 && ChildPanelsCount == 0); @@ -244,6 +249,7 @@ namespace FlaxEditor.GUI.Docking { _masterPanel?.FloatingPanels.Remove(this); + UnlinkWindow(); base.OnDestroy(); } } diff --git a/Source/Editor/Modules/WindowsModule.cs b/Source/Editor/Modules/WindowsModule.cs index 1a03c7c57..8074ab814 100644 --- a/Source/Editor/Modules/WindowsModule.cs +++ b/Source/Editor/Modules/WindowsModule.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.Globalization; using System.IO; +using System.Linq; using System.Reflection; using System.Runtime.InteropServices; using System.Text; @@ -33,11 +34,12 @@ namespace FlaxEditor.Modules private float _projectIconScreenshotTimeout = -1; private string _windowsLayoutPath; - private struct WindowRestoreData + private class WindowRestoreData { public string AssemblyName; public string TypeName; + public IntPtr WindowHandle; public DockState DockState; public DockPanel DockedTo; public int DockedTabIndex; @@ -45,20 +47,10 @@ namespace FlaxEditor.Modules 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(); + private readonly Dictionary _restoreWindows = new(); /// /// The main editor window. @@ -822,10 +814,14 @@ namespace FlaxEditor.Modules internal void AddToRestore(AssetEditorWindow win) { - AddToRestore(win, win.GetType(), new WindowRestoreData + var assetItemId = win.Item.ID; + if (!_restoreWindows.TryGetValue(assetItemId, out var winData)) { - AssetItemID = win.Item.ID, - }); + winData = new WindowRestoreData(); + _restoreWindows.Add(assetItemId, winData); + } + winData.AssetItemID = assetItemId; + AddToRestore(win, win.GetType(), winData); } internal void AddToRestore(CustomEditorWindow win) @@ -836,62 +832,65 @@ namespace FlaxEditor.Modules if (constructor == null || type.IsGenericType) return; - AddToRestore(win.Window, type, new WindowRestoreData()); + // TODO: Restore data for custom editors + var assetItemId = Guid.NewGuid(); + if (!_restoreWindows.TryGetValue(assetItemId, out var winData)) + { + winData = new WindowRestoreData(); + _restoreWindows.Add(assetItemId, winData); + } + winData.AssetItemID = assetItemId; + AddToRestore(win.Window, type, winData); } 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.AssemblyName = type.Assembly.GetName().Name; + winData.TypeName = type.FullName; + if (panel is FloatWindowDockPanel) { - winData.DockState = DockState.Float; - winData.FloatPosition = window.Position; - winData.FloatSize = window.ClientSize; - winData.Maximize = window.IsMaximized; - winData.Minimize = window.IsMinimized; - winData.DockedTo = panel; + // Populate data for other tabs now, the tab index may change after tab is destroyed + if (winData.DockedTo == null) + { + for (int i = 0; i < panel.Tabs.Count; i++) + { + if (panel.Tabs[i] is AssetEditorWindow assetEditorWindow) + { + window ??= assetEditorWindow.RootWindow?.Window; // The window handle is sometimes missing in some tabs + var assetItemId = assetEditorWindow.Item.ID; + if (!_restoreWindows.TryGetValue(assetItemId, out var tabWinData)) + { + tabWinData = new WindowRestoreData(); + _restoreWindows.Add(assetItemId, tabWinData); + } + + tabWinData.DockedTabIndex = i; + tabWinData.SelectOnShow = panel.SelectedTab == assetEditorWindow; + tabWinData.DockState = DockState.DockFill; + tabWinData.DockedTo = panel; + } + } + winData.DockState = DockState.Float; + winData.WindowHandle = window?.NativePtr ?? IntPtr.Zero; + } } 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.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 assetEditorWindows = new Dictionary(); + + // Create float windows first, then add docked windows in order of the tabs + foreach (var (_, winData) in _restoreWindows) { - var winData = _restoreWindows[i]; - try { var assembly = Utils.GetAssemblyByName(winData.AssemblyName); @@ -908,75 +907,24 @@ namespace FlaxEditor.Modules var assetType = assetItem.GetType(); var ctor = type.GetConstructor(new Type[] { typeof(Editor), assetType }); 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); + var previouslyDockedTo = winData.DockedTo; + win.Restore(winData.WindowHandle, 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)) + foreach (var key in _restoreWindows.Keys) { - if (otherData.DockedTo == winData.DockedTo) + ref var otherData = ref CollectionsMarshal.GetValueRefOrNullRef(_restoreWindows, key); + if (otherData.DockedTo == previouslyDockedTo) 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); + assetEditorWindows.Add(winData.AssetItemID, win); } 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; - } - } + win.Restore(winData.WindowHandle, winData.DockState, winData.DockedTo, winData.SelectOnShow, winData.SplitterValue); } } catch (Exception ex) @@ -985,6 +933,46 @@ namespace FlaxEditor.Modules Editor.LogWarning(string.Format("Failed to restore window {0} (assembly: {1})", winData.TypeName, winData.AssemblyName)); } } + + // Reorder tabs to previous order + foreach (var (_, winData) in _restoreWindows) + { + var win = assetEditorWindows.GetValueOrDefault(winData.AssetItemID); + if (win == null) + continue; + + 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++; + } + } + + // Restore last selected tab + foreach (var (_, winData) in _restoreWindows) + { + var win = assetEditorWindows.GetValueOrDefault(winData.AssetItemID); + if (win != null && winData.SelectOnShow) + { + win.ParentDockPanel.SelectTab(win, false); + win.ParentDockPanel.PerformLayout(true); + } + } // Restored windows stole the focus from Editor if (_restoreWindows.Count > 0) diff --git a/Source/Editor/Windows/Assets/AssetEditorWindow.cs b/Source/Editor/Windows/Assets/AssetEditorWindow.cs index 93d6c850c..802686b76 100644 --- a/Source/Editor/Windows/Assets/AssetEditorWindow.cs +++ b/Source/Editor/Windows/Assets/AssetEditorWindow.cs @@ -177,7 +177,7 @@ namespace FlaxEditor.Windows.Assets Save(); } Editor.Instance.Windows.AddToRestore(this); - Close(); + Close(ClosingReason.ScriptsReload); } } diff --git a/Source/Editor/Windows/Assets/BehaviorTreeWindow.cs b/Source/Editor/Windows/Assets/BehaviorTreeWindow.cs index 7772d59b2..461668783 100644 --- a/Source/Editor/Windows/Assets/BehaviorTreeWindow.cs +++ b/Source/Editor/Windows/Assets/BehaviorTreeWindow.cs @@ -274,7 +274,7 @@ namespace FlaxEditor.Windows.Assets base.OnScriptsReloadBegin(); // TODO: impl hot-reload for BT to nicely refresh state (save asset, clear undo/properties, reload surface) - Close(); + Close(ClosingReason.ScriptsReload); } private void UpdateKnowledge() diff --git a/Source/Editor/Windows/Assets/JsonAssetWindow.cs b/Source/Editor/Windows/Assets/JsonAssetWindow.cs index 4d9c04942..696f63b25 100644 --- a/Source/Editor/Windows/Assets/JsonAssetWindow.cs +++ b/Source/Editor/Windows/Assets/JsonAssetWindow.cs @@ -148,7 +148,7 @@ namespace FlaxEditor.Windows.Assets protected override void OnScriptsReloadBegin() { base.OnScriptsReloadBegin(); - Close(); + Close(ClosingReason.ScriptsReload); } /// diff --git a/Source/Editor/Windows/Assets/PrefabWindow.cs b/Source/Editor/Windows/Assets/PrefabWindow.cs index 06e007a17..da41b88c5 100644 --- a/Source/Editor/Windows/Assets/PrefabWindow.cs +++ b/Source/Editor/Windows/Assets/PrefabWindow.cs @@ -366,7 +366,7 @@ namespace FlaxEditor.Windows.Assets _viewport.Prefab = null; _undo?.Clear(); // TODO: maybe don't clear undo? - Close(); + Close(ClosingReason.ScriptsReload); } private void OnUndoEvent(IUndoAction action) diff --git a/Source/Engine/Platform/Base/Enums.h b/Source/Engine/Platform/Base/Enums.h index 4a903ad19..4af4df785 100644 --- a/Source/Engine/Platform/Base/Enums.h +++ b/Source/Engine/Platform/Base/Enums.h @@ -28,6 +28,11 @@ API_ENUM() enum class ClosingReason /// The close event. /// CloseEvent, + + /// + /// The scripts reload event. + /// + ScriptsReload, }; ///