Merge pull request #80260 from raulsntos/dotnet/msbuild-panel

C#: Redesign MSBuild panel
This commit is contained in:
Rémi Verschelde 2023-09-27 09:20:19 +02:00
commit c7a5a284d3
No known key found for this signature in database
GPG key ID: C3336907360768E1
8 changed files with 1080 additions and 482 deletions

View file

@ -0,0 +1 @@
<svg height="16" viewBox="0 0 16 16" width="16" xmlns="http://www.w3.org/2000/svg"><path d="m2 2v2h2v5h2v5h8v-2h-6v-3h6v-2h-8v-3h8v-2z" fill="#e0e0e0"/></svg>

After

Width:  |  Height:  |  Size: 159 B

View file

@ -0,0 +1,23 @@
#nullable enable
namespace GodotTools.Build
{
public class BuildDiagnostic
{
public enum DiagnosticType
{
Hidden,
Info,
Warning,
Error,
}
public DiagnosticType Type { get; set; }
public string? File { get; set; }
public int Line { get; set; }
public int Column { get; set; }
public string? Code { get; set; }
public string Message { get; set; } = "";
public string? ProjectFile { get; set; }
}
}

View file

@ -40,9 +40,6 @@ namespace GodotTools.Build
plugin.MakeBottomPanelItemVisible(plugin.MSBuildPanel);
}
public static void RestartBuild(BuildOutputView buildOutputView) => throw new NotImplementedException();
public static void StopBuild(BuildOutputView buildOutputView) => throw new NotImplementedException();
private static string GetLogFilePath(BuildInfo buildInfo)
{
return Path.Combine(buildInfo.LogsDirPath, MsBuildLogFileName);

View file

@ -1,425 +1,150 @@
using Godot;
using System;
using System.Diagnostics.CodeAnalysis;
using GodotTools.Internals;
using File = GodotTools.Utils.File;
using Path = System.IO.Path;
using static GodotTools.Internals.Globals;
#nullable enable
namespace GodotTools.Build
{
public partial class BuildOutputView : VBoxContainer, ISerializationListener
public partial class BuildOutputView : HBoxContainer
{
[Serializable]
private partial class BuildIssue : RefCounted // TODO Remove RefCounted once we have proper serialization
#nullable disable
private RichTextLabel _log;
private Button _clearButton;
private Button _copyButton;
#nullable enable
public void Append(string text)
{
public bool Warning { get; set; }
public string File { get; set; }
public int Line { get; set; }
public int Column { get; set; }
public string Code { get; set; }
public string Message { get; set; }
public string ProjectFile { get; set; }
_log.AddText(text);
}
[Signal]
public delegate void BuildStateChangedEventHandler();
public bool HasBuildExited { get; private set; } = false;
public BuildResult? BuildResult { get; private set; } = null;
public int ErrorCount { get; private set; } = 0;
public int WarningCount { get; private set; } = 0;
public bool ErrorsVisible { get; set; } = true;
public bool WarningsVisible { get; set; } = true;
public Texture2D BuildStateIcon
public void Clear()
{
get
{
if (!HasBuildExited)
return GetThemeIcon("Stop", "EditorIcons");
if (BuildResult == Build.BuildResult.Error)
return GetThemeIcon("Error", "EditorIcons");
if (WarningCount > 1)
return GetThemeIcon("Warning", "EditorIcons");
return null;
}
_log.Clear();
}
public bool LogVisible
private void CopyRequested()
{
set => _buildLog.Visible = value;
}
string text = _log.GetSelectedText();
// TODO Use List once we have proper serialization.
private Godot.Collections.Array<BuildIssue> _issues = new();
private ItemList _issuesList;
private PopupMenu _issuesListContextMenu;
private TextEdit _buildLog;
private BuildInfo _buildInfo;
if (string.IsNullOrEmpty(text))
text = _log.GetParsedText();
private readonly object _pendingBuildLogTextLock = new object();
[NotNull] private string _pendingBuildLogText = string.Empty;
private void LoadIssuesFromFile(string csvFile)
{
using var file = FileAccess.Open(csvFile, FileAccess.ModeFlags.Read);
if (file == null)
return;
while (!file.EofReached())
{
string[] csvColumns = file.GetCsvLine();
if (csvColumns.Length == 1 && string.IsNullOrEmpty(csvColumns[0]))
return;
if (csvColumns.Length != 7)
{
GD.PushError($"Expected 7 columns, got {csvColumns.Length}");
continue;
}
var issue = new BuildIssue
{
Warning = csvColumns[0] == "warning",
File = csvColumns[1],
Line = int.Parse(csvColumns[2]),
Column = int.Parse(csvColumns[3]),
Code = csvColumns[4],
Message = csvColumns[5],
ProjectFile = csvColumns[6]
};
if (issue.Warning)
WarningCount += 1;
else
ErrorCount += 1;
_issues.Add(issue);
}
}
private void IssueActivated(long idx)
{
if (idx < 0 || idx >= _issuesList.ItemCount)
throw new ArgumentOutOfRangeException(nameof(idx), "Item list index out of range.");
// Get correct issue idx from issue list
int issueIndex = (int)_issuesList.GetItemMetadata((int)idx);
if (issueIndex < 0 || issueIndex >= _issues.Count)
throw new InvalidOperationException("Issue index out of range.");
BuildIssue issue = _issues[issueIndex];
if (string.IsNullOrEmpty(issue.ProjectFile) && string.IsNullOrEmpty(issue.File))
return;
string projectDir = !string.IsNullOrEmpty(issue.ProjectFile) ?
issue.ProjectFile.GetBaseDir() :
_buildInfo.Solution.GetBaseDir();
string file = Path.Combine(projectDir.SimplifyGodotPath(), issue.File.SimplifyGodotPath());
if (!File.Exists(file))
return;
file = ProjectSettings.LocalizePath(file);
if (file.StartsWith("res://"))
{
var script = (Script)ResourceLoader.Load(file, typeHint: Internal.CSharpLanguageType);
// Godot's ScriptEditor.Edit is 0-based but the issue lines are 1-based.
if (script != null && Internal.ScriptEditorEdit(script, issue.Line - 1, issue.Column - 1))
Internal.EditorNodeShowScriptScreen();
}
}
public void UpdateIssuesList()
{
_issuesList.Clear();
using (var warningIcon = GetThemeIcon("Warning", "EditorIcons"))
using (var errorIcon = GetThemeIcon("Error", "EditorIcons"))
{
for (int i = 0; i < _issues.Count; i++)
{
BuildIssue issue = _issues[i];
if (!(issue.Warning ? WarningsVisible : ErrorsVisible))
continue;
string tooltip = string.Empty;
tooltip += $"Message: {issue.Message}";
if (!string.IsNullOrEmpty(issue.Code))
tooltip += $"\nCode: {issue.Code}";
tooltip += $"\nType: {(issue.Warning ? "warning" : "error")}";
string text = string.Empty;
if (!string.IsNullOrEmpty(issue.File))
{
text += $"{issue.File}({issue.Line},{issue.Column}): ";
tooltip += $"\nFile: {issue.File}";
tooltip += $"\nLine: {issue.Line}";
tooltip += $"\nColumn: {issue.Column}";
}
if (!string.IsNullOrEmpty(issue.ProjectFile))
tooltip += $"\nProject: {issue.ProjectFile}";
text += issue.Message;
int lineBreakIdx = text.IndexOf("\n", StringComparison.Ordinal);
string itemText = lineBreakIdx == -1 ? text : text.Substring(0, lineBreakIdx);
_issuesList.AddItem(itemText, issue.Warning ? warningIcon : errorIcon);
int index = _issuesList.ItemCount - 1;
_issuesList.SetItemTooltip(index, tooltip);
_issuesList.SetItemMetadata(index, i);
}
}
}
private void BuildLaunchFailed(BuildInfo buildInfo, string cause)
{
HasBuildExited = true;
BuildResult = Build.BuildResult.Error;
_issuesList.Clear();
var issue = new BuildIssue { Message = cause, Warning = false };
ErrorCount += 1;
_issues.Add(issue);
UpdateIssuesList();
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 UpdateBuildLogText()
{
lock (_pendingBuildLogTextLock)
{
_buildLog.Text += _pendingBuildLogText;
_pendingBuildLogText = string.Empty;
ScrollToLastNonEmptyLogLine();
}
}
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";
}
}
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.SetCaretLine(line);
}
public void RestartBuild()
{
if (!HasBuildExited)
throw new InvalidOperationException("Build already started.");
BuildManager.RestartBuild(this);
}
public void StopBuild()
{
if (!HasBuildExited)
throw new InvalidOperationException("Build is not in progress.");
BuildManager.StopBuild(this);
}
private enum IssuesContextMenuOption
{
Copy
}
private void IssuesListContextOptionPressed(long 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 IssuesListClicked(long index, Vector2 atPosition, long mouseButtonIndex)
{
if (mouseButtonIndex != (long)MouseButton.Right)
{
return;
}
_ = 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.ItemCount > 0)
{
_issuesListContextMenu.Position = (Vector2I)(_issuesList.GlobalPosition + atPosition);
_issuesListContextMenu.Popup();
}
if (!string.IsNullOrEmpty(text))
DisplayServer.ClipboardSet(text);
}
public override void _Ready()
{
base._Ready();
Name = "Output".TTR();
SizeFlagsVertical = SizeFlags.ExpandFill;
var vbLeft = new VBoxContainer
{
CustomMinimumSize = new Vector2(0, 180 * EditorScale),
SizeFlagsVertical = SizeFlags.ExpandFill,
SizeFlagsHorizontal = SizeFlags.ExpandFill,
};
AddChild(vbLeft);
var hsc = new HSplitContainer
// Log - Rich Text Label.
_log = new RichTextLabel
{
BbcodeEnabled = true,
ScrollFollowing = true,
SelectionEnabled = true,
ContextMenuEnabled = true,
FocusMode = FocusModeEnum.Click,
SizeFlagsVertical = SizeFlags.ExpandFill,
SizeFlagsHorizontal = SizeFlags.ExpandFill,
DeselectOnFocusLossEnabled = false,
};
vbLeft.AddChild(_log);
var vbRight = new VBoxContainer();
AddChild(vbRight);
// Tools grid
var hbTools = new HBoxContainer
{
SizeFlagsHorizontal = SizeFlags.ExpandFill,
SizeFlagsVertical = SizeFlags.ExpandFill
};
AddChild(hsc);
vbRight.AddChild(hbTools);
_issuesList = new ItemList
// Clear.
_clearButton = new Button
{
SizeFlagsVertical = SizeFlags.ExpandFill,
SizeFlagsHorizontal = SizeFlags.ExpandFill // Avoid being squashed by the build log
ThemeTypeVariation = "FlatButton",
FocusMode = FocusModeEnum.None,
Shortcut = EditorDefShortcut("editor/clear_output", "Clear Output".TTR(), (Key)KeyModifierMask.MaskCmdOrCtrl | (Key)KeyModifierMask.MaskShift | Key.K),
};
_issuesList.ItemActivated += IssueActivated;
_issuesList.AllowRmbSelect = true;
_issuesList.ItemClicked += IssuesListClicked;
hsc.AddChild(_issuesList);
_clearButton.Pressed += Clear;
hbTools.AddChild(_clearButton);
_issuesListContextMenu = new PopupMenu();
_issuesListContextMenu.IdPressed += IssuesListContextOptionPressed;
_issuesList.AddChild(_issuesListContextMenu);
_buildLog = new TextEdit
// Copy.
_copyButton = new Button
{
Editable = false,
SizeFlagsVertical = SizeFlags.ExpandFill,
SizeFlagsHorizontal = SizeFlags.ExpandFill // Avoid being squashed by the issues list
ThemeTypeVariation = "FlatButton",
FocusMode = FocusModeEnum.None,
Shortcut = EditorDefShortcut("editor/copy_output", "Copy Selection".TTR(), (Key)KeyModifierMask.MaskCmdOrCtrl | Key.C),
ShortcutContext = this,
};
hsc.AddChild(_buildLog);
_copyButton.Pressed += CopyRequested;
hbTools.AddChild(_copyButton);
AddBuildEventListeners();
UpdateTheme();
}
private void AddBuildEventListeners()
public override void _Notification(int what)
{
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;
base._Notification(what);
if (what == NotificationThemeChanged)
{
UpdateTheme();
}
}
public void OnBeforeSerialize()
private void UpdateTheme()
{
// In case it didn't update yet. We don't want to have to serialize any pending output.
UpdateBuildLogText();
// Nodes will be null until _Ready is called.
if (_log == null)
return;
// 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;
}
var normalFont = GetThemeFont("output_source", "EditorFonts");
if (normalFont != null)
_log.AddThemeFontOverride("normal_font", normalFont);
public void OnAfterDeserialize()
{
AddBuildEventListeners(); // Re-add them
var boldFont = GetThemeFont("output_source_bold", "EditorFonts");
if (boldFont != null)
_log.AddThemeFontOverride("bold_font", boldFont);
var italicsFont = GetThemeFont("output_source_italic", "EditorFonts");
if (italicsFont != null)
_log.AddThemeFontOverride("italics_font", italicsFont);
var boldItalicsFont = GetThemeFont("output_source_bold_italic", "EditorFonts");
if (boldItalicsFont != null)
_log.AddThemeFontOverride("bold_italics_font", boldItalicsFont);
var monoFont = GetThemeFont("output_source_mono", "EditorFonts");
if (monoFont != null)
_log.AddThemeFontOverride("mono_font", monoFont);
// Disable padding for highlighted background/foreground to prevent highlights from overlapping on close lines.
// This also better matches terminal output, which does not use any form of padding.
_log.AddThemeConstantOverride("text_highlight_h_padding", 0);
_log.AddThemeConstantOverride("text_highlight_v_padding", 0);
int font_size = GetThemeFontSize("output_source_size", "EditorFonts");
_log.AddThemeFontSizeOverride("normal_font_size", font_size);
_log.AddThemeFontSizeOverride("bold_font_size", font_size);
_log.AddThemeFontSizeOverride("italics_font_size", font_size);
_log.AddThemeFontSizeOverride("mono_font_size", font_size);
_clearButton.Icon = GetThemeIcon("Clear", "EditorIcons");
_copyButton.Icon = GetThemeIcon("ActionCopy", "EditorIcons");
}
}
}

