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/CustomEditors/Editors/FlaxObjectRefEditor.cs b/Source/Editor/CustomEditors/Editors/FlaxObjectRefEditor.cs index b63866c9c..29328e318 100644 --- a/Source/Editor/CustomEditors/Editors/FlaxObjectRefEditor.cs +++ b/Source/Editor/CustomEditors/Editors/FlaxObjectRefEditor.cs @@ -322,7 +322,11 @@ namespace FlaxEditor.CustomEditors.Editors // Deselect if (_value != null && button1Rect.Contains(ref location)) + { + Focus(); Value = null; + return true; + } // Picker dropdown menu if (_supportsPickDropDown && (isSelected ? button2Rect : button1Rect).Contains(ref location)) @@ -372,6 +376,18 @@ namespace FlaxEditor.CustomEditors.Editors if (_hasValidDragOver) _hasValidDragOver = false; } + if (button == MouseButton.Middle) + { + // Clear value + Focus(); + Value = null; + return true; + } + if (button == MouseButton.Right && _supportsPickDropDown) + { + // Show picker + ShowDropDownMenu(); + } return base.OnMouseUp(location, button); } diff --git a/Source/Editor/Editor.cpp b/Source/Editor/Editor.cpp index 18bf43a3a..f3065c1a1 100644 --- a/Source/Editor/Editor.cpp +++ b/Source/Editor/Editor.cpp @@ -642,9 +642,11 @@ int32 Editor::LoadProduct() } } +#if !FLAX_TESTS // Update the last opened project path if (lastProjectPath.Compare(Project->ProjectFolderPath) != 0) File::WriteAllText(lastProjectSettingPath, Project->ProjectFolderPath, Encoding::UTF8); +#endif return 0; } diff --git a/Source/Editor/GUI/AssetPicker.cs b/Source/Editor/GUI/AssetPicker.cs index f9c322c32..7d0e6c20b 100644 --- a/Source/Editor/GUI/AssetPicker.cs +++ b/Source/Editor/GUI/AssetPicker.cs @@ -299,6 +299,7 @@ namespace FlaxEditor.GUI } else if (Button1Rect.Contains(location)) { + // Show picker Focus(); OnSubmit(); } @@ -318,6 +319,18 @@ namespace FlaxEditor.GUI } } } + if (button == MouseButton.Middle && IconRect.Contains(ref location)) + { + // Clear value + Focus(); + Validator.SelectedItem = null; + } + if (button == MouseButton.Right && IconRect.Contains(ref location)) + { + // Show picker + Focus(); + OnSubmit(); + } // Handled return true; diff --git a/Source/Editor/GUI/Docking/DockWindow.cs b/Source/Editor/GUI/Docking/DockWindow.cs index 8cde79ce8..bf08695d8 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; @@ -50,7 +52,7 @@ namespace FlaxEditor.GUI.Docking /// /// Gets a value indicating whether this window is docked. /// - public bool IsDocked => _dockedTo != null; + public bool IsDocked => _dockedTo != null && _dockedTo.TabsCount > 1; /// /// Gets a value indicating whether this window is selected. @@ -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(); @@ -488,7 +536,7 @@ namespace FlaxEditor.GUI.Docking base.Focus(); SelectTab(false); - BringToFront(); + _dockedTo?.RootWindow?.Focus(); } /// diff --git a/Source/Editor/GUI/Docking/FloatWindowDockPanel.cs b/Source/Editor/GUI/Docking/FloatWindowDockPanel.cs index b47d76bdd..f8eb26ded 100644 --- a/Source/Editor/GUI/Docking/FloatWindowDockPanel.cs +++ b/Source/Editor/GUI/Docking/FloatWindowDockPanel.cs @@ -49,6 +49,7 @@ namespace FlaxEditor.GUI.Docking private MasterDockPanel _masterPanel; private WindowRootControl _window; + private bool _showDecorations; /// /// Gets the master panel. @@ -65,6 +66,31 @@ namespace FlaxEditor.GUI.Docking /// public bool IsDragging { get; internal set; } + /// + /// Shows client-side window decorations around this panel. + /// + public bool ShowDecorations + { + get => _showDecorations; + set + { + if (value == _showDecorations) + return; + _showDecorations = value; + if (value) + { + var decorations = Parent.AddChild(new FloatWindowDecorations(this)); + decorations.SetAnchorPreset(AnchorPresets.HorizontalStretchTop, false); + } + else + { + var decorations = Parent.GetChild(); + if (decorations != null) + decorations.Dispose(); + } + } + } + /// /// Initializes a new instance of the class. /// @@ -82,10 +108,16 @@ namespace FlaxEditor.GUI.Docking _window.Window.Closing += OnClosing; _window.Window.LeftButtonHit += OnLeftButtonHit; - if (Utilities.Utils.UseCustomWindowDecorations()) + ShowDecorations = Utilities.Utils.UseCustomWindowDecorations(); + } + + internal void UnlinkWindow() + { + if (_window != null) { - var decorations = Parent.AddChild(new FloatWindowDecorations(this)); - decorations.SetAnchorPreset(AnchorPresets.HorizontalStretchTop, false); + _window.Window.Closing -= OnClosing; + _window.Window.LeftButtonHit -= OnLeftButtonHit; + _window = null; } } @@ -94,13 +126,10 @@ namespace FlaxEditor.GUI.Docking { base.PerformLayoutBeforeChildren(); - var decorations = Parent.GetChild(); - if (decorations != null) - { - // Apply offset for the title bar - foreach (var child in Children) - child.Bounds = child.Bounds with { Y = decorations.Height, Height = Parent.Height - decorations.Height }; - } + // Apply offset for the title bar + var decorationsHeight = Parent.GetChild()?.Height ?? 0; + foreach (var child in Children) + child.Bounds = child.Bounds with { Y = decorationsHeight, Height = Parent.Height - decorationsHeight }; } /// @@ -191,9 +220,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 +271,7 @@ namespace FlaxEditor.GUI.Docking { _masterPanel?.FloatingPanels.Remove(this); + UnlinkWindow(); base.OnDestroy(); } } diff --git a/Source/Editor/Modules/ContentDatabaseModule.cs b/Source/Editor/Modules/ContentDatabaseModule.cs index 34e35bcc0..f7278a478 100644 --- a/Source/Editor/Modules/ContentDatabaseModule.cs +++ b/Source/Editor/Modules/ContentDatabaseModule.cs @@ -748,21 +748,27 @@ namespace FlaxEditor.Modules if (item.Path.Contains(".Build.cs", StringComparison.Ordinal) && item.ItemType == ContentItemType.Script) Editor.Instance.CodeEditing.RemoveModule(item.Path); - // Check if it's an asset - if (item.IsAsset) - { - // Delete asset by using content pool - FlaxEngine.Content.DeleteAsset(path); - } - else if (item is ScriptItem) - { - FlaxEngine.Content.DeleteScript(path); - } - else if (deletedByUser) - { - // Delete file - if (File.Exists(path)) - File.Delete(path); + // Delete asset file only if it was explicitly deleted by the user. + // Some applications might modify the file by deleting the original and replacing + // it with a new file which sometimes is caught in the middle of these operations. + if (deletedByUser) + { + // Check if it's an asset + if (item.IsAsset) + { + // Delete asset by using content pool + FlaxEngine.Content.DeleteAsset(path); + } + else if (item is ScriptItem) + { + FlaxEngine.Content.DeleteScript(path); + } + else + { + // Delete file + if (File.Exists(path)) + File.Delete(path); + } } // Unlink from the parent @@ -826,23 +832,29 @@ namespace FlaxEditor.Modules var startTime = Platform.TimeSeconds; _rebuildFlag = false; _rebuildInitFlag = false; - _enableEvents = false; - // Load all folders - // TODO: we should create async task for gathering content and whole workspace contents if it takes too long - // TODO: create progress bar in content window and after end we should enable events and update it + _enableEvents = false; _isDuringFastSetup = true; var startItems = _itemsCreated; - foreach (var project in Projects) + try { - if (project.Content != null) - LoadFolder(project.Content, true); - if (project.Source != null) - LoadFolder(project.Source, true); + // Load all folders + // TODO: we should create async task for gathering content and whole workspace contents if it takes too long + // TODO: create progress bar in content window and after end we should enable events and update it + foreach (var project in Projects) + { + if (project.Content != null) + LoadFolder(project.Content, true); + if (project.Source != null) + LoadFolder(project.Source, true); + } } - _isDuringFastSetup = false; - - _enableEvents = enableEvents; + finally + { + _isDuringFastSetup = false; + _enableEvents = enableEvents; + } + var endTime = Platform.TimeSeconds; Editor.Log(string.Format("Project database created in {0} ms. Items count: {1}", (int)((endTime - startTime) * 1000.0), _itemsCreated - startItems)); Profiler.EndEvent(); @@ -1340,39 +1352,44 @@ namespace FlaxEditor.Modules _enableEvents = false; _isDuringFastSetup = true; var startItems = _itemsCreated; - foreach (var project in Projects) + try { - if (project.Content != null) + foreach (var project in Projects) { - //Dispose(project.Content.Folder); - for (int i = 0; i < project.Content.Folder.Children.Count; i++) + if (project.Content != null) { - Dispose(project.Content.Folder.Children[i]); - i--; + //Dispose(project.Content.Folder); + for (int i = 0; i < project.Content.Folder.Children.Count; i++) + { + Dispose(project.Content.Folder.Children[i]); + i--; + } + } + if (project.Source != null) + { + //Dispose(project.Source.Folder); + for (int i = 0; i < project.Source.Folder.Children.Count; i++) + { + Dispose(project.Source.Folder.Children[i]); + i--; + } } } - if (project.Source != null) + + List removeProxies = new List(); + foreach (var proxy in Editor.Instance.ContentDatabase.Proxy) { - //Dispose(project.Source.Folder); - for (int i = 0; i < project.Source.Folder.Children.Count; i++) - { - Dispose(project.Source.Folder.Children[i]); - i--; - } + if (proxy.GetType().IsCollectible) + removeProxies.Add(proxy); } + foreach (var proxy in removeProxies) + RemoveProxy(proxy, false); } - - List removeProxies = new List(); - foreach (var proxy in Editor.Instance.ContentDatabase.Proxy) + finally { - if (proxy.GetType().IsCollectible) - removeProxies.Add(proxy); + _isDuringFastSetup = false; + _enableEvents = enabledEvents; } - foreach (var proxy in removeProxies) - RemoveProxy(proxy, false); - - _isDuringFastSetup = false; - _enableEvents = enabledEvents; } private void OnScriptsReloadEnd() @@ -1383,18 +1400,22 @@ namespace FlaxEditor.Modules /// public override void OnUpdate() { - // Update all dirty content tree nodes - lock (_dirtyNodes) + // Check for updates only when the editor is focused to only check once for all changes done in background. + if (FlaxEngine.Engine.HasFocus) { - Profiler.BeginEvent("ContentDatabase.Refresh"); - foreach (var node in _dirtyNodes) + // Update all dirty content tree nodes + lock (_dirtyNodes) { - LoadFolder(node, true); + Profiler.BeginEvent("ContentDatabase.Refresh"); + foreach (var node in _dirtyNodes) + { + LoadFolder(node, true); + } + if (_enableEvents && _dirtyNodes.Count != 0) + WorkspaceModified?.Invoke(); + _dirtyNodes.Clear(); + Profiler.EndEvent(); } - if (_enableEvents && _dirtyNodes.Count != 0) - WorkspaceModified?.Invoke(); - _dirtyNodes.Clear(); - Profiler.EndEvent(); } // Lazy-rebuilds diff --git a/Source/Editor/Modules/SourceCodeEditing/CodeEditingModule.cs b/Source/Editor/Modules/SourceCodeEditing/CodeEditingModule.cs index 12cd89060..d29b6f89d 100644 --- a/Source/Editor/Modules/SourceCodeEditing/CodeEditingModule.cs +++ b/Source/Editor/Modules/SourceCodeEditing/CodeEditingModule.cs @@ -505,7 +505,7 @@ namespace FlaxEditor.Modules.SourceCodeEditing base.OnUpdate(); // Automatic project files generation after workspace modifications - if (_autoGenerateScriptsProjectFiles && ScriptsBuilder.IsSourceWorkspaceDirty && !ScriptsBuilder.IsCompiling) + if (_autoGenerateScriptsProjectFiles && ScriptsBuilder.IsSourceWorkspaceDirty && !ScriptsBuilder.IsCompiling && Engine.HasFocus) { // Try to delay generation when a lot of files are added at once if (ScriptsBuilder.IsSourceDirtyFor(TimeSpan.FromMilliseconds(150))) diff --git a/Source/Editor/Modules/WindowsModule.cs b/Source/Editor/Modules/WindowsModule.cs index b4f8089e2..b667e3879 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. @@ -818,10 +810,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) @@ -832,62 +828,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); @@ -904,75 +903,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) @@ -981,6 +929,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/States/PlayingState.cs b/Source/Editor/States/PlayingState.cs index 51dfd3677..a81487e43 100644 --- a/Source/Editor/States/PlayingState.cs +++ b/Source/Editor/States/PlayingState.cs @@ -159,11 +159,11 @@ namespace FlaxEditor.States SceneDuplicated?.Invoke(); RestoreSelection(); + Time.Synchronize(true); + Editor.OnPlayBegin(); IsPlayModeStarting = false; Profiler.EndEvent(); - - Time.Synchronize(true); } private void SetupEditorEnvOptions() diff --git a/Source/Editor/Surface/SurfaceNode.cs b/Source/Editor/Surface/SurfaceNode.cs index d28d04e1e..b050d5ae2 100644 --- a/Source/Editor/Surface/SurfaceNode.cs +++ b/Source/Editor/Surface/SurfaceNode.cs @@ -76,6 +76,11 @@ namespace FlaxEditor.Surface /// The footer rectangle (local space). /// protected Rectangle _footerRect; + + /// + /// The last mouse down position. + /// + protected Float2 _mouseDownMousePosition; /// /// The node archetype. @@ -163,8 +168,6 @@ namespace FlaxEditor.Surface /// protected virtual Color ArchetypeColor => GroupArchetype.Color; - private Float2 mouseDownMousePosition; - /// /// Calculates the size of the node including header, footer, and margins. /// @@ -1166,10 +1169,9 @@ namespace FlaxEditor.Surface if (base.OnMouseDown(location, button)) return true; + _mouseDownMousePosition = location; if (button == MouseButton.Left && (Archetype.Flags & NodeFlags.NoCloseButton) == 0 && _closeButtonRect.Contains(ref location)) return true; - if (button == MouseButton.Right) - mouseDownMousePosition = Input.Mouse.Position; return false; } @@ -1180,18 +1182,22 @@ namespace FlaxEditor.Surface if (base.OnMouseUp(location, button)) return true; - // Close/ delete - bool canDelete = !Surface.IsConnecting && !Surface.WasSelecting && !Surface.WasMovingSelection; - if (button == MouseButton.Left && canDelete && (Archetype.Flags & NodeFlags.NoCloseButton) == 0 && _closeButtonRect.Contains(ref location)) + if (button == MouseButton.Left) { - Surface.Delete(this); - return true; + // Close/delete + bool canDelete = !Surface.IsConnecting && !Surface.WasSelecting && !Surface.WasMovingSelection; + if (canDelete && (Archetype.Flags & NodeFlags.NoCloseButton) == 0 && + _closeButtonRect.Contains(ref location) && _closeButtonRect.Contains(ref _mouseDownMousePosition)) + { + Surface.Delete(this); + return true; + } } // Secondary Context Menu if (button == MouseButton.Right) { - float distance = Float2.Distance(mouseDownMousePosition, Input.Mouse.Position); + float distance = Float2.Distance(_mouseDownMousePosition, location); if (distance > 2.5f) return true; diff --git a/Source/Editor/Surface/VisjectSurface.Connecting.cs b/Source/Editor/Surface/VisjectSurface.Connecting.cs index 22616f6ad..621f6105f 100644 --- a/Source/Editor/Surface/VisjectSurface.Connecting.cs +++ b/Source/Editor/Surface/VisjectSurface.Connecting.cs @@ -287,6 +287,7 @@ namespace FlaxEditor.Surface // Reset instigator list _connectionInstigators.Clear(); + EndMouseCapture(); } } } 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 3c721c09e..9cc1a6ab5 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/Editor/Windows/GameWindow.cs b/Source/Editor/Windows/GameWindow.cs index 8c3793627..4032e0aff 100644 --- a/Source/Editor/Windows/GameWindow.cs +++ b/Source/Editor/Windows/GameWindow.cs @@ -5,6 +5,7 @@ using System.Collections.Generic; using System.Xml; using FlaxEditor.Gizmo; using FlaxEditor.GUI.ContextMenu; +using FlaxEditor.GUI.Docking; using FlaxEditor.GUI.Input; using FlaxEditor.Modules; using FlaxEditor.Options; @@ -132,6 +133,8 @@ namespace FlaxEditor.Windows private float _gameStartTime; private GUI.Docking.DockState _maximizeRestoreDockState; private GUI.Docking.DockPanel _maximizeRestoreDockTo; + private GUI.Docking.DockPanel _maximizeRestoreDockToParent; + private float _maximizeRestoreSplitterValue; private CursorLockMode _cursorLockMode = CursorLockMode.None; // Viewport scaling variables @@ -295,7 +298,10 @@ namespace FlaxEditor.Windows if (value) { _maximizeRestoreDockTo = _dockedTo; - _maximizeRestoreDockState = _dockedTo.TryGetDockState(out _); + _maximizeRestoreDockToParent = _dockedTo.ParentDockPanel; + _maximizeRestoreDockState = _dockedTo.TryGetDockState(out _maximizeRestoreSplitterValue); + if (_dockedTo.Tabs.Count > 1) + _maximizeRestoreDockState = DockState.DockFill; if (_maximizeRestoreDockState != GUI.Docking.DockState.Float) { var monitorBounds = Platform.GetMonitorBounds(PointToScreen(Size * 0.5f)); @@ -309,9 +315,11 @@ namespace FlaxEditor.Windows // Restore if (rootWindow != null) rootWindow.Restore(); - if (_maximizeRestoreDockTo != null && _maximizeRestoreDockTo.IsDisposing) - _maximizeRestoreDockTo = null; - Show(_maximizeRestoreDockState, _maximizeRestoreDockTo); + var dockTo = _maximizeRestoreDockTo; + if (dockTo != null && dockTo.IsDisposing) + dockTo = _maximizeRestoreDockToParent; + if (_dockedTo != dockTo) + Show(_maximizeRestoreDockState, dockTo, splitterValue: _maximizeRestoreSplitterValue); } } } @@ -331,6 +339,9 @@ namespace FlaxEditor.Windows { IsFloating = true; var rootWindow = RootWindow; + var floatWin = rootWindow.GetChild(); + if (floatWin != null && floatWin.ShowDecorations) + floatWin.ShowDecorations = false; var monitorBounds = Platform.GetMonitorBounds(rootWindow.RootWindow.Window.ClientPosition); rootWindow.Window.Position = monitorBounds.Location; rootWindow.Window.SetBorderless(true); @@ -338,6 +349,9 @@ namespace FlaxEditor.Windows } else { + var floatWin = RootWindow.GetChild(); + if (floatWin != null && !floatWin.ShowDecorations && Utilities.Utils.UseCustomWindowDecorations()) + floatWin.ShowDecorations = true; IsFloating = false; } } @@ -658,9 +672,10 @@ namespace FlaxEditor.Windows IsBorderless = false; Cursor = CursorType.Default; Screen.CursorLock = CursorLockMode.None; - if (Screen.MainWindow.IsMouseTracking) - Screen.MainWindow.EndTrackingMouse(); - RootControl.GameRoot.EndMouseCapture(); + var mainWindow = Screen.MainWindow; + if (mainWindow != null && mainWindow.IsMouseTracking) + mainWindow.EndTrackingMouse(); + RootControl.GameRoot?.EndMouseCapture(); } /// @@ -899,7 +914,7 @@ namespace FlaxEditor.Windows /// public void FocusGameViewport() { - if (!IsDocked) + if (ParentDockPanel == null) { ShowFloating(); } @@ -1004,7 +1019,7 @@ namespace FlaxEditor.Windows Screen.CursorVisible = true; Screen.CursorLock = CursorLockMode.None; - if (Editor.IsPlayMode && IsDocked && IsSelected && RootWindow.FocusedControl == null) + if (Editor.IsPlayMode && ParentDockPanel != null && IsSelected && RootWindow.FocusedControl == null) { // Game UI cleared focus so regain it to maintain UI navigation just like game window does FlaxEngine.Scripting.InvokeOnUpdate(Focus); diff --git a/Source/Engine/Content/Storage/ContentStorageManager.cpp b/Source/Engine/Content/Storage/ContentStorageManager.cpp index 33a16e031..5656fd647 100644 --- a/Source/Engine/Content/Storage/ContentStorageManager.cpp +++ b/Source/Engine/Content/Storage/ContentStorageManager.cpp @@ -122,11 +122,8 @@ FlaxStorageReference ContentStorageManager::EnsureAccess(const StringView& path) // Note: because we want to create new storage package it may exists. // So let's check if any storage container is referencing that location and try to close it. auto storage = TryGetStorage(path); - if (storage && storage->IsLoaded()) - { - LOG(Info, "File \'{0}\' is in use. Trying to release handle to it.", path); - storage->CloseFileHandles(); - } + if (storage && storage->IsLoaded() && storage->CloseFileHandles()) + LOG(Warning, "Cannot release content storage handle for '{0}'.", path); return storage; } diff --git a/Source/Engine/Content/Storage/FlaxStorage.cpp b/Source/Engine/Content/Storage/FlaxStorage.cpp index 4eaea48f8..b7be69468 100644 --- a/Source/Engine/Content/Storage/FlaxStorage.cpp +++ b/Source/Engine/Content/Storage/FlaxStorage.cpp @@ -1438,10 +1438,6 @@ FileReadStream* FlaxStorage::OpenFile() bool FlaxStorage::CloseFileHandles() { - // Guard the whole process so if new thread wants to lock the chunks will need to wait for this to end - Platform::InterlockedIncrement(&_isUnloadingData); - SCOPE_EXIT{ Platform::InterlockedDecrement(&_isUnloadingData); }; - if (Platform::AtomicRead(&_chunksLock) == 0 && Platform::AtomicRead(&_files) == 0) return false; // Early out when no files are opened PROFILE_CPU(); @@ -1469,7 +1465,13 @@ bool FlaxStorage::CloseFileHandles() } } } - waitTime = 100; + + // Guard the whole process so if new thread wants to lock the chunks will need to wait for this to end + Platform::InterlockedIncrement(&_isUnloadingData); + SCOPE_EXIT{ Platform::InterlockedDecrement(&_isUnloadingData); }; + + // Wait for chunks lock again (with larger timeout for longer tasks) + waitTime = 1000; while (Platform::AtomicRead(&_chunksLock) != 0 && waitTime-- > 0) Platform::Sleep(1); if (Platform::AtomicRead(&_chunksLock) != 0) diff --git a/Source/Engine/Engine/NativeInterop.Unmanaged.cs b/Source/Engine/Engine/NativeInterop.Unmanaged.cs index cb8688dcf..d9f5729f9 100644 --- a/Source/Engine/Engine/NativeInterop.Unmanaged.cs +++ b/Source/Engine/Engine/NativeInterop.Unmanaged.cs @@ -901,20 +901,21 @@ namespace FlaxEngine.Interop Assembly assembly; #if FLAX_EDITOR - // Load assembly from loaded bytes to prevent file locking in Editor - var assemblyBytes = File.ReadAllBytes(assemblyPath); - using MemoryStream stream = new MemoryStream(assemblyBytes); + // Load assembly with stream to prevent runtime from locking the assembly file + using FileStream stream = new FileStream(assemblyPath, FileMode.Open, FileAccess.Read); var pdbPath = Path.ChangeExtension(assemblyPath, "pdb"); if (File.Exists(pdbPath)) { // Load including debug symbols - using FileStream pdbStream = new FileStream(Path.ChangeExtension(assemblyPath, "pdb"), FileMode.Open); + using FileStream pdbStream = new FileStream(Path.ChangeExtension(assemblyPath, "pdb"), FileMode.Open, FileAccess.Read); assembly = scriptingAssemblyLoadContext.LoadFromStream(stream, pdbStream); } else { assembly = scriptingAssemblyLoadContext.LoadFromStream(stream); } + + // TODO: Use new .NET 11 AssemblyLoadContext.SetAssemblyLocationOverride to specify correct Assembly.Location #else // Load assembly from file assembly = scriptingAssemblyLoadContext.LoadFromAssemblyPath(assemblyPath); diff --git a/Source/Engine/Graphics/Models/Mesh.cpp b/Source/Engine/Graphics/Models/Mesh.cpp index 27959b727..938fef145 100644 --- a/Source/Engine/Graphics/Models/Mesh.cpp +++ b/Source/Engine/Graphics/Models/Mesh.cpp @@ -197,7 +197,7 @@ bool Mesh::UpdateMesh(uint32 vertexCount, uint32 triangleCount, const Float3* ve bool Mesh::UpdateMesh(uint32 vertexCount, uint32 triangleCount, const Float3* vertices, const uint32* triangles, const Float3* normals, const Float3* tangents, const Float2* uvs, const Color32* colors) { - return ::UpdateMesh(this, vertexCount, triangleCount, PixelFormat::R16_UInt, vertices, triangles, normals, tangents, uvs, colors); + return ::UpdateMesh(this, vertexCount, triangleCount, PixelFormat::R32_UInt, vertices, triangles, normals, tangents, uvs, colors); } bool Mesh::Load(uint32 vertices, uint32 triangles, const void* vb0, const void* vb1, const void* vb2, const void* ib, bool use16BitIndexBuffer) diff --git a/Source/Engine/Graphics/Models/SkinnedMesh.cpp b/Source/Engine/Graphics/Models/SkinnedMesh.cpp index 58ab94abd..bd315decd 100644 --- a/Source/Engine/Graphics/Models/SkinnedMesh.cpp +++ b/Source/Engine/Graphics/Models/SkinnedMesh.cpp @@ -305,7 +305,7 @@ bool SkinnedMesh::UpdateMesh(uint32 vertexCount, uint32 triangleCount, const Flo bool SkinnedMesh::UpdateMesh(uint32 vertexCount, uint32 triangleCount, const Float3* vertices, const uint32* triangles, const Int4* blendIndices, const Float4* blendWeights, const Float3* normals, const Float3* tangents, const Float2* uvs, const Color32* colors) { - return ::UpdateMesh(this, vertexCount, triangleCount, PixelFormat::R16_UInt, vertices, triangles, blendIndices, blendWeights, normals, tangents, uvs, colors); + return ::UpdateMesh(this, vertexCount, triangleCount, PixelFormat::R32_UInt, vertices, triangles, blendIndices, blendWeights, normals, tangents, uvs, colors); } void SkinnedMesh::Draw(const RenderContext& renderContext, const SkinnedMeshBones& pose, MaterialBase* material, const Matrix& world, StaticFlags flags, bool receiveDecals, DrawPass drawModes, float perInstanceRandom, int8 sortOrder, uint8 stencilValue) const diff --git a/Source/Engine/Platform/Base/Enums.h b/Source/Engine/Platform/Base/Enums.h index df83ec0cb..89c1f614b 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, }; /// diff --git a/Source/Engine/ShadersCompilation/ShadersCompilation.cpp b/Source/Engine/ShadersCompilation/ShadersCompilation.cpp index d147225b4..47a9c5306 100644 --- a/Source/Engine/ShadersCompilation/ShadersCompilation.cpp +++ b/Source/Engine/ShadersCompilation/ShadersCompilation.cpp @@ -26,6 +26,8 @@ #if USE_EDITOR #define COMPILE_WITH_ASSETS_IMPORTER 1 // Hack to use shaders importing in this module #include "Engine/ContentImporters/AssetsImportingManager.h" +#include "Engine/Content/Storage/ContentStorageManager.h" +#include "Engine/Utilities/Encryption.h" #include "Engine/Platform/FileSystemWatcher.h" #include "Engine/Platform/FileSystem.h" #include "Engine/Platform/File.h" @@ -499,6 +501,37 @@ String ShadersCompilation::CompactShaderPath(StringView path) #if USE_EDITOR +bool ShadersCompilation::IsShaderSourceAssetUpToDate(const StringView& sourcePath, const StringView& assetPath) +{ + PROFILE_CPU(); + StringAnsi source; + if (File::ReadAllText(sourcePath, source)) + return false; + if (!source.HasChars() || source[source.Length() - 1] != '\n') + source.Append('\n'); + + const auto storage = ContentStorageManager::GetStorage(assetPath); + AssetInitData data; + if (!storage + || storage->GetEntriesCount() != 1 + || storage->GetEntry(0).TypeName != Shader::TypeName + || storage->LoadAssetHeader(0, data) + || data.SerializedVersion != Shader::SerializedVersion) + return false; + + FlaxChunk* sourceChunk = data.Header.Chunks[SHADER_FILE_CHUNK_SOURCE]; + if (!sourceChunk || storage->LoadAssetChunk(sourceChunk) || !sourceChunk->Data.IsValid()) + return false; + + BytesContainer embeddedSource; + embeddedSource.Copy(sourceChunk->Data); + if (embeddedSource.Length() != source.Length() + 1) + return false; + Encryption::DecryptBytes(embeddedSource.Get(), embeddedSource.Length()); + embeddedSource.Get()[embeddedSource.Length() - 1] = 0; + return Platform::MemoryCompare(embeddedSource.Get(), source.Get(), source.Length()) == 0; +} + namespace { Array ShadersSourcesWatchers; @@ -515,6 +548,13 @@ namespace return result; } + bool ImportShaderIfChanged(const StringView& sourcePath, const StringView& assetPath, Guid& assetId) + { + if (ShadersCompilation::IsShaderSourceAssetUpToDate(sourcePath, assetPath)) + return false; + return AssetsImportingManager::Import(sourcePath, assetPath, assetId); + } + void OnWatcherShadersEvent(const String& path, FileSystemAction action) { if (action == FileSystemAction::Delete || !path.EndsWith(TEXT(".shader"))) @@ -538,7 +578,7 @@ namespace const String name = StringUtils::GetPathWithoutExtension(localPath); const String outputPath = shadersAssetsPath / name + ASSET_FILES_EXTENSION_WITH_DOT; Guid id = GetShaderAssetId(name); - AssetsImportingManager::ImportIfEdited(path, outputPath, id); + ImportShaderIfChanged(path, outputPath, id); } void RegisterShaderWatchers(const ProjectInfo* project, HashSet& projects) @@ -567,7 +607,7 @@ namespace const String name = StringUtils::GetPathWithoutExtension(localPath); const String outputPath = shadersAssetsPath / name + ASSET_FILES_EXTENSION_WITH_DOT; Guid id = GetShaderAssetId(name); - AssetsImportingManager::ImportIfEdited(path, outputPath, id); + ImportShaderIfChanged(path, outputPath, id); } } diff --git a/Source/Engine/ShadersCompilation/ShadersCompilation.h b/Source/Engine/ShadersCompilation/ShadersCompilation.h index b32c9f51a..af0a9ec61 100644 --- a/Source/Engine/ShadersCompilation/ShadersCompilation.h +++ b/Source/Engine/ShadersCompilation/ShadersCompilation.h @@ -47,6 +47,16 @@ public: // Compacts the full shader file path into portable format with project name prefix such as './/ShaderFile.hlsl'. static String CompactShaderPath(StringView path); +#if USE_EDITOR + /// + /// Checks whether a shader asset embeds the current source file contents. + /// + /// The shader source file path. + /// The shader asset file path. + /// True when the embedded source matches, otherwise false. + static bool IsShaderSourceAssetUpToDate(const StringView& sourcePath, const StringView& assetPath); +#endif + private: static ShaderCompiler* RequestCompiler(ShaderProfile profile, PlatformType platform); static void FreeCompiler(ShaderCompiler* compiler); diff --git a/Source/Engine/Tests/TestContainerControl.cs b/Source/Engine/Tests/TestContainerControl.cs index 3b3c4856a..ce2b4503e 100644 --- a/Source/Engine/Tests/TestContainerControl.cs +++ b/Source/Engine/Tests/TestContainerControl.cs @@ -26,6 +26,29 @@ namespace FlaxEngine.Tests } } + private sealed class RemovingControl : MyControl + { + private readonly Control[] _controlsToRemove; + + public RemovingControl(float x, float y, float width, float height, params Control[] controlsToRemove) + : base(x, y, width, height) + { + _controlsToRemove = controlsToRemove; + } + + public override void OnMouseEnter(Float2 location) + { + for (int i = 0; i < _controlsToRemove.Length; i++) + { + var control = _controlsToRemove[i]; + if (control.Parent == Parent) + control.Parent = null; + } + + base.OnMouseEnter(location); + } + } + [Test] public void TestChildren() { @@ -53,6 +76,25 @@ namespace FlaxEngine.Tests Assert.AreEqual(cc1.GetChildAt(new Vector2(15, 5)), cc2); Assert.AreEqual(cc1.GetChildAtRecursive(new Vector2(35, 25)), c3); } + + [Test] + public void TestMouseMoveAllowsChildrenRemoval() + { + var container = new MyContainerControl(0, 0, 100, 100); + var first = new MyControl(0, 0, 100, 100); + var second = new MyControl(0, 0, 100, 100); + var removing = new RemovingControl(0, 0, 100, 100, first, second); + container.AddChild(first); + container.AddChild(second); + container.AddChild(removing); + + // The top-most child removes multiple siblings during input dispatch. + // Traversal must not use the now-stale next index. + container.OnMouseMove(new Float2(50, 50)); + + Assert.AreEqual(1, container.ChildrenCount); + Assert.AreEqual(removing, container.GetChild(0)); + } } } #endif diff --git a/Source/Engine/Tests/TestShaderSourceSync.cpp b/Source/Engine/Tests/TestShaderSourceSync.cpp new file mode 100644 index 000000000..96bad398b --- /dev/null +++ b/Source/Engine/Tests/TestShaderSourceSync.cpp @@ -0,0 +1,42 @@ +// Copyright (c) Wojciech Figat. All rights reserved. + +#include "Engine/Core/ScopeExit.h" +#include "Engine/Core/Types/DataContainer.h" +#include "Engine/Engine/Globals.h" +#include "Engine/Platform/File.h" +#include "Engine/Platform/FileSystem.h" +#include "Engine/ShadersCompilation/ShadersCompilation.h" +#include + +#if COMPILE_WITH_SHADER_COMPILER && USE_EDITOR + +TEST_CASE("Shader source asset synchronization ignores timestamps") +{ + const String sourcePath = Globals::StartupFolder / TEXT("Source/Shaders/VolumetricFog.shader"); + const String assetPath = Globals::EngineContentFolder / TEXT("Shaders/VolumetricFog.flax"); + REQUIRE(FileSystem::FileExists(sourcePath)); + REQUIRE(FileSystem::FileExists(assetPath)); + CHECK(ShadersCompilation::IsShaderSourceAssetUpToDate(sourcePath, assetPath)); + + const String tempRoot = Globals::TemporaryFolder / (TEXT("ShaderSourceSync-") + Guid::New().ToString(Guid::FormatType::N)); + REQUIRE(!FileSystem::CreateDirectory(tempRoot)); + SCOPE_EXIT + { + FileSystem::DeleteDirectory(tempRoot, true); + }; + + DataContainer assetData; + StringAnsi modifiedSource; + REQUIRE(!File::ReadAllBytes(assetPath, assetData)); + REQUIRE(!File::ReadAllText(sourcePath, modifiedSource)); + modifiedSource.Append("// Deliberately different source\n"); + + const String tempSourcePath = tempRoot / TEXT("VolumetricFog.shader"); + const String tempAssetPath = tempRoot / TEXT("VolumetricFog.flax"); + REQUIRE(!File::WriteAllBytes(tempSourcePath, modifiedSource.Get(), modifiedSource.Length())); + REQUIRE(!File::WriteAllBytes(tempAssetPath, assetData.Get(), assetData.Length())); + CHECK_FALSE(FileSystem::GetFileLastEditTime(tempSourcePath) > FileSystem::GetFileLastEditTime(tempAssetPath)); + CHECK_FALSE(ShadersCompilation::IsShaderSourceAssetUpToDate(tempSourcePath, tempAssetPath)); +} + +#endif diff --git a/Source/Engine/Tests/Tests.Build.cs b/Source/Engine/Tests/Tests.Build.cs index 1056d1002..d55456176 100644 --- a/Source/Engine/Tests/Tests.Build.cs +++ b/Source/Engine/Tests/Tests.Build.cs @@ -21,6 +21,7 @@ public class Tests : EngineModule base.Setup(options); options.PrivateDependencies.Add("ModelTool"); + options.PrivateDependencies.Add("ShadersCompilation"); } /// diff --git a/Source/Engine/Tools/ModelTool/ModelTool.Assimp.cpp b/Source/Engine/Tools/ModelTool/ModelTool.Assimp.cpp index d3ef6a016..ee2b76d9f 100644 --- a/Source/Engine/Tools/ModelTool/ModelTool.Assimp.cpp +++ b/Source/Engine/Tools/ModelTool/ModelTool.Assimp.cpp @@ -20,26 +20,29 @@ #include #include #include -#include #include #include -class AssimpLogStream : public Assimp::LogStream +class AssimpLogger final : public Assimp::Logger { public: - AssimpLogStream() + AssimpLogger() + : Logger(NORMAL) { - Assimp::DefaultLogger::create(""); - Assimp::DefaultLogger::get()->attachStream(this); } - ~AssimpLogStream() + bool attachStream(Assimp::LogStream*, unsigned int) override { - Assimp::DefaultLogger::get()->detachStream(this); - Assimp::DefaultLogger::kill(); + return false; } - void write(const char* message) override + bool detachStream(Assimp::LogStream*, unsigned int) override + { + return false; + } + +private: + static void Write(const Char* type, const char* message) { String s(message); if (s.Length() <= 0) @@ -52,10 +55,49 @@ public: else if (c >= 255) c = '?'; } - LOG(Info, "[Assimp]: {0}", s); + LOG(Info, "[Assimp]: {0}: {1}", type, s); + } + + void OnDebug(const char* message) override + { + if (m_Severity >= DEBUGGING) + Write(TEXT("Debug"), message); + } + + void OnVerboseDebug(const char* message) override + { + if (m_Severity >= VERBOSE) + Write(TEXT("Debug"), message); + } + + void OnInfo(const char* message) override + { + Write(TEXT("Info"), message); + } + + void OnWarn(const char* message) override + { + Write(TEXT("Warn"), message); + } + + void OnError(const char* message) override + { + Write(TEXT("Error"), message); } }; +Assimp::Logger* GetAssimpLogger() +{ + static Assimp::Logger* logger = []() + { + auto result = new AssimpLogger(); + Assimp::DefaultLogger::set(result); + LOG(Info, "Assimp {0}.{1}.{2}", aiGetVersionMajor(), aiGetVersionMinor(), aiGetVersionRevision()); + return result; + }(); + return logger; +} + Float2 ToFloat2(const aiVector2D& v) { return Float2(v.x, v.y); @@ -148,7 +190,6 @@ struct AssimpBone struct AssimpImporterData { Assimp::Importer AssimpImporter; - AssimpLogStream AssimpLogStream; const String Path; const aiScene* Scene = nullptr; const ModelTool::Options& Options; @@ -705,12 +746,7 @@ void ImportAnimation(int32 index, ModelData& data, AssimpImporterData& importerD bool ModelTool::ImportDataAssimp(const String& path, ModelData& data, Options& options, String& errorMsg) { - static bool AssimpInited = false; - if (!AssimpInited) - { - AssimpInited = true; - LOG(Info, "Assimp {0}.{1}.{2}", aiGetVersionMajor(), aiGetVersionMinor(), aiGetVersionRevision()); - } + GetAssimpLogger(); bool importMeshes = EnumHasAnyFlags(options.ImportTypes, ImportDataTypes::Geometry); bool importAnimations = EnumHasAnyFlags(options.ImportTypes, ImportDataTypes::Animations); AssimpImporterData context(path, options); diff --git a/Source/Engine/UI/GUI/ContainerControl.cs b/Source/Engine/UI/GUI/ContainerControl.cs index 55fcecf80..aa0078c19 100644 --- a/Source/Engine/UI/GUI/ContainerControl.cs +++ b/Source/Engine/UI/GUI/ContainerControl.cs @@ -910,7 +910,7 @@ namespace FlaxEngine.GUI return false; } } - for (int i = _children.Count - 1; i >= 0 && _children.Count > 0; i--) + for (int i = _children.Count - 1; i >= 0 && i < _children.Count; i--) { var child = _children[i]; if (child.Visible) @@ -928,7 +928,7 @@ namespace FlaxEngine.GUI public override void OnMouseEnter(Float2 location) { // Check all children collisions with mouse and fire events for them - for (int i = _children.Count - 1; i >= 0 && _children.Count > 0; i--) + for (int i = _children.Count - 1; i >= 0 && i < _children.Count; i--) { var child = _children[i]; if (child.Visible && child.Enabled) @@ -948,7 +948,7 @@ namespace FlaxEngine.GUI public override void OnMouseMove(Float2 location) { // Check all children collisions with mouse and fire events for them - for (int i = _children.Count - 1; i >= 0 && _children.Count > 0; i--) + for (int i = _children.Count - 1; i >= 0 && i < _children.Count; i--) { var child = _children[i]; if (child.Visible && child.Enabled) @@ -998,7 +998,7 @@ namespace FlaxEngine.GUI public override bool OnMouseWheel(Float2 location, float delta) { // Check all children collisions with mouse and fire events for them - for (int i = _children.Count - 1; i >= 0 && _children.Count > 0; i--) + for (int i = _children.Count - 1; i >= 0 && i < _children.Count; i--) { var child = _children[i]; if (child.Visible && child.Enabled) @@ -1019,7 +1019,7 @@ namespace FlaxEngine.GUI public override bool OnMouseDown(Float2 location, MouseButton button) { // Check all children collisions with mouse and fire events for them - for (int i = _children.Count - 1; i >= 0 && _children.Count > 0; i--) + for (int i = _children.Count - 1; i >= 0 && i < _children.Count; i--) { var child = _children[i]; if (child.Visible && child.Enabled) @@ -1040,7 +1040,7 @@ namespace FlaxEngine.GUI public override bool OnMouseUp(Float2 location, MouseButton button) { // Check all children collisions with mouse and fire events for them - for (int i = _children.Count - 1; i >= 0 && _children.Count > 0; i--) + for (int i = _children.Count - 1; i >= 0 && i < _children.Count; i--) { var child = _children[i]; if (child.Visible && child.Enabled) @@ -1061,7 +1061,7 @@ namespace FlaxEngine.GUI public override bool OnMouseDoubleClick(Float2 location, MouseButton button) { // Check all children collisions with mouse and fire events for them - for (int i = _children.Count - 1; i >= 0 && _children.Count > 0; i--) + for (int i = _children.Count - 1; i >= 0 && i < _children.Count; i--) { var child = _children[i]; if (child.Visible && child.Enabled) @@ -1096,7 +1096,7 @@ namespace FlaxEngine.GUI /// public override void OnTouchEnter(Float2 location, int pointerId) { - for (int i = _children.Count - 1; i >= 0 && _children.Count > 0; i--) + for (int i = _children.Count - 1; i >= 0 && i < _children.Count; i--) { var child = _children[i]; if (child.Visible && child.Enabled && !child.IsTouchPointerOver(pointerId)) @@ -1114,7 +1114,7 @@ namespace FlaxEngine.GUI /// public override bool OnTouchDown(Float2 location, int pointerId) { - for (int i = _children.Count - 1; i >= 0 && _children.Count > 0; i--) + for (int i = _children.Count - 1; i >= 0 && i < _children.Count; i--) { var child = _children[i]; if (child.Visible && child.Enabled) @@ -1139,7 +1139,7 @@ namespace FlaxEngine.GUI /// public override void OnTouchMove(Float2 location, int pointerId) { - for (int i = _children.Count - 1; i >= 0 && _children.Count > 0; i--) + for (int i = _children.Count - 1; i >= 0 && i < _children.Count; i--) { var child = _children[i]; if (child.Visible && child.Enabled) @@ -1168,7 +1168,7 @@ namespace FlaxEngine.GUI /// public override bool OnTouchUp(Float2 location, int pointerId) { - for (int i = _children.Count - 1; i >= 0 && _children.Count > 0; i--) + for (int i = _children.Count - 1; i >= 0 && i < _children.Count; i--) { var child = _children[i]; if (child.Visible && child.Enabled && child.IsTouchPointerOver(pointerId)) @@ -1250,7 +1250,7 @@ namespace FlaxEngine.GUI var result = base.OnDragEnter(ref location, data); // Check all children collisions with mouse and fire events for them - for (int i = _children.Count - 1; i >= 0 && _children.Count > 0; i--) + for (int i = _children.Count - 1; i >= 0 && i < _children.Count; i--) { var child = _children[i]; if (child.Visible && child.Enabled) @@ -1275,7 +1275,7 @@ namespace FlaxEngine.GUI var result = base.OnDragMove(ref location, data); // Check all children collisions with mouse and fire events for them - for (int i = _children.Count - 1; i >= 0 && _children.Count > 0; i--) + for (int i = _children.Count - 1; i >= 0 && i < _children.Count; i--) { var child = _children[i]; if (child.Visible && child.Enabled) @@ -1333,7 +1333,7 @@ namespace FlaxEngine.GUI var result = base.OnDragDrop(ref location, data); // Check all children collisions with mouse and fire events for them - for (int i = _children.Count - 1; i >= 0 && _children.Count > 0; i--) + for (int i = _children.Count - 1; i >= 0 && i < _children.Count; i--) { var child = _children[i]; if (child.Visible && child.Enabled) diff --git a/Source/Shaders/Editor/Grid.shader b/Source/Shaders/Editor/Grid.shader index e18cfef90..c4fc7c827 100644 --- a/Source/Shaders/Editor/Grid.shader +++ b/Source/Shaders/Editor/Grid.shader @@ -42,6 +42,7 @@ META_VS_IN_ELEMENT(POSITION, 0, R32G32B32_FLOAT, 0, ALIGN, PER_VERTEX, 0, true) VertexOutput VS_Grid(ModelInput input) { VertexOutput output; + input.Position.xyz += float3(ViewPos.x, 0, ViewPos.z); output.WorldPosition = input.Position.xyz + ViewOrigin; float3 geoPosition = input.Position.xyz - float3(0, ViewOrigin.y, 0); output.Position = mul(float4(geoPosition, 1), ViewProjectionMatrix); diff --git a/Source/Tools/Flax.Build/Build/DotNet/DotNetSdk.cs b/Source/Tools/Flax.Build/Build/DotNet/DotNetSdk.cs index 0d2889368..a0c518870 100644 --- a/Source/Tools/Flax.Build/Build/DotNet/DotNetSdk.cs +++ b/Source/Tools/Flax.Build/Build/DotNet/DotNetSdk.cs @@ -136,7 +136,7 @@ namespace Flax.Build /// /// The maximum SDK version. /// - public static Version MaximumVersion => new Version(10, 0); + public static Version MaximumVersion => new Version(8, 0); /// public override TargetPlatform[] Platforms diff --git a/Source/Tools/Flax.Build/Build/NativeCpp/BuildOptions.cs b/Source/Tools/Flax.Build/Build/NativeCpp/BuildOptions.cs index 9cca0dfa6..f158078f5 100644 --- a/Source/Tools/Flax.Build/Build/NativeCpp/BuildOptions.cs +++ b/Source/Tools/Flax.Build/Build/NativeCpp/BuildOptions.cs @@ -110,6 +110,11 @@ namespace Flax.Build.NativeCpp return libFolder; // Try to find nearest framework folder + if (Framework.StartsWith("netcoreapp")) + { + var version = System.Version.Parse(Framework.Substring(10)); + return string.Empty; + } if (Framework.StartsWith("net")) { var baseVersion = int.Parse(Framework.Substring(3, Framework.IndexOf('.') - 3)); @@ -144,7 +149,10 @@ namespace Flax.Build.NativeCpp var dlls = Directory.Exists(libFolder) ? Directory.GetFiles(libFolder, "*.dll", SearchOption.TopDirectoryOnly) : []; if (dlls.Length == 0) { - Log.Error($"Missing NuGet package \"{Name}, {Version}, {Framework}\" binaries (folder: {libFolder})"); + if (!File.Exists(Path.Combine(libFolder, "_._"))) // Skip error for packages without a binary (eg. Microsoft.NET.Test.Sdk) + { + Log.Error($"Missing NuGet package \"{Name}, {Version}, {Framework}\" binaries (folder: {libFolder})"); + } return string.Empty; } return dlls[0]; diff --git a/Source/Tools/Flax.Build/Projects/VisualStudio/CSSDKProjectGenerator.cs b/Source/Tools/Flax.Build/Projects/VisualStudio/CSSDKProjectGenerator.cs index ffc95e047..e99b5b328 100644 --- a/Source/Tools/Flax.Build/Projects/VisualStudio/CSSDKProjectGenerator.cs +++ b/Source/Tools/Flax.Build/Projects/VisualStudio/CSSDKProjectGenerator.cs @@ -330,9 +330,7 @@ namespace Flax.Build.Projects.VisualStudio foreach (var analyzer in configuration.TargetBuildOptions.ScriptingAPI.Analyzers) { csProjectFileContent.AppendLine(string.Format(" ", configuration.Name)); - csProjectFileContent.AppendLine(string.Format(" ", Path.GetFileNameWithoutExtension(analyzer))); - csProjectFileContent.AppendLine(string.Format(" {0}", Utilities.MakePathRelativeTo(analyzer, projectDirectory).Replace('/', '\\'))); - csProjectFileContent.AppendLine(" "); + csProjectFileContent.AppendLine(string.Format(" ", Utilities.MakePathRelativeTo(analyzer, projectDirectory).Replace('/', '\\'))); csProjectFileContent.AppendLine(" "); }