Merge pull request #43028 from neikeq/rework-csharp-build-panel

C#: Re-work solution build output panel
This commit is contained in:
Rémi Verschelde 2020-10-23 08:40:25 +02:00 committed by GitHub
commit d5073c6b4c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 512 additions and 539 deletions

View file

@ -15,14 +15,14 @@ namespace GodotTools.BuildLogger
public void Initialize(IEventSource eventSource)
{
if (null == Parameters)
throw new LoggerException("Log directory was not set.");
throw new LoggerException("Log directory parameter not specified.");
var parameters = Parameters.Split(new[] { ';' });
string logDir = parameters[0];
if (string.IsNullOrEmpty(logDir))
throw new LoggerException("Log directory was not set.");
throw new LoggerException("Log directory parameter is empty.");
if (parameters.Length > 1)
throw new LoggerException("Too many parameters passed.");
@ -51,22 +51,31 @@ namespace GodotTools.BuildLogger
{
throw new LoggerException("Failed to create log file: " + ex.Message);
}
else
{
// Unexpected failure
throw;
}
// Unexpected failure
throw;
}
eventSource.ProjectStarted += eventSource_ProjectStarted;
eventSource.TaskStarted += eventSource_TaskStarted;
eventSource.ProjectFinished += eventSource_ProjectFinished;
eventSource.MessageRaised += eventSource_MessageRaised;
eventSource.WarningRaised += eventSource_WarningRaised;
eventSource.ErrorRaised += eventSource_ErrorRaised;
eventSource.ProjectFinished += eventSource_ProjectFinished;
}
void eventSource_ErrorRaised(object sender, BuildErrorEventArgs e)
private void eventSource_ProjectStarted(object sender, ProjectStartedEventArgs e)
{
WriteLine(e.Message);
indent++;
}
private void eventSource_ProjectFinished(object sender, ProjectFinishedEventArgs e)
{
indent--;
WriteLine(e.Message);
}
private void eventSource_ErrorRaised(object sender, BuildErrorEventArgs e)
{
string line = $"{e.File}({e.LineNumber},{e.ColumnNumber}): error {e.Code}: {e.Message}";
@ -81,7 +90,7 @@ namespace GodotTools.BuildLogger
issuesStreamWriter.WriteLine(errorLine);
}
void eventSource_WarningRaised(object sender, BuildWarningEventArgs e)
private void eventSource_WarningRaised(object sender, BuildWarningEventArgs e)
{
string line = $"{e.File}({e.LineNumber},{e.ColumnNumber}): warning {e.Code}: {e.Message}";
@ -108,40 +117,6 @@ namespace GodotTools.BuildLogger
}
}
private void eventSource_TaskStarted(object sender, TaskStartedEventArgs e)
{
// TaskStartedEventArgs adds ProjectFile, TaskFile, TaskName
// To keep this log clean, this logger will ignore these events.
}
private void eventSource_ProjectStarted(object sender, ProjectStartedEventArgs e)
{
WriteLine(e.Message);
indent++;
}
private void eventSource_ProjectFinished(object sender, ProjectFinishedEventArgs e)
{
indent--;
WriteLine(e.Message);
}
/// <summary>
/// Write a line to the log, adding the SenderName
/// </summary>
private void WriteLineWithSender(string line, BuildEventArgs e)
{
if (0 == string.Compare(e.SenderName, "MSBuild", StringComparison.OrdinalIgnoreCase))
{
// Well, if the sender name is MSBuild, let's leave it out for prettiness
WriteLine(line);
}
else
{
WriteLine(e.SenderName + ": " + line);
}
}
/// <summary>
/// Write a line to the log, adding the SenderName and Message
/// (these parameters are on all MSBuild event argument objects)

View file

