From 62ad8bee35475d4af660b02c37b073c2234a7532 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20L=C3=BCbe=C3=9F?= Date: Fri, 14 Aug 2026 23:25:55 +0200 Subject: [PATCH] Auto detect all Visual Studio and Rider installs --- GlitchyEditor/src/CodeEditors/IIdeAdapter.bf | 6 +- .../src/CodeEditors/RiderIdeAdapter.bf | 19 +- .../src/CodeEditors/VisualStudioIdeAdapter.bf | 28 +- .../src/EditWindows/ContentBrowserWindow.bf | 11 +- GlitchyEditor/src/EditWindows/LogWindow.bf | 2 +- GlitchyEditor/src/Editor.bf | 31 ++ GlitchyEditor/src/EditorApp.bf | 4 +- GlitchyEditor/src/EditorSettings.bf | 292 ------------- GlitchyEditor/src/Project.bf | 1 + GlitchyEditor/src/Settings.bf | 146 ------- .../src/Settings/DetectIDEsBackgroundTask.bf | 192 +++++++++ GlitchyEditor/src/Settings/Settings.bf | 386 ++++++++++++++++++ .../src/{ => Settings}/SettingsWindow.bf | 148 +++---- 13 files changed, 703 insertions(+), 563 deletions(-) delete mode 100644 GlitchyEditor/src/EditorSettings.bf delete mode 100644 GlitchyEditor/src/Settings.bf create mode 100644 GlitchyEditor/src/Settings/DetectIDEsBackgroundTask.bf create mode 100644 GlitchyEditor/src/Settings/Settings.bf rename GlitchyEditor/src/{ => Settings}/SettingsWindow.bf (78%) diff --git a/GlitchyEditor/src/CodeEditors/IIdeAdapter.bf b/GlitchyEditor/src/CodeEditors/IIdeAdapter.bf index 196f1cf..41147b7 100644 --- a/GlitchyEditor/src/CodeEditors/IIdeAdapter.bf +++ b/GlitchyEditor/src/CodeEditors/IIdeAdapter.bf @@ -3,9 +3,7 @@ namespace GlitchyEditor.CodeEditors; interface IIdeAdapter { - static void OpenScript(StringView fileName); + void OpenScript(StringView fileName, int lineNumber = 0); - static void OpenScript(StringView fileName, int lineNumber); - - static void OpenScriptProject(); + void OpenScriptProject(); } diff --git a/GlitchyEditor/src/CodeEditors/RiderIdeAdapter.bf b/GlitchyEditor/src/CodeEditors/RiderIdeAdapter.bf index fde52e8..38c6429 100644 --- a/GlitchyEditor/src/CodeEditors/RiderIdeAdapter.bf +++ b/GlitchyEditor/src/CodeEditors/RiderIdeAdapter.bf @@ -6,29 +6,34 @@ using System.IO; using System.IO; using GlitchyEditor.EditWindows; using ImGui; +using GlitchyEditor.Settings; namespace GlitchyEditor.CodeEditors; class RiderIdeAdapter : IIdeAdapter { - public static void OpenScript(StringView fileName) + private IdeInstallation _ideInstallation; + + public this(IdeInstallation ideInstallation) { - OpenScript(fileName, 0); + Debug.Assert(ideInstallation.Ide == .Rider); + + _ideInstallation = ideInstallation; } - public static void OpenScript(StringView fileName, int lineNumber) + public void OpenScript(StringView fileName, int lineNumber) { String solutionPath = scope .(); Editor.Instance.CurrentProject.GetPathToScriptSolutionFile(solutionPath); ProcessStartInfo startInfo = scope .(); - startInfo.SetFileName(EditorApp.Instance.Settings.ScriptSettings.RiderPath); + startInfo.SetFileName(_ideInstallation.Path); startInfo.SetArguments(scope $"{solutionPath} --line {lineNumber} {fileName}"); scope SpawnedProcess().Start(startInfo); } - public static void OpenScriptProject() + public void OpenScriptProject() { String solutionPath = scope .(); Editor.Instance.CurrentProject.GetPathToScriptSolutionFile(solutionPath); @@ -39,7 +44,7 @@ class RiderIdeAdapter : IIdeAdapter return; } - if (!File.Exists(EditorApp.Instance.Settings.ScriptSettings.RiderPath)) + if (!File.Exists(_ideInstallation.Path)) { Editor.Instance.ShowSettings(); Editor.Instance.SettingsWindow.HighlightSetting("Tools", "Rider path"); @@ -51,7 +56,7 @@ class RiderIdeAdapter : IIdeAdapter } ProcessStartInfo startInfo = scope .(); - startInfo.SetFileName(EditorApp.Instance.Settings.ScriptSettings.RiderPath); + startInfo.SetFileName(_ideInstallation.Path); startInfo.SetArguments(solutionPath); scope SpawnedProcess().Start(startInfo); diff --git a/GlitchyEditor/src/CodeEditors/VisualStudioIdeAdapter.bf b/GlitchyEditor/src/CodeEditors/VisualStudioIdeAdapter.bf index fff684a..b4c58ef 100644 --- a/GlitchyEditor/src/CodeEditors/VisualStudioIdeAdapter.bf +++ b/GlitchyEditor/src/CodeEditors/VisualStudioIdeAdapter.bf @@ -2,12 +2,22 @@ using System; using System.Diagnostics; using GlitchyEngine; using System.Collections; +using GlitchyEditor.Settings; namespace GlitchyEditor.CodeEditors; class VisualStudioIdeAdapter : IIdeAdapter { - public static bool IsRunning() + private IdeInstallation _ideInstallation; + + public this(IdeInstallation ideInstallation) + { + Debug.Assert(ideInstallation.Ide == .VisualStudio); + + _ideInstallation = ideInstallation; + } + + public bool IsRunning() { List processes = scope .(); @@ -19,6 +29,7 @@ class VisualStudioIdeAdapter : IIdeAdapter for (Process process in processes) { + // TODO? } ClearAndDeleteItems!(processes); @@ -26,12 +37,12 @@ class VisualStudioIdeAdapter : IIdeAdapter return false; } - public static void OpenScript(StringView fileName) + public void OpenScript(StringView fileName, int lineNumber) { if (IsRunning()) { ProcessStartInfo startInfo = scope .(); - startInfo.SetFileName(EditorApp.Instance.Settings.ScriptSettings.VisualStudioPath); + startInfo.SetFileName(_ideInstallation.Path); startInfo.SetArguments(scope $"/Edit {fileName}"); scope SpawnedProcess().Start(startInfo); @@ -39,25 +50,20 @@ class VisualStudioIdeAdapter : IIdeAdapter else { ProcessStartInfo startInfo = scope .(); - startInfo.SetFileName(EditorApp.Instance.Settings.ScriptSettings.VisualStudioPath); + startInfo.SetFileName(_ideInstallation.Path); startInfo.SetArguments(scope $"/Edit {fileName}"); scope SpawnedProcess().Start(startInfo); } } - public static void OpenScript(StringView fileName, int lineNumber) - { - OpenScript(fileName); - } - - public static void OpenScriptProject() + public void OpenScriptProject() { String solutionPath = scope .(); Editor.Instance.CurrentProject.GetPathToScriptSolutionFile(solutionPath); ProcessStartInfo psi = scope .(); - psi.SetFileName("devenv"); + psi.SetFileName(_ideInstallation.Path); psi.SetArguments(solutionPath); scope SpawnedProcess().Start(psi); diff --git a/GlitchyEditor/src/EditWindows/ContentBrowserWindow.bf b/GlitchyEditor/src/EditWindows/ContentBrowserWindow.bf index 33deb62..ba9652b 100644 --- a/GlitchyEditor/src/EditWindows/ContentBrowserWindow.bf +++ b/GlitchyEditor/src/EditWindows/ContentBrowserWindow.bf @@ -604,13 +604,7 @@ namespace GlitchyEditor.EditWindows if (ImGui.MenuItem("Open C# Project...")) { - switch (EditorApp.Instance.Settings.ScriptSettings.SelectedIde) - { - case .VisualStudio: - VisualStudioIdeAdapter.OpenScriptProject(); - case .Rider: - RiderIdeAdapter.OpenScriptProject(); - } + Editor.Instance.IdeAdapter.OpenScriptProject(); } ImGui.AttachTooltip("Opens the C# Solution of this project."); @@ -1440,8 +1434,7 @@ namespace GlitchyEditor.EditWindows // Special treatment for scripts, open them in Visual Studio. if (entry->Path.EndsWith(".cs")) { - // Obviously windows only - RiderIdeAdapter.OpenScript(entry->Path); + Editor.Instance.IdeAdapter.OpenScript(entry->Path); } else if (Path.OpenFolder(entry->Path) case .Err) Log.EngineLogger.Error("Failed to open file."); diff --git a/GlitchyEditor/src/EditWindows/LogWindow.bf b/GlitchyEditor/src/EditWindows/LogWindow.bf index c94e91e..51e3a27 100644 --- a/GlitchyEditor/src/EditWindows/LogWindow.bf +++ b/GlitchyEditor/src/EditWindows/LogWindow.bf @@ -317,7 +317,7 @@ class LogWindow : EditorWindow { if (message.Source.MessageOrigin != null) { - RiderIdeAdapter.OpenScript(message.Source.MessageOrigin.FileName, message.Source.MessageOrigin.LineNumber); + Editor.Instance.IdeAdapter.OpenScript(message.Source.MessageOrigin.FileName, message.Source.MessageOrigin.LineNumber); } } diff --git a/GlitchyEditor/src/Editor.bf b/GlitchyEditor/src/Editor.bf index 6882a0a..4692797 100644 --- a/GlitchyEditor/src/Editor.bf +++ b/GlitchyEditor/src/Editor.bf @@ -6,6 +6,7 @@ using GlitchyEngine.Collections; using GlitchyEditor.EditWindows; using GlitchyEngine; using GlitchyEditor.Assets; +using GlitchyEditor.CodeEditors; namespace GlitchyEditor { @@ -79,6 +80,18 @@ namespace GlitchyEditor private static Editor s_Instance; + private IIdeAdapter _ideAdapter = null ~ delete _; + + public IIdeAdapter IdeAdapter + { + get => _ideAdapter; + private set + { + delete _ideAdapter; + _ideAdapter = value; + } + } + public static Editor Instance => s_Instance; /// Creates a new editor for the given world @@ -87,6 +100,24 @@ namespace GlitchyEditor Log.EngineLogger.AssertDebug(s_Instance == null, "Cannot create a second instance of a singleton."); s_Instance = this; + EditorApp.Instance.Settings.OnApplySettings.Add(new (s, e) => { + let activeIde = EditorApp.Instance.Settings.ScriptSettings.ActiveIde; + if (activeIde != null) + { + switch (activeIde.Ide) + { + case .VisualStudio: + IdeAdapter = new VisualStudioIdeAdapter(activeIde); + case .Rider: + IdeAdapter = new RiderIdeAdapter(activeIde); + } + } + else + { + IdeAdapter = null; + } + }); + _activeScene = activeScene; _editorScene = editorScene; _contentManager = contentManager; diff --git a/GlitchyEditor/src/EditorApp.bf b/GlitchyEditor/src/EditorApp.bf index 57b1a94..094edfe 100644 --- a/GlitchyEditor/src/EditorApp.bf +++ b/GlitchyEditor/src/EditorApp.bf @@ -13,6 +13,7 @@ using GlitchyEditor.Platform; using System.Diagnostics; using GlitchyEditor.ImGui; using GlitchyEngine.Events; +using GlitchyEditor.Settings; namespace GlitchyEditor { @@ -47,6 +48,8 @@ namespace GlitchyEditor _backgroundTaskManager.Init(); DragDropManager.Init(); + + GlitchyEditor.Settings.Settings.Load(); #if IMGUI _imGuiLayer = new ImGuiLayer(); @@ -56,7 +59,6 @@ namespace GlitchyEditor _editorLayer = new EditorLayer(args, _contentManager); PushLayer(_editorLayer); - GlitchyEditor.Settings.Load(); Settings.OnApplySettings.Add(new (s, e) => { OnEvent(scope SettingsAppliedEvent()); }); diff --git a/GlitchyEditor/src/EditorSettings.bf b/GlitchyEditor/src/EditorSettings.bf deleted file mode 100644 index b3204aa..0000000 --- a/GlitchyEditor/src/EditorSettings.bf +++ /dev/null @@ -1,292 +0,0 @@ -using System; -using GlitchyEngine; -using Bon; -using GlitchyEditor; -using GlitchyEditor.CodeEditors; -using System.Collections; -using System.IO; -using System.Diagnostics; -using System.Threading; - -namespace GlitchyEditor -{ - extension Settings - { -#if DEBUG - [SettingContainer, BonInclude] - public readonly DevSettings DevSettings = new .() ~ delete _; -#endif - - [SettingContainer, BonInclude] - public readonly EditorSettings EditorSettings = new .() ~ delete _; - - [SettingContainer, BonInclude] - public readonly ScriptSettings ScriptSettings = new .() ~ delete _; - - protected override void RegisterEventListeners() - { -#if DEBUG - OnApplySettings.Add(new (s, e) => DevSettings.Apply()); -#endif - OnApplySettings.Add(new (s, e) => EditorSettings.Apply()); - OnApplySettings.Add(new (s, e) => ScriptSettings.Apply()); - } - } -} - -namespace GlitchyEditor; - -#if DEBUG -[Reflect] -class DevSettings -{ - [Setting("Dev", "Use ScriptCore csproj", "If enabled the Editor will include the csproj of the ScriptCore instead of the compiled dll."), BonInclude] - public bool UseScriptCoreDll = true; - - public void Apply() - { - - } -} -#endif - -[Reflect] -enum ScriptIde -{ - Rider, - VisualStudio -} - -[Reflect] -class ScriptSettings -{ - [Setting("Tools", "Visual Studio path", "The path of Visual Studio's \"devenv.exe\""), BonInclude] - public String VisualStudioPath ~ delete _; - - [Setting("Tools", "Rider path", "The path to JetBrains Rider IDE (rider64.exe)"), BonInclude] - public String RiderPath ~ delete _; - - [Setting("Tools", "IDE", "The IDE that will be used to open scripts for editing."), BonInclude] - public ScriptIde SelectedIde; - - private bool _lookedForVs = false; - private bool _lookedForRider = false; - - public void Apply() - { -#if BF_PLATFORM_WINDOWS - if (!_lookedForVs && String.IsNullOrWhiteSpace(VisualStudioPath)) - { - FindVisualStudio(); - } -#endif - - if (!_lookedForRider && String.IsNullOrWhiteSpace(RiderPath)) - { - FindRider(); - } - } - - void FindVisualStudio() - { - _lookedForVs = true; - - Thread thread = new Thread(new => FindVisualStudioImpl); - thread.AutoDelete = true; - thread.IsBackground = true; - thread.Start(); - } - - void FindVisualStudioImpl() - { - Log.EngineLogger.Info("Using vswhere.exe to search for Visual Studio..."); - - String exeFile = scope .(); - Environment.GetExecutableFilePath(exeFile); - - String exeDirectory = scope .(); - Path.GetDirectoryPath(exeFile, exeDirectory); - - String vsWherePath = scope .(); - Path.Combine(vsWherePath, exeDirectory, "vswhere.exe"); - - ProcessStartInfo processInfo = scope .(); - processInfo.UseShellExecute = false; - processInfo.RedirectStandardOutput = true; - processInfo.CreateNoWindow = true; - - processInfo.SetFileName(vsWherePath); - // Find the installation path of the latest visual studio with an installed IDE. - processInfo.SetArguments("-latest -requires Microsoft.VisualStudio.Workload.NativeDesktop -property productPath"); - - let process = scope SpawnedProcess(); - if (process.Start(processInfo) case .Ok) - { - FileStream outputStream = scope FileStream(); - process.AttachStandardOutput(outputStream); - - process.WaitFor(1000); - - if (VisualStudioPath == null) - { - VisualStudioPath = new String(); - } - else - { - VisualStudioPath.Clear(); - } - - if (outputStream.Length > 0) - { - StreamReader streamReaderOut = scope StreamReader(outputStream, null, false, 4096); - streamReaderOut.ReadToEnd(VisualStudioPath).IgnoreError(); - - VisualStudioPath.Trim(); - - if (File.Exists(VisualStudioPath)) - { - Log.EngineLogger.Info($"Located Visual Studio: {VisualStudioPath}"); - - Apply(); - } - else - { - Log.EngineLogger.Error($"Failed to find Visual Studio. vswhere output: \n{VisualStudioPath}\n\n"); - VisualStudioPath.Clear(); - } - } - } - else - { - Log.EngineLogger.Error("Couldn't automatically determine location of Visual Studio: Failed to launch vswhere.exe"); - } - } - - void FindRider() - { - _lookedForRider = true; - - Thread thread = new Thread(new => FindRiderImpl); - thread.AutoDelete = true; - thread.IsBackground = true; - thread.Start(); - } - - void FindRiderImpl() - { - List installs = new .(); - - RiderPathLocator.CollectAllPaths(installs); - - // Don't touch the settings or the log from this thread: the editor's log window appends to - // a plain List and isn't synchronized, so logging from here corrupts it. - Application.Instance.InvokeOnMainThread(new () => ReportRiderInstalls(installs)); - } - - /// Logs the located Rider installations and stores the newest one in RiderPath. - /// @param installs The located installations, newest first. Takes ownership. - bool ReportRiderInstalls(List installs) - { - defer { DeleteContainerAndItems!(installs); } - - for (RiderInstallInfo install in installs) - { - Log.EngineLogger.Info($"Found Rider {install.Version} at \"{install.Path}\" ({install.InstallType})"); - } - - if (installs.IsEmpty) - { - Log.EngineLogger.Warning("Couldn't automatically determine location of Rider: No installation found."); - return true; - } - - // CollectAllPaths sorts the installations, newest first. - RiderInstallInfo newestInstall = installs.Front; - - if (RiderPath == null) - { - RiderPath = new String(newestInstall.Path); - } - else - { - RiderPath..Clear().Append(newestInstall.Path); - } - - Log.EngineLogger.Info($"Located Rider: {RiderPath}"); - - Apply(); - - return true; - } -} - -[Reflect] -class EditorSettings -{ - [Setting("Editor", "Switch to Player on play", "If checked the editor will automatically switch to the \"Play\" window after starting the game."), BonInclude] - public bool SwitchToPlayerOnPlay = true; - - [Setting("Editor", "Switch to Player on simulate", "If checked the editor will automatically switch to the \"Play\" window after starting the simulation."), BonInclude] - public bool SwitchToPlayerOnSimulate = true; - - [Setting("Editor", "Switch to Player on continue", "If checked the editor will automatically switch to the \"Play\" window when the game is continued after pausing."), BonInclude] - public bool SwitchToPlayerOnResume = false; - - [Setting("Editor", "Switch to Editor on stop", "If checked the editor will automatically switch to the \"Editor\" window after stopping the game."), BonInclude] - public bool SwitchToEditorOnStop = true; - - [Setting("Editor", "Switch to Editor on pause", "If checked the editor will automatically switch to the \"Editor\" window when the game is being paused."), BonInclude] - public bool SwitchToEditorOnPause = false; - - [Setting("Editor", "Clear log on play", "If checked the message log will be cleared when the play-mode is entered."), BonInclude] - public bool ClearLogOnPlay = true; - - [BonInclude] - private List _recentProjects ~ DeleteContainerAndItems!(_); - - /// Gets or sets the path of the Project that was last open. - public StringView LastOpenedProject - { - get - { - if (_recentProjects == null || _recentProjects.Count == 0) - return ""; - - return _recentProjects[0]; - } - set - { - if (_recentProjects == null) - _recentProjects = new List(); - - // Remove duplicates - for (var entry in _recentProjects) - { - if (entry == value) - { - delete entry; - @entry.Remove(); - } - } - - _recentProjects.Insert(0, new String(value)); - - if (_recentProjects.Count > 10) - { - // We only store 10 entries, delete the rest - for (int i = 10; i < _recentProjects.Count; i++) - { - delete _recentProjects[i]; - } - - _recentProjects.Count = 10; - } - } - } - - public List RecentProjects => _recentProjects; - - public void Apply() - { - } -} \ No newline at end of file diff --git a/GlitchyEditor/src/Project.bf b/GlitchyEditor/src/Project.bf index 9f46d71..5d3481b 100644 --- a/GlitchyEditor/src/Project.bf +++ b/GlitchyEditor/src/Project.bf @@ -4,6 +4,7 @@ using Bon; using GlitchyEngine; using GlitchyEngine.Scripting; using Xml_Beef; +using GlitchyEditor.Settings; namespace GlitchyEditor; diff --git a/GlitchyEditor/src/Settings.bf b/GlitchyEditor/src/Settings.bf deleted file mode 100644 index 7e3a391..0000000 --- a/GlitchyEditor/src/Settings.bf +++ /dev/null @@ -1,146 +0,0 @@ -using System; -using ImGui; -using System.Collections; -using System.IO; -using Bon; -using GlitchyEngine; - -namespace GlitchyEditor -{ - interface ISettings - { - void Apply(); - } - - /// Fields with this Attribute will be scanned for Settings. - [AttributeUsage(.Field, .ReflectAttribute)] - struct SettingContainerAttribute : Attribute - { - } - - enum SettingEditor - { - case Default; - case Path;//(bool MultiSelect, bool OpenFolderDialog, StringView Filter); - } - - /// Fields with this Attribute will be exposed as settings. - [AttributeUsage(.Field, .ReflectAttribute)] - struct SettingAttribute : Attribute - { - public String Category; - public String Name; - public String Tooltip; - public SettingEditor EditorMode; - - public this(String category, String name, String tooltip = "", SettingEditor editorMode = .Default) - { - Category = category; - Name = name; - Tooltip = tooltip; - EditorMode = editorMode; - } - } - - [Reflect, BonTarget] - class Settings - { -#if IMGUI - [SettingContainer, BonInclude] - public readonly ImGuiSettings ImGuiSettings = new .() ~ delete _; -#endif - [BonIgnore] - public Event OnApplySettings = .() ~ _.Dispose(); - - /* - [BonInclude] - private List _userSettings ~ ClearAndDeleteItems!(_); - */ - - public this() - { - /*List userSettings = append .(); - _userSettings = userSettings;*/ - RegisterEventListeners(); - } - - protected virtual void RegisterEventListeners() - { - - } - - public void Apply() - { -#if IMGUI - ImGuiSettings.Apply(); -#endif - - OnApplySettings.Invoke(this, .Empty); - - /*for (let settings in _userSettings) - { - settings.Apply(); - }*/ - } - - public static void Load() - { - Settings settings = EditorApp.Instance.Settings; - - if (!File.Exists("./settings.bon")) - { - settings.Save(); - } - - var result = Bon.DeserializeFromFile(ref settings, "./settings.bon"); - - if (result case .Err) - Log.EngineLogger.Error("Failed to deserialze settings."); - - settings.Apply(); - } - - public void Save() - { - gBonEnv.serializeFlags |= .Verbose; - Bon.SerializeIntoFile(this, "./settings.bon"); - } - - /* - /// Registers a instance of a settings interface. Note: Takes ownership of the instance. - public void RegisterUserSettings(ISettings settings) - { - _userSettings.Add(settings); - } - - public T GetUserSettings() where T : ISettings, class - { - for (let v in _userSettings) - { - if (v is T) - { - return (T)v; - } - } - - return null; - }*/ - } - -#if IMGUI - [Reflect] - class ImGuiSettings - { - [Setting("UI", "Font Size"), BonInclude] - public int32 FontSize = 14; - - [Setting("UI", "Font name"), BonInclude] - public readonly String FontName = new .("Fonts/CascadiaCode.ttf") ~ delete _; - - public void Apply() - { - EditorApp.Instance.[Friend]_imGuiLayer.SettingsInvalid = true; - } - } -#endif -} \ No newline at end of file diff --git a/GlitchyEditor/src/Settings/DetectIDEsBackgroundTask.bf b/GlitchyEditor/src/Settings/DetectIDEsBackgroundTask.bf new file mode 100644 index 0000000..f4905af --- /dev/null +++ b/GlitchyEditor/src/Settings/DetectIDEsBackgroundTask.bf @@ -0,0 +1,192 @@ +using GlitchyEngine; +using System.Collections; +using GlitchyEditor.CodeEditors; +using System.IO; +using System; +using System.Diagnostics; +using ImGui; +using GlitchyEditor.Multithreading; + +namespace GlitchyEditor.Settings; + + +class DetectIDEsBackgroundTask : BackgroundTask +{ + enum Stage + { + None, + SearchVisualStudio, + SearchRider + } + + private Stage _stage = .None; + + private ScriptSettings _settings; + + public this(ScriptSettings settings) + { + _settings = settings; + } + + public override RunResult Run() + { + // Remove all auto detected IDEs, keep manual ones + using (_settings.EnterAndGetIdeInstallations(let installations)) + { + for (let ide in installations) + { + if (ide.IsAutoDetected) + { + @ide.Remove(); + delete ide; + } + } + } + +#if BF_PLATFORM_WINDOWS + _stage = .SearchVisualStudio; + FindVisualStudio(); +#endif + + _stage = .SearchRider; + FindRider(); + + return .Finished; + } + + public override void OnRenderPopup() + { + if (ImGui.Begin("Detecting IDEs...", null, .NoDocking | .NoCollapse | .Modal | .NoResize | .AlwaysAutoResize)) + { + if (_stage == .SearchVisualStudio) + { + ImGui.Text("Searching Visual Studio installations..."); + } + else if (_stage == .SearchRider) + { + ImGui.Text("Searching Rider installations..."); + } + + ImGui.End(); + } + } + +#if BF_PLATFORM_WINDOWS + void FindVisualStudio() + { + Log.EngineLogger.Info("Using vswhere.exe to search for Visual Studio..."); + + String exeFile = scope .(); + Environment.GetExecutableFilePath(exeFile); + + String exeDirectory = scope .(); + Path.GetDirectoryPath(exeFile, exeDirectory); + + String vsWherePath = scope .(); + Path.Combine(vsWherePath, exeDirectory, "vswhere.exe"); + + ProcessStartInfo processInfo = scope .(); + processInfo.UseShellExecute = false; + processInfo.RedirectStandardOutput = true; + processInfo.CreateNoWindow = true; + + processInfo.SetFileName(vsWherePath); + // Find the installation path of the latest visual studio with an installed IDE. + processInfo.SetArguments("-sort -requires Microsoft.VisualStudio.Component.Roslyn.LanguageServices -format text"); + + let process = scope SpawnedProcess(); + if (process.Start(processInfo) case .Ok) + { + FileStream outputStream = scope FileStream(); + process.AttachStandardOutput(outputStream); + + process.WaitFor(1000); + + StreamReader sr = scope .(outputStream); + + IdeInstallation installation = null; + + String line = scope .(256); + while (sr.ReadLine(line..Clear()) case .Ok) + { + const String InstanceIdPropertyName = "instanceId: "; + const String ProductPathPropertyName = "productPath: "; + const String DisplayNamePropertyName = "displayName: "; + + if (line.StartsWith(InstanceIdPropertyName)) + { + if (installation != null) + { + using (_settings.EnterAndGetIdeInstallations(let installations)) + { + installations.Add(installation); + } + } + + installation = new IdeInstallation(); + installation.Ide = .VisualStudio; + installation.IsAutoDetected = true; + } + else if (line.StartsWith(ProductPathPropertyName)) + { + installation.Path = line.Substring(ProductPathPropertyName.Length); + } + else if (line.StartsWith(DisplayNamePropertyName)) + { + installation.Name = line.Substring(DisplayNamePropertyName.Length); + } + } + + if (!installation.Name.IsEmpty && !installation.Path.IsEmpty) + { + using (_settings.EnterAndGetIdeInstallations(let installations)) + { + installations.Add(installation); + } + } + else + { + delete installation; + } + } + else + { + Log.EngineLogger.Error("Couldn't automatically determine location of Visual Studio: Failed to launch vswhere.exe"); + } + } +#endif + + private void FindRider() + { + List installs = scope .(); + + RiderPathLocator.CollectAllPaths(installs); + + ReportRiderInstalls(installs); + + ClearAndDeleteItems!(installs); + } + + private void ReportRiderInstalls(List installs) + { + if (installs.IsEmpty) + { + Log.EngineLogger.Warning("Couldn't automatically determine location of Rider: No installation found."); + } + + using (_settings.EnterAndGetIdeInstallations(let installations)) + { + for (RiderInstallInfo install in installs) + { + Log.EngineLogger.Info($"Found Rider {install.Version} at \"{install.Path}\" ({install.InstallType})"); + IdeInstallation ide = new IdeInstallation(); + ide.IsAutoDetected = true; + ide.Ide = .Rider; + ide.Path = install.Path; + ide.Name = scope $"Rider {install.Version}"; + + installations.Add(ide); + } + } + } +} diff --git a/GlitchyEditor/src/Settings/Settings.bf b/GlitchyEditor/src/Settings/Settings.bf new file mode 100644 index 0000000..7d81053 --- /dev/null +++ b/GlitchyEditor/src/Settings/Settings.bf @@ -0,0 +1,386 @@ +using System; +using ImGui; +using System.Collections; +using System.IO; +using Bon; +using GlitchyEngine; +using System.Threading; +using System.Linq; + +using static GlitchyEditor.SettingsWindow; + +namespace GlitchyEditor.Settings; + +interface ISettings +{ + void Apply(); +} + +function void RenderMethod(SettingsWindow.Category category); +function void FieldRenderMethod(SettingsWindow.Binding setting); + +/// Fields with this Attribute will be scanned for Settings. +[AttributeUsage(.Field, .ReflectAttribute)] +struct SettingContainerAttribute : Attribute +{ + public RenderMethod RenderMethod; +} + +enum SettingEditor +{ + case Default; + case Path;//(bool MultiSelect, bool OpenFolderDialog, StringView Filter); +} + +/// Fields with this Attribute will be exposed as settings. +[AttributeUsage(.Field, .ReflectAttribute)] +struct SettingAttribute : Attribute +{ + public String Category; + public String Name; + public String Tooltip; + public SettingEditor EditorMode; + + public this(String category, String name, String tooltip = "", SettingEditor editorMode = .Default) + { + Category = category; + Name = name; + Tooltip = tooltip; + EditorMode = editorMode; + } +} + +/// Fields with this Attribute will be exposed as settings. +[AttributeUsage(.Method, .ReflectAttribute | .AlwaysIncludeTarget)] +struct CustomSettingRendererAttribute : Attribute +{ + public String Category; + + public this(String category) + { + Category = category; + } +} + +[Reflect, BonTarget] +class Settings +{ + private bool _areFromDisk = false; + + public bool AreFromDisk => _areFromDisk; + +#if IMGUI + [SettingContainer, BonInclude] + public readonly ImGuiSettings ImGuiSettings = new .() ~ delete _; +#endif + [BonIgnore] + public Event OnApplySettings = .() ~ _.Dispose(); + +#if DEBUG + [SettingContainer, BonInclude] + public readonly DevSettings DevSettings = new .() ~ delete _; +#endif + + [SettingContainer, BonInclude] + public readonly EditorSettings EditorSettings = new .() ~ delete _; + + [SettingContainer, BonInclude] + public readonly ScriptSettings ScriptSettings = new .() ~ delete _; + + /* + [BonInclude] + private List _userSettings ~ ClearAndDeleteItems!(_); + */ + + public this() + { + /*List userSettings = append .(); + _userSettings = userSettings;*/ + RegisterEventListeners(); + } + + protected virtual void RegisterEventListeners() + { + + } + + public void Apply() + { +#if IMGUI + ImGuiSettings.Apply(); +#endif + +#if DEBUG + DevSettings.Apply(); +#endif + EditorSettings.Apply(); + ScriptSettings.Apply(); + + OnApplySettings.Invoke(this, .Empty); + + /*for (let settings in _userSettings) + { + settings.Apply(); + }*/ + } + + public static void Load() + { + Settings settings = EditorApp.Instance.Settings; + + if (!File.Exists("./settings.bon")) + { + settings.Save(); + } + + var result = Bon.DeserializeFromFile(ref settings, "./settings.bon"); + + settings._areFromDisk = true; + + if (result case .Err) + Log.EngineLogger.Error("Failed to deserialze settings."); + + settings.Apply(); + } + + public void Save() + { + gBonEnv.serializeFlags |= .Verbose; + Bon.SerializeIntoFile(this, "./settings.bon"); + } + + /* + /// Registers a instance of a settings interface. Note: Takes ownership of the instance. + public void RegisterUserSettings(ISettings settings) + { + _userSettings.Add(settings); + } + + public T GetUserSettings() where T : ISettings, class + { + for (let v in _userSettings) + { + if (v is T) + { + return (T)v; + } + } + + return null; + }*/ +} + +#if IMGUI +[Reflect] +class ImGuiSettings +{ + [Setting("UI", "Font Size"), BonInclude] + public int32 FontSize = 14; + + [Setting("UI", "Font name"), BonInclude] + public readonly String FontName = new .("Fonts/CascadiaCode.ttf") ~ delete _; + + public void Apply() + { + EditorApp.Instance.[Friend]_imGuiLayer?.SettingsInvalid = true; + } +} +#endif + +#if DEBUG +[Reflect] +class DevSettings +{ + [Setting("Dev", "Use ScriptCore csproj", "If enabled the Editor will include the csproj of the ScriptCore instead of the compiled dll."), BonInclude] + public bool UseScriptCoreDll = true; + + public void Apply() + { + + } +} +#endif + +[Reflect] +enum ScriptIde +{ + Rider, + VisualStudio +} + +[Reflect] +class IdeInstallation +{ + [BonInclude] + public ScriptIde Ide; + [BonInclude] + private String _path ~ delete _; + [BonInclude] + private String _name ~ delete _; + + public StringView Path + { + get => _path; + set => String.NewOrSet!(_path, value); + } + + public StringView Name + { + get => _name; + set => String.NewOrSet!(_name, value); + } + + [BonInclude] + public bool IsAutoDetected; + // TODO: allow custom IDEs? We then need something like "'Open Project' command line", "'Open file' command line" and "'Open file at location' command line" + // This would however be quite complicated to implement well, when looking at how different Visual Studio and Rider already are. +} + +[Reflect] +class ScriptSettings +{ + [BonInclude] + private List _ideInstallations = new .() ~ DeleteContainerAndItems!(_); + + // TODO: It's probably overkill to have a lock for the ide list. + // If we cared enough about multi threading, InvokeOnMainThread would probably be enough. + private Monitor _ideInstallationsLock = new .() ~ delete _; + + [BonInclude] + private String _activeIdePath = new String() ~ delete _; + + public IdeInstallation ActiveIde => _ideInstallations.Where((i) => i.Path == _activeIdePath).FirstOrDefault(); + + private bool _lookedForVs = false; + private bool _lookedForRider = false; + + /// Enters the lock and gets the list of Ide installations. + public Monitor.MonitorLockInstance EnterAndGetIdeInstallations(out List ideInstallations) + { + let lock = _ideInstallationsLock.Enter(); + ideInstallations = _ideInstallations; + return lock; + } + + public void Apply() + { + if (ActiveIde == null) + { + using (EnterAndGetIdeInstallations(let ideInstallations)) + { + // If no IDE is selected, just select the first one + if (ideInstallations?.Count > 0) + { + _activeIdePath.Set(ideInstallations.Front.Path); + } + } + } + } + + [CustomSettingRenderer("Tools")] + public bool RenderUI(SettingsWindow.Category category) + { + bool settingsChanged = false; + + ImGui.Text("IDE:"); + + if (ImGui.BeginCombo("##ide", ActiveIde?.Name.ToScopeCStr!())) + { + using (EnterAndGetIdeInstallations(let installations)) + { + for (let ide in installations) + { + if (ImGui.Selectable(ide.Name.ToScopeCStr!()) && ActiveIde != ide) + { + _activeIdePath.Set(ide.Path); + settingsChanged = true; + } + + ImGui.AttachTooltip(ide.Path); + } + } + + ImGui.EndCombo(); + } + + if (ImGui.Button("Detect IDEs...")) + { + //BackgroundTask task = new BackgroundTask(); + + EditorApp.Instance.BackgroundTaskManager.StartBackgroundTask(new DetectIDEsBackgroundTask(this)); + } + + return settingsChanged; + } +} + +[Reflect] +class EditorSettings +{ + [Setting("Editor", "Switch to Player on play", "If checked the editor will automatically switch to the \"Play\" window after starting the game."), BonInclude] + public bool SwitchToPlayerOnPlay = true; + + [Setting("Editor", "Switch to Player on simulate", "If checked the editor will automatically switch to the \"Play\" window after starting the simulation."), BonInclude] + public bool SwitchToPlayerOnSimulate = true; + + [Setting("Editor", "Switch to Player on continue", "If checked the editor will automatically switch to the \"Play\" window when the game is continued after pausing."), BonInclude] + public bool SwitchToPlayerOnResume = false; + + [Setting("Editor", "Switch to Editor on stop", "If checked the editor will automatically switch to the \"Editor\" window after stopping the game."), BonInclude] + public bool SwitchToEditorOnStop = true; + + [Setting("Editor", "Switch to Editor on pause", "If checked the editor will automatically switch to the \"Editor\" window when the game is being paused."), BonInclude] + public bool SwitchToEditorOnPause = false; + + [Setting("Editor", "Clear log on play", "If checked the message log will be cleared when the play-mode is entered."), BonInclude] + public bool ClearLogOnPlay = true; + + [BonInclude] + private List _recentProjects ~ DeleteContainerAndItems!(_); + + /// Gets or sets the path of the Project that was last open. + public StringView LastOpenedProject + { + get + { + if (_recentProjects == null || _recentProjects.Count == 0) + return ""; + + return _recentProjects[0]; + } + set + { + if (_recentProjects == null) + _recentProjects = new List(); + + // Remove duplicates + for (var entry in _recentProjects) + { + if (entry == value) + { + delete entry; + @entry.Remove(); + } + } + + _recentProjects.Insert(0, new String(value)); + + if (_recentProjects.Count > 10) + { + // We only store 10 entries, delete the rest + for (int i = 10; i < _recentProjects.Count; i++) + { + delete _recentProjects[i]; + } + + _recentProjects.Count = 10; + } + } + } + + public List RecentProjects => _recentProjects; + + public void Apply() + { + } +} diff --git a/GlitchyEditor/src/SettingsWindow.bf b/GlitchyEditor/src/Settings/SettingsWindow.bf similarity index 78% rename from GlitchyEditor/src/SettingsWindow.bf rename to GlitchyEditor/src/Settings/SettingsWindow.bf index 6088176..1e424e1 100644 --- a/GlitchyEditor/src/SettingsWindow.bf +++ b/GlitchyEditor/src/Settings/SettingsWindow.bf @@ -6,12 +6,13 @@ using System.Collections; using System.Reflection; using GlitchyEngine.Math; using System.IO; +using GlitchyEditor.Settings; namespace GlitchyEditor { class SettingsWindow : EditorWindow { - class Binding + public class Binding { public Object SettingsObject; public String Name ~ delete _; @@ -29,9 +30,22 @@ namespace GlitchyEditor } } - class Category + public struct MethodBinding + { + public Object SettingsObject; + public MethodInfo Method; + + public this(MethodInfo method, Object settingsObject) + { + Method = method; + SettingsObject = settingsObject; + } + } + + public class Category { public List _bindings = new .() ~ DeleteContainerAndItems!(_); + public List _methodBindings = new .() ~ delete _; public String Header ~ delete _; @@ -47,6 +61,11 @@ namespace GlitchyEditor Binding binding = new .(name, fieldName, settingsObject ?? SettingsObject, tooltip, editorType); _bindings.Add(binding); } + + public void AddRenderer(MethodInfo method, Object settingsObject = null) + { + _methodBindings.Add(MethodBinding(method, settingsObject)); + } } Settings _settings; @@ -87,6 +106,8 @@ namespace GlitchyEditor void ScanForSettings(Object container) { + Log.EngineLogger.Info($"Visiting type: {container.GetType().GetName(.. scope .())}"); + Type type = container.GetType(); for (var field in type.GetFields()) @@ -114,6 +135,18 @@ namespace GlitchyEditor ScanForSettings(childContainer); } } + + for (let method in type.GetMethods(.Instance | .Static | .Public | .NonPublic)) + { + Log.EngineLogger.Info($"Visiting method: {method.Name}"); + + Result rendererResult = method.GetCustomAttribute(); + + if (rendererResult case .Ok(let rendererInfo)) + { + AddRenderer(rendererInfo.Category, method, container); + } + } } void AddSetting(String categoryName, String name, StringView fieldName, Object container, StringView tooltip, SettingEditor settingEditor) @@ -123,6 +156,12 @@ namespace GlitchyEditor category.AddSetting(name, fieldName, container, tooltip, settingEditor); } + void AddRenderer(String categoryName, MethodInfo method, Object container) + { + Category category = AddCategory(categoryName); + category.AddRenderer(method, container); + } + private String _tabToSelect = new String() ~ delete _; private String _SettingToHighlight = new String() ~ delete _; private float _timeToHighlight; @@ -157,6 +196,7 @@ namespace GlitchyEditor if (ImGui.BeginTabItem(category.Header, null, flags)) { ShowCategory(category); + ImGui.EndTabItem(); } } @@ -209,6 +249,12 @@ namespace GlitchyEditor ImGui.TableNextColumn(); + /*if (setting.EditorType case .Custom(let renderMethod)) + { + renderMethod(setting); + continue; + }*/ + // Name of setting ImGui.TextUnformatted(setting.Name); @@ -370,98 +416,16 @@ namespace GlitchyEditor _SettingToHighlight.Clear(); } } - - /*if (fieldInfo.FieldType.IsEnum) + } + + for (let methodBinding in category._methodBindings) + { + if (methodBinding.Method.Invoke(methodBinding.Method.IsStatic ? null : methodBinding.SettingsObject, category) case .Ok(var returnValue)) { - uint64 enumValue = 0; + _settingsChanged |= returnValue.Get(); - fieldInfo.GetValueReference(setting.SettingsObject); - - // Assign value - switch (fieldInfo.FieldType.Size) - { - case 1: enumValue = *(uint8*)&enumValue; - case 2: *(uint16*)&enumValue = *(uint16*)&enumValue; - case 4: *(uint32*)&enumValue = *(uint32*)&enumValue; - case 8: *(uint64*)&enumValue = *(uint64*)&enumValue; - } - - bool found = false; - for (var field in valType.GetFields()) - { - if (field.[Friend]mFieldData.mFlags.HasFlag(.EnumCase) && - *(int64*)&field.[Friend]mFieldData.[Friend]mData == valueData) - { - writer.Enum(field.Name); - found = true; - break; - } - } - - // Find field on enum - bool found = false; - for (var field in valType.GetFields()) - if (field.[Friend]mFieldData.mFlags.HasFlag(.EnumCase) - && name == field.Name) - { - // Add value of enum case to current enum value - enumValue |= *(int64*)&field.[Friend]mFieldData.[Friend]mData; - found = true; - break; - } - - if (!found) - Error!("Enum case not found", reader, valType); - - - uint64 value = 0; - StringView selectedValue; - - for (FieldInfo enumField in fieldInfo.FieldType.GetFields()) - { - if (enumField.[Friend]mFieldData.mFlags.HasFlag(.EnumCase)) - { - hasCaseData = true; - - if (name == enumField.Name) - { - unionPayload = ValueView(enumField.FieldType, val.dataPtr); - - foundCase = true; - break; - } - - unionDiscrIndex++; - } - else if (enumField.[Friend]mFieldData.mFlags.HasFlag(.EnumDiscriminator)) - { - let discrType = enumField.FieldType; - Debug.Assert(discrType.IsInteger); - discrVal = ValueView(discrType, (uint8*)val.dataPtr + enumField.[Friend]mFieldData.mData); - } - } - - ImGui.BeginCombo(scope $"##{setting.Name}", null); - - for (var v in Enum.GetValues(fieldInfo.FieldType)) - { - - } - - ImGui.EndCombo(); - - /*// Find field on enum - bool found = false; - for (var field in valType.GetFields()) - if (field.[Friend]mFieldData.mFlags.HasFlag(.EnumCase) - && name == field.Name) - { - // Add value of enum case to current enum value - enumValue |= *(int64*)&field.[Friend]mFieldData.[Friend]mData; - found = true; - break; - }*/ - }*/ + returnValue.Dispose(); + } } ImGui.EndTable();