View file

@ -0,0 +1,40 @@
using Godot;
#nullable enable
namespace GodotTools.Build
{
public class BuildProblemsFilter
{
public BuildDiagnostic.DiagnosticType Type { get; }
public Button ToggleButton { get; }
private int _problemsCount;
public int ProblemsCount
{
get => _problemsCount;
set
{
_problemsCount = value;
ToggleButton.Text = _problemsCount.ToString();
}
}
public bool IsActive => ToggleButton.ButtonPressed;
public BuildProblemsFilter(BuildDiagnostic.DiagnosticType type)
{
Type = type;
ToggleButton = new Button
{
ToggleMode = true,
ButtonPressed = true,
Text = "0",
FocusMode = Control.FocusModeEnum.None,
ThemeTypeVariation = "EditorLogFilterButton",
};
}
}
}

View file

@ -0,0 +1,694 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using Godot;
using GodotTools.Internals;
using static GodotTools.Internals.Globals;
using FileAccess = Godot.FileAccess;
#nullable enable
namespace GodotTools.Build
{
public partial class BuildProblemsView : HBoxContainer
{
#nullable disable
private Button _clearButton;
private Button _copyButton;
private Button _toggleLayoutButton;
private Button _showSearchButton;
private LineEdit _searchBox;
#nullable enable
private readonly Dictionary<BuildDiagnostic.DiagnosticType, BuildProblemsFilter> _filtersByType = new();
#nullable disable
private Tree _problemsTree;
private PopupMenu _problemsContextMenu;
#nullable enable
public enum ProblemsLayout { List, Tree }
private ProblemsLayout _layout = ProblemsLayout.Tree;
private readonly List<BuildDiagnostic> _diagnostics = new();
public int TotalDiagnosticCount => _diagnostics.Count;
private readonly Dictionary<BuildDiagnostic.DiagnosticType, int> _problemCountByType = new();
public int WarningCount =>
GetProblemCountForType(BuildDiagnostic.DiagnosticType.Warning);
public int ErrorCount =>
GetProblemCountForType(BuildDiagnostic.DiagnosticType.Error);
private int GetProblemCountForType(BuildDiagnostic.DiagnosticType type)
{
if (!_problemCountByType.TryGetValue(type, out int count))
{
count = _diagnostics.Count(d => d.Type == type);
_problemCountByType[type] = count;
}
return count;
}
private static IEnumerable<BuildDiagnostic> ReadDiagnosticsFromFile(string csvFile)
{
using var file = FileAccess.Open(csvFile, FileAccess.ModeFlags.Read);
if (file == null)
yield break;
while (!file.EofReached())
{
string[] csvColumns = file.GetCsvLine();
if (csvColumns.Length == 1 && string.IsNullOrEmpty(csvColumns[0]))
yield break;
if (csvColumns.Length != 7)
{
GD.PushError($"Expected 7 columns, got {csvColumns.Length}");
continue;
}
var diagnostic = new BuildDiagnostic
{
Type = csvColumns[0] switch
{
"warning" => BuildDiagnostic.DiagnosticType.Warning,
"error" or _ => BuildDiagnostic.DiagnosticType.Error,
},
File = csvColumns[1],
Line = int.Parse(csvColumns[2]),
Column = int.Parse(csvColumns[3]),
Code = csvColumns[4],
Message = csvColumns[5],
ProjectFile = csvColumns[6],
};
// If there's no ProjectFile but the File is a csproj, then use that.
if (string.IsNullOrEmpty(diagnostic.ProjectFile) &&
!string.IsNullOrEmpty(diagnostic.File) &&
diagnostic.File.EndsWith(".csproj"))
{
diagnostic.ProjectFile = diagnostic.File;
}
yield return diagnostic;
}
}
public void SetDiagnosticsFromFile(string csvFile)
{
var diagnostics = ReadDiagnosticsFromFile(csvFile);
SetDiagnostics(diagnostics);
}
public void SetDiagnostics(IEnumerable<BuildDiagnostic> diagnostics)
{
_diagnostics.Clear();
_problemCountByType.Clear();
_diagnostics.AddRange(diagnostics);
UpdateProblemsView();
}
public void Clear()
{
_problemsTree.Clear();
_diagnostics.Clear();
_problemCountByType.Clear();
UpdateProblemsView();
}
private void CopySelectedProblems()
{
var selectedItem = _problemsTree.GetNextSelected(null);
if (selectedItem == null)
return;
var selectedIdxs = new List<int>();
while (selectedItem != null)
{
int selectedIdx = (int)selectedItem.GetMetadata(0);
selectedIdxs.Add(selectedIdx);
selectedItem = _problemsTree.GetNextSelected(selectedItem);
}
if (selectedIdxs.Count == 0)
return;
var selectedDiagnostics = selectedIdxs.Select(i => _diagnostics[i]);
var sb = new StringBuilder();
foreach (var diagnostic in selectedDiagnostics)
{
if (!string.IsNullOrEmpty(diagnostic.Code))
sb.Append($"{diagnostic.Code}: ");
sb.AppendLine($"{diagnostic.Message} {diagnostic.File}({diagnostic.Line},{diagnostic.Column})");
}
string text = sb.ToString();
if (!string.IsNullOrEmpty(text))
DisplayServer.ClipboardSet(text);
}
private void ToggleLayout(bool pressed)
{
_layout = pressed ? ProblemsLayout.List : ProblemsLayout.Tree;
var editorSettings = EditorInterface.Singleton.GetEditorSettings();
editorSettings.SetSetting(GodotSharpEditor.Settings.ProblemsLayout, Variant.From(_layout));
_toggleLayoutButton.Icon = GetToggleLayoutIcon();
_toggleLayoutButton.TooltipText = GetToggleLayoutTooltipText();
UpdateProblemsView();
}
private bool GetToggleLayoutPressedState()
{
// If pressed: List layout.
// If not pressed: Tree layout.
return _layout == ProblemsLayout.List;
}
private Texture2D? GetToggleLayoutIcon()
{
return _layout switch
{
ProblemsLayout.List => GetThemeIcon("FileList", "EditorIcons"),
ProblemsLayout.Tree or _ => GetThemeIcon("FileTree", "EditorIcons"),
};
}
private string GetToggleLayoutTooltipText()
{
return _layout switch
{
ProblemsLayout.List => "View as a Tree".TTR(),
ProblemsLayout.Tree or _ => "View as a List".TTR(),
};
}
private void ToggleSearchBoxVisibility(bool pressed)
{
_searchBox.Visible = pressed;
if (pressed)
{
_searchBox.GrabFocus();
}
}
private void SearchTextChanged(string text)
{
UpdateProblemsView();
}
private void ToggleFilter(bool pressed)
{
UpdateProblemsView();
}
private void GoToSelectedProblem()
{
var selectedItem = _problemsTree.GetSelected();
if (selectedItem == null)
throw new InvalidOperationException("Item tree has no selected items.");
// Get correct diagnostic index from problems tree.
int diagnosticIndex = (int)selectedItem.GetMetadata(0);
if (diagnosticIndex < 0 || diagnosticIndex >= _diagnostics.Count)
throw new InvalidOperationException("Diagnostic index out of range.");
var diagnostic = _diagnostics[diagnosticIndex];
if (string.IsNullOrEmpty(diagnostic.ProjectFile) && string.IsNullOrEmpty(diagnostic.File))
return;
string? projectDir = !string.IsNullOrEmpty(diagnostic.ProjectFile) ?
diagnostic.ProjectFile.GetBaseDir() :
GodotSharpEditor.Instance.MSBuildPanel.LastBuildInfo?.Solution.GetBaseDir();
if (string.IsNullOrEmpty(projectDir))
return;
string file = Path.Combine(projectDir.SimplifyGodotPath(), diagnostic.File.SimplifyGodotPath());
if (!File.Exists(file))
return;
file = ProjectSettings.LocalizePath(file);
if (file.StartsWith("res://"))
{
var script = (Script)ResourceLoader.Load(file, typeHint: Internal.CSharpLanguageType);
// Godot's ScriptEditor.Edit is 0-based but the diagnostic lines are 1-based.
if (script != null && Internal.ScriptEditorEdit(script, diagnostic.Line - 1, diagnostic.Column - 1))
Internal.EditorNodeShowScriptScreen();
}
}
private void ShowProblemContextMenu(Vector2 position, long mouseButtonIndex)
{
if (mouseButtonIndex != (long)MouseButton.Right)
return;
_problemsContextMenu.Clear();
_problemsContextMenu.Size = new Vector2I(1, 1);
var selectedItem = _problemsTree.GetSelected();
if (selectedItem != null)
{
// Add menu entries for the selected item.
_problemsContextMenu.AddIconItem(GetThemeIcon("ActionCopy", "EditorIcons"),
label: "Copy Error".TTR(), (int)ProblemContextMenuOption.Copy);
}
if (_problemsContextMenu.ItemCount > 0)
{
_problemsContextMenu.Position = (Vector2I)(_problemsTree.GlobalPosition + position);
_problemsContextMenu.Popup();
}
}
private enum ProblemContextMenuOption
{
Copy,
}
private void ProblemContextOptionPressed(long id)
{
switch ((ProblemContextMenuOption)id)
{
case ProblemContextMenuOption.Copy:
CopySelectedProblems();
break;
default:
throw new ArgumentOutOfRangeException(nameof(id), id, "Invalid problem context menu option.");
}
}
private bool ShouldDisplayDiagnostic(BuildDiagnostic diagnostic)
{
if (!_filtersByType[diagnostic.Type].IsActive)
return false;
string searchText = _searchBox.Text;
if (!string.IsNullOrEmpty(searchText) &&
(!diagnostic.Message.Contains(searchText, StringComparison.OrdinalIgnoreCase) ||
!(diagnostic.File?.Contains(searchText, StringComparison.OrdinalIgnoreCase) ?? false)))
{
return false;
}
return true;
}
private Color? GetProblemItemColor(BuildDiagnostic diagnostic)
{
return diagnostic.Type switch
{
BuildDiagnostic.DiagnosticType.Warning => GetThemeColor("warning_color", "Editor"),
BuildDiagnostic.DiagnosticType.Error => GetThemeColor("error_color", "Editor"),
_ => null,
};
}
public void UpdateProblemsView()
{
switch (_layout)
{
case ProblemsLayout.List:
UpdateProblemsList();
break;
case ProblemsLayout.Tree:
default:
UpdateProblemsTree();
break;
}
foreach (var (type, filter) in _filtersByType)
{
int count = _diagnostics.Count(d => d.Type == type);
filter.ProblemsCount = count;
}
if (_diagnostics.Count == 0)
Name = "Problems".TTR();
else
Name = $"{"Problems".TTR()} ({_diagnostics.Count})";
}
private void UpdateProblemsList()
{
_problemsTree.Clear();
var root = _problemsTree.CreateItem();
for (int i = 0; i < _diagnostics.Count; i++)
{
var diagnostic = _diagnostics[i];
if (!ShouldDisplayDiagnostic(diagnostic))
continue;
var item = CreateProblemItem(diagnostic, includeFileInText: true);
var problemItem = _problemsTree.CreateItem(root);
problemItem.SetIcon(0, item.Icon);
problemItem.SetText(0, item.Text);
problemItem.SetTooltipText(0, item.TooltipText);
problemItem.SetMetadata(0, i);
var color = GetProblemItemColor(diagnostic);
if (color.HasValue)
problemItem.SetCustomColor(0, color.Value);
}
}
private void UpdateProblemsTree()
{
_problemsTree.Clear();
var root = _problemsTree.CreateItem();
var groupedDiagnostics = _diagnostics.Select((d, i) => (Diagnostic: d, Index: i))
.Where(x => ShouldDisplayDiagnostic(x.Diagnostic))
.GroupBy(x => x.Diagnostic.ProjectFile)
.Select(g => (ProjectFile: g.Key, Diagnostics: g.GroupBy(x => x.Diagnostic.File)
.Select(x => (File: x.Key, Diagnostics: x.ToArray()))))
.ToArray();
if (groupedDiagnostics.Length == 0)
return;
foreach (var (projectFile, projectDiagnostics) in groupedDiagnostics)
{
TreeItem projectItem;
if (groupedDiagnostics.Length == 1)
{
// Don't create a project item if there's only one project.
projectItem = root;
}
else
{
string projectFilePath = !string.IsNullOrEmpty(projectFile)
? projectFile
: "Unknown project".TTR();
projectItem = _problemsTree.CreateItem(root);
projectItem.SetText(0, projectFilePath);
projectItem.SetSelectable(0, false);
}
foreach (var (file, fileDiagnostics) in projectDiagnostics)
{
if (fileDiagnostics.Length == 0)
continue;
string? projectDir = Path.GetDirectoryName(projectFile);
string relativeFilePath = !string.IsNullOrEmpty(file) && !string.IsNullOrEmpty(projectDir)
? Path.GetRelativePath(projectDir, file)
: "Unknown file".TTR();
string fileItemText = string.Format("{0} ({1} issues)".TTR(), relativeFilePath, fileDiagnostics.Length);
var fileItem = _problemsTree.CreateItem(projectItem);
fileItem.SetText(0, fileItemText);
fileItem.SetSelectable(0, false);
foreach (var (diagnostic, index) in fileDiagnostics)
{
var item = CreateProblemItem(diagnostic);
var problemItem = _problemsTree.CreateItem(fileItem);
problemItem.SetIcon(0, item.Icon);
problemItem.SetText(0, item.Text);
problemItem.SetTooltipText(0, item.TooltipText);
problemItem.SetMetadata(0, index);
var color = GetProblemItemColor(diagnostic);
if (color.HasValue)
problemItem.SetCustomColor(0, color.Value);
}
}
}
}
private class ProblemItem
{
public string? Text { get; set; }
public string? TooltipText { get; set; }
public Texture2D? Icon { get; set; }
}
private ProblemItem CreateProblemItem(BuildDiagnostic diagnostic, bool includeFileInText = false)
{
var text = new StringBuilder();
var tooltip = new StringBuilder();
ReadOnlySpan<char> shortMessage = diagnostic.Message.AsSpan();
int lineBreakIdx = shortMessage.IndexOf('\n');
if (lineBreakIdx != -1)
shortMessage = shortMessage[..lineBreakIdx];
text.Append(shortMessage);
tooltip.Append($"Message: {diagnostic.Message}");
if (!string.IsNullOrEmpty(diagnostic.Code))
tooltip.Append($"\nCode: {diagnostic.Code}");
string type = diagnostic.Type switch
{
BuildDiagnostic.DiagnosticType.Hidden => "hidden",
BuildDiagnostic.DiagnosticType.Info => "info",
BuildDiagnostic.DiagnosticType.Warning => "warning",
BuildDiagnostic.DiagnosticType.Error => "error",
_ => "unknown",
};
tooltip.Append($"\nType: {type}");
if (!string.IsNullOrEmpty(diagnostic.File))
{
text.Append(' ');
if (includeFileInText)
{
text.Append(diagnostic.File);
}
text.Append($"({diagnostic.Line},{diagnostic.Column})");
tooltip.Append($"\nFile: {diagnostic.File}");
tooltip.Append($"\nLine: {diagnostic.Line}");
tooltip.Append($"\nColumn: {diagnostic.Column}");
}
if (!string.IsNullOrEmpty(diagnostic.ProjectFile))
tooltip.Append($"\nProject: {diagnostic.ProjectFile}");
return new ProblemItem()
{
Text = text.ToString(),
TooltipText = tooltip.ToString(),
Icon = diagnostic.Type switch
{
BuildDiagnostic.DiagnosticType.Warning => GetThemeIcon("Warning", "EditorIcons"),
BuildDiagnostic.DiagnosticType.Error => GetThemeIcon("Error", "EditorIcons"),
_ => null,
},
};
}
public override void _Ready()
{
var editorSettings = GodotSharpEditor.Instance.GetEditorInterface().GetEditorSettings();
_layout = editorSettings.GetSetting(GodotSharpEditor.Settings.ProblemsLayout).As<ProblemsLayout>();
Name = "Problems".TTR();
var vbLeft = new VBoxContainer
{
CustomMinimumSize = new Vector2(0, 180 * EditorScale),
SizeFlagsVertical = SizeFlags.ExpandFill,
SizeFlagsHorizontal = SizeFlags.ExpandFill,
};
AddChild(vbLeft);
// Problem Tree.
_problemsTree = new Tree
{
SizeFlagsVertical = SizeFlags.ExpandFill,
SizeFlagsHorizontal = SizeFlags.ExpandFill,
AllowRmbSelect = true,
HideRoot = true,
};
_problemsTree.ItemActivated += GoToSelectedProblem;
_problemsTree.ItemMouseSelected += ShowProblemContextMenu;
vbLeft.AddChild(_problemsTree);
// Problem context menu.
_problemsContextMenu = new PopupMenu();
_problemsContextMenu.IdPressed += ProblemContextOptionPressed;
_problemsTree.AddChild(_problemsContextMenu);
// Search box.
_searchBox = new LineEdit
{
SizeFlagsHorizontal = SizeFlags.ExpandFill,
PlaceholderText = "Filter Problems".TTR(),
ClearButtonEnabled = true,
};
_searchBox.TextChanged += SearchTextChanged;
vbLeft.AddChild(_searchBox);
var vbRight = new VBoxContainer();
AddChild(vbRight);
// Tools grid.
var hbTools = new HBoxContainer
{
SizeFlagsHorizontal = SizeFlags.ExpandFill,
};
vbRight.AddChild(hbTools);
// Clear.
_clearButton = new Button
{
ThemeTypeVariation = "FlatButton",
FocusMode = FocusModeEnum.None,
Shortcut = EditorDefShortcut("editor/clear_output", "Clear Output".TTR(), (Key)KeyModifierMask.MaskCmdOrCtrl | (Key)KeyModifierMask.MaskShift | Key.K),
ShortcutContext = this,
};
_clearButton.Pressed += Clear;
hbTools.AddChild(_clearButton);
// Copy.
_copyButton = new Button
{
ThemeTypeVariation = "FlatButton",
FocusMode = FocusModeEnum.None,
Shortcut = EditorDefShortcut("editor/copy_output", "Copy Selection".TTR(), (Key)KeyModifierMask.MaskCmdOrCtrl | Key.C),
ShortcutContext = this,
};
_copyButton.Pressed += CopySelectedProblems;
hbTools.AddChild(_copyButton);
// A second hbox to make a 2x2 grid of buttons.
var hbTools2 = new HBoxContainer
{
SizeFlagsHorizontal = SizeFlags.ShrinkCenter,
};
vbRight.AddChild(hbTools2);
// Toggle List/Tree.
_toggleLayoutButton = new Button
{
Flat = true,
FocusMode = FocusModeEnum.None,
TooltipText = GetToggleLayoutTooltipText(),
ToggleMode = true,
ButtonPressed = GetToggleLayoutPressedState(),
};
// Don't tint the icon even when in "pressed" state.
_toggleLayoutButton.AddThemeColorOverride("icon_pressed_color", Colors.White);
_toggleLayoutButton.Toggled += ToggleLayout;
hbTools2.AddChild(_toggleLayoutButton);
// Show Search.
_showSearchButton = new Button
{
ThemeTypeVariation = "FlatButton",
FocusMode = FocusModeEnum.None,
ToggleMode = true,
ButtonPressed = true,
Shortcut = EditorDefShortcut("editor/open_search", "Focus Search/Filter Bar".TTR(), (Key)KeyModifierMask.MaskCmdOrCtrl | Key.F),
ShortcutContext = this,
};
_showSearchButton.Toggled += ToggleSearchBoxVisibility;
hbTools2.AddChild(_showSearchButton);
// Diagnostic Type Filters.
vbRight.AddChild(new HSeparator());
var infoFilter = new BuildProblemsFilter(BuildDiagnostic.DiagnosticType.Info);
infoFilter.ToggleButton.TooltipText = "Toggle visibility of info diagnostics.".TTR();
infoFilter.ToggleButton.Toggled += ToggleFilter;
vbRight.AddChild(infoFilter.ToggleButton);
_filtersByType[BuildDiagnostic.DiagnosticType.Info] = infoFilter;
var errorFilter = new BuildProblemsFilter(BuildDiagnostic.DiagnosticType.Error);
errorFilter.ToggleButton.TooltipText = "Toggle visibility of errors.".TTR();
errorFilter.ToggleButton.Toggled += ToggleFilter;
vbRight.AddChild(errorFilter.ToggleButton);
_filtersByType[BuildDiagnostic.DiagnosticType.Error] = errorFilter;
var warningFilter = new BuildProblemsFilter(BuildDiagnostic.DiagnosticType.Warning);
warningFilter.ToggleButton.TooltipText = "Toggle visibility of warnings.".TTR();
warningFilter.ToggleButton.Toggled += ToggleFilter;
vbRight.AddChild(warningFilter.ToggleButton);
_filtersByType[BuildDiagnostic.DiagnosticType.Warning] = warningFilter;
UpdateTheme();
UpdateProblemsView();
}
public override void _Notification(int what)
{
base._Notification(what);
switch ((long)what)
{
case EditorSettings.NotificationEditorSettingsChanged:
var editorSettings = GodotSharpEditor.Instance.GetEditorInterface().GetEditorSettings();
_layout = editorSettings.GetSetting(GodotSharpEditor.Settings.ProblemsLayout).As<ProblemsLayout>();
_toggleLayoutButton.ButtonPressed = GetToggleLayoutPressedState();
UpdateProblemsView();
break;
case NotificationThemeChanged:
UpdateTheme();
break;
}
}
private void UpdateTheme()
{
// Nodes will be null until _Ready is called.
if (_clearButton == null)
return;
foreach (var (type, filter) in _filtersByType)
{
filter.ToggleButton.Icon = type switch
{
BuildDiagnostic.DiagnosticType.Info => GetThemeIcon("Popup", "EditorIcons"),
BuildDiagnostic.DiagnosticType.Warning => GetThemeIcon("StatusWarning", "EditorIcons"),
BuildDiagnostic.DiagnosticType.Error => GetThemeIcon("StatusError", "EditorIcons"),
_ => null,
};
}
_clearButton.Icon = GetThemeIcon("Clear", "EditorIcons");
_copyButton.Icon = GetThemeIcon("ActionCopy", "EditorIcons");
_toggleLayoutButton.Icon = GetToggleLayoutIcon();
_showSearchButton.Icon = GetThemeIcon("Search", "EditorIcons");
_searchBox.RightIcon = GetThemeIcon("Search", "EditorIcons");
}
}
}