@ -1,339 +0,0 @@
using Godot;
using System;
using System.IO;
using Godot.Collections;
using GodotTools.Internals;
using static GodotTools.Internals.Globals;
using File = GodotTools.Utils.File;
using Path = System.IO.Path;
namespace GodotTools
{
public class BottomPanel : VBoxContainer
{
private EditorInterface editorInterface;
private TabContainer panelTabs;
private VBoxContainer panelBuildsTab;
private ItemList buildTabsList;
private TabContainer buildTabs;
private Button warningsBtn;
private Button errorsBtn;
private Button viewLogBtn;
private void _UpdateBuildTab(int index, int? currentTab)
{
var tab = (BuildTab)buildTabs.GetChild(index);
string itemName = Path.GetFileNameWithoutExtension(tab.BuildInfo.Solution);
itemName += " [" + tab.BuildInfo.Configuration + "]";
buildTabsList.AddItem(itemName, tab.IconTexture);
string itemTooltip = "Solution: " + tab.BuildInfo.Solution;
itemTooltip += "\nConfiguration: " + tab.BuildInfo.Configuration;
itemTooltip += "\nStatus: ";
if (tab.BuildExited)
itemTooltip += tab.BuildResult == BuildTab.BuildResults.Success ? "Succeeded" : "Errored";
else
itemTooltip += "Running";
if (!tab.BuildExited || tab.BuildResult == BuildTab.BuildResults.Error)
itemTooltip += $"\nErrors: {tab.ErrorCount}";
itemTooltip += $"\nWarnings: {tab.WarningCount}";
buildTabsList.SetItemTooltip(index, itemTooltip);
// If this tab was already selected before the changes or if no tab was selected
if (currentTab == null || currentTab == index)
{
buildTabsList.Select(index);
_BuildTabsItemSelected(index);
}
}
private void _UpdateBuildTabsList()
{
buildTabsList.Clear();
int? currentTab = buildTabs.CurrentTab;
if (currentTab < 0 || currentTab >= buildTabs.GetTabCount())
currentTab = null;
for (int i = 0; i < buildTabs.GetChildCount(); i++)
_UpdateBuildTab(i, currentTab);
}
public BuildTab GetBuildTabFor(BuildInfo buildInfo)
{
foreach (var buildTab in new Array<BuildTab>(buildTabs.GetChildren()))
{
if (buildTab.BuildInfo.Equals(buildInfo))
return buildTab;
}
var newBuildTab = new BuildTab(buildInfo);
AddBuildTab(newBuildTab);
return newBuildTab;
}
private void _BuildTabsItemSelected(int idx)
{
if (idx < 0 || idx >= buildTabs.GetTabCount())
throw new IndexOutOfRangeException();
buildTabs.CurrentTab = idx;
if (!buildTabs.Visible)
buildTabs.Visible = true;
warningsBtn.Visible = true;
errorsBtn.Visible = true;
viewLogBtn.Visible = true;
}
private void _BuildTabsNothingSelected()
{
if (buildTabs.GetTabCount() != 0)
{
// just in case
buildTabs.Visible = false;
// This callback is called when clicking on the empty space of the list.
// ItemList won't deselect the items automatically, so we must do it ourselves.
buildTabsList.UnselectAll();
}
warningsBtn.Visible = false;
errorsBtn.Visible = false;
viewLogBtn.Visible = false;
}
private void _WarningsToggled(bool pressed)
{
int currentTab = buildTabs.CurrentTab;
if (currentTab < 0 || currentTab >= buildTabs.GetTabCount())
throw new InvalidOperationException("No tab selected");
var buildTab = (BuildTab)buildTabs.GetChild(currentTab);
buildTab.WarningsVisible = pressed;
buildTab.UpdateIssuesList();
}
private void _ErrorsToggled(bool pressed)
{
int currentTab = buildTabs.CurrentTab;
if (currentTab < 0 || currentTab >= buildTabs.GetTabCount())
throw new InvalidOperationException("No tab selected");
var buildTab = (BuildTab)buildTabs.GetChild(currentTab);
buildTab.ErrorsVisible = pressed;
buildTab.UpdateIssuesList();
}
public void BuildProjectPressed()
{
if (!File.Exists(GodotSharpDirs.ProjectSlnPath))
return; // No solution to build
string editorScriptsMetadataPath = Path.Combine(GodotSharpDirs.ResMetadataDir, "scripts_metadata.editor");
string playerScriptsMetadataPath = Path.Combine(GodotSharpDirs.ResMetadataDir, "scripts_metadata.editor_player");
CsProjOperations.GenerateScriptsMetadata(GodotSharpDirs.ProjectCsProjPath, editorScriptsMetadataPath);
if (File.Exists(editorScriptsMetadataPath))
{
try
{
File.Copy(editorScriptsMetadataPath, playerScriptsMetadataPath);
}
catch (IOException e)
{
GD.PushError($"Failed to copy scripts metadata file. Exception message: {e.Message}");
return;
}
}
bool buildSuccess = BuildManager.BuildProjectBlocking("Debug");
if (!buildSuccess)
return;
// Notify running game for hot-reload
Internal.EditorDebuggerNodeReloadScripts();
// Hot-reload in the editor
GodotSharpEditor.Instance.GetNode<HotReloadAssemblyWatcher>("HotReloadAssemblyWatcher").RestartTimer();
if (Internal.IsAssembliesReloadingNeeded())
Internal.ReloadAssemblies(softReload: false);
}
private void _ViewLogPressed()
{
if (!buildTabsList.IsAnythingSelected())
return;
var selectedItems = buildTabsList.GetSelectedItems();
if (selectedItems.Length != 1)
throw new InvalidOperationException($"Expected 1 selected item, got {selectedItems.Length}");
int selectedItem = selectedItems[0];
var buildTab = (BuildTab)buildTabs.GetTabControl(selectedItem);
OS.ShellOpen(Path.Combine(buildTab.BuildInfo.LogsDirPath, BuildManager.MsBuildLogFileName));
}
public override void _Notification(int what)
{
base._Notification(what);
if (what == EditorSettings.NotificationEditorSettingsChanged)
{
var editorBaseControl = editorInterface.GetBaseControl();
panelTabs.AddThemeStyleboxOverride("panel", editorBaseControl.GetThemeStylebox("DebuggerPanel", "EditorStyles"));
panelTabs.AddThemeStyleboxOverride("tab_fg", editorBaseControl.GetThemeStylebox("DebuggerTabFG", "EditorStyles"));
panelTabs.AddThemeStyleboxOverride("tab_bg", editorBaseControl.GetThemeStylebox("DebuggerTabBG", "EditorStyles"));
}
}
public void AddBuildTab(BuildTab buildTab)
{
buildTabs.AddChild(buildTab);
RaiseBuildTab(buildTab);
}
public void RaiseBuildTab(BuildTab buildTab)
{
if (buildTab.GetParent() != buildTabs)
throw new InvalidOperationException("Build tab is not in the tabs list");
buildTabs.MoveChild(buildTab, 0);
_UpdateBuildTabsList();
}
public void ShowBuildTab()
{
for (int i = 0; i < panelTabs.GetTabCount(); i++)
{
if (panelTabs.GetTabControl(i) == panelBuildsTab)
{
panelTabs.CurrentTab = i;
GodotSharpEditor.Instance.MakeBottomPanelItemVisible(this);
return;
}
}
GD.PushError("Builds tab not found");
}
public override void _Ready()
{
base._Ready();
editorInterface = GodotSharpEditor.Instance.GetEditorInterface();
var editorBaseControl = editorInterface.GetBaseControl();
SizeFlagsVertical = (int)SizeFlags.ExpandFill;
SetAnchorsAndMarginsPreset(LayoutPreset.Wide);
panelTabs = new TabContainer
{
TabAlign = TabContainer.TabAlignEnum.Left,
RectMinSize = new Vector2(0, 228) * EditorScale,
SizeFlagsVertical = (int)SizeFlags.ExpandFill
};
panelTabs.AddThemeStyleboxOverride("panel", editorBaseControl.GetThemeStylebox("DebuggerPanel", "EditorStyles"));
panelTabs.AddThemeStyleboxOverride("tab_fg", editorBaseControl.GetThemeStylebox("DebuggerTabFG", "EditorStyles"));
panelTabs.AddThemeStyleboxOverride("tab_bg", editorBaseControl.GetThemeStylebox("DebuggerTabBG", "EditorStyles"));
AddChild(panelTabs);
{
// Builds tab
panelBuildsTab = new VBoxContainer
{
Name = "Builds".TTR(),
SizeFlagsHorizontal = (int)SizeFlags.ExpandFill
};
panelTabs.AddChild(panelBuildsTab);
var toolBarHBox = new HBoxContainer {SizeFlagsHorizontal = (int)SizeFlags.ExpandFill};
panelBuildsTab.AddChild(toolBarHBox);
var buildProjectBtn = new Button
{
Text = "Build Project".TTR(),
FocusMode = FocusModeEnum.None
};
buildProjectBtn.PressedSignal += BuildProjectPressed;
toolBarHBox.AddChild(buildProjectBtn);
toolBarHBox.AddSpacer(begin: false);
warningsBtn = new Button
{
Text = "Warnings".TTR(),
ToggleMode = true,
Pressed = true,
Visible = false,
FocusMode = FocusModeEnum.None
};
warningsBtn.Toggled += _WarningsToggled;
toolBarHBox.AddChild(warningsBtn);
errorsBtn = new Button
{
Text = "Errors".TTR(),
ToggleMode = true,
Pressed = true,
Visible = false,
FocusMode = FocusModeEnum.None
};
errorsBtn.Toggled += _ErrorsToggled;
toolBarHBox.AddChild(errorsBtn);
toolBarHBox.AddSpacer(begin: false);
viewLogBtn = new Button
{
Text = "View log".TTR(),
FocusMode = FocusModeEnum.None,
Visible = false
};
viewLogBtn.PressedSignal += _ViewLogPressed;
toolBarHBox.AddChild(viewLogBtn);
var hsc = new HSplitContainer
{
SizeFlagsHorizontal = (int)SizeFlags.ExpandFill,
SizeFlagsVertical = (int)SizeFlags.ExpandFill
};
panelBuildsTab.AddChild(hsc);
buildTabsList = new ItemList {SizeFlagsHorizontal = (int)SizeFlags.ExpandFill};
buildTabsList.ItemSelected += _BuildTabsItemSelected;
buildTabsList.NothingSelected += _BuildTabsNothingSelected;
hsc.AddChild(buildTabsList);
buildTabs = new TabContainer
{
TabAlign = TabContainer.TabAlignEnum.Left,
SizeFlagsHorizontal = (int)SizeFlags.ExpandFill,
TabsVisible = false
};
hsc.AddChild(buildTabs);
}
}
}
}

