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).
This commit is contained in:
Ari Vuollet
2026-08-18 00:49:05 +03:00
parent 1146620d4f
commit 373ce6b212
9 changed files with 183 additions and 122 deletions
+16 -2
View File
@@ -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);
}
/// <summary>
/// Reattaches the window control to existing floating window.
/// </summary>
/// <param name="windowHandle">The window handle.</param>
/// <param name="state">Initial window state.</param>
/// <param name="toDock">The panel to dock to, if any.</param>
/// <param name="autoSelect">Only used if <paramref name="toDock"/> is set. If true the window will be selected after docking it.</param>
/// <param name="splitterValue">The splitter value to use if toDock is not null. If not specified, a default value will be used.</param>
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);
}
/// <summary>
+48
View File
@@ -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
}
}
/// <summary>
/// Reattaches the window control to existing floating window.
/// </summary>
/// <param name="windowHandle">The window handle.</param>
/// <param name="state">Initial window state.</param>
/// <param name="toDock">Panel to dock to it.</param>
/// <param name="autoSelect">Only used if <paramref name="toDock"/> is set. If true the window will be selected after docking it.</param>
/// <param name="splitterValue">Only used if <paramref name="toDock"/> is set. The splitter value to use. If not specified, a default value will be used.</param>
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();
}
/// <summary>
/// Shows the window.
/// </summary>
@@ -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();
@@ -89,6 +89,13 @@ namespace FlaxEditor.GUI.Docking
}
}
internal void UnlinkWindow()
{
_window?.Window.Closing -= OnClosing;
_window?.Window.LeftButtonHit -= OnLeftButtonHit;
_window = null;
}
/// <inheritdoc />
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();
}
}
+101 -113
View File
@@ -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<WindowRestoreData> _restoreWindows = new List<WindowRestoreData>();
private readonly Dictionary<Guid, WindowRestoreData> _restoreWindows = new();
/// <summary>
/// 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<Guid, AssetEditorWindow>();
// 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)
@@ -177,7 +177,7 @@ namespace FlaxEditor.Windows.Assets
Save();
}
Editor.Instance.Windows.AddToRestore(this);
Close();
Close(ClosingReason.ScriptsReload);
}
}
@@ -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()
@@ -148,7 +148,7 @@ namespace FlaxEditor.Windows.Assets
protected override void OnScriptsReloadBegin()
{
base.OnScriptsReloadBegin();
Close();
Close(ClosingReason.ScriptsReload);
}
/// <inheritdoc />
+1 -1
View File
@@ -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)
+5
View File
@@ -28,6 +28,11 @@ API_ENUM() enum class ClosingReason
/// The close event.
/// </summary>
CloseEvent,
/// <summary>
/// The scripts reload event.
/// </summary>
ScriptsReload,
};
/// <summary>