using System;
using System.IO;
using Godot;
using GodotTools.Internals;
using static GodotTools.Internals.Globals;
using File = GodotTools.Utils.File;
namespace GodotTools.Build
{
public partial class MSBuildPanel : MarginContainer, ISerializationListener
{
[Signal]
public delegate void BuildStateChangedEventHandler();
#nullable disable
private MenuButton _buildMenuButton;
private Button _openLogsFolderButton;
private BuildProblemsView _problemsView;
private BuildOutputView _outputView;
#nullable enable
public BuildInfo? LastBuildInfo { get; private set; }
public bool IsBuildingOngoing { get; private set; }
public BuildResult? BuildResult { get; private set; }
private readonly object _pendingBuildLogTextLock = new object();
private string _pendingBuildLogText = string.Empty;
public Texture2D? GetBuildStateIcon()
{
if (IsBuildingOngoing)
return GetThemeIcon("Stop", "EditorIcons");
if (_problemsView.WarningCount > 0 && _problemsView.ErrorCount > 0)
return GetThemeIcon("ErrorWarning", "EditorIcons");
if (_problemsView.WarningCount > 0)
return GetThemeIcon("Warning", "EditorIcons");
if (_problemsView.ErrorCount > 0)
return GetThemeIcon("Error", "EditorIcons");
return null;
}
private enum BuildMenuOptions
{
BuildProject,
RebuildProject,
CleanProject,
}
private void BuildMenuOptionPressed(long id)
{
switch ((BuildMenuOptions)id)
{
case BuildMenuOptions.BuildProject:
BuildProject();
break;
case BuildMenuOptions.RebuildProject:
RebuildProject();
break;
case BuildMenuOptions.CleanProject:
CleanProject();
break;
default:
throw new ArgumentOutOfRangeException(nameof(id), id, "Invalid build menu option");
}
}
public void BuildProject()
{
if (!File.Exists(GodotSharpDirs.ProjectCsProjPath))
return; // No project to build.
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())
{
BuildManager.UpdateLastValidBuildDateTime();
Internal.ReloadAssemblies(softReload: false);
}
}
private void RebuildProject()
{
if (!File.Exists(GodotSharpDirs.ProjectCsProjPath))
return; // No project to build.
if (!BuildManager.BuildProjectBlocking("Debug", rebuild: true))
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())
{
BuildManager.UpdateLastValidBuildDateTime();
Internal.ReloadAssemblies(softReload: false);
}
}
private void CleanProject()
{
if (!File.Exists(GodotSharpDirs.ProjectCsProjPath))
return; // No project to build.
_ = BuildManager.CleanProjectBlocking("Debug");
}
private void OpenLogsFolder() => OS.ShellOpen(
$"file://{GodotSharpDirs.LogsDirPathFor("Debug")}"
);
private void BuildLaunchFailed(BuildInfo buildInfo, string cause)
{
IsBuildingOngoing = false;
BuildResult = Build.BuildResult.Error;
_problemsView.Clear();
_outputView.Clear();
var diagnostic = new BuildDiagnostic
{
Type = BuildDiagnostic.DiagnosticType.Error,
Message = cause,
};
_problemsView.SetDiagnostics(new[] { diagnostic });
EmitSignal(SignalName.BuildStateChanged);
}
private void BuildStarted(BuildInfo buildInfo)
{
LastBuildInfo = buildInfo;
IsBuildingOngoing = true;
BuildResult = null;
_problemsView.Clear();
_outputView.Clear();
_problemsView.UpdateProblemsView();
EmitSignal(SignalName.BuildStateChanged);
}
private void BuildFinished(BuildResult result)
{
IsBuildingOngoing = false;
BuildResult = result;
string csvFile = Path.Combine(LastBuildInfo!.LogsDirPath, BuildManager.MsBuildIssuesFileName);
_problemsView.SetDiagnosticsFromFile(csvFile);
_problemsView.UpdateProblemsView();
EmitSignal(SignalName.BuildStateChanged);
}
private void UpdateBuildLogText()
{
lock (_pendingBuildLogTextLock)
{
_outputView.Append(_pendingBuildLogText);
_pendingBuildLogText = string.Empty;
}
}
private void StdOutputReceived(string? text)
{
lock (_pendingBuildLogTextLock)
{
if (_pendingBuildLogText.Length == 0)
CallDeferred(nameof(UpdateBuildLogText));
_pendingBuildLogText += text + "\n";
}
}
private void StdErrorReceived(string? text)
{
lock (_pendingBuildLogTextLock)
{
if (_pendingBuildLogText.Length == 0)
CallDeferred(nameof(UpdateBuildLogText));
_pendingBuildLogText += text + "\n";
}
}
public override void _Ready()
{
base._Ready();
var bottomPanelStylebox = EditorInterface.Singleton.GetBaseControl().GetThemeStylebox("BottomPanel", "EditorStyles");
AddThemeConstantOverride("margin_top", -(int)bottomPanelStylebox.ContentMarginTop);
AddThemeConstantOverride("margin_left", -(int)bottomPanelStylebox.ContentMarginLeft);
AddThemeConstantOverride("margin_right", -(int)bottomPanelStylebox.ContentMarginRight);
var tabs = new TabContainer();
AddChild(tabs);
var tabActions = new HBoxContainer
{
SizeFlagsVertical = SizeFlags.ExpandFill,
SizeFlagsHorizontal = SizeFlags.ExpandFill,
Alignment = BoxContainer.AlignmentMode.End,
};
tabActions.SetAnchorsAndOffsetsPreset(LayoutPreset.FullRect);
tabs.GetTabBar().AddChild(tabActions);
_buildMenuButton = new MenuButton
{
TooltipText = "Build".TTR(),
Flat = true,
};
tabActions.AddChild(_buildMenuButton);
var buildMenu = _buildMenuButton.GetPopup();
buildMenu.AddItem("Build Project".TTR(), (int)BuildMenuOptions.BuildProject);
buildMenu.AddItem("Rebuild Project".TTR(), (int)BuildMenuOptions.RebuildProject);
buildMenu.AddItem("Clean Project".TTR(), (int)BuildMenuOptions.CleanProject);
buildMenu.IdPressed += BuildMenuOptionPressed;
_openLogsFolderButton = new Button
{
TooltipText = "Show Logs in File Manager".TTR(),
Flat = true,
};
_openLogsFolderButton.Pressed += OpenLogsFolder;
tabActions.AddChild(_openLogsFolderButton);
_problemsView = new BuildProblemsView();
tabs.AddChild(_problemsView);
_outputView = new BuildOutputView();
tabs.AddChild(_outputView);
UpdateTheme();
AddBuildEventListeners();
}
public override void _Notification(int what)
{
base._Notification(what);
if (what == NotificationThemeChanged)
{
UpdateTheme();
}
}
private void UpdateTheme()
{
// Nodes will be null until _Ready is called.
if (_buildMenuButton == null)
return;
_buildMenuButton.Icon = GetThemeIcon("BuildCSharp", "EditorIcons");
_openLogsFolderButton.Icon = GetThemeIcon("Filesystem", "EditorIcons");
}
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 += StdOutputReceived;
BuildManager.StdErrorReceived += StdErrorReceived;
}
public void OnBeforeSerialize()
{
// In case it didn't update yet. We don't want to have to serialize any pending output.
UpdateBuildLogText();
// NOTE:
// Currently, GodotTools is loaded in its own load context. This load context is not reloaded, but the script still are.
// Until that changes, we need workarounds like this one because events keep strong references to disposed objects.
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 -= StdOutputReceived;
BuildManager.StdErrorReceived -= StdErrorReceived;
}
public void OnAfterDeserialize()
{
AddBuildEventListeners(); // Re-add them.
}
}
}