View file

@ -4,7 +4,7 @@ using Godot.Collections;
using GodotTools.Internals;
using Path = System.IO.Path;
namespace GodotTools
namespace GodotTools.Build
{
[Serializable]
public sealed class BuildInfo : Reference // TODO Remove Reference once we have proper serialization
@ -20,7 +20,9 @@ namespace GodotTools
public override bool Equals(object obj)
{
if (obj is BuildInfo other)
return other.Solution == Solution && other.Configuration == Configuration;
return other.Solution == Solution && other.Targets == Targets &&
other.Configuration == Configuration && other.Restore == Restore &&
other.CustomProperties == CustomProperties && other.LogsDirPath == LogsDirPath;
return false;
}
@ -31,7 +33,11 @@ namespace GodotTools
{
int hash = 17;
hash = hash * 29 + Solution.GetHashCode();
hash = hash * 29 + Targets.GetHashCode();
hash = hash * 29 + Configuration.GetHashCode();
hash = hash * 29 + Restore.GetHashCode();
hash = hash * 29 + CustomProperties.GetHashCode();
hash = hash * 29 + LogsDirPath.GetHashCode();
return hash;
}
}

View file

@ -1,20 +1,19 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks;
using GodotTools.Build;
using GodotTools.Ides.Rider;
using GodotTools.Internals;
using GodotTools.Utils;
using JetBrains.Annotations;
using static GodotTools.Internals.Globals;
using File = GodotTools.Utils.File;
using OS = GodotTools.Utils.OS;
using Path = System.IO.Path;
namespace GodotTools
namespace GodotTools.Build
{
public static class BuildManager
{
private static readonly List<BuildInfo> BuildsInProgress = new List<BuildInfo>();
private static BuildInfo _buildInProgress;
public const string PropNameMSBuildMono = "MSBuild (Mono)";
public const string PropNameMSBuildVs = "MSBuild (VS Build Tools)";
@ -24,6 +23,14 @@ namespace GodotTools
public const string MsBuildIssuesFileName = "msbuild_issues.csv";
public const string MsBuildLogFileName = "msbuild_log.txt";
public delegate void BuildLaunchFailedEventHandler(BuildInfo buildInfo, string reason);
public static event BuildLaunchFailedEventHandler BuildLaunchFailed;
public static event Action<BuildInfo> BuildStarted;
public static event Action<BuildResult> BuildFinished;
public static event Action<string> StdOutputReceived;
public static event Action<string> StdErrorReceived;
private static void RemoveOldIssuesFile(BuildInfo buildInfo)
{
var issuesFile = GetIssuesFilePath(buildInfo);
@ -36,12 +43,13 @@ namespace GodotTools
private static void ShowBuildErrorDialog(string message)
{
GodotSharpEditor.Instance.ShowErrorDialog(message, "Build error");
GodotSharpEditor.Instance.BottomPanel.ShowBuildTab();
var plugin = GodotSharpEditor.Instance;
plugin.ShowErrorDialog(message, "Build error");
plugin.MakeBottomPanelItemVisible(plugin.MSBuildPanel);
}
public static void RestartBuild(BuildTab buildTab) => throw new NotImplementedException();
public static void StopBuild(BuildTab buildTab) => throw new NotImplementedException();
public static void RestartBuild(BuildOutputView buildOutputView) => throw new NotImplementedException();
public static void StopBuild(BuildOutputView buildOutputView) => throw new NotImplementedException();
private static string GetLogFilePath(BuildInfo buildInfo)
{
@ -61,15 +69,14 @@ namespace GodotTools
public static bool Build(BuildInfo buildInfo)
{
if (BuildsInProgress.Contains(buildInfo))
if (_buildInProgress != null)
throw new InvalidOperationException("A build is already in progress");
BuildsInProgress.Add(buildInfo);
_buildInProgress = buildInfo;
try
{
BuildTab buildTab = GodotSharpEditor.Instance.BottomPanel.GetBuildTabFor(buildInfo);
buildTab.OnBuildStart();
BuildStarted?.Invoke(buildInfo);
// Required in order to update the build tasks list
Internal.GodotMainIteration();
@ -80,44 +87,44 @@ namespace GodotTools
}
catch (IOException e)
{
buildTab.OnBuildExecFailed($"Cannot remove issues file: {GetIssuesFilePath(buildInfo)}");
BuildLaunchFailed?.Invoke(buildInfo, $"Cannot remove issues file: {GetIssuesFilePath(buildInfo)}");
Console.Error.WriteLine(e);
}
try
{
int exitCode = BuildSystem.Build(buildInfo);
int exitCode = BuildSystem.Build(buildInfo, StdOutputReceived, StdErrorReceived);
if (exitCode != 0)
PrintVerbose($"MSBuild exited with code: {exitCode}. Log file: {GetLogFilePath(buildInfo)}");
buildTab.OnBuildExit(exitCode == 0 ? BuildTab.BuildResults.Success : BuildTab.BuildResults.Error);
BuildFinished?.Invoke(exitCode == 0 ? BuildResult.Success : BuildResult.Error);
return exitCode == 0;
}
catch (Exception e)
{
buildTab.OnBuildExecFailed($"The build method threw an exception.\n{e.GetType().FullName}: {e.Message}");
BuildLaunchFailed?.Invoke(buildInfo, $"The build method threw an exception.\n{e.GetType().FullName}: {e.Message}");
Console.Error.WriteLine(e);
return false;
}
}
finally
{
BuildsInProgress.Remove(buildInfo);
_buildInProgress = null;
}
}
public static async Task<bool> BuildAsync(BuildInfo buildInfo)
{
if (BuildsInProgress.Contains(buildInfo))
if (_buildInProgress != null)
throw new InvalidOperationException("A build is already in progress");
BuildsInProgress.Add(buildInfo);
_buildInProgress = buildInfo;
try
{
BuildTab buildTab = GodotSharpEditor.Instance.BottomPanel.GetBuildTabFor(buildInfo);
BuildStarted?.Invoke(buildInfo);
try
{
@ -125,43 +132,57 @@ namespace GodotTools
}
catch (IOException e)
{
buildTab.OnBuildExecFailed($"Cannot remove issues file: {GetIssuesFilePath(buildInfo)}");
BuildLaunchFailed?.Invoke(buildInfo, $"Cannot remove issues file: {GetIssuesFilePath(buildInfo)}");
Console.Error.WriteLine(e);
}
try
{
int exitCode = await BuildSystem.BuildAsync(buildInfo);
int exitCode = await BuildSystem.BuildAsync(buildInfo, StdOutputReceived, StdErrorReceived);
if (exitCode != 0)
PrintVerbose($"MSBuild exited with code: {exitCode}. Log file: {GetLogFilePath(buildInfo)}");
buildTab.OnBuildExit(exitCode == 0 ? BuildTab.BuildResults.Success : BuildTab.BuildResults.Error);
BuildFinished?.Invoke(exitCode == 0 ? BuildResult.Success : BuildResult.Error);
return exitCode == 0;
}
catch (Exception e)
{
buildTab.OnBuildExecFailed($"The build method threw an exception.\n{e.GetType().FullName}: {e.Message}");
BuildLaunchFailed?.Invoke(buildInfo, $"The build method threw an exception.\n{e.GetType().FullName}: {e.Message}");
Console.Error.WriteLine(e);
return false;
}
}
finally
{
BuildsInProgress.Remove(buildInfo);
_buildInProgress = null;
}
}
public static bool BuildProjectBlocking(string config, [CanBeNull] string platform = null)
public static bool BuildProjectBlocking(string config, [CanBeNull] string[] targets = null, [CanBeNull] string platform = null)
{
if (!File.Exists(GodotSharpDirs.ProjectSlnPath))
var buildInfo = new BuildInfo(GodotSharpDirs.ProjectSlnPath, targets ?? new[] {"Build"}, config, restore: true);
// If a platform was not specified, try determining the current one. If that fails, let MSBuild auto-detect it.
if (platform != null || OS.PlatformNameMap.TryGetValue(Godot.OS.GetName(), out platform))
buildInfo.CustomProperties.Add($"GodotTargetPlatform={platform}");
if (Internal.GodotIsRealTDouble())
buildInfo.CustomProperties.Add("GodotRealTIsDouble=true");
return BuildProjectBlocking(buildInfo);
}
private static bool BuildProjectBlocking(BuildInfo buildInfo)
{
if (!File.Exists(buildInfo.Solution))
return true; // No solution to build
// Make sure the API assemblies are up to date before building the project.
// We may not have had the chance to update the release API assemblies, and the debug ones
// may have been deleted by the user at some point after they were loaded by the Godot editor.
string apiAssembliesUpdateError = Internal.UpdateApiAssembliesFromPrebuilt(config == "ExportRelease" ? "Release" : "Debug");
string apiAssembliesUpdateError = Internal.UpdateApiAssembliesFromPrebuilt(buildInfo.Configuration == "ExportRelease" ? "Release" : "Debug");
if (!string.IsNullOrEmpty(apiAssembliesUpdateError))
{
@ -173,15 +194,6 @@ namespace GodotTools
{
pr.Step("Building project solution", 0);
var buildInfo = new BuildInfo(GodotSharpDirs.ProjectSlnPath, targets: new[] {"Build"}, config, restore: true);
// If a platform was not specified, try determining the current one. If that fails, let MSBuild auto-detect it.
if (platform != null || OS.PlatformNameMap.TryGetValue(Godot.OS.GetName(), out platform))
buildInfo.CustomProperties.Add($"GodotTargetPlatform={platform}");
if (Internal.GodotIsRealTDouble())
buildInfo.CustomProperties.Add("GodotRealTIsDouble=true");
if (!Build(buildInfo))
{
ShowBuildErrorDialog("Failed to build project solution");
@ -197,13 +209,7 @@ namespace GodotTools
if (!File.Exists(GodotSharpDirs.ProjectSlnPath))
return true; // No solution to build
string editorScriptsMetadataPath = Path.Combine(GodotSharpDirs.ResMetadataDir, "scripts_metadata.editor");
string playerScriptsMetadataPath = Path.Combine(GodotSharpDirs.ResMetadataDir, "scripts_metadata.editor_player");
CsProjOperations.GenerateScriptsMetadata(GodotSharpDirs.ProjectCsProjPath, editorScriptsMetadataPath);
if (File.Exists(editorScriptsMetadataPath))
File.Copy(editorScriptsMetadataPath, playerScriptsMetadataPath);
GenerateEditorScriptMetadata();
if (GodotSharpEditor.Instance.SkipBuildBeforePlaying)
return true; // Requested play from an external editor/IDE which already built the project
@ -211,6 +217,35 @@ namespace GodotTools
return BuildProjectBlocking("Debug");
}
// NOTE: This will be replaced with C# source generators in 4.0
public static void GenerateEditorScriptMetadata()
{
string editorScriptsMetadataPath = Path.Combine(GodotSharpDirs.ResMetadataDir, "scripts_metadata.editor");
string playerScriptsMetadataPath = Path.Combine(GodotSharpDirs.ResMetadataDir, "scripts_metadata.editor_player");
CsProjOperations.GenerateScriptsMetadata(GodotSharpDirs.ProjectCsProjPath, editorScriptsMetadataPath);
if (!File.Exists(editorScriptsMetadataPath))
return;
try
{
File.Copy(editorScriptsMetadataPath, playerScriptsMetadataPath);
}
catch (IOException e)
{
throw new IOException("Failed to copy scripts metadata file.", innerException: e);
}
}
// NOTE: This will be replaced with C# source generators in 4.0
public static string GenerateExportedGameScriptMetadata(bool isDebug)
{
string scriptsMetadataPath = Path.Combine(GodotSharpDirs.ResMetadataDir, $"scripts_metadata.{(isDebug ? "debug" : "release")}");
CsProjOperations.GenerateScriptsMetadata(GodotSharpDirs.ProjectCsProjPath, scriptsMetadataPath);
return scriptsMetadataPath;
}
public static void Initialize()
{
// Build tool settings
@ -254,8 +289,6 @@ namespace GodotTools
["hint"] = Godot.PropertyHint.Enum,
["hint_string"] = hintString
});
EditorDef("mono/builds/print_build_output", false);
}
}
}

View file

@ -5,16 +5,10 @@ using GodotTools.Internals;
using File = GodotTools.Utils.File;
using Path = System.IO.Path;
namespace GodotTools
namespace GodotTools.Build
{
public class BuildTab : VBoxContainer
public class BuildOutputView : VBoxContainer, ISerializationListener
{
public enum BuildResults
{
Error,
Success
}
[Serializable]
private class BuildIssue : Reference // TODO Remove Reference once we have proper serialization
{
@ -29,10 +23,14 @@ namespace GodotTools
private readonly Array<BuildIssue> issues = new Array<BuildIssue>(); // TODO Use List once we have proper serialization
private ItemList issuesList;
private TextEdit buildLog;
private PopupMenu issuesListContextMenu;
public bool BuildExited { get; private set; } = false;
[Signal] public event Action BuildStateChanged;
public BuildResults? BuildResult { get; private set; } = null;
public bool HasBuildExited { get; private set; } = false;
public BuildResult? BuildResult { get; private set; } = null;
public int ErrorCount { get; private set; } = 0;
@ -41,23 +39,31 @@ namespace GodotTools
public bool ErrorsVisible { get; set; } = true;
public bool WarningsVisible { get; set; } = true;
public Texture2D IconTexture
public Texture2D BuildStateIcon
{
get
{
if (!BuildExited)
if (!HasBuildExited)
return GetThemeIcon("Stop", "EditorIcons");
if (BuildResult == BuildResults.Error)
return GetThemeIcon("StatusError", "EditorIcons");
if (BuildResult == Build.BuildResult.Error)
return GetThemeIcon("Error", "EditorIcons");
return GetThemeIcon("StatusSuccess", "EditorIcons");
if (WarningCount > 1)
return GetThemeIcon("Warning", "EditorIcons");
return null;
}
}
public BuildInfo BuildInfo { get; private set; }
private BuildInfo BuildInfo { get; set; }
private void _LoadIssuesFromFile(string csvFile)
public bool LogVisible
{
set => buildLog.Visible = value;
}
private void LoadIssuesFromFile(string csvFile)
{
using (var file = new Godot.File())
{
@ -107,7 +113,7 @@ namespace GodotTools
}
}
private void _IssueActivated(int idx)
private void IssueActivated(int idx)
{
if (idx < 0 || idx >= issuesList.GetItemCount())
throw new IndexOutOfRangeException("Item list index out of range");
@ -190,49 +196,79 @@ namespace GodotTools
}
}
public void OnBuildStart()
private void BuildLaunchFailed(BuildInfo buildInfo, string cause)
{
BuildExited = false;
issues.Clear();
WarningCount = 0;
ErrorCount = 0;
UpdateIssuesList();
GodotSharpEditor.Instance.BottomPanel.RaiseBuildTab(this);
}
public void OnBuildExit(BuildResults result)
{
BuildExited = true;
BuildResult = result;
_LoadIssuesFromFile(Path.Combine(BuildInfo.LogsDirPath, BuildManager.MsBuildIssuesFileName));
UpdateIssuesList();
GodotSharpEditor.Instance.BottomPanel.RaiseBuildTab(this);
}
public void OnBuildExecFailed(string cause)
{
BuildExited = true;
BuildResult = BuildResults.Error;
HasBuildExited = true;
BuildResult = Build.BuildResult.Error;
issuesList.Clear();
var issue = new BuildIssue { Message = cause, Warning = false };
var issue = new BuildIssue {Message = cause, Warning = false};
ErrorCount += 1;
issues.Add(issue);
UpdateIssuesList();
GodotSharpEditor.Instance.BottomPanel.RaiseBuildTab(this);
EmitSignal(nameof(BuildStateChanged));
}
private void BuildStarted(BuildInfo buildInfo)
{
BuildInfo = buildInfo;
HasBuildExited = false;
issues.Clear();
WarningCount = 0;
ErrorCount = 0;
buildLog.Text = string.Empty;
UpdateIssuesList();
EmitSignal(nameof(BuildStateChanged));
}
private void BuildFinished(BuildResult result)
{
HasBuildExited = true;
BuildResult = result;
LoadIssuesFromFile(Path.Combine(BuildInfo.LogsDirPath, BuildManager.MsBuildIssuesFileName));
UpdateIssuesList();
EmitSignal(nameof(BuildStateChanged));
}
private void StdOutputReceived(string text)
{
buildLog.Text += text + "\n";
ScrollToLastNonEmptyLogLine();
}
private void StdErrorReceived(string text)
{
buildLog.Text += text + "\n";
ScrollToLastNonEmptyLogLine();
}
private void ScrollToLastNonEmptyLogLine()
{
int line;
for (line = buildLog.GetLineCount(); line > 0; line--)
{
string lineText = buildLog.GetLine(line);
if (!string.IsNullOrEmpty(lineText) || !string.IsNullOrEmpty(lineText?.Trim()))
break;
}
buildLog.CursorSetLine(line);
}
public void RestartBuild()
{
if (!BuildExited)
if (!HasBuildExited)
throw new InvalidOperationException("Build already started");
BuildManager.RestartBuild(this);
@ -240,28 +276,118 @@ namespace GodotTools
public void StopBuild()
{
if (!BuildExited)
if (!HasBuildExited)
throw new InvalidOperationException("Build is not in progress");
BuildManager.StopBuild(this);
}
private enum IssuesContextMenuOption
{
Copy
}
private void IssuesListContextOptionPressed(int id)
{
switch ((IssuesContextMenuOption)id)
{
case IssuesContextMenuOption.Copy:
{
// We don't allow multi-selection but just in case that changes later...
string text = null;
foreach (int issueIndex in issuesList.GetSelectedItems())
{
if (text != null)
text += "\n";
text += issuesList.GetItemText(issueIndex);
}
if (text != null)
DisplayServer.ClipboardSet(text);
break;
}
default:
throw new ArgumentOutOfRangeException(nameof(id), id, "Invalid issue context menu option");
}
}
private void IssuesListRmbSelected(int index, Vector2 atPosition)
{
_ = index; // Unused
issuesListContextMenu.Clear();
issuesListContextMenu.Size = new Vector2i(1, 1);
if (issuesList.IsAnythingSelected())
{
// Add menu entries for the selected item
issuesListContextMenu.AddIconItem(GetThemeIcon("ActionCopy", "EditorIcons"),
label: "Copy Error".TTR(), (int)IssuesContextMenuOption.Copy);
}
if (issuesListContextMenu.GetItemCount() > 0)
{
issuesListContextMenu.Position = (Vector2i)(issuesList.RectGlobalPosition + atPosition);
issuesListContextMenu.Popup();
}
}
public override void _Ready()
{
base._Ready();
issuesList = new ItemList { SizeFlagsVertical = (int)SizeFlags.ExpandFill };
issuesList.ItemActivated += _IssueActivated;
AddChild(issuesList);
SizeFlagsVertical = (int)SizeFlags.ExpandFill;
var hsc = new HSplitContainer
{
SizeFlagsHorizontal = (int)SizeFlags.ExpandFill,
SizeFlagsVertical = (int)SizeFlags.ExpandFill
};
AddChild(hsc);
issuesList = new ItemList
{
SizeFlagsVertical = (int)SizeFlags.ExpandFill,
SizeFlagsHorizontal = (int)SizeFlags.ExpandFill // Avoid being squashed by the build log
};
issuesList.ItemActivated += IssueActivated;
issuesList.AllowRmbSelect = true;
issuesList.ItemRmbSelected += IssuesListRmbSelected;
hsc.AddChild(issuesList);
issuesListContextMenu = new PopupMenu();
issuesListContextMenu.IdPressed += IssuesListContextOptionPressed;
issuesList.AddChild(issuesListContextMenu);
buildLog = new TextEdit
{
Readonly = true,
SizeFlagsVertical = (int)SizeFlags.ExpandFill,
SizeFlagsHorizontal = (int)SizeFlags.ExpandFill // Avoid being squashed by the issues list
};
hsc.AddChild(buildLog);
AddBuildEventListeners();
}
private BuildTab()
private void AddBuildEventListeners()
{
BuildManager.BuildLaunchFailed += BuildLaunchFailed;
BuildManager.BuildStarted += BuildStarted;
BuildManager.BuildFinished += BuildFinished;
// StdOutput/Error can be received from different threads, so we need to use CallDeferred
BuildManager.StdOutputReceived += line => CallDeferred(nameof(StdOutputReceived), line);
BuildManager.StdErrorReceived += line => CallDeferred(nameof(StdErrorReceived), line);
}
public void OnBeforeSerialize()
{
}
public BuildTab(BuildInfo buildInfo)
public void OnAfterDeserialize()
{
BuildInfo = buildInfo;
AddBuildEventListeners(); // Re-add them
}
}
}

View file

@ -0,0 +1,8 @@
namespace GodotTools.Build
{
public enum BuildResult
{
Error,
Success
}
}

View file

@ -44,10 +44,7 @@ namespace GodotTools.Build
}
}
private static bool PrintBuildOutput =>
(bool)EditorSettings.GetSetting("mono/builds/print_build_output");
private static Process LaunchBuild(BuildInfo buildInfo)
private static Process LaunchBuild(BuildInfo buildInfo, Action<string> stdOutHandler, Action<string> stdErrHandler)
{
(string msbuildPath, BuildTool buildTool) = MsBuildFinder.FindMsBuild();
@ -58,13 +55,13 @@ namespace GodotTools.Build
var startInfo = new ProcessStartInfo(msbuildPath, compilerArgs);
bool redirectOutput = !IsDebugMsBuildRequested() && !PrintBuildOutput;
string launchMessage = $"Running: \"{startInfo.FileName}\" {startInfo.Arguments}";
stdOutHandler?.Invoke(launchMessage);
if (Godot.OS.IsStdoutVerbose())
Console.WriteLine(launchMessage);
if (!redirectOutput || Godot.OS.IsStdoutVerbose())
Console.WriteLine($"Running: \"{startInfo.FileName}\" {startInfo.Arguments}");
startInfo.RedirectStandardOutput = redirectOutput;
startInfo.RedirectStandardError = redirectOutput;
startInfo.RedirectStandardOutput = true;
startInfo.RedirectStandardError = true;
startInfo.UseShellExecute = false;
if (UsingMonoMsBuildOnWindows)
@ -82,20 +79,22 @@ namespace GodotTools.Build
var process = new Process {StartInfo = startInfo};
if (stdOutHandler != null)
process.OutputDataReceived += (s, e) => stdOutHandler.Invoke(e.Data);
if (stdErrHandler != null)
process.ErrorDataReceived += (s, e) => stdErrHandler.Invoke(e.Data);
process.Start();
if (redirectOutput)
{
process.BeginOutputReadLine();
process.BeginErrorReadLine();
}
process.BeginOutputReadLine();
process.BeginErrorReadLine();
return process;
}
public static int Build(BuildInfo buildInfo)
public static int Build(BuildInfo buildInfo, Action<string> stdOutHandler, Action<string> stdErrHandler)
{
using (var process = LaunchBuild(buildInfo))
using (var process = LaunchBuild(buildInfo, stdOutHandler, stdErrHandler))
{
process.WaitForExit();
@ -103,9 +102,9 @@ namespace GodotTools.Build
}
}
public static async Task<int> BuildAsync(BuildInfo buildInfo)
public static async Task<int> BuildAsync(BuildInfo buildInfo, Action<string> stdOutHandler, Action<string> stdErrHandler)
{
using (var process = LaunchBuild(buildInfo))
using (var process = LaunchBuild(buildInfo, stdOutHandler, stdErrHandler))
{
await process.WaitForExitAsync();
@ -152,10 +151,5 @@ namespace GodotTools.Build
foreach (string env in platformEnvironmentVariables)
environmentVariables.Remove(env);
}
private static bool IsDebugMsBuildRequested()
{
return Environment.GetEnvironmentVariable("GODOT_DEBUG_MSBUILD")?.Trim() == "1";
}
}
}