View file

@ -5,28 +5,73 @@ using GodotTools.Internals;
using static GodotTools.Internals.Globals;
using File = GodotTools.Utils.File;
#nullable enable
namespace GodotTools.Build
{
public partial class MSBuildPanel : VBoxContainer
public partial class MSBuildPanel : MarginContainer, ISerializationListener
{
public BuildOutputView BuildOutputView { get; private set; }
[Signal]
public delegate void BuildStateChangedEventHandler();
private MenuButton _buildMenuBtn;
private Button _errorsBtn;
private Button _warningsBtn;
private Button _viewLogBtn;
private Button _openLogsFolderBtn;
#nullable disable
private MenuButton _buildMenuButton;
private Button _openLogsFolderButton;
private void WarningsToggled(bool pressed)
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()
{
BuildOutputView.WarningsVisible = pressed;
BuildOutputView.UpdateIssuesList();
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 void ErrorsToggled(bool pressed)
private enum BuildMenuOptions
{
BuildOutputView.ErrorsVisible = pressed;
BuildOutputView.UpdateIssuesList();
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()
@ -73,108 +118,136 @@ namespace GodotTools.Build
_ = BuildManager.CleanProjectBlocking("Debug");
}
private void ViewLogToggled(bool pressed) => BuildOutputView.LogVisible = pressed;
private void OpenLogsFolderPressed() => OS.ShellOpen(
private void OpenLogsFolder() => OS.ShellOpen(
$"file://{GodotSharpDirs.LogsDirPathFor("Debug")}"
);
private void BuildMenuOptionPressed(long id)
private void BuildLaunchFailed(BuildInfo buildInfo, string cause)
{
switch ((BuildMenuOptions)id)
IsBuildingOngoing = false;
BuildResult = Build.BuildResult.Error;
_problemsView.Clear();
_outputView.Clear();
var diagnostic = new BuildDiagnostic
{
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");
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 enum BuildMenuOptions
private void StdOutputReceived(string text)
{
BuildProject,
RebuildProject,
CleanProject
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();
CustomMinimumSize = new Vector2(0, 228 * EditorScale);
SizeFlagsVertical = SizeFlags.ExpandFill;
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 toolBarHBox = new HBoxContainer { SizeFlagsHorizontal = SizeFlags.ExpandFill };
AddChild(toolBarHBox);
var tabs = new TabContainer();
AddChild(tabs);
_buildMenuBtn = new MenuButton { Text = "Build", Icon = GetThemeIcon("BuildCSharp", "EditorIcons") };
toolBarHBox.AddChild(_buildMenuBtn);
var tabActions = new HBoxContainer
{
SizeFlagsVertical = SizeFlags.ExpandFill,
SizeFlagsHorizontal = SizeFlags.ExpandFill,
Alignment = BoxContainer.AlignmentMode.End,
};
tabActions.SetAnchorsAndOffsetsPreset(LayoutPreset.FullRect);
tabs.GetTabBar().AddChild(tabActions);
var buildMenu = _buildMenuBtn.GetPopup();
_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;
_errorsBtn = new Button
_openLogsFolderButton = new Button
{
TooltipText = "Show Errors".TTR(),
Icon = GetThemeIcon("StatusError", "EditorIcons"),
ExpandIcon = false,
ToggleMode = true,
ButtonPressed = true,
FocusMode = FocusModeEnum.None
TooltipText = "Show Logs in File Manager".TTR(),
Flat = true,
};
_errorsBtn.Toggled += ErrorsToggled;
toolBarHBox.AddChild(_errorsBtn);
_openLogsFolderButton.Pressed += OpenLogsFolder;
tabActions.AddChild(_openLogsFolderButton);
_warningsBtn = new Button
{
TooltipText = "Show Warnings".TTR(),
Icon = GetThemeIcon("NodeWarning", "EditorIcons"),
ExpandIcon = false,
ToggleMode = true,
ButtonPressed = true,
FocusMode = FocusModeEnum.None
};
_warningsBtn.Toggled += WarningsToggled;
toolBarHBox.AddChild(_warningsBtn);
_problemsView = new BuildProblemsView();
tabs.AddChild(_problemsView);
_viewLogBtn = new Button
{
Text = "Show Output".TTR(),
ToggleMode = true,
ButtonPressed = true,
FocusMode = FocusModeEnum.None
};
_viewLogBtn.Toggled += ViewLogToggled;
toolBarHBox.AddChild(_viewLogBtn);
_outputView = new BuildOutputView();
tabs.AddChild(_outputView);
// Horizontal spacer, push everything to the right.
toolBarHBox.AddChild(new Control
{
SizeFlagsHorizontal = SizeFlags.ExpandFill,
});
UpdateTheme();
_openLogsFolderBtn = new Button
{
Text = "Show Logs in File Manager".TTR(),
Icon = GetThemeIcon("Filesystem", "EditorIcons"),
ExpandIcon = false,
FocusMode = FocusModeEnum.None,
};
_openLogsFolderBtn.Pressed += OpenLogsFolderPressed;
toolBarHBox.AddChild(_openLogsFolderBtn);
BuildOutputView = new BuildOutputView();
AddChild(BuildOutputView);
AddBuildEventListeners();
}
public override void _Notification(int what)
@ -183,13 +256,49 @@ namespace GodotTools.Build
if (what == NotificationThemeChanged)
{
if (_buildMenuBtn != null)
_buildMenuBtn.Icon = GetThemeIcon("BuildCSharp", "EditorIcons");
if (_errorsBtn != null)
_errorsBtn.Icon = GetThemeIcon("StatusError", "EditorIcons");
if (_warningsBtn != null)
_warningsBtn.Icon = GetThemeIcon("NodeWarning", "EditorIcons");
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.
}
}
}

View file

@ -30,6 +30,7 @@ namespace GodotTools
public const string VerbosityLevel = "dotnet/build/verbosity_level";
public const string NoConsoleLogging = "dotnet/build/no_console_logging";
public const string CreateBinaryLog = "dotnet/build/create_binary_log";
public const string ProblemsLayout = "dotnet/build/problems_layout";
}
private EditorSettings _editorSettings;
@ -437,7 +438,7 @@ namespace GodotTools
private void BuildStateChanged()
{
if (_bottomPanelBtn != null)
_bottomPanelBtn.Icon = MSBuildPanel.BuildOutputView.BuildStateIcon;
_bottomPanelBtn.Icon = MSBuildPanel.GetBuildStateIcon();
}
public override void _EnablePlugin()
@ -489,8 +490,7 @@ namespace GodotTools
editorBaseControl.AddChild(_confirmCreateSlnDialog);
MSBuildPanel = new MSBuildPanel();
MSBuildPanel.Ready += () =>
MSBuildPanel.BuildOutputView.BuildStateChanged += BuildStateChanged;
MSBuildPanel.BuildStateChanged += BuildStateChanged;
_bottomPanelBtn = AddControlToBottomPanel(MSBuildPanel, "MSBuild".TTR());
AddChild(new HotReloadAssemblyWatcher { Name = "HotReloadAssemblyWatcher" });
@ -535,6 +535,7 @@ namespace GodotTools
EditorDef(Settings.VerbosityLevel, Variant.From(VerbosityLevelId.Normal));
EditorDef(Settings.NoConsoleLogging, false);
EditorDef(Settings.CreateBinaryLog, false);
EditorDef(Settings.ProblemsLayout, Variant.From(BuildProblemsView.ProblemsLayout.Tree));
string settingsHintStr = "Disabled";
@ -593,6 +594,14 @@ namespace GodotTools
["hint_string"] = string.Join(",", verbosityLevels),
});
_editorSettings.AddPropertyInfo(new Godot.Collections.Dictionary
{
["type"] = (int)Variant.Type.Int,
["name"] = Settings.ProblemsLayout,
["hint"] = (int)PropertyHint.Enum,
["hint_string"] = "View as List,View as Tree",
});
OnSettingsChanged();
_editorSettings.SettingsChanged += OnSettingsChanged;