diff --git a/Content/Editor/Scripting/ScriptTemplate.cs b/Content/Editor/Scripting/ScriptTemplate.cs index 2a150306d..663acf05f 100644 --- a/Content/Editor/Scripting/ScriptTemplate.cs +++ b/Content/Editor/Scripting/ScriptTemplate.cs @@ -2,35 +2,35 @@ using System.Collections.Generic; using FlaxEngine; -namespace %namespace% +namespace %namespace%; + +/// +/// %class% Script. +/// +public class %class% : Script { - /// - /// %class% Script. - /// - public class %class% : Script + /// + public override void OnStart() { - /// - public override void OnStart() - { - // Here you can add code that needs to be called when script is created, just before the first game update - } - - /// - public override void OnEnable() - { - // Here you can add code that needs to be called when script is enabled (eg. register for events) - } + // Here you can add code that needs to be called when script is created, just before the first game update + } + + /// + public override void OnEnable() + { + // Here you can add code that needs to be called when script is enabled (eg. register for events) + } - /// - public override void OnDisable() - { - // Here you can add code that needs to be called when script is disabled (eg. unregister from events) - } + /// + public override void OnDisable() + { + // Here you can add code that needs to be called when script is disabled (eg. unregister from events) + } - /// - public override void OnUpdate() - { - // Here you can add code that needs to be called every frame - } + /// + public override void OnUpdate() + { + // Here you can add code that needs to be called every frame } } + diff --git a/Source/Editor/Content/GUI/ContentView.DragDrop.cs b/Source/Editor/Content/GUI/ContentView.DragDrop.cs index 348e2b443..ffce81e2d 100644 --- a/Source/Editor/Content/GUI/ContentView.DragDrop.cs +++ b/Source/Editor/Content/GUI/ContentView.DragDrop.cs @@ -68,7 +68,7 @@ namespace FlaxEditor.Content.GUI _validDragOver = true; result = DragDropEffect.Copy; } - else if (_dragActors.HasValidDrag) + else if (_dragActors != null && _dragActors.HasValidDrag) { _validDragOver = true; result = DragDropEffect.Move; @@ -94,7 +94,7 @@ namespace FlaxEditor.Content.GUI result = DragDropEffect.Copy; } // Check if drop actor(s) - else if (_dragActors.HasValidDrag) + else if (_dragActors != null && _dragActors.HasValidDrag) { // Import actors var currentFolder = Editor.Instance.Windows.ContentWin.CurrentViewFolder; diff --git a/Source/Editor/Content/Import/ImportFilesDialog.cs b/Source/Editor/Content/Import/ImportFilesDialog.cs index ab2c7be15..967583cf6 100644 --- a/Source/Editor/Content/Import/ImportFilesDialog.cs +++ b/Source/Editor/Content/Import/ImportFilesDialog.cs @@ -139,7 +139,7 @@ namespace FlaxEditor.Content.Import var menu = new ContextMenu(); menu.AddButton("Rename", OnRenameClicked); menu.AddButton("Don't import", OnDontImportClicked); - menu.AddButton("Show in Explorer", OnShowInExplorerClicked); + menu.AddButton(Utilities.Constants.ShowInExplorer, OnShowInExplorerClicked); menu.Tag = node; menu.Show(node, location); } diff --git a/Source/Editor/Content/Items/ContentItem.cs b/Source/Editor/Content/Items/ContentItem.cs index 94db3a5b9..6bbdc0a51 100644 --- a/Source/Editor/Content/Items/ContentItem.cs +++ b/Source/Editor/Content/Items/ContentItem.cs @@ -486,7 +486,7 @@ namespace FlaxEditor.Content else Render2D.FillRectangle(rectangle, Color.Black); } - + /// /// Draws the item thumbnail. /// @@ -684,7 +684,7 @@ namespace FlaxEditor.Content var thumbnailSize = size.X; thumbnailRect = new Rectangle(0, 0, thumbnailSize, thumbnailSize); nameAlignment = TextAlignment.Center; - + if (this is ContentFolder) { // Small shadow @@ -692,7 +692,7 @@ namespace FlaxEditor.Content var color = Color.Black.AlphaMultiplied(0.2f); Render2D.FillRectangle(shadowRect, color); Render2D.FillRectangle(clientRect, style.Background.RGBMultiplied(1.25f)); - + if (isSelected) Render2D.FillRectangle(clientRect, Parent.ContainsFocus ? style.BackgroundSelected : style.LightBackground); else if (IsMouseOver) @@ -706,14 +706,14 @@ namespace FlaxEditor.Content var shadowRect = new Rectangle(2, 2, clientRect.Width + 1, clientRect.Height + 1); var color = Color.Black.AlphaMultiplied(0.2f); Render2D.FillRectangle(shadowRect, color); - + Render2D.FillRectangle(clientRect, style.Background.RGBMultiplied(1.25f)); Render2D.FillRectangle(TextRectangle, style.LightBackground); - + var accentHeight = 2 * view.ViewScale; var barRect = new Rectangle(0, thumbnailRect.Height - accentHeight, clientRect.Width, accentHeight); Render2D.FillRectangle(barRect, Color.DimGray); - + DrawThumbnail(ref thumbnailRect, false); if (isSelected) { @@ -733,18 +733,18 @@ namespace FlaxEditor.Content var thumbnailSize = size.Y - 2 * DefaultMarginSize; thumbnailRect = new Rectangle(DefaultMarginSize, DefaultMarginSize, thumbnailSize, thumbnailSize); nameAlignment = TextAlignment.Near; - + if (isSelected) Render2D.FillRectangle(clientRect, Parent.ContainsFocus ? style.BackgroundSelected : style.LightBackground); else if (IsMouseOver) Render2D.FillRectangle(clientRect, style.BackgroundHighlighted); - + DrawThumbnail(ref thumbnailRect); break; } default: throw new ArgumentOutOfRangeException(); } - + // Draw short name Render2D.PushClip(ref textRect); Render2D.DrawText(style.FontMedium, ShowFileExtension || view.ShowFileExtensions ? FileName : ShortName, textRect, style.Foreground, nameAlignment, TextAlignment.Center, TextWrapping.WrapWords, 1f, 0.95f); diff --git a/Source/Editor/Content/Tree/MainContentTreeNode.cs b/Source/Editor/Content/Tree/MainContentTreeNode.cs index 43b36986d..def873622 100644 --- a/Source/Editor/Content/Tree/MainContentTreeNode.cs +++ b/Source/Editor/Content/Tree/MainContentTreeNode.cs @@ -29,7 +29,7 @@ namespace FlaxEditor.Content //_watcher.Changed += OnEvent; _watcher.Created += OnEvent; _watcher.Deleted += OnEvent; - //_watcher.Renamed += OnEvent; + _watcher.Renamed += OnEvent; } private void OnEvent(object sender, FileSystemEventArgs e) diff --git a/Source/Editor/Cooker/Platform/Mac/MacPlatformTools.cpp b/Source/Editor/Cooker/Platform/Mac/MacPlatformTools.cpp index b66a96a9b..0008228f7 100644 --- a/Source/Editor/Cooker/Platform/Mac/MacPlatformTools.cpp +++ b/Source/Editor/Cooker/Platform/Mac/MacPlatformTools.cpp @@ -17,7 +17,9 @@ #include "Editor/ProjectInfo.h" #include "Editor/Cooker/GameCooker.h" #include "Editor/Utilities/EditorUtilities.h" -#include + +#include "pugixml/pugixml_extra.hpp" + using namespace pugi; IMPLEMENT_SETTINGS_GETTER(MacPlatformSettings, MacPlatform); @@ -170,17 +172,17 @@ bool MacPlatformTools::OnPostProcess(CookingData& data) const String plistPath = data.DataOutputPath / TEXT("Info.plist"); { xml_document doc; - xml_node plist = doc.child_or_append(PUGIXML_TEXT("plist")); + xml_node_extra plist = xml_node_extra(doc).child_or_append(PUGIXML_TEXT("plist")); plist.append_attribute(PUGIXML_TEXT("version")).set_value(PUGIXML_TEXT("1.0")); - xml_node dict = plist.child_or_append(PUGIXML_TEXT("dict")); + xml_node_extra dict = plist.child_or_append(PUGIXML_TEXT("dict")); #define ADD_ENTRY(key, value) \ - dict.append_child(PUGIXML_TEXT("key")).set_child_value(PUGIXML_TEXT(key)); \ - dict.append_child(PUGIXML_TEXT("string")).set_child_value(PUGIXML_TEXT(value)) + dict.append_child_with_value(PUGIXML_TEXT("key"), PUGIXML_TEXT(key)); \ + dict.append_child_with_value(PUGIXML_TEXT("string"), PUGIXML_TEXT(value)) #define ADD_ENTRY_STR(key, value) \ - dict.append_child(PUGIXML_TEXT("key")).set_child_value(PUGIXML_TEXT(key)); \ + dict.append_child_with_value(PUGIXML_TEXT("key"), PUGIXML_TEXT(key)); \ { std::u16string valueStr(value.GetText()); \ - dict.append_child(PUGIXML_TEXT("string")).set_child_value(pugi::string_t(valueStr.begin(), valueStr.end()).c_str()); } + dict.append_child_with_value(PUGIXML_TEXT("string"), pugi::string_t(valueStr.begin(), valueStr.end()).c_str()); } ADD_ENTRY("CFBundleDevelopmentRegion", "English"); ADD_ENTRY("CFBundlePackageType", "APPL"); @@ -194,22 +196,22 @@ bool MacPlatformTools::OnPostProcess(CookingData& data) ADD_ENTRY_STR("CFBundleVersion", projectVersion); ADD_ENTRY_STR("NSHumanReadableCopyright", gameSettings->CopyrightNotice); - dict.append_child(PUGIXML_TEXT("key")).set_child_value(PUGIXML_TEXT("CFBundleSupportedPlatforms")); - xml_node CFBundleSupportedPlatforms = dict.append_child(PUGIXML_TEXT("array")); - CFBundleSupportedPlatforms.append_child(PUGIXML_TEXT("string")).set_child_value(PUGIXML_TEXT("MacOSX")); + dict.append_child_with_value(PUGIXML_TEXT("key"), PUGIXML_TEXT("CFBundleSupportedPlatforms")); + xml_node_extra CFBundleSupportedPlatforms = dict.append_child(PUGIXML_TEXT("array")); + CFBundleSupportedPlatforms.append_child_with_value(PUGIXML_TEXT("string"), PUGIXML_TEXT("MacOSX")); - dict.append_child(PUGIXML_TEXT("key")).set_child_value(PUGIXML_TEXT("LSMinimumSystemVersionByArchitecture")); - xml_node LSMinimumSystemVersionByArchitecture = dict.append_child(PUGIXML_TEXT("dict")); + dict.append_child_with_value(PUGIXML_TEXT("key"), PUGIXML_TEXT("LSMinimumSystemVersionByArchitecture")); + xml_node_extra LSMinimumSystemVersionByArchitecture = dict.append_child(PUGIXML_TEXT("dict")); switch (_arch) { case ArchitectureType::x64: - LSMinimumSystemVersionByArchitecture.append_child(PUGIXML_TEXT("key")).set_child_value(PUGIXML_TEXT("x86_64")); + LSMinimumSystemVersionByArchitecture.append_child_with_value(PUGIXML_TEXT("key"), PUGIXML_TEXT("x86_64")); break; case ArchitectureType::ARM64: - LSMinimumSystemVersionByArchitecture.append_child(PUGIXML_TEXT("key")).set_child_value(PUGIXML_TEXT("arm64")); + LSMinimumSystemVersionByArchitecture.append_child_with_value(PUGIXML_TEXT("key"), PUGIXML_TEXT("arm64")); break; } - LSMinimumSystemVersionByArchitecture.append_child(PUGIXML_TEXT("string")).set_child_value(PUGIXML_TEXT("10.15")); + LSMinimumSystemVersionByArchitecture.append_child_with_value(PUGIXML_TEXT("string"), PUGIXML_TEXT("10.15")); #undef ADD_ENTRY #undef ADD_ENTRY_STR diff --git a/Source/Editor/CustomEditors/Dedicated/MissingScriptEditor.cs b/Source/Editor/CustomEditors/Dedicated/MissingScriptEditor.cs new file mode 100644 index 000000000..a6c4e6623 --- /dev/null +++ b/Source/Editor/CustomEditors/Dedicated/MissingScriptEditor.cs @@ -0,0 +1,26 @@ +using FlaxEditor.CustomEditors.Editors; +using FlaxEngine; +using FlaxEngine.GUI; + +namespace FlaxEditor.CustomEditors.Dedicated; + +/// +/// The missing script editor. +/// +[CustomEditor(typeof(MissingScript)), DefaultEditor] +public class MissingScriptEditor : GenericEditor +{ + /// + public override void Initialize(LayoutElementsContainer layout) + { + if (layout.ContainerControl is not DropPanel dropPanel) + { + base.Initialize(layout); + return; + } + + dropPanel.HeaderTextColor = Color.OrangeRed; + + base.Initialize(layout); + } +} diff --git a/Source/Editor/CustomEditors/Dedicated/SplineEditor.cs b/Source/Editor/CustomEditors/Dedicated/SplineEditor.cs index 03d58ef33..7b9b65c5c 100644 --- a/Source/Editor/CustomEditors/Dedicated/SplineEditor.cs +++ b/Source/Editor/CustomEditors/Dedicated/SplineEditor.cs @@ -348,7 +348,7 @@ namespace FlaxEditor.CustomEditors.Dedicated if (!CanEditTangent()) return; - + var index = _lastPointSelected.Index; var currentTangentInPosition = _selectedSpline.GetSplineLocalTangent(index, true).Translation; var currentTangentOutPosition = _selectedSpline.GetSplineLocalTangent(index, false).Translation; diff --git a/Source/Editor/CustomEditors/Editors/ActorTransformEditor.cs b/Source/Editor/CustomEditors/Editors/ActorTransformEditor.cs index 4aa02ac78..4c153e759 100644 --- a/Source/Editor/CustomEditors/Editors/ActorTransformEditor.cs +++ b/Source/Editor/CustomEditors/Editors/ActorTransformEditor.cs @@ -88,7 +88,7 @@ namespace FlaxEditor.CustomEditors.Editors LinkValues = Editor.Instance.Windows.PropertiesWin.ScaleLinked; // Add button with the link icon - + _linkButton = new Button { BackgroundBrush = new SpriteBrush(Editor.Instance.Icons.Link32), diff --git a/Source/Editor/CustomEditors/Editors/ObjectSwitcherEditor.cs b/Source/Editor/CustomEditors/Editors/ObjectSwitcherEditor.cs index f2398bbef..0369d679d 100644 --- a/Source/Editor/CustomEditors/Editors/ObjectSwitcherEditor.cs +++ b/Source/Editor/CustomEditors/Editors/ObjectSwitcherEditor.cs @@ -160,7 +160,6 @@ namespace FlaxEditor.CustomEditors.Editors var option = _options[comboBox.SelectedIndex]; if (option.Type != null) value = option.Creator(option.Type); - } SetValue(value); RebuildLayoutOnRefresh(); diff --git a/Source/Editor/CustomEditors/Editors/TagEditor.cs b/Source/Editor/CustomEditors/Editors/TagEditor.cs index 911397785..dbd5d124c 100644 --- a/Source/Editor/CustomEditors/Editors/TagEditor.cs +++ b/Source/Editor/CustomEditors/Editors/TagEditor.cs @@ -634,7 +634,7 @@ namespace FlaxEditor.CustomEditors.Editors var textSize = FlaxEngine.GUI.Style.Current.FontMedium.MeasureText(buttonText); if (textSize.Y > button.Width) button.Width = textSize.Y + 2; - + button.SetAnchorPreset(AnchorPresets.MiddleRight, false, true); button.Clicked += ShowPicker; } diff --git a/Source/Editor/CustomEditors/GUI/PropertiesList.cs b/Source/Editor/CustomEditors/GUI/PropertiesList.cs index 2b2f0a3d3..93aacbd34 100644 --- a/Source/Editor/CustomEditors/GUI/PropertiesList.cs +++ b/Source/Editor/CustomEditors/GUI/PropertiesList.cs @@ -175,7 +175,7 @@ namespace FlaxEditor.CustomEditors.GUI { // Clear flag _mouseOverSplitter = false; - + if (_cursorChanged) { Cursor = CursorType.Default; diff --git a/Source/Editor/GUI/ContextMenu/ContextMenuBase.cs b/Source/Editor/GUI/ContextMenu/ContextMenuBase.cs index 7ecd197eb..628af18e1 100644 --- a/Source/Editor/GUI/ContextMenu/ContextMenuBase.cs +++ b/Source/Editor/GUI/ContextMenu/ContextMenuBase.cs @@ -154,6 +154,7 @@ namespace FlaxEditor.GUI.ContextMenu } // Unlock and perform controls update + Location = Float2.Zero; UnlockChildrenRecursive(); PerformLayout(); @@ -162,7 +163,6 @@ namespace FlaxEditor.GUI.ContextMenu var dpiSize = Size * dpiScale; var locationWS = parent.PointToWindow(location); var locationSS = parentWin.PointToScreen(locationWS); - Location = Float2.Zero; var monitorBounds = Platform.GetMonitorBounds(locationSS); var rightBottomLocationSS = locationSS + dpiSize; bool isUp = false, isLeft = false; diff --git a/Source/Editor/Modules/ContentDatabaseModule.cs b/Source/Editor/Modules/ContentDatabaseModule.cs index e6391d9b2..eec55c788 100644 --- a/Source/Editor/Modules/ContentDatabaseModule.cs +++ b/Source/Editor/Modules/ContentDatabaseModule.cs @@ -919,6 +919,11 @@ namespace FlaxEditor.Modules // Check if node already has that element (skip during init when we want to walk project dir very fast) if (_isDuringFastSetup || !parent.Folder.ContainsChild(path)) { +#if PLATFORM_MAC + if (path.EndsWith(".DS_Store", StringComparison.Ordinal)) + continue; +#endif + // Create file item ContentItem item; if (path.EndsWith(".cs")) @@ -960,6 +965,11 @@ namespace FlaxEditor.Modules // Check if node already has that element (skip during init when we want to walk project dir very fast) if (_isDuringFastSetup || !parent.Folder.ContainsChild(path)) { +#if PLATFORM_MAC + if (path.EndsWith(".DS_Store", StringComparison.Ordinal)) + continue; +#endif + // Create file item ContentItem item = null; if (FlaxEngine.Content.GetAssetInfo(path, out var assetInfo)) @@ -1186,6 +1196,7 @@ namespace FlaxEditor.Modules { case WatcherChangeTypes.Created: case WatcherChangeTypes.Deleted: + case WatcherChangeTypes.Renamed: { lock (_dirtyNodes) { diff --git a/Source/Editor/Modules/UIModule.cs b/Source/Editor/Modules/UIModule.cs index 299865dea..c882efb87 100644 --- a/Source/Editor/Modules/UIModule.cs +++ b/Source/Editor/Modules/UIModule.cs @@ -553,6 +553,13 @@ namespace FlaxEditor.Modules cm.AddSeparator(); _menuEditSelectAll = cm.AddButton("Select all", inputOptions.SelectAll, Editor.SceneEditing.SelectAllScenes); _menuEditFind = cm.AddButton("Find", inputOptions.Search, Editor.Windows.SceneWin.Search); + cm.AddSeparator(); + cm.AddButton("Game Settings", () => + { + var item = Editor.ContentDatabase.Find(GameSettings.GameSettingsAssetPath); + if(item != null) + Editor.ContentEditing.Open(item); + }); // Scene MenuScene = MainMenu.AddButton("Scene"); diff --git a/Source/Editor/Options/GeneralOptions.cs b/Source/Editor/Options/GeneralOptions.cs index 63822a7ca..33124bab0 100644 --- a/Source/Editor/Options/GeneralOptions.cs +++ b/Source/Editor/Options/GeneralOptions.cs @@ -106,7 +106,7 @@ namespace FlaxEditor.Options [DefaultValue(60.0f), Limit(0, 666)] [EditorDisplay("General", "Editor FPS"), EditorOrder(110), Tooltip("Limit for the editor draw/update frames per second rate (FPS). Use higher values if you need more responsive interface or lower values to use less device power. Value 0 disables any limits.")] public float EditorFPS { get; set; } = 60.0f; - + /// /// Gets or sets The FPS of the editor when the editor window is not focused. Usually set to lower then the editor FPS. /// @@ -203,7 +203,7 @@ namespace FlaxEditor.Options [DefaultValue(5), Limit(1)] [EditorDisplay("Auto Save", "Auto Save Frequency"), EditorOrder(801), Tooltip("The interval between auto saves (in minutes)")] public int AutoSaveFrequency { get; set; } = 5; - + /// /// Gets or sets a value indicating the time before the auto save that the popup shows (in seconds). /// diff --git a/Source/Editor/Options/InterfaceOptions.cs b/Source/Editor/Options/InterfaceOptions.cs index 5fd8c31c7..95a273f19 100644 --- a/Source/Editor/Options/InterfaceOptions.cs +++ b/Source/Editor/Options/InterfaceOptions.cs @@ -166,7 +166,19 @@ namespace FlaxEditor.Options /// Gets or sets the output log text font. /// [EditorDisplay("Output Log", "Text Font"), EditorOrder(320), Tooltip("The output log text font.")] - public FontReference OutputLogTextFont { get; set; } = new FontReference(FlaxEngine.Content.LoadAsyncInternal(EditorAssets.InconsolataRegularFont), 10); + public FontReference OutputLogTextFont + { + get => _outputLogFont; + set + { + if (value == null) + _outputLogFont = new FontReference(FlaxEngine.Content.LoadAsyncInternal(EditorAssets.InconsolataRegularFont), 10); + else if (!value.Font) + _outputLogFont.Font = FlaxEngine.Content.LoadAsyncInternal(EditorAssets.InconsolataRegularFont); + else + _outputLogFont = value; + } + } /// /// Gets or sets the output log text color. @@ -225,29 +237,82 @@ namespace FlaxEditor.Options public int NumberOfGameClientsToLaunch = 1; private static FontAsset DefaultFont => FlaxEngine.Content.LoadAsyncInternal(EditorAssets.PrimaryFont); + private FontReference _titleFont = new FontReference(DefaultFont, 18); + private FontReference _largeFont = new FontReference(DefaultFont, 14); + private FontReference _mediumFont = new FontReference(DefaultFont, 9); + private FontReference _smallFont = new FontReference(DefaultFont, 9); + private FontReference _outputLogFont = new FontReference(FlaxEngine.Content.LoadAsyncInternal(EditorAssets.InconsolataRegularFont), 10); /// /// Gets or sets the title font for editor UI. /// [EditorDisplay("Fonts"), EditorOrder(600), Tooltip("The title font for editor UI.")] - public FontReference TitleFont { get; set; } = new FontReference(DefaultFont, 18); + public FontReference TitleFont + { + get => _titleFont; + set + { + if (value == null) + _titleFont = new FontReference(DefaultFont, 18); + else if (!value.Font) + _titleFont.Font = DefaultFont; + else + _titleFont = value; + } + } /// /// Gets or sets the large font for editor UI. /// [EditorDisplay("Fonts"), EditorOrder(610), Tooltip("The large font for editor UI.")] - public FontReference LargeFont { get; set; } = new FontReference(DefaultFont, 14); + public FontReference LargeFont + { + get => _largeFont; + set + { + if (value == null) + _largeFont = new FontReference(DefaultFont, 14); + else if (!value.Font) + _largeFont.Font = DefaultFont; + else + _largeFont = value; + } + } /// /// Gets or sets the medium font for editor UI. /// [EditorDisplay("Fonts"), EditorOrder(620), Tooltip("The medium font for editor UI.")] - public FontReference MediumFont { get; set; } = new FontReference(DefaultFont, 9); + public FontReference MediumFont + { + get => _mediumFont; + set + { + if (value == null) + _mediumFont = new FontReference(DefaultFont, 9); + else if (!value.Font) + _mediumFont.Font = DefaultFont; + else + _mediumFont = value; + } + } /// /// Gets or sets the small font for editor UI. /// [EditorDisplay("Fonts"), EditorOrder(630), Tooltip("The small font for editor UI.")] - public FontReference SmallFont { get; set; } = new FontReference(DefaultFont, 9); + public FontReference SmallFont + { + get => _smallFont; + set + { + if (value == null) + _smallFont = new FontReference(DefaultFont, 9); + else if (!value.Font) + _smallFont.Font = DefaultFont; + else + _smallFont = value; + } + } } } diff --git a/Source/Editor/Scripting/ScriptType.cs b/Source/Editor/Scripting/ScriptType.cs index 454c3a5d2..b0c3a36dd 100644 --- a/Source/Editor/Scripting/ScriptType.cs +++ b/Source/Editor/Scripting/ScriptType.cs @@ -1393,5 +1393,32 @@ namespace FlaxEditor.Scripting return _custom.GetMembers(name, MemberTypes.Method, bindingAttr).FirstOrDefault(); return ScriptMemberInfo.Null; } + + /// + /// Basic check to see if a type could be casted to another type + /// + /// Source type + /// Target type + /// True if the type can be casted + public static bool CanCast(ScriptType from, ScriptType to) + { + if (from == to) + return true; + if (from == Null || to == Null) + return false; + return (from.Type != typeof(void) && from.Type != typeof(FlaxEngine.Object)) && + (to.Type != typeof(void) && to.Type != typeof(FlaxEngine.Object)) && + from.IsAssignableFrom(to); + } + + /// + /// Basic check to see if this type could be casted to another type + /// + /// Target type + /// True if the type can be casted + public bool CanCastTo(ScriptType to) + { + return CanCast(this, to); + } } } diff --git a/Source/Editor/Surface/Archetypes/Function.cs b/Source/Editor/Surface/Archetypes/Function.cs index 54b561e4c..de02e9336 100644 --- a/Source/Editor/Surface/Archetypes/Function.cs +++ b/Source/Editor/Surface/Archetypes/Function.cs @@ -744,6 +744,16 @@ namespace FlaxEditor.Surface.Archetypes base.OnDestroy(); } + + internal static bool IsInputCompatible(NodeArchetype nodeArch, ScriptType outputType, ConnectionsHint hint) + { + return false; + } + + internal static bool IsOutputCompatible(NodeArchetype nodeArch, ScriptType inputType, ConnectionsHint hint) + { + return inputType.IsVoid; + } } private sealed class InvokeMethodNode : SurfaceNode @@ -1151,6 +1161,54 @@ namespace FlaxEditor.Surface.Archetypes base.OnDestroy(); } + + internal static bool IsInputCompatible(NodeArchetype nodeArch, ScriptType outputType, ConnectionsHint hint) + { + if (nodeArch.Tag is not ScriptMemberInfo memberInfo) + return false; + + if (!memberInfo.IsStatic) + { + if (VisjectSurface.FullCastCheck(memberInfo.DeclaringType, outputType, hint)) + return true; + } + + var parameters = memberInfo.GetParameters(); + bool isPure = (parameters.Length == 0 && !memberInfo.ValueType.IsVoid); + if (outputType.IsVoid) + return !isPure; + + foreach (var param in parameters) + { + if (param.IsOut) + continue; + if (VisjectSurface.FullCastCheck(param.Type, outputType, hint)) + return true; + } + return false; + } + + internal static bool IsOutputCompatible(NodeArchetype nodeArch, ScriptType inputType, ConnectionsHint hint) + { + if (nodeArch.Tag is not ScriptMemberInfo memberInfo) + return false; + if (VisjectSurface.FullCastCheck(memberInfo.ValueType, inputType, hint)) + return true; + + var parameters = memberInfo.GetParameters(); + bool isPure = (parameters.Length == 0 && !memberInfo.ValueType.IsVoid); + if (inputType.IsVoid) + return !isPure; + + foreach (var param in memberInfo.GetParameters()) + { + if (!param.IsOut) + continue; + if (VisjectSurface.FullCastCheck(param.Type, inputType, hint)) + return true; + } + return false; + } } private sealed class ReturnNode : SurfaceNode @@ -1777,6 +1835,16 @@ namespace FlaxEditor.Surface.Archetypes base.OnDestroy(); } + + internal static bool IsInputCompatible(NodeArchetype nodeArch, ScriptType outputType, ConnectionsHint hint) + { + return false; + } + + internal static bool IsOutputCompatible(NodeArchetype nodeArch, ScriptType inputType, ConnectionsHint hint) + { + return inputType.IsVoid; + } } private abstract class FieldNodeBase : SurfaceNode @@ -1913,6 +1981,64 @@ namespace FlaxEditor.Surface.Archetypes Title = "Get " + SurfaceUtils.GetMethodDisplayName((string)Values[1]); UpdateSignature(); } + + internal static bool IsInputCompatible(NodeArchetype nodeArch, ScriptType outputType, ConnectionsHint hint) + { + var scriptType = TypeUtils.GetType((string)nodeArch.DefaultValues[0]); + if (scriptType == ScriptType.Null) + return false; + + var members = scriptType.GetMembers((string)nodeArch.DefaultValues[1], MemberTypes.Field, BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static | BindingFlags.DeclaredOnly); + foreach (var member in members) + { + if (!SurfaceUtils.IsValidVisualScriptField(member)) + continue; + + if (member) + { + if (!member.IsStatic && VisjectSurface.FullCastCheck(scriptType, outputType, hint)) + return true; + } + else + { + var isStatic = (bool)nodeArch.DefaultValues[3]; + if (!isStatic && VisjectSurface.FullCastCheck(scriptType, outputType, hint)) + return true; + } + break; + } + + return false; + } + + internal static bool IsOutputCompatible(NodeArchetype nodeArch, ScriptType inputType, ConnectionsHint hint) + { + var scriptType = TypeUtils.GetType((string)nodeArch.DefaultValues[0]); + if (scriptType == ScriptType.Null) + return false; + + var members = scriptType.GetMembers((string)nodeArch.DefaultValues[1], MemberTypes.Field, BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static | BindingFlags.DeclaredOnly); + foreach (var member in members) + { + if (!SurfaceUtils.IsValidVisualScriptField(member)) + continue; + + if (member) + { + if (VisjectSurface.FullCastCheck(member.ValueType, inputType, hint)) + return true; + } + else + { + var typeName = (string)nodeArch.DefaultValues[2]; + if (VisjectSurface.FullCastCheck(TypeUtils.GetType(typeName), inputType, hint)) + return true; + } + break; + } + + return false; + } } private sealed class SetFieldNode : FieldNodeBase @@ -1966,6 +2092,48 @@ namespace FlaxEditor.Surface.Archetypes Title = "Set " + SurfaceUtils.GetMethodDisplayName((string)Values[1]); UpdateSignature(); } + + internal static bool IsInputCompatible(NodeArchetype nodeArch, ScriptType outputType, ConnectionsHint hint) + { + if (outputType.IsVoid) + return true; + + var scriptType = TypeUtils.GetType((string)nodeArch.DefaultValues[0]); + if (scriptType == ScriptType.Null) + return false; + + var members = scriptType.GetMembers((string)nodeArch.DefaultValues[1], MemberTypes.Field, BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static | BindingFlags.DeclaredOnly); + foreach (var member in members) + { + if (!SurfaceUtils.IsValidVisualScriptField(member)) + continue; + + if (member) + { + if (VisjectSurface.FullCastCheck(member.ValueType, outputType, hint)) + return true; + if (!member.IsStatic && VisjectSurface.FullCastCheck(scriptType, outputType, hint)) + return true; + } + else + { + var typeName = (string)nodeArch.DefaultValues[2]; + if (VisjectSurface.FullCastCheck(TypeUtils.GetType(typeName), outputType, hint)) + return true; + var isStatic = (bool)nodeArch.DefaultValues[3]; + if (!isStatic && VisjectSurface.FullCastCheck(scriptType, outputType, hint)) + return true; + } + break; + } + + return false; + } + + internal static bool IsOutputCompatible(NodeArchetype nodeArch, ScriptType inputType, ConnectionsHint hint) + { + return inputType.IsVoid; + } } private abstract class EventBaseNode : SurfaceNode, IFunctionsDependantNode @@ -2184,6 +2352,43 @@ namespace FlaxEditor.Surface.Archetypes base.OnDestroy(); } + + internal static bool IsInputCompatible(NodeArchetype nodeArch, ScriptType outputType, ConnectionsHint hint) + { + // Event based nodes always have a pulse input, so it's always compatible with void + if (outputType.IsVoid) + return true; + + var eventName = (string)nodeArch.DefaultValues[1]; + var eventType = TypeUtils.GetType((string)nodeArch.DefaultValues[0]); + var member = eventType.GetMember(eventName, MemberTypes.Event, BindingFlags.Public | BindingFlags.Static | BindingFlags.Instance); + if (member && SurfaceUtils.IsValidVisualScriptEvent(member)) + { + if (!member.IsStatic) + { + if (VisjectSurface.FullCastCheck(eventType, outputType, hint)) + return true; + } + } + return false; + } + + internal static bool IsOutputCompatible(NodeArchetype nodeArch, ScriptType inputType, ConnectionsHint hint) + { + // Event based nodes always have a pulse output, so it's always compatible with void + if (inputType.IsVoid) + return true; + + var eventName = (string)nodeArch.DefaultValues[1]; + var eventType = TypeUtils.GetType((string)nodeArch.DefaultValues[0]); + var member = eventType.GetMember(eventName, MemberTypes.Event, BindingFlags.Public | BindingFlags.Static | BindingFlags.Instance); + if (member && SurfaceUtils.IsValidVisualScriptEvent(member)) + { + if (VisjectSurface.FullCastCheck(member.ValueType, inputType, hint)) + return true; + } + return false; + } } private sealed class BindEventNode : EventBaseNode @@ -2265,6 +2470,8 @@ namespace FlaxEditor.Surface.Archetypes Title = string.Empty, Description = "Overrides the base class method with custom implementation", Flags = NodeFlags.VisualScriptGraph | NodeFlags.NoSpawnViaGUI | NodeFlags.NoSpawnViaPaste, + IsInputCompatible = MethodOverrideNode.IsInputCompatible, + IsOutputCompatible = MethodOverrideNode.IsOutputCompatible, Size = new Float2(240, 60), DefaultValues = new object[] { @@ -2277,6 +2484,8 @@ namespace FlaxEditor.Surface.Archetypes { TypeID = 4, Create = (id, context, arch, groupArch) => new InvokeMethodNode(id, context, arch, groupArch), + IsInputCompatible = InvokeMethodNode.IsInputCompatible, + IsOutputCompatible = InvokeMethodNode.IsOutputCompatible, Title = string.Empty, Flags = NodeFlags.VisualScriptGraph | NodeFlags.NoSpawnViaGUI, Size = new Float2(240, 60), @@ -2317,6 +2526,8 @@ namespace FlaxEditor.Surface.Archetypes { TypeID = 6, Create = (id, context, arch, groupArch) => new VisualScriptFunctionNode(id, context, arch, groupArch), + IsInputCompatible = VisualScriptFunctionNode.IsInputCompatible, + IsOutputCompatible = VisualScriptFunctionNode.IsOutputCompatible, Title = "New Function", Description = "Adds a new function to the script", Flags = NodeFlags.VisualScriptGraph, @@ -2330,6 +2541,8 @@ namespace FlaxEditor.Surface.Archetypes { TypeID = 7, Create = (id, context, arch, groupArch) => new GetFieldNode(id, context, arch, groupArch), + IsInputCompatible = GetFieldNode.IsInputCompatible, + IsOutputCompatible = GetFieldNode.IsOutputCompatible, Title = string.Empty, Flags = NodeFlags.VisualScriptGraph | NodeFlags.NoSpawnViaGUI, Size = new Float2(240, 60), @@ -2345,6 +2558,8 @@ namespace FlaxEditor.Surface.Archetypes { TypeID = 8, Create = (id, context, arch, groupArch) => new SetFieldNode(id, context, arch, groupArch), + IsInputCompatible = SetFieldNode.IsInputCompatible, + IsOutputCompatible = SetFieldNode.IsOutputCompatible, Title = string.Empty, Flags = NodeFlags.VisualScriptGraph | NodeFlags.NoSpawnViaGUI, Size = new Float2(240, 60), @@ -2361,6 +2576,8 @@ namespace FlaxEditor.Surface.Archetypes { TypeID = 9, Create = (id, context, arch, groupArch) => new BindEventNode(id, context, arch, groupArch), + IsInputCompatible = EventBaseNode.IsInputCompatible, + IsOutputCompatible = EventBaseNode.IsOutputCompatible, Title = string.Empty, Flags = NodeFlags.VisualScriptGraph | NodeFlags.NoSpawnViaGUI, Size = new Float2(260, 60), @@ -2383,6 +2600,8 @@ namespace FlaxEditor.Surface.Archetypes { TypeID = 10, Create = (id, context, arch, groupArch) => new UnbindEventNode(id, context, arch, groupArch), + IsInputCompatible = EventBaseNode.IsInputCompatible, + IsOutputCompatible = EventBaseNode.IsOutputCompatible, Title = string.Empty, Flags = NodeFlags.VisualScriptGraph | NodeFlags.NoSpawnViaGUI, Size = new Float2(260, 60), diff --git a/Source/Editor/Surface/Archetypes/Packing.cs b/Source/Editor/Surface/Archetypes/Packing.cs index 0abae2d0b..16f20809c 100644 --- a/Source/Editor/Surface/Archetypes/Packing.cs +++ b/Source/Editor/Surface/Archetypes/Packing.cs @@ -216,6 +216,35 @@ namespace FlaxEditor.Surface.Archetypes : base(id, context, nodeArch, groupArch, false) { } + + internal static bool IsInputCompatible(NodeArchetype nodeArch, ScriptType outputType, ConnectionsHint hint) + { + var typeName = (string)nodeArch.DefaultValues[0]; + var type = TypeUtils.GetType(typeName); + if (type) + { + var fields = type.GetMembers(BindingFlags.Public | BindingFlags.Instance).Where(x => x.IsField).ToArray(); + var fieldsLength = fields.Length; + for (var i = 0; i < fieldsLength; i++) + { + if (VisjectSurface.FullCastCheck(fields[i].ValueType, outputType, hint)) + return true; + } + } + return false; + } + + internal static bool IsOutputCompatible(NodeArchetype nodeArch, ScriptType inputType, ConnectionsHint hint) + { + var typeName = (string)nodeArch.DefaultValues[0]; + var type = TypeUtils.GetType(typeName); + if (type) + { + if (VisjectSurface.FullCastCheck(type, inputType, hint)) + return true; + } + return false; + } } private sealed class UnpackStructureNode : StructureNode @@ -225,6 +254,35 @@ namespace FlaxEditor.Surface.Archetypes : base(id, context, nodeArch, groupArch, true) { } + + internal static bool IsInputCompatible(NodeArchetype nodeArch, ScriptType outputType, ConnectionsHint hint) + { + var typeName = (string)nodeArch.DefaultValues[0]; + var type = TypeUtils.GetType(typeName); + if (type) + { + if (VisjectSurface.FullCastCheck(type, outputType, hint)) + return true; + } + return false; + } + + internal static bool IsOutputCompatible(NodeArchetype nodeArch, ScriptType inputType, ConnectionsHint hint) + { + var typeName = (string)nodeArch.DefaultValues[0]; + var type = TypeUtils.GetType(typeName); + if (type) + { + var fields = type.GetMembers(BindingFlags.Public | BindingFlags.Instance).Where(x => x.IsField).ToArray(); + var fieldsLength = fields.Length; + for (var i = 0; i < fieldsLength; i++) + { + if (VisjectSurface.FullCastCheck(fields[i].ValueType, inputType, hint)) + return true; + } + } + return false; + } } /// @@ -351,6 +409,8 @@ namespace FlaxEditor.Surface.Archetypes TypeID = 26, Title = "Pack Structure", Create = (id, context, arch, groupArch) => new PackStructureNode(id, context, arch, groupArch), + IsInputCompatible = PackStructureNode.IsInputCompatible, + IsOutputCompatible = PackStructureNode.IsOutputCompatible, Description = "Makes the structure data to from the components.", Flags = NodeFlags.VisualScriptGraph | NodeFlags.AnimGraph | NodeFlags.NoSpawnViaGUI, Size = new Float2(180, 20), @@ -461,6 +521,8 @@ namespace FlaxEditor.Surface.Archetypes TypeID = 36, Title = "Unpack Structure", Create = (id, context, arch, groupArch) => new UnpackStructureNode(id, context, arch, groupArch), + IsInputCompatible = UnpackStructureNode.IsInputCompatible, + IsOutputCompatible = UnpackStructureNode.IsOutputCompatible, Description = "Breaks the structure data to allow extracting components from it.", Flags = NodeFlags.VisualScriptGraph | NodeFlags.AnimGraph | NodeFlags.NoSpawnViaGUI, Size = new Float2(180, 20), diff --git a/Source/Editor/Surface/ContextMenu/VisjectCM.cs b/Source/Editor/Surface/ContextMenu/VisjectCM.cs index e7b71ddb0..8369d9c4d 100644 --- a/Source/Editor/Surface/ContextMenu/VisjectCM.cs +++ b/Source/Editor/Surface/ContextMenu/VisjectCM.cs @@ -5,7 +5,6 @@ using System.Collections.Generic; using System.Linq; using FlaxEditor.GUI.ContextMenu; using FlaxEditor.GUI.Input; -using FlaxEditor.Scripting; using FlaxEngine; using FlaxEngine.GUI; using FlaxEngine.Utilities; @@ -41,6 +40,8 @@ namespace FlaxEditor.Surface.ContextMenu public delegate List ParameterGetterDelegate(); private readonly List _groups = new List(16); + private CheckBox _contextSensitiveToggle; + private bool _contextSensitiveSearchEnabled = true; private readonly TextBox _searchBox; private bool _waitingForInput; private VisjectCMGroup _surfaceParametersGroup; @@ -128,7 +129,7 @@ namespace FlaxEditor.Surface.ContextMenu _parameterSetNodeArchetype = info.ParameterSetNodeArchetype ?? Archetypes.Parameters.Nodes[3]; // Context menu dimensions - Size = new Float2(320, 248); + Size = new Float2(300, 400); var headerPanel = new Panel(ScrollBars.None) { @@ -140,17 +141,41 @@ namespace FlaxEditor.Surface.ContextMenu }; // Title bar + var titleFontReference = new FontReference(Style.Current.FontLarge.Asset, 10); var titleLabel = new Label { - Width = Width - 8, + Width = Width * 0.5f - 8f, Height = 20, X = 4, Parent = headerPanel, Text = "Select Node", - HorizontalAlignment = TextAlignment.Center, - Font = new FontReference(Style.Current.FontLarge.Asset, 10), + HorizontalAlignment = TextAlignment.Near, + Font = titleFontReference, }; + // Context sensitive toggle + var contextSensitiveLabel = new Label + { + Width = Width * 0.5f - 28, + Height = 20, + X = Width * 0.5f, + Parent = headerPanel, + Text = "Context Sensitive", + TooltipText = "Should the nodes be filtered to only show those that can be connected in the current context?", + HorizontalAlignment = TextAlignment.Far, + Font = titleFontReference, + }; + + _contextSensitiveToggle = new CheckBox + { + Width = 20, + Height = 20, + X = Width - 24, + Parent = headerPanel, + Checked = _contextSensitiveSearchEnabled, + }; + _contextSensitiveToggle.StateChanged += OnContextSensitiveToggleStateChanged; + // Search box _searchBox = new SearchBox(false, 2, 22) { @@ -291,6 +316,10 @@ namespace FlaxEditor.Surface.ContextMenu OnSearchFilterChanged(); } } + else if (_contextSensitiveSearchEnabled) + { + group.EvaluateVisibilityWithBox(_selectedBox); + } Profiler.EndEvent(); } @@ -324,6 +353,8 @@ namespace FlaxEditor.Surface.ContextMenu Parent = group }; } + if (_contextSensitiveSearchEnabled) + group.EvaluateVisibilityWithBox(_selectedBox); group.SortChildren(); if (ShowExpanded) group.Open(false); @@ -423,8 +454,26 @@ namespace FlaxEditor.Surface.ContextMenu return; Profiler.BeginEvent("VisjectCM.OnSearchFilterChanged"); - - if (string.IsNullOrEmpty(_searchBox.Text)) + UpdateFilters(); + _searchBox.Focus(); + Profiler.EndEvent(); + } + + private void OnContextSensitiveToggleStateChanged(CheckBox checkBox) + { + // Skip events during setup or init stuff + if (IsLayoutLocked) + return; + + Profiler.BeginEvent("VisjectCM.OnContextSensitiveToggleStateChanged"); + _contextSensitiveSearchEnabled = checkBox.Checked; + UpdateFilters(); + Profiler.EndEvent(); + } + + private void UpdateFilters() + { + if (string.IsNullOrEmpty(_searchBox.Text) && _selectedBox == null) { ResetView(); Profiler.EndEvent(); @@ -435,7 +484,7 @@ namespace FlaxEditor.Surface.ContextMenu LockChildrenRecursive(); for (int i = 0; i < _groups.Count; i++) { - _groups[i].UpdateFilter(_searchBox.Text); + _groups[i].UpdateFilter(_searchBox.Text, _contextSensitiveSearchEnabled ? _selectedBox : null); _groups[i].UpdateItemSort(_selectedBox); } SortGroups(); @@ -448,9 +497,6 @@ namespace FlaxEditor.Surface.ContextMenu PerformLayout(); if (SelectedItem != null) _panel1.ScrollViewTo(SelectedItem); - _searchBox.Focus(); - Profiler.EndEvent(); - Profiler.EndEvent(); } @@ -508,7 +554,11 @@ namespace FlaxEditor.Surface.ContextMenu _searchBox.Clear(); SelectedItem = null; for (int i = 0; i < _groups.Count; i++) + { _groups[i].ResetView(); + if (_contextSensitiveSearchEnabled) + _groups[i].EvaluateVisibilityWithBox(_selectedBox); + } UnlockChildrenRecursive(); SortGroups(); @@ -764,5 +814,12 @@ namespace FlaxEditor.Surface.ContextMenu { return GetPreviousSiblings(item).OfType(); } + + /// + public override void OnDestroy() + { + _contextSensitiveToggle.StateChanged -= OnContextSensitiveToggleStateChanged; + base.OnDestroy(); + } } } diff --git a/Source/Editor/Surface/ContextMenu/VisjectCMGroup.cs b/Source/Editor/Surface/ContextMenu/VisjectCMGroup.cs index 446840f2c..e52e10482 100644 --- a/Source/Editor/Surface/ContextMenu/VisjectCMGroup.cs +++ b/Source/Editor/Surface/ContextMenu/VisjectCMGroup.cs @@ -66,7 +66,7 @@ namespace FlaxEditor.Surface.ContextMenu { if (_children[i] is VisjectCMItem item) { - item.UpdateFilter(null); + item.UpdateFilter(null, null); item.UpdateScore(null); } } @@ -84,23 +84,42 @@ namespace FlaxEditor.Surface.ContextMenu /// Updates the filter. /// /// The filter text. - public void UpdateFilter(string filterText) + /// The optionally selected box to show hints for it. + public void UpdateFilter(string filterText, Box selectedBox) { Profiler.BeginEvent("VisjectCMGroup.UpdateFilter"); // Update items bool isAnyVisible = false; + bool groupHeaderMatches = QueryFilterHelper.Match(filterText, HeaderText); for (int i = 0; i < _children.Count; i++) { if (_children[i] is VisjectCMItem item) { - item.UpdateFilter(filterText); + item.UpdateFilter(filterText, selectedBox, groupHeaderMatches); isAnyVisible |= item.Visible; } } - // Update header title - if (QueryFilterHelper.Match(filterText, HeaderText)) + // Update itself + if (isAnyVisible) + { + if (!string.IsNullOrEmpty(filterText)) + Open(false); + Visible = true; + } + else + { + // Hide group if none of the items matched the filter + Visible = false; + } + + Profiler.EndEvent(); + } + + internal void EvaluateVisibilityWithBox(Box selectedBox) + { + if (selectedBox == null) { for (int i = 0; i < _children.Count; i++) { @@ -109,14 +128,25 @@ namespace FlaxEditor.Surface.ContextMenu item.Visible = true; } } - isAnyVisible = true; + Visible = true; + return; + } + + Profiler.BeginEvent("VisjectCMGroup.EvaluateVisibilityWithBox"); + + bool isAnyVisible = false; + for (int i = 0; i < _children.Count; i++) + { + if (_children[i] is VisjectCMItem item) + { + item.Visible = item.CanConnectTo(selectedBox); + isAnyVisible |= item.Visible; + } } // Update itself if (isAnyVisible) { - if (!string.IsNullOrEmpty(filterText)) - Open(false); Visible = true; } else diff --git a/Source/Editor/Surface/ContextMenu/VisjectCMItem.cs b/Source/Editor/Surface/ContextMenu/VisjectCMItem.cs index b68d529a1..7fd583202 100644 --- a/Source/Editor/Surface/ContextMenu/VisjectCMItem.cs +++ b/Source/Editor/Surface/ContextMenu/VisjectCMItem.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; using System.Linq; +using FlaxEditor.Scripting; using FlaxEditor.Surface.Elements; using FlaxEditor.Utilities; using FlaxEngine; @@ -56,7 +57,7 @@ namespace FlaxEditor.Surface.ContextMenu /// The group archetype. /// The archetype. public VisjectCMItem(VisjectCMGroup group, GroupArchetype groupArchetype, NodeArchetype archetype) - : base(0, 0, 120, 12) + : base(0, 0, 120, 14) { Group = group; _groupArchetype = groupArchetype; @@ -77,7 +78,7 @@ namespace FlaxEditor.Surface.ContextMenu if (!Visible) return; - if (selectedBox != null && CanConnectTo(selectedBox, NodeArchetype)) + if (selectedBox != null && CanConnectTo(selectedBox)) SortScore += 1; if (Data != null) SortScore += 1; @@ -92,35 +93,93 @@ namespace FlaxEditor.Surface.ContextMenu textRect = new Rectangle(22, 0, Width - 24, Height); } - private bool CanConnectTo(Box startBox, NodeArchetype nodeArchetype) + /// + /// Checks if this context menu item can be connected to a given box, before a node is actually spawned. + /// + /// The connected box + /// True if the connected box is compatible with this item + public bool CanConnectTo(Box startBox) { + // Is compatible if box is null for reset reasons if (startBox == null) - return false; - if (!startBox.IsOutput) - return false; // For now, I'm only handing the output box case + return true; - if (nodeArchetype.Elements != null) + if (_archetype == null) + return false; + + bool isCompatible = false; + if (startBox.IsOutput && _archetype.IsInputCompatible != null) { - for (int i = 0; i < nodeArchetype.Elements.Length; i++) - { - if (nodeArchetype.Elements[i].Type == NodeElementType.Input && - startBox.CanUseType(nodeArchetype.Elements[i].ConnectionsType)) - { - return true; - } - } + isCompatible |= _archetype.IsInputCompatible.Invoke(_archetype, startBox.CurrentType, _archetype.ConnectionsHints); } - return false; + else if (!startBox.IsOutput && _archetype.IsOutputCompatible != null) + { + isCompatible |= _archetype.IsOutputCompatible.Invoke(_archetype, startBox.CurrentType, startBox.ParentNode.Archetype.ConnectionsHints); + } + else if (_archetype.Elements != null) + { + // Check compatibility based on the defined elements in the archetype. This handles all the default groups and items + isCompatible = CheckElementsCompatibility(startBox); + } + + return isCompatible; + } + + private bool CheckElementsCompatibility(Box startBox) + { + bool isCompatible = false; + foreach (NodeElementArchetype element in _archetype.Elements) + { + // Ignore all elements that aren't inputs or outputs (e.g. input fields) + if (element.Type != NodeElementType.Output && element.Type != NodeElementType.Input) + continue; + + // Ignore elements with the same direction as the box + if ((startBox.IsOutput && element.Type == NodeElementType.Output) || (!startBox.IsOutput && element.Type == NodeElementType.Input)) + continue; + + ScriptType fromType; + ScriptType toType; + ConnectionsHint hint; + if (startBox.IsOutput) + { + fromType = element.ConnectionsType; + toType = startBox.CurrentType; + hint = _archetype.ConnectionsHints; + } + else + { + fromType = startBox.CurrentType; + toType = element.ConnectionsType; + hint = startBox.ParentNode.Archetype.ConnectionsHints; + } + + isCompatible |= VisjectSurface.FullCastCheck(fromType, toType, hint); + } + + return isCompatible; } /// /// Updates the filter. /// /// The filter text. - public void UpdateFilter(string filterText) + /// The optionally selected box to show hints for it. + /// True if item's group header got a filter match and item should stay visible. + public void UpdateFilter(string filterText, Box selectedBox, bool groupHeaderMatches = false) { + if (selectedBox != null) + { + Visible = CanConnectTo(selectedBox); + if (!Visible) + { + _highlights?.Clear(); + return; + } + } + _isStartsWithMatch = _isFullMatch = false; - if (filterText == null) + if (string.IsNullOrEmpty(filterText)) { // Clear filter _highlights?.Clear(); @@ -184,7 +243,7 @@ namespace FlaxEditor.Surface.ContextMenu Data = data; } - else + else if (!groupHeaderMatches) { // Hide _highlights?.Clear(); diff --git a/Source/Editor/Surface/Elements/Box.cs b/Source/Editor/Surface/Elements/Box.cs index da081446d..53bba2d68 100644 --- a/Source/Editor/Surface/Elements/Box.cs +++ b/Source/Editor/Surface/Elements/Box.cs @@ -91,7 +91,7 @@ namespace FlaxEditor.Surface.Elements _currentType = value; // Check if will need to update box connections due to type change - if ((Surface == null || Surface._isUpdatingBoxTypes == 0) && HasAnyConnection && !CanCast(prev, _currentType)) + if ((Surface == null || Surface._isUpdatingBoxTypes == 0) && HasAnyConnection && !prev.CanCastTo(_currentType)) { // Remove all invalid connections and update those which still can be valid var connections = Connections.ToArray(); @@ -236,58 +236,8 @@ namespace FlaxEditor.Surface.Elements } // Check using connection hints - var connectionsHints = ParentNode.Archetype.ConnectionsHints; - if (Archetype.ConnectionsType == ScriptType.Null && connectionsHints != ConnectionsHint.None) - { - if ((connectionsHints & ConnectionsHint.Anything) == ConnectionsHint.Anything) - return true; - if ((connectionsHints & ConnectionsHint.Value) == ConnectionsHint.Value && type.Type != typeof(void)) - return true; - if ((connectionsHints & ConnectionsHint.Enum) == ConnectionsHint.Enum && type.IsEnum) - return true; - if ((connectionsHints & ConnectionsHint.Array) == ConnectionsHint.Array && type.IsArray) - return true; - if ((connectionsHints & ConnectionsHint.Dictionary) == ConnectionsHint.Dictionary && type.IsDictionary) - return true; - if ((connectionsHints & ConnectionsHint.Vector) == ConnectionsHint.Vector) - { - var t = type.Type; - if (t == typeof(Vector2) || - t == typeof(Vector3) || - t == typeof(Vector4) || - t == typeof(Float2) || - t == typeof(Float3) || - t == typeof(Float4) || - t == typeof(Double2) || - t == typeof(Double3) || - t == typeof(Double4) || - t == typeof(Int2) || - t == typeof(Int3) || - t == typeof(Int4) || - t == typeof(Color)) - { - return true; - } - } - if ((connectionsHints & ConnectionsHint.Scalar) == ConnectionsHint.Scalar) - { - var t = type.Type; - if (t == typeof(bool) || - t == typeof(char) || - t == typeof(byte) || - t == typeof(short) || - t == typeof(ushort) || - t == typeof(int) || - t == typeof(uint) || - t == typeof(long) || - t == typeof(ulong) || - t == typeof(float) || - t == typeof(double)) - { - return true; - } - } - } + if (VisjectSurface.IsTypeCompatible(Archetype.ConnectionsType, type, ParentNode.Archetype.ConnectionsHints)) + return true; // Check independent and if there is box with bigger potential because it may block current one from changing type var parentArch = ParentNode.Archetype; @@ -301,7 +251,7 @@ namespace FlaxEditor.Surface.Elements var b = ParentNode.GetBox(boxes[i]); // Check if its the same and tested type matches the default value type - if (b == this && CanCast(parentArch.DefaultType, type)) + if (b == this && parentArch.DefaultType.CanCastTo(type)) { // Can return true; @@ -690,17 +640,6 @@ namespace FlaxEditor.Surface.Elements } } - private static bool CanCast(ScriptType oB, ScriptType iB) - { - if (oB == iB) - return true; - if (oB == ScriptType.Null || iB == ScriptType.Null) - return false; - return (oB.Type != typeof(void) && oB.Type != typeof(FlaxEngine.Object)) && - (iB.Type != typeof(void) && iB.Type != typeof(FlaxEngine.Object)) && - oB.IsAssignableFrom(iB); - } - /// public bool AreConnected(IConnectionInstigator other) { @@ -759,7 +698,7 @@ namespace FlaxEditor.Surface.Elements { if (!iB.CanUseType(oB.CurrentType)) { - if (!CanCast(oB.CurrentType, iB.CurrentType)) + if (!oB.CurrentType.CanCastTo(iB.CurrentType)) { // Cannot return false; @@ -770,7 +709,7 @@ namespace FlaxEditor.Surface.Elements { if (!oB.CanUseType(iB.CurrentType)) { - if (!CanCast(oB.CurrentType, iB.CurrentType)) + if (!oB.CurrentType.CanCastTo(iB.CurrentType)) { // Cannot return false; @@ -843,7 +782,7 @@ namespace FlaxEditor.Surface.Elements bool useCaster = false; if (!iB.CanUseType(oB.CurrentType)) { - if (CanCast(oB.CurrentType, iB.CurrentType)) + if (oB.CurrentType.CanCastTo(iB.CurrentType)) useCaster = true; else return; diff --git a/Source/Editor/Surface/Elements/OutputBox.cs b/Source/Editor/Surface/Elements/OutputBox.cs index b6efa1aa3..497e2001a 100644 --- a/Source/Editor/Surface/Elements/OutputBox.cs +++ b/Source/Editor/Surface/Elements/OutputBox.cs @@ -42,7 +42,7 @@ namespace FlaxEditor.Surface.Elements // Calculate control points CalculateBezierControlPoints(start, end, out var control1, out var control2); - + // Draw line Render2D.DrawBezier(start, control1, control2, end, color, thickness); @@ -61,16 +61,16 @@ namespace FlaxEditor.Surface.Elements const float maxControlLength = 150f; var dst = (end - start).Length; var yDst = Mathf.Abs(start.Y - end.Y); - + // Calculate control points var minControlDst = dst * 0.5f; var maxControlDst = Mathf.Max(Mathf.Min(maxControlLength, dst), minControlLength); var controlDst = Mathf.Lerp(minControlDst, maxControlDst, Mathf.Clamp(yDst / minControlLength, 0f, 1f)); - + control1 = new Float2(start.X + controlDst, start.Y); control2 = new Float2(end.X - controlDst, end.Y); } - + /// /// Checks if a point intersects a connection /// @@ -93,11 +93,13 @@ namespace FlaxEditor.Surface.Elements public static bool IntersectsConnection(ref Float2 start, ref Float2 end, ref Float2 point, float distance) { // Pretty much a point in rectangle check - if ((point.X - start.X) * (end.X - point.X) < 0) return false; + if ((point.X - start.X) * (end.X - point.X) < 0) + return false; float offset = Mathf.Sign(end.Y - start.Y) * distance; - if ((point.Y - (start.Y - offset)) * ((end.Y + offset) - point.Y) < 0) return false; - + if ((point.Y - (start.Y - offset)) * ((end.Y + offset) - point.Y) < 0) + return false; + float squaredDistance = distance; CalculateBezierControlPoints(start, end, out var control1, out var control2); diff --git a/Source/Editor/Surface/NodeArchetype.cs b/Source/Editor/Surface/NodeArchetype.cs index 6dc923ce2..fe54268e2 100644 --- a/Source/Editor/Surface/NodeArchetype.cs +++ b/Source/Editor/Surface/NodeArchetype.cs @@ -89,6 +89,11 @@ namespace FlaxEditor.Surface /// The created node object. public delegate SurfaceNode CreateCustomNodeFunc(uint id, VisjectSurfaceContext context, NodeArchetype nodeArch, GroupArchetype groupArch); + /// + /// Checks if the given type is compatible with the given node archetype. Used for custom nodes + /// + public delegate bool IsCompatible(NodeArchetype nodeArch, ScriptType portType, ConnectionsHint hint); + /// /// Unique node type ID within a single group. /// @@ -99,6 +104,16 @@ namespace FlaxEditor.Surface /// public CreateCustomNodeFunc Create; + /// + /// Function for asynchronously loaded nodes to check if input ports are compatible, for filtering. + /// + public IsCompatible IsInputCompatible; + + /// + /// Function for asynchronously loaded nodes to check if output ports are compatible, for filtering. + /// + public IsCompatible IsOutputCompatible; + /// /// Default initial size of the node. /// @@ -184,6 +199,8 @@ namespace FlaxEditor.Surface { TypeID = TypeID, Create = Create, + IsInputCompatible = IsInputCompatible, + IsOutputCompatible = IsOutputCompatible, Size = Size, Flags = Flags, Title = Title, diff --git a/Source/Editor/Surface/SurfaceUtils.cs b/Source/Editor/Surface/SurfaceUtils.cs index 08b9c6d63..5f4c3ef07 100644 --- a/Source/Editor/Surface/SurfaceUtils.cs +++ b/Source/Editor/Surface/SurfaceUtils.cs @@ -406,8 +406,8 @@ namespace FlaxEditor.Surface internal static bool IsValidVisualScriptType(ScriptType scriptType) { - if (!scriptType.IsPublic || - scriptType.HasAttribute(typeof(HideInEditorAttribute), true) || + if (!scriptType.IsPublic || + scriptType.HasAttribute(typeof(HideInEditorAttribute), true) || scriptType.HasAttribute(typeof(System.Runtime.CompilerServices.CompilerGeneratedAttribute), false)) return false; if (scriptType.IsGenericType) diff --git a/Source/Editor/Surface/VisjectSurface.Connecting.cs b/Source/Editor/Surface/VisjectSurface.Connecting.cs index 2fbff7cb8..dd34f9c7c 100644 --- a/Source/Editor/Surface/VisjectSurface.Connecting.cs +++ b/Source/Editor/Surface/VisjectSurface.Connecting.cs @@ -136,6 +136,88 @@ namespace FlaxEditor.Surface return result; } + /// + /// Checks if a type is compatible with another type and can be casted by using a connection hint + /// + /// Source type + /// Type to check compatibility with + /// Hint to check if casting is possible + /// True if the source type is compatible with the target type + public static bool IsTypeCompatible(ScriptType from, ScriptType to, ConnectionsHint hint) + { + if (from == ScriptType.Null && hint != ConnectionsHint.None) + { + if ((hint & ConnectionsHint.Anything) == ConnectionsHint.Anything) + return true; + if ((hint & ConnectionsHint.Value) == ConnectionsHint.Value && to.Type != typeof(void)) + return true; + if ((hint & ConnectionsHint.Enum) == ConnectionsHint.Enum && to.IsEnum) + return true; + if ((hint & ConnectionsHint.Array) == ConnectionsHint.Array && to.IsArray) + return true; + if ((hint & ConnectionsHint.Dictionary) == ConnectionsHint.Dictionary && to.IsDictionary) + return true; + if ((hint & ConnectionsHint.Vector) == ConnectionsHint.Vector) + { + var t = to.Type; + if (t == typeof(Vector2) || + t == typeof(Vector3) || + t == typeof(Vector4) || + t == typeof(Float2) || + t == typeof(Float3) || + t == typeof(Float4) || + t == typeof(Double2) || + t == typeof(Double3) || + t == typeof(Double4) || + t == typeof(Int2) || + t == typeof(Int3) || + t == typeof(Int4) || + t == typeof(Color)) + { + return true; + } + } + if ((hint & ConnectionsHint.Scalar) == ConnectionsHint.Scalar) + { + var t = to.Type; + if (t == typeof(bool) || + t == typeof(char) || + t == typeof(byte) || + t == typeof(short) || + t == typeof(ushort) || + t == typeof(int) || + t == typeof(uint) || + t == typeof(long) || + t == typeof(ulong) || + t == typeof(float) || + t == typeof(double)) + { + return true; + } + } + } + + return false; + } + + /// + /// Checks if a type is compatible with another type and can be casted by using a connection hint + /// + /// Source type + /// Target type + /// Connection hint + /// True if any method of casting or compatibility check succeeds + public static bool FullCastCheck(ScriptType from, ScriptType to, ConnectionsHint hint) + { + // Yes, from and to are switched on purpose + if (CanUseDirectCastStatic(to, from, false)) + return true; + if (IsTypeCompatible(from, to, hint)) + return true; + // Same here + return to.CanCastTo(from); + } + /// /// Checks if can use direct conversion from one type to another. /// diff --git a/Source/Editor/Utilities/Constants.cs b/Source/Editor/Utilities/Constants.cs index 176fed5f7..fb9f82cf5 100644 --- a/Source/Editor/Utilities/Constants.cs +++ b/Source/Editor/Utilities/Constants.cs @@ -14,5 +14,11 @@ namespace FlaxEditor.Utilities public const string FacebookUrl = "https://facebook.com/FlaxEngine"; public const string YoutubeUrl = "https://youtube.com/c/FlaxEngine"; public const string TwitterUrl = "https://twitter.com/FlaxEngine"; + +#if PLATFORM_MAC + public const string ShowInExplorer = "Show in Finder"; +#else + public const string ShowInExplorer = "Show in explorer"; +#endif } } diff --git a/Source/Editor/Windows/Assets/SkinnedModelWindow.cs b/Source/Editor/Windows/Assets/SkinnedModelWindow.cs index d360e5570..d8790172b 100644 --- a/Source/Editor/Windows/Assets/SkinnedModelWindow.cs +++ b/Source/Editor/Windows/Assets/SkinnedModelWindow.cs @@ -321,6 +321,7 @@ namespace FlaxEditor.Windows.Assets var tree = group.Tree(); tree.TreeControl.RightClick += OnTreeNodeRightClick; + tree.TreeControl.SelectedChanged += OnTreeSelectedChanged; for (int i = 0; i < nodes.Length; i++) { if (nodes[i].ParentIndex == -1) @@ -367,6 +368,12 @@ namespace FlaxEditor.Windows.Assets menu.Show(node, location); } + private void OnTreeSelectedChanged(List before, List after) + { + if (after.Count != 0) + ((SkeletonPropertiesProxy)Values[0]).Window._preview.ShowDebugDraw = true; + } + private void OnTreeNodeCopyName(ContextMenuButton b) { Clipboard.Text = (string)b.Tag; diff --git a/Source/Editor/Windows/ContentWindow.ContextMenu.cs b/Source/Editor/Windows/ContentWindow.ContextMenu.cs index 6923d634d..1dd26ecf5 100644 --- a/Source/Editor/Windows/ContentWindow.ContextMenu.cs +++ b/Source/Editor/Windows/ContentWindow.ContextMenu.cs @@ -58,7 +58,7 @@ namespace FlaxEditor.Windows if (item is ContentFolder contentFolder && contentFolder.Node is ProjectTreeNode) { - cm.AddButton("Show in explorer", () => FileSystem.ShowFileExplorer(CurrentViewFolder.Path)); + cm.AddButton(Utilities.Constants.ShowInExplorer, () => FileSystem.ShowFileExplorer(CurrentViewFolder.Path)); } else if (isValidElement) { @@ -72,7 +72,7 @@ namespace FlaxEditor.Windows Open(e); }); - cm.AddButton("Show in explorer", () => FileSystem.ShowFileExplorer(System.IO.Path.GetDirectoryName(item.Path))); + cm.AddButton(Utilities.Constants.ShowInExplorer, () => FileSystem.ShowFileExplorer(System.IO.Path.GetDirectoryName(item.Path))); if (item.HasDefaultThumbnail == false) { @@ -135,7 +135,7 @@ namespace FlaxEditor.Windows } else { - cm.AddButton("Show in explorer", () => FileSystem.ShowFileExplorer(CurrentViewFolder.Path)); + cm.AddButton(Utilities.Constants.ShowInExplorer, () => FileSystem.ShowFileExplorer(CurrentViewFolder.Path)); b = cm.AddButton("Paste", _view.Paste); b.Enabled = _view.CanPaste(); diff --git a/Source/Editor/Windows/ContentWindow.cs b/Source/Editor/Windows/ContentWindow.cs index af2eee09f..b1183753e 100644 --- a/Source/Editor/Windows/ContentWindow.cs +++ b/Source/Editor/Windows/ContentWindow.cs @@ -988,7 +988,7 @@ namespace FlaxEditor.Windows } _view.ShowItems(items, _sortType, false, true); } - else + else if (target != null) { // Show folder contents var items = target.Folder.Children; diff --git a/Source/Editor/Windows/GameWindow.cs b/Source/Editor/Windows/GameWindow.cs index bbdf763fb..fdedbb3c2 100644 --- a/Source/Editor/Windows/GameWindow.cs +++ b/Source/Editor/Windows/GameWindow.cs @@ -463,6 +463,18 @@ namespace FlaxEditor.Windows Cursor = CursorType.Default; } + /// + public override void OnMouseLeave() + { + base.OnMouseLeave(); + + // Remove focus from game window when mouse moves out and the cursor is hidden during game + if ((IsFocused || ContainsFocus) && Parent != null && Editor.IsPlayMode && !Screen.CursorVisible) + { + Parent.Focus(); + } + } + /// public override void OnShowContextMenu(ContextMenu menu) { diff --git a/Source/Editor/Windows/OutputLogWindow.cs b/Source/Editor/Windows/OutputLogWindow.cs index 1d188a32c..3665b7073 100644 --- a/Source/Editor/Windows/OutputLogWindow.cs +++ b/Source/Editor/Windows/OutputLogWindow.cs @@ -195,7 +195,7 @@ namespace FlaxEditor.Windows _contextMenu.AddButton("Clear log", Clear); _contextMenu.AddButton("Copy selection", _output.Copy); _contextMenu.AddButton("Select All", _output.SelectAll); - _contextMenu.AddButton("Show in explorer", () => FileSystem.ShowFileExplorer(Path.Combine(Globals.ProjectFolder, "Logs"))); + _contextMenu.AddButton(Utilities.Constants.ShowInExplorer, () => FileSystem.ShowFileExplorer(Path.Combine(Globals.ProjectFolder, "Logs"))); _contextMenu.AddButton("Scroll to bottom", () => { _vScroll.TargetValue = _vScroll.Maximum; }).Icon = Editor.Icons.ArrowDown12; // Setup editor options diff --git a/Source/Editor/Windows/Profiler/Assets.cs b/Source/Editor/Windows/Profiler/Assets.cs index e2a65d496..3ccf6e2eb 100644 --- a/Source/Editor/Windows/Profiler/Assets.cs +++ b/Source/Editor/Windows/Profiler/Assets.cs @@ -274,7 +274,7 @@ namespace FlaxEditor.Windows.Profiler ContextMenuButton b; b = cm.AddButton("Open", () => Editor.Instance.ContentEditing.Open(assetItem)); cm.AddButton("Show in content window", () => Editor.Instance.Windows.ContentWin.Select(assetItem)); - cm.AddButton("Show in explorer", () => FileSystem.ShowFileExplorer(System.IO.Path.GetDirectoryName(assetItem.Path))); + cm.AddButton(Utilities.Constants.ShowInExplorer, () => FileSystem.ShowFileExplorer(System.IO.Path.GetDirectoryName(assetItem.Path))); cm.AddButton("Select actors using this asset", () => Editor.Instance.SceneEditing.SelectActorsUsingAsset(assetItem.ID)); cm.AddButton("Show asset references graph", () => Editor.Instance.Windows.Open(new AssetReferencesGraphWindow(Editor.Instance, assetItem))); cm.AddButton("Copy name", () => Clipboard.Text = assetItem.NamePath); diff --git a/Source/Editor/Windows/Search/SearchItem.cs b/Source/Editor/Windows/Search/SearchItem.cs index e8bdc4e56..506d30d0d 100644 --- a/Source/Editor/Windows/Search/SearchItem.cs +++ b/Source/Editor/Windows/Search/SearchItem.cs @@ -157,7 +157,7 @@ namespace FlaxEditor.Windows.Search var cm = new FlaxEditor.GUI.ContextMenu.ContextMenu { Tag = assetItem }; b = cm.AddButton("Open", () => Editor.Instance.ContentFinding.Open(Item)); cm.AddSeparator(); - cm.AddButton("Show in explorer", () => FileSystem.ShowFileExplorer(System.IO.Path.GetDirectoryName(assetItem.Path))); + cm.AddButton(Utilities.Constants.ShowInExplorer, () => FileSystem.ShowFileExplorer(System.IO.Path.GetDirectoryName(assetItem.Path))); cm.AddButton("Show in Content window", () => Editor.Instance.Windows.ContentWin.Select(assetItem, true)); b.Enabled = proxy != null && proxy.CanReimport(assetItem); if (assetItem is BinaryAssetItem binaryAsset) diff --git a/Source/Engine/Content/Asset.h b/Source/Engine/Content/Asset.h index 2d2ba4b3f..be188b2b9 100644 --- a/Source/Engine/Content/Asset.h +++ b/Source/Engine/Content/Asset.h @@ -227,7 +227,7 @@ protected: bool onLoad(LoadAssetTask* task); void onLoaded(); - void onLoaded_MainThread(); + virtual void onLoaded_MainThread(); virtual void onUnload_MainThread(); #if USE_EDITOR virtual void onRename(const StringView& newPath) = 0; diff --git a/Source/Engine/Content/JsonAsset.cpp b/Source/Engine/Content/JsonAsset.cpp index 1fbc2abed..9977a28e1 100644 --- a/Source/Engine/Content/JsonAsset.cpp +++ b/Source/Engine/Content/JsonAsset.cpp @@ -12,6 +12,7 @@ #include "FlaxEngine.Gen.h" #include "Cache/AssetsCache.h" #include "Engine/Core/Log.h" +#include "Engine/Core/Config/Settings.h" #include "Engine/Serialization/JsonTools.h" #include "Engine/Serialization/JsonWriters.h" #include "Engine/Content/Factories/JsonAssetFactory.h" @@ -126,8 +127,7 @@ void FindIds(ISerializable::DeserializeStream& node, Array& output) } else if (node.IsString()) { - const auto length = node.GetStringLength(); - if (length == 32) + if (node.GetStringLength() == 32) { // Try parse as Guid in format `N` (32 hex chars) Guid id; @@ -362,10 +362,25 @@ void JsonAsset::unload(bool isReloading) #endif Scripting::ScriptsUnload.Unbind(this); DeleteInstance(); + _isAfterReload |= isReloading; JsonAssetBase::unload(isReloading); } +void JsonAsset::onLoaded_MainThread() +{ + JsonAssetBase::onLoaded_MainThread(); + + // Special case for Settings assets to flush them after edited and saved in Editor + const StringAsANSI<> dataTypeNameAnsi(DataTypeName.Get(), DataTypeName.Length()); + const auto typeHandle = Scripting::FindScriptingType(StringAnsiView(dataTypeNameAnsi.Get(), DataTypeName.Length())); + if (Instance && typeHandle && typeHandle.IsSubclassOf(SettingsBase::TypeInitializer) && _isAfterReload) + { + _isAfterReload = false; + ((SettingsBase*)Instance)->Apply(); + } +} + bool JsonAsset::CreateInstance() { ScopeLock lock(Locker); diff --git a/Source/Engine/Content/JsonAsset.h b/Source/Engine/Content/JsonAsset.h index c53649d33..a8e89cec0 100644 --- a/Source/Engine/Content/JsonAsset.h +++ b/Source/Engine/Content/JsonAsset.h @@ -118,6 +118,7 @@ API_CLASS(NoSpawn) class FLAXENGINE_API JsonAsset : public JsonAssetBase DECLARE_ASSET_HEADER(JsonAsset); private: ScriptingType::Dtor _dtor; + bool _isAfterReload = false; public: /// @@ -149,6 +150,7 @@ protected: // [JsonAssetBase] LoadResult loadAsset() override; void unload(bool isReloading) override; + void onLoaded_MainThread() override; private: bool CreateInstance(); diff --git a/Source/Engine/Core/Collections/ArrayExtensions.h b/Source/Engine/Core/Collections/ArrayExtensions.h index 788b31edb..2ae6da9c8 100644 --- a/Source/Engine/Core/Collections/ArrayExtensions.h +++ b/Source/Engine/Core/Collections/ArrayExtensions.h @@ -4,7 +4,7 @@ #include "../Collections/Array.h" #include "../Collections/Dictionary.h" -#include +#include "../Delegate.h" class ArrayExtensions; @@ -23,7 +23,6 @@ public: /// /// Gets the common key. /// - /// The key. FORCE_INLINE const TKey& GetKey() const { return _key; @@ -32,7 +31,6 @@ public: /// /// Gets the common key. /// - /// The key. FORCE_INLINE TKey GetKey() { return _key; @@ -52,7 +50,7 @@ public: /// The prediction function. Should return true for the target element to find. /// The index of the element or -1 if nothing found. template - static int32 IndexOf(const Array& obj, const std::function& predicate) + static int32 IndexOf(const Array& obj, const Function& predicate) { for (int32 i = 0; i < obj.Count(); i++) { @@ -71,7 +69,7 @@ public: /// The prediction function. /// True if any element in the collection matches the prediction, otherwise false. template - static bool Any(const Array& obj, const std::function& predicate) + static bool Any(const Array& obj, const Function& predicate) { for (int32 i = 0; i < obj.Count(); i++) { @@ -90,7 +88,7 @@ public: /// The prediction function. /// True if all elements in the collection matches the prediction, otherwise false. template - static int32 All(const Array& obj, const std::function& predicate) + static int32 All(const Array& obj, const Function& predicate) { for (int32 i = 0; i < obj.Count(); i++) { @@ -109,7 +107,7 @@ public: /// A function to extract the key for each element. /// The result collection with groups. template - static void GroupBy(const Array& obj, const std::function& keySelector, Array, AllocationType>& result) + static void GroupBy(const Array& obj, const Function& keySelector, Array, AllocationType>& result) { Dictionary> data(static_cast(obj.Count() * 3.0f)); for (int32 i = 0; i < obj.Count(); i++) diff --git a/Source/Engine/Engine/Engine.cpp b/Source/Engine/Engine/Engine.cpp index ec56861ba..5b93ab588 100644 --- a/Source/Engine/Engine/Engine.cpp +++ b/Source/Engine/Engine/Engine.cpp @@ -105,6 +105,14 @@ int32 Engine::Main(const Char* cmdLine) Globals::StartupFolder = Globals::BinariesFolder = Platform::GetMainDirectory(); #if USE_EDITOR Globals::StartupFolder /= TEXT("../../../.."); +#if PLATFORM_MAC + if (Globals::BinariesFolder.EndsWith(TEXT(".app/Contents"))) + { + // If running editor from application package on macOS + Globals::StartupFolder = Globals::BinariesFolder; + Globals::BinariesFolder /= TEXT("MacOS"); + } +#endif #endif StringUtils::PathRemoveRelativeParts(Globals::StartupFolder); FileSystem::NormalizePath(Globals::BinariesFolder); @@ -122,7 +130,6 @@ int32 Engine::Main(const Char* cmdLine) } EngineImpl::InitPaths(); - EngineImpl::InitLog(); #if USE_EDITOR @@ -542,7 +549,8 @@ void EngineImpl::InitLog() LOG(Info, "Product: {0}, Company: {1}", Globals::ProductName, Globals::CompanyName); LOG(Info, "Current culture: {0}", Platform::GetUserLocaleName()); LOG(Info, "Command line: {0}", CommandLine); - LOG(Info, "Base directory: {0}", Globals::StartupFolder); + LOG(Info, "Base folder: {0}", Globals::StartupFolder); + LOG(Info, "Binaries folder: {0}", Globals::BinariesFolder); LOG(Info, "Temporary folder: {0}", Globals::TemporaryFolder); LOG(Info, "Project folder: {0}", Globals::ProjectFolder); #if USE_EDITOR diff --git a/Source/Engine/Engine/InputAxis.cs b/Source/Engine/Engine/InputAxis.cs index f8ba7e72b..c70bada74 100644 --- a/Source/Engine/Engine/InputAxis.cs +++ b/Source/Engine/Engine/InputAxis.cs @@ -1,5 +1,7 @@ // Copyright (c) 2012-2023 Wojciech Figat. All rights reserved. +using System; + namespace FlaxEngine { /// @@ -23,11 +25,17 @@ namespace FlaxEngine /// public float ValueRaw => Input.GetAxisRaw(Name); + /// + /// Occurs when axis is changed. Called before scripts update. + /// + public event Action ValueChanged; + /// /// Initializes a new instance of the class. /// public InputAxis() { + Input.AxisValueChanged += Handler; } /// @@ -36,7 +44,31 @@ namespace FlaxEngine /// The axis name. public InputAxis(string name) { + Input.AxisValueChanged += Handler; Name = name; } + + private void Handler(string name) + { + if (string.Equals(Name, name, StringComparison.OrdinalIgnoreCase)) + ValueChanged?.Invoke(); + } + + /// + /// Finalizes an instance of the class. + /// + ~InputAxis() + { + Input.AxisValueChanged -= Handler; + } + + /// + /// Releases this object. + /// + public void Dispose() + { + Input.AxisValueChanged -= Handler; + GC.SuppressFinalize(this); + } } } diff --git a/Source/Engine/Engine/InputEvent.cs b/Source/Engine/Engine/InputEvent.cs index 176d21fd0..0267aed96 100644 --- a/Source/Engine/Engine/InputEvent.cs +++ b/Source/Engine/Engine/InputEvent.cs @@ -16,15 +16,36 @@ namespace FlaxEngine public string Name; /// - /// Returns true if the event has been triggered during the current frame (e.g. user pressed a key). Use to catch events without active waiting. + /// Returns true if the event has been triggered during the current frame (e.g. user pressed a key). Use to catch events without active waiting. /// public bool Active => Input.GetAction(Name); + /// + /// Returns the event state. Use Use , , to catch events without active waiting. + /// + public InputActionState State => Input.GetActionState(Name); + /// /// Occurs when event is triggered (e.g. user pressed a key). Called before scripts update. /// + [System.Obsolete("Depreciated in 1.7, use Pressed Action.")] public event Action Triggered; + /// + /// Occurs when event is pressed (e.g. user pressed a key). Called before scripts update. + /// + public event Action Pressed; + + /// + /// Occurs when event is being pressing (e.g. user pressing a key). Called before scripts update. + /// + public event Action Pressing; + + /// + /// Occurs when event is released (e.g. user releases a key). Called before scripts update. + /// + public event Action Released; + /// /// Initializes a new instance of the class. /// @@ -51,10 +72,26 @@ namespace FlaxEngine Input.ActionTriggered -= Handler; } - private void Handler(string name) + private void Handler(string name, InputActionState state) { - if (string.Equals(name, Name, StringComparison.OrdinalIgnoreCase)) + if (!string.Equals(name, Name, StringComparison.OrdinalIgnoreCase)) + return; + switch (state) + { + case InputActionState.None: break; + case InputActionState.Waiting: break; + case InputActionState.Pressing: + Pressing?.Invoke(); + break; + case InputActionState.Press: Triggered?.Invoke(); + Pressed?.Invoke(); + break; + case InputActionState.Release: + Released?.Invoke(); + break; + default: break; + } } /// diff --git a/Source/Engine/Foliage/FoliageType.cpp b/Source/Engine/Foliage/FoliageType.cpp index 785010c44..7d530dfed 100644 --- a/Source/Engine/Foliage/FoliageType.cpp +++ b/Source/Engine/Foliage/FoliageType.cpp @@ -130,7 +130,7 @@ void FoliageType::Serialize(SerializeStream& stream, const void* otherObj) SERIALIZE(Model); - const std::function IsValidMaterial = [](const ModelInstanceEntry& e) -> bool + const Function IsValidMaterial = [](const ModelInstanceEntry& e) -> bool { return e.Material; }; diff --git a/Source/Engine/Graphics/Models/SkeletonMapping.h b/Source/Engine/Graphics/Models/SkeletonMapping.h index ac2af245b..e3ec0c793 100644 --- a/Source/Engine/Graphics/Models/SkeletonMapping.h +++ b/Source/Engine/Graphics/Models/SkeletonMapping.h @@ -66,7 +66,7 @@ public: const auto parentModelIndex = node.ParentIndex; // Find matching node in skeleton (or map to best parent) - const std::function f = [node](const T& x) -> bool + const Function f = [node](const T& x) -> bool { return x.Name == node.Name; }; diff --git a/Source/Engine/GraphicsDevice/Vulkan/GPUContextVulkan.cpp b/Source/Engine/GraphicsDevice/Vulkan/GPUContextVulkan.cpp index 64d8bfb67..28957d9f3 100644 --- a/Source/Engine/GraphicsDevice/Vulkan/GPUContextVulkan.cpp +++ b/Source/Engine/GraphicsDevice/Vulkan/GPUContextVulkan.cpp @@ -459,9 +459,9 @@ void GPUContextVulkan::UpdateDescriptorSets(const SpirvShaderDescriptorInfo& des { case VK_DESCRIPTOR_TYPE_SAMPLER: { - const VkSampler sampler = _samplerHandles[slot]; - ASSERT(sampler); - needsWrite |= dsWriter.WriteSampler(descriptorIndex, sampler, index); + const VkSampler handle = _samplerHandles[slot]; + ASSERT(handle); + needsWrite |= dsWriter.WriteSampler(descriptorIndex, handle, index); break; } case VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE: @@ -547,12 +547,18 @@ void GPUContextVulkan::UpdateDescriptorSets(const SpirvShaderDescriptorInfo& des } case VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER_DYNAMIC: { - auto cb = handles[slot]; - ASSERT(cb); - VkBuffer buffer; - VkDeviceSize offset, range; - uint32 dynamicOffset; - cb->DescriptorAsDynamicUniformBuffer(this, buffer, offset, range, dynamicOffset); + auto handle = handles[slot]; + VkBuffer buffer = VK_NULL_HANDLE; + VkDeviceSize offset = 0, range = 0; + uint32 dynamicOffset = 0; + if (handle) + handle->DescriptorAsDynamicUniformBuffer(this, buffer, offset, range, dynamicOffset); + else + { + const auto dummy = _device->HelperResources.GetDummyBuffer(); + buffer = dummy->GetHandle(); + range = dummy->GetSize(); + } needsWrite |= dsWriter.WriteDynamicUniformBuffer(descriptorIndex, buffer, offset, range, dynamicOffset, index); break; } diff --git a/Source/Engine/GraphicsDevice/Vulkan/GPUDeviceVulkan.Layers.cpp b/Source/Engine/GraphicsDevice/Vulkan/GPUDeviceVulkan.Layers.cpp index bdd92c762..0b857d394 100644 --- a/Source/Engine/GraphicsDevice/Vulkan/GPUDeviceVulkan.Layers.cpp +++ b/Source/Engine/GraphicsDevice/Vulkan/GPUDeviceVulkan.Layers.cpp @@ -35,6 +35,10 @@ static const char* GValidationLayers[] = static const char* GInstanceExtensions[] = { +#if PLATFORM_APPLE_FAMILY && defined(VK_KHR_PORTABILITY_ENUMERATION_EXTENSION_NAME) + VK_KHR_PORTABILITY_ENUMERATION_EXTENSION_NAME, + VK_KHR_GET_PHYSICAL_DEVICE_PROPERTIES_2_EXTENSION_NAME, +#endif #if VK_EXT_validation_cache VK_EXT_VALIDATION_CACHE_EXTENSION_NAME, #endif @@ -46,6 +50,9 @@ static const char* GInstanceExtensions[] = static const char* GDeviceExtensions[] = { +#if PLATFORM_APPLE_FAMILY && defined(VK_KHR_PORTABILITY_SUBSET_EXTENSION_NAME) + VK_KHR_PORTABILITY_SUBSET_EXTENSION_NAME, +#endif VK_KHR_SWAPCHAIN_EXTENSION_NAME, #if VK_KHR_maintenance1 VK_KHR_MAINTENANCE1_EXTENSION_NAME, @@ -571,7 +578,7 @@ void GPUDeviceVulkan::ParseOptionalDeviceExtensions(const Array& de const auto HasExtension = [&deviceExtensions](const char* name) -> bool { - const std::function CheckCallback = [&name](const char* const& extension) -> bool + const Function CheckCallback = [&name](const char* const& extension) -> bool { return StringUtils::Compare(extension, name) == 0; }; diff --git a/Source/Engine/GraphicsDevice/Vulkan/GPUDeviceVulkan.cpp b/Source/Engine/GraphicsDevice/Vulkan/GPUDeviceVulkan.cpp index eb23c0871..b4a6112a9 100644 --- a/Source/Engine/GraphicsDevice/Vulkan/GPUDeviceVulkan.cpp +++ b/Source/Engine/GraphicsDevice/Vulkan/GPUDeviceVulkan.cpp @@ -431,7 +431,7 @@ void DeferredDeletionQueueVulkan::EnqueueGenericResource(Type type, uint64 handl ScopeLock lock(_locker); #if BUILD_DEBUG - const std::function ContainsHandle = [handle](const Entry& e) + const Function ContainsHandle = [handle](const Entry& e) { return e.Handle == handle; }; @@ -868,7 +868,7 @@ GPUBufferVulkan* HelperResourcesVulkan::GetDummyBuffer() if (!_dummyBuffer) { _dummyBuffer = (GPUBufferVulkan*)_device->CreateBuffer(TEXT("DummyBuffer")); - _dummyBuffer->Init(GPUBufferDescription::Buffer(sizeof(int32), GPUBufferFlags::ShaderResource | GPUBufferFlags::UnorderedAccess, PixelFormat::R32_SInt)); + _dummyBuffer->Init(GPUBufferDescription::Buffer(sizeof(int32) * 256, GPUBufferFlags::ShaderResource | GPUBufferFlags::UnorderedAccess, PixelFormat::R32_SInt)); } return _dummyBuffer; @@ -1078,13 +1078,16 @@ GPUDevice* GPUDeviceVulkan::Create() VkInstanceCreateInfo instInfo; RenderToolsVulkan::ZeroStruct(instInfo, VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO); +#if PLATFORM_APPLE_FAMILY + instInfo.flags |= VK_INSTANCE_CREATE_ENUMERATE_PORTABILITY_BIT_KHR; +#endif instInfo.pApplicationInfo = &appInfo; GetInstanceLayersAndExtensions(InstanceExtensions, InstanceLayers, SupportsDebugUtilsExt); const auto hasExtension = [](const Array& extensions, const char* name) -> bool { - const std::function callback = [&name](const char* const& extension) -> bool + const Function callback = [&name](const char* const& extension) -> bool { return extension && StringUtils::Compare(extension, name) == 0; }; diff --git a/Source/Engine/GraphicsDevice/Vulkan/GraphicsDeviceVulkan.Build.cs b/Source/Engine/GraphicsDevice/Vulkan/GraphicsDeviceVulkan.Build.cs index 252652978..9ffa68272 100644 --- a/Source/Engine/GraphicsDevice/Vulkan/GraphicsDeviceVulkan.Build.cs +++ b/Source/Engine/GraphicsDevice/Vulkan/GraphicsDeviceVulkan.Build.cs @@ -51,7 +51,8 @@ public sealed class VulkanSdk : Sdk var subDirs = Directory.GetDirectories(path); if (subDirs.Length != 0) { - path = Path.Combine(subDirs[0], "macOS"); + Flax.Build.Utilities.SortVersionDirectories(subDirs); + path = Path.Combine(subDirs.Last(), "macOS"); if (Directory.Exists(path)) vulkanSdk = path; } diff --git a/Source/Engine/GraphicsDevice/Vulkan/IncludeVulkanHeaders.h b/Source/Engine/GraphicsDevice/Vulkan/IncludeVulkanHeaders.h index 3a1b6a615..4e2694aab 100644 --- a/Source/Engine/GraphicsDevice/Vulkan/IncludeVulkanHeaders.h +++ b/Source/Engine/GraphicsDevice/Vulkan/IncludeVulkanHeaders.h @@ -41,4 +41,15 @@ #define VMA_NOT_NULL #include +#if PLATFORM_APPLE_FAMILY +// Declare potentially missing extensions from newer SDKs +#ifndef VK_KHR_PORTABILITY_ENUMERATION_EXTENSION_NAME +#define VK_KHR_PORTABILITY_ENUMERATION_EXTENSION_NAME "VK_KHR_portability_enumeration" +#define VK_INSTANCE_CREATE_ENUMERATE_PORTABILITY_BIT_KHR 0x00000001 +#endif +#ifndef VK_KHR_PORTABILITY_SUBSET_EXTENSION_NAME +#define VK_KHR_PORTABILITY_SUBSET_EXTENSION_NAME "VK_KHR_portability_subset" +#endif +#endif + #endif diff --git a/Source/Engine/Input/Input.cpp b/Source/Engine/Input/Input.cpp index 81411ed76..dbcdf66bd 100644 --- a/Source/Engine/Input/Input.cpp +++ b/Source/Engine/Input/Input.cpp @@ -97,7 +97,8 @@ Action Input::MouseLeave; Delegate Input::TouchDown; Delegate Input::TouchMove; Delegate Input::TouchUp; -Delegate Input::ActionTriggered; +Delegate Input::ActionTriggered; +Delegate Input::AxisValueChanged; Array Input::ActionMappings; Array Input::AxisMappings; @@ -1017,14 +1018,22 @@ void InputService::Update() Input::SetMousePosition(Screen::GetSize() * 0.5f); } - // Send events for the active actions (send events only in play mode) + // Send events for the active actions and axes (send events only in play mode) if (!Time::GetGamePaused()) { + for (auto i = Axes.Begin(); i.IsNotEnd(); ++i) + { + if (Math::NotNearEqual(i->Value.Value, i->Value.PrevKeyValue)) + { + Input::AxisValueChanged(i->Key); + } + } + for (auto i = Actions.Begin(); i.IsNotEnd(); ++i) { - if (i->Value.Active) + if (i->Value.State != InputActionState::Waiting) { - Input::ActionTriggered(i->Key); + Input::ActionTriggered(i->Key, i->Value.State); } } } diff --git a/Source/Engine/Input/Input.h b/Source/Engine/Input/Input.h index 84d312fa3..c11026eff 100644 --- a/Source/Engine/Input/Input.h +++ b/Source/Engine/Input/Input.h @@ -293,7 +293,13 @@ public: /// Event fired when virtual input action is triggered. Called before scripts update. See to edit configuration. /// /// - API_EVENT() static Delegate ActionTriggered; + API_EVENT() static Delegate ActionTriggered; + + /// + /// Event fired when virtual input axis is changed. Called before scripts update. See to edit configuration. + /// + /// + API_EVENT() static Delegate AxisValueChanged; /// /// Gets the value of the virtual action identified by name. Use to get the current config. diff --git a/Source/Engine/Level/Actors/AnimatedModel.cpp b/Source/Engine/Level/Actors/AnimatedModel.cpp index 2ff681628..459e1fb6b 100644 --- a/Source/Engine/Level/Actors/AnimatedModel.cpp +++ b/Source/Engine/Level/Actors/AnimatedModel.cpp @@ -697,7 +697,8 @@ void AnimatedModel::UpdateBounds() } else if (model && model->IsLoaded() && model->LODs.Count() != 0) { - BoundingBox box = model->LODs[0].GetBox(_transform, _deformation); + const BoundingBox modelBox = model->GetBox(_transform.GetWorld()); + BoundingBox box = modelBox; if (GraphInstance.NodesPose.Count() != 0) { // Per-bone bounds estimated from positions @@ -705,11 +706,11 @@ void AnimatedModel::UpdateBounds() const int32 bonesCount = skeleton.Bones.Count(); for (int32 boneIndex = 0; boneIndex < bonesCount; boneIndex++) box.Merge(_transform.LocalToWorld(GraphInstance.NodesPose[skeleton.Bones.Get()[boneIndex].NodeIndex].GetTranslation())); + _box = box; } - _box = box; // Apply margin based on model dimensions - const Vector3 modelBoxSize = model->GetBox().GetSize(); + const Vector3 modelBoxSize = modelBox.GetSize(); const Vector3 center = _box.GetCenter(); const Vector3 sizeHalf = Vector3::Max(_box.GetSize() + modelBoxSize * 0.2f, modelBoxSize) * 0.5f; _box = BoundingBox(center - sizeHalf, center + sizeHalf); diff --git a/Source/Engine/Level/Components/MissingScript.h b/Source/Engine/Level/Components/MissingScript.h new file mode 100644 index 000000000..bfb73498f --- /dev/null +++ b/Source/Engine/Level/Components/MissingScript.h @@ -0,0 +1,65 @@ +// Copyright (c) 2012-2023 Wojciech Figat. All rights reserved. + +#pragma once + +#if USE_EDITOR + +#include "Engine/Core/Cache.h" +#include "Engine/Scripting/Script.h" +#include "Engine/Scripting/ScriptingObjectReference.h" +#include "Engine/Serialization/JsonWriters.h" + +/// +/// Actor script component that represents missing script. +/// +API_CLASS(Attributes="HideInEditor") class FLAXENGINE_API MissingScript : public Script +{ + API_AUTO_SERIALIZATION(); + DECLARE_SCRIPTING_TYPE(MissingScript); + +private: + ScriptingObjectReference