View file

@ -0,0 +1,165 @@
using System;
using Godot;
using GodotTools.Internals;
using JetBrains.Annotations;
using static GodotTools.Internals.Globals;
using File = GodotTools.Utils.File;
namespace GodotTools.Build
{
public class MSBuildPanel : VBoxContainer
{
public BuildOutputView BuildOutputView { get; private set; }
private Button errorsBtn;
private Button warningsBtn;
private Button viewLogBtn;
private void WarningsToggled(bool pressed)
{
BuildOutputView.WarningsVisible = pressed;
BuildOutputView.UpdateIssuesList();
}
private void ErrorsToggled(bool pressed)
{
BuildOutputView.ErrorsVisible = pressed;
BuildOutputView.UpdateIssuesList();
}
[UsedImplicitly]
public void BuildSolution()
{
if (!File.Exists(GodotSharpDirs.ProjectSlnPath))
return; // No solution to build
BuildManager.GenerateEditorScriptMetadata();
if (!BuildManager.BuildProjectBlocking("Debug"))
return; // Build failed
// Notify running game for hot-reload
Internal.EditorDebuggerNodeReloadScripts();
// Hot-reload in the editor
GodotSharpEditor.Instance.GetNode<HotReloadAssemblyWatcher>("HotReloadAssemblyWatcher").RestartTimer();
if (Internal.IsAssembliesReloadingNeeded())
Internal.ReloadAssemblies(softReload: false);
}
[UsedImplicitly]
private void RebuildSolution()
{
if (!File.Exists(GodotSharpDirs.ProjectSlnPath))
return; // No solution to build
BuildManager.GenerateEditorScriptMetadata();
if (!BuildManager.BuildProjectBlocking("Debug", targets: new[] {"Rebuild"}))
return; // Build failed
// Notify running game for hot-reload
Internal.EditorDebuggerNodeReloadScripts();
// Hot-reload in the editor
GodotSharpEditor.Instance.GetNode<HotReloadAssemblyWatcher>("HotReloadAssemblyWatcher").RestartTimer();
if (Internal.IsAssembliesReloadingNeeded())
Internal.ReloadAssemblies(softReload: false);
}
[UsedImplicitly]
private void CleanSolution()
{
if (!File.Exists(GodotSharpDirs.ProjectSlnPath))
return; // No solution to build
BuildManager.BuildProjectBlocking("Debug", targets: new[] {"Clean"});
}
private void ViewLogToggled(bool pressed) => BuildOutputView.LogVisible = pressed;
private void BuildMenuOptionPressed(int id)
{
switch ((BuildMenuOptions)id)
{
case BuildMenuOptions.BuildSolution:
BuildSolution();
break;
case BuildMenuOptions.RebuildSolution:
RebuildSolution();
break;
case BuildMenuOptions.CleanSolution:
CleanSolution();
break;
default:
throw new ArgumentOutOfRangeException(nameof(id), id, "Invalid build menu option");
}
}
private enum BuildMenuOptions
{
BuildSolution,
RebuildSolution,
CleanSolution
}
public override void _Ready()
{
base._Ready();
RectMinSize = new Vector2(0, 228) * EditorScale;
SizeFlagsVertical = (int)SizeFlags.ExpandFill;
var toolBarHBox = new HBoxContainer {SizeFlagsHorizontal = (int)SizeFlags.ExpandFill};
AddChild(toolBarHBox);
var buildMenuBtn = new MenuButton {Text = "Build", Icon = GetThemeIcon("Play", "EditorIcons")};
toolBarHBox.AddChild(buildMenuBtn);
var buildMenu = buildMenuBtn.GetPopup();
buildMenu.AddItem("Build Solution".TTR(), (int)BuildMenuOptions.BuildSolution);
buildMenu.AddItem("Rebuild Solution".TTR(), (int)BuildMenuOptions.RebuildSolution);
buildMenu.AddItem("Clean Solution".TTR(), (int)BuildMenuOptions.CleanSolution);
buildMenu.IdPressed += BuildMenuOptionPressed;
errorsBtn = new Button
{
HintTooltip = "Show Errors".TTR(),
Icon = GetThemeIcon("StatusError", "EditorIcons"),
ExpandIcon = false,
ToggleMode = true,
Pressed = true,
FocusMode = FocusModeEnum.None
};
errorsBtn.Toggled += ErrorsToggled;
toolBarHBox.AddChild(errorsBtn);
warningsBtn = new Button
{
HintTooltip = "Show Warnings".TTR(),
Icon = GetThemeIcon("NodeWarning", "EditorIcons"),
ExpandIcon = false,
ToggleMode = true,
Pressed = true,
FocusMode = FocusModeEnum.None
};
warningsBtn.Toggled += WarningsToggled;
toolBarHBox.AddChild(warningsBtn);
viewLogBtn = new Button
{
Text = "Show Output".TTR(),
ToggleMode = true,
Pressed = true,
FocusMode = FocusModeEnum.None
};
viewLogBtn.Toggled += ViewLogToggled;
toolBarHBox.AddChild(viewLogBtn);
BuildOutputView = new BuildOutputView();
AddChild(BuildOutputView);
}
}
}

View file

@ -31,7 +31,7 @@ namespace GodotTools.Build
string dotnetCliPath = OS.PathWhich("dotnet");
if (!string.IsNullOrEmpty(dotnetCliPath))
return (dotnetCliPath, BuildTool.DotnetCli);
GD.PushError("Cannot find dotnet CLI executable. Fallback to MSBuild from Visual Studio.");
GD.PushError($"Cannot find executable for '{BuildManager.PropNameDotnetCli}'. Fallback to MSBuild from Visual Studio.");
goto case BuildTool.MsBuildVs;
}
case BuildTool.MsBuildVs:
@ -89,7 +89,7 @@ namespace GodotTools.Build
string dotnetCliPath = OS.PathWhich("dotnet");
if (!string.IsNullOrEmpty(dotnetCliPath))
return (dotnetCliPath, BuildTool.DotnetCli);
GD.PushError("Cannot find dotnet CLI executable. Fallback to MSBuild from Mono.");
GD.PushError($"Cannot find executable for '{BuildManager.PropNameDotnetCli}'. Fallback to MSBuild from Mono.");
goto case BuildTool.MsBuildMono;
}
case BuildTool.MsBuildMono:
@ -161,7 +161,7 @@ namespace GodotTools.Build
// Try to find 15.0 with vswhere
var envNames = Internal.GodotIs32Bits() ? new[] { "ProgramFiles", "ProgramW6432" } : new[] { "ProgramFiles(x86)", "ProgramFiles" };
var envNames = Internal.GodotIs32Bits() ? new[] {"ProgramFiles", "ProgramW6432"} : new[] {"ProgramFiles(x86)", "ProgramFiles"};
string vsWherePath = null;
foreach (var envName in envNames)

View file

@ -5,6 +5,7 @@ using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Runtime.CompilerServices;
using GodotTools.Build;
using GodotTools.Core;
using GodotTools.Internals;
using JetBrains.Annotations;
@ -143,6 +144,8 @@ namespace GodotTools.Export
private void _ExportBeginImpl(string[] features, bool isDebug, string path, int flags)
{
_ = flags; // Unused
if (!File.Exists(GodotSharpDirs.ProjectSlnPath))
return;
@ -154,12 +157,10 @@ namespace GodotTools.Export
string buildConfig = isDebug ? "ExportDebug" : "ExportRelease";
string scriptsMetadataPath = Path.Combine(GodotSharpDirs.ResMetadataDir, $"scripts_metadata.{(isDebug ? "debug" : "release")}");
CsProjOperations.GenerateScriptsMetadata(GodotSharpDirs.ProjectCsProjPath, scriptsMetadataPath);
string scriptsMetadataPath = BuildManager.GenerateExportedGameScriptMetadata(isDebug);
AddFile(scriptsMetadataPath, scriptsMetadataPath);
if (!BuildManager.BuildProjectBlocking(buildConfig, platform))
if (!BuildManager.BuildProjectBlocking(buildConfig, platform: platform))
throw new Exception("Failed to build project");
// Add dependency assemblies

View file

@ -4,9 +4,9 @@ using GodotTools.Export;
using GodotTools.Utils;
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq;
using GodotTools.Build;
using GodotTools.Ides;
using GodotTools.Ides.Rider;
using GodotTools.Internals;
@ -19,7 +19,6 @@ using Path = System.IO.Path;
namespace GodotTools
{
[SuppressMessage("ReSharper", "ClassNeverInstantiated.Global")]
public class GodotSharpEditor : EditorPlugin, ISerializationListener
{
private EditorSettings editorSettings;
@ -37,7 +36,7 @@ namespace GodotTools
private WeakRef exportPluginWeak; // TODO Use WeakReference once we have proper serialization
public BottomPanel BottomPanel { get; private set; }
public MSBuildPanel MSBuildPanel { get; private set; }
public bool SkipBuildBeforePlaying { get; set; } = false;
@ -153,7 +152,7 @@ namespace GodotTools
}
}
private void _BuildSolutionPressed()
private void BuildSolutionPressed()
{
if (!File.Exists(GodotSharpDirs.ProjectSlnPath))
{
@ -161,23 +160,22 @@ namespace GodotTools
return; // Failed to create solution
}
Instance.BottomPanel.BuildProjectPressed();
Instance.MSBuildPanel.BuildSolution();
}
public override void _Notification(int what)
public override void _Ready()
{
base._Notification(what);
base._Ready();
if (what == NotificationReady)
MSBuildPanel.BuildOutputView.BuildStateChanged += BuildStateChanged;
bool showInfoDialog = (bool)editorSettings.GetSetting("mono/editor/show_info_on_start");
if (showInfoDialog)
{
bool showInfoDialog = (bool)editorSettings.GetSetting("mono/editor/show_info_on_start");
if (showInfoDialog)
{
aboutDialog.Exclusive = true;
_ShowAboutDialog();
// Once shown a first time, it can be seen again via the Mono menu - it doesn't have to be exclusive from that time on.
aboutDialog.Exclusive = false;
}
aboutDialog.Exclusive = true;
_ShowAboutDialog();
// Once shown a first time, it can be seen again via the Mono menu - it doesn't have to be exclusive from that time on.
aboutDialog.Exclusive = false;
}
}
@ -393,6 +391,12 @@ namespace GodotTools
}
}
private void BuildStateChanged()
{
if (bottomPanelBtn != null)
bottomPanelBtn.Icon = MSBuildPanel.BuildOutputView.BuildStateIcon;
}
public override void EnablePlugin()
{
base.EnablePlugin();
@ -409,16 +413,15 @@ namespace GodotTools
errorDialog = new AcceptDialog();
editorBaseControl.AddChild(errorDialog);
BottomPanel = new BottomPanel();
bottomPanelBtn = AddControlToBottomPanel(BottomPanel, "Mono".TTR());
MSBuildPanel = new MSBuildPanel();
bottomPanelBtn = AddControlToBottomPanel(MSBuildPanel, "MSBuild".TTR());
AddChild(new HotReloadAssemblyWatcher {Name = "HotReloadAssemblyWatcher"});
menuPopup = new PopupMenu();
menuPopup.Hide();
AddToolSubmenuItem("Mono", menuPopup);
AddToolSubmenuItem("C#", menuPopup);
// TODO: Remove or edit this info dialog once Mono support is no longer in alpha
{
@ -476,7 +479,7 @@ namespace GodotTools
HintTooltip = "Build solution",
FocusMode = Control.FocusModeEnum.None
};
toolBarBuildButton.PressedSignal += _BuildSolutionPressed;
toolBarBuildButton.PressedSignal += BuildSolutionPressed;
AddControlToContainer(CustomControlContainer.Toolbar, toolBarBuildButton);
if (File.Exists(GodotSharpDirs.ProjectSlnPath) && File.Exists(GodotSharpDirs.ProjectCsProjPath))
@ -570,6 +573,7 @@ namespace GodotTools
public static GodotSharpEditor Instance { get; private set; }
[UsedImplicitly]
private GodotSharpEditor()
{
}