diff --git a/editor/icons/FileTree.svg b/editor/icons/FileTree.svg
new file mode 100644
index 000000000000..995715c993b4
--- /dev/null
+++ b/editor/icons/FileTree.svg
@@ -0,0 +1 @@
+
diff --git a/modules/mono/editor/GodotTools/GodotTools/Build/BuildDiagnostic.cs b/modules/mono/editor/GodotTools/GodotTools/Build/BuildDiagnostic.cs
new file mode 100644
index 000000000000..6e0c63dd433e
--- /dev/null
+++ b/modules/mono/editor/GodotTools/GodotTools/Build/BuildDiagnostic.cs
@@ -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; }
+ }
+}
diff --git a/modules/mono/editor/GodotTools/GodotTools/Build/BuildManager.cs b/modules/mono/editor/GodotTools/GodotTools/Build/BuildManager.cs
index 312c65e36496..9bb4fd153b28 100644
--- a/modules/mono/editor/GodotTools/GodotTools/Build/BuildManager.cs
+++ b/modules/mono/editor/GodotTools/GodotTools/Build/BuildManager.cs
@@ -40,9 +40,6 @@ private static void ShowBuildErrorDialog(string message)
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);
diff --git a/modules/mono/editor/GodotTools/GodotTools/Build/BuildOutputView.cs b/modules/mono/editor/GodotTools/GodotTools/Build/BuildOutputView.cs
index 54f7ed02f511..f9e85c36e563 100644
--- a/modules/mono/editor/GodotTools/GodotTools/Build/BuildOutputView.cs
+++ b/modules/mono/editor/GodotTools/GodotTools/Build/BuildOutputView.cs
@@ -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
- {
- 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; }
- }
-
- [Signal]
- public delegate void BuildStateChangedEventHandler();
-
- public bool HasBuildExited { get; private set; } = false;
+#nullable disable
+ private RichTextLabel _log;
- public BuildResult? BuildResult { get; private set; } = null;
+ private Button _clearButton;
+ private Button _copyButton;
+#nullable enable
- 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 Append(string text)
{
- 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.AddText(text);
}
- public bool LogVisible
+ public void Clear()
{
- set => _buildLog.Visible = value;
+ _log.Clear();
}
- // TODO Use List once we have proper serialization.
- private Godot.Collections.Array _issues = new();
- private ItemList _issuesList;
- private PopupMenu _issuesListContextMenu;
- private TextEdit _buildLog;
- private BuildInfo _buildInfo;
-
- private readonly object _pendingBuildLogTextLock = new object();
- [NotNull] private string _pendingBuildLogText = string.Empty;
-
- private void LoadIssuesFromFile(string csvFile)
+ private void CopyRequested()
{
- using var file = FileAccess.Open(csvFile, FileAccess.ModeFlags.Read);
-
- if (file == null)
- return;
+ string text = _log.GetSelectedText();
- while (!file.EofReached())
- {
- string[] csvColumns = file.GetCsvLine();
+ if (string.IsNullOrEmpty(text))
+ text = _log.GetParsedText();
- 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);
- }
+ if (!string.IsNullOrEmpty(text))
+ DisplayServer.ClipboardSet(text);
}
- private void IssueActivated(long idx)
+ public override void _Ready()
{
- 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];
+ Name = "Output".TTR();
- 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 vbLeft = new VBoxContainer
{
- 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();
+ CustomMinimumSize = new Vector2(0, 180 * EditorScale),
+ SizeFlagsVertical = SizeFlags.ExpandFill,
+ SizeFlagsHorizontal = SizeFlags.ExpandFill,
+ };
+ AddChild(vbLeft);
- using (var warningIcon = GetThemeIcon("Warning", "EditorIcons"))
- using (var errorIcon = GetThemeIcon("Error", "EditorIcons"))
+ // Log - Rich Text Label.
+ _log = new RichTextLabel
{
- 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));
+ BbcodeEnabled = true,
+ ScrollFollowing = true,
+ SelectionEnabled = true,
+ ContextMenuEnabled = true,
+ FocusMode = FocusModeEnum.Click,
+ SizeFlagsVertical = SizeFlags.ExpandFill,
+ SizeFlagsHorizontal = SizeFlags.ExpandFill,
+ DeselectOnFocusLossEnabled = false,
- UpdateIssuesList();
+ };
+ vbLeft.AddChild(_log);
- EmitSignal(nameof(BuildStateChanged));
- }
+ var vbRight = new VBoxContainer();
+ AddChild(vbRight);
- private void UpdateBuildLogText()
- {
- lock (_pendingBuildLogTextLock)
+ // Tools grid
+ var hbTools = new HBoxContainer
{
- _buildLog.Text += _pendingBuildLogText;
- _pendingBuildLogText = string.Empty;
- ScrollToLastNonEmptyLogLine();
- }
- }
-
- private void StdOutputReceived(string text)
- {
- lock (_pendingBuildLogTextLock)
- {
- if (_pendingBuildLogText.Length == 0)
- CallDeferred(nameof(UpdateBuildLogText));
- _pendingBuildLogText += text + "\n";
- }
- }
+ SizeFlagsHorizontal = SizeFlags.ExpandFill,
+ };
+ vbRight.AddChild(hbTools);
- private void StdErrorReceived(string text)
- {
- lock (_pendingBuildLogTextLock)
+ // Clear.
+ _clearButton = new Button
{
- if (_pendingBuildLogText.Length == 0)
- CallDeferred(nameof(UpdateBuildLogText));
- _pendingBuildLogText += text + "\n";
- }
- }
+ ThemeTypeVariation = "FlatButton",
+ FocusMode = FocusModeEnum.None,
+ Shortcut = EditorDefShortcut("editor/clear_output", "Clear Output".TTR(), (Key)KeyModifierMask.MaskCmdOrCtrl | (Key)KeyModifierMask.MaskShift | Key.K),
+ };
+ _clearButton.Pressed += Clear;
+ hbTools.AddChild(_clearButton);
- private void ScrollToLastNonEmptyLogLine()
- {
- int line;
- for (line = _buildLog.GetLineCount(); line > 0; line--)
+ // Copy.
+ _copyButton = new Button
{
- 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.");
+ ThemeTypeVariation = "FlatButton",
+ FocusMode = FocusModeEnum.None,
+ Shortcut = EditorDefShortcut("editor/copy_output", "Copy Selection".TTR(), (Key)KeyModifierMask.MaskCmdOrCtrl | Key.C),
+ ShortcutContext = this,
+ };
+ _copyButton.Pressed += CopyRequested;
+ hbTools.AddChild(_copyButton);
- BuildManager.StopBuild(this);
+ UpdateTheme();
}
- private enum IssuesContextMenuOption
+ public override void _Notification(int what)
{
- Copy
- }
+ base._Notification(what);
- private void IssuesListContextOptionPressed(long id)
- {
- switch ((IssuesContextMenuOption)id)
+ if (what == NotificationThemeChanged)
{
- 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");
+ UpdateTheme();
}
}
- private void IssuesListClicked(long index, Vector2 atPosition, long mouseButtonIndex)
+ private void UpdateTheme()
{
- if (mouseButtonIndex != (long)MouseButton.Right)
- {
+ // Nodes will be null until _Ready is called.
+ if (_log == null)
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();
- }
- }
-
- public override void _Ready()
- {
- base._Ready();
- SizeFlagsVertical = SizeFlags.ExpandFill;
+ var normalFont = GetThemeFont("output_source", "EditorFonts");
+ if (normalFont != null)
+ _log.AddThemeFontOverride("normal_font", normalFont);
- var hsc = new HSplitContainer
- {
- SizeFlagsHorizontal = SizeFlags.ExpandFill,
- SizeFlagsVertical = SizeFlags.ExpandFill
- };
- AddChild(hsc);
+ var boldFont = GetThemeFont("output_source_bold", "EditorFonts");
+ if (boldFont != null)
+ _log.AddThemeFontOverride("bold_font", boldFont);
- _issuesList = new ItemList
- {
- SizeFlagsVertical = SizeFlags.ExpandFill,
- SizeFlagsHorizontal = SizeFlags.ExpandFill // Avoid being squashed by the build log
- };
- _issuesList.ItemActivated += IssueActivated;
- _issuesList.AllowRmbSelect = true;
- _issuesList.ItemClicked += IssuesListClicked;
- hsc.AddChild(_issuesList);
+ var italicsFont = GetThemeFont("output_source_italic", "EditorFonts");
+ if (italicsFont != null)
+ _log.AddThemeFontOverride("italics_font", italicsFont);
- _issuesListContextMenu = new PopupMenu();
- _issuesListContextMenu.IdPressed += IssuesListContextOptionPressed;
- _issuesList.AddChild(_issuesListContextMenu);
+ var boldItalicsFont = GetThemeFont("output_source_bold_italic", "EditorFonts");
+ if (boldItalicsFont != null)
+ _log.AddThemeFontOverride("bold_italics_font", boldItalicsFont);
- _buildLog = new TextEdit
- {
- Editable = false,
- SizeFlagsVertical = SizeFlags.ExpandFill,
- SizeFlagsHorizontal = SizeFlags.ExpandFill // Avoid being squashed by the issues list
- };
- hsc.AddChild(_buildLog);
+ var monoFont = GetThemeFont("output_source_mono", "EditorFonts");
+ if (monoFont != null)
+ _log.AddThemeFontOverride("mono_font", monoFont);
- AddBuildEventListeners();
- }
-
- 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;
- }
+ // 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);
- 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;
- }
+ 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);
- public void OnAfterDeserialize()
- {
- AddBuildEventListeners(); // Re-add them
+ _clearButton.Icon = GetThemeIcon("Clear", "EditorIcons");
+ _copyButton.Icon = GetThemeIcon("ActionCopy", "EditorIcons");
}
}
}
diff --git a/modules/mono/editor/GodotTools/GodotTools/Build/BuildProblemsFilter.cs b/modules/mono/editor/GodotTools/GodotTools/Build/BuildProblemsFilter.cs
new file mode 100644
index 000000000000..9c165e57674c
--- /dev/null
+++ b/modules/mono/editor/GodotTools/GodotTools/Build/BuildProblemsFilter.cs
@@ -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",
+ };
+ }
+ }
+}
diff --git a/modules/mono/editor/GodotTools/GodotTools/Build/BuildProblemsView.cs b/modules/mono/editor/GodotTools/GodotTools/Build/BuildProblemsView.cs
new file mode 100644
index 000000000000..b23b3f42ef7b
--- /dev/null
+++ b/modules/mono/editor/GodotTools/GodotTools/Build/BuildProblemsView.cs
@@ -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 _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 _diagnostics = new();
+
+ public int TotalDiagnosticCount => _diagnostics.Count;
+
+ private readonly Dictionary _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 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 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();
+ 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 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();
+
+ 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();
+ _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");
+ }
+ }
+}
diff --git a/modules/mono/editor/GodotTools/GodotTools/Build/MSBuildPanel.cs b/modules/mono/editor/GodotTools/GodotTools/Build/MSBuildPanel.cs
index cc11132a553a..bae87dd1ddf2 100644
--- a/modules/mono/editor/GodotTools/GodotTools/Build/MSBuildPanel.cs
+++ b/modules/mono/editor/GodotTools/GodotTools/Build/MSBuildPanel.cs
@@ -5,28 +5,73 @@
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();
+
+#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 MenuButton _buildMenuBtn;
- private Button _errorsBtn;
- private Button _warningsBtn;
- private Button _viewLogBtn;
- private Button _openLogsFolderBtn;
+ private readonly object _pendingBuildLogTextLock = new object();
+ private string _pendingBuildLogText = string.Empty;
- private void WarningsToggled(bool pressed)
+ 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 @@ private void CleanProject()
_ = 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 tabs = new TabContainer();
+ AddChild(tabs);
- var toolBarHBox = new HBoxContainer { SizeFlagsHorizontal = SizeFlags.ExpandFill };
- AddChild(toolBarHBox);
+ var tabActions = new HBoxContainer
+ {
+ SizeFlagsVertical = SizeFlags.ExpandFill,
+ SizeFlagsHorizontal = SizeFlags.ExpandFill,
+ Alignment = BoxContainer.AlignmentMode.End,
+ };
+ tabActions.SetAnchorsAndOffsetsPreset(LayoutPreset.FullRect);
+ tabs.GetTabBar().AddChild(tabActions);
- _buildMenuBtn = new MenuButton { Text = "Build", Icon = GetThemeIcon("BuildCSharp", "EditorIcons") };
- toolBarHBox.AddChild(_buildMenuBtn);
+ _buildMenuButton = new MenuButton
+ {
+ TooltipText = "Build".TTR(),
+ Flat = true,
+ };
+ tabActions.AddChild(_buildMenuButton);
- var buildMenu = _buildMenuBtn.GetPopup();
+ 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);
-
- // Horizontal spacer, push everything to the right.
- toolBarHBox.AddChild(new Control
- {
- SizeFlagsHorizontal = SizeFlags.ExpandFill,
- });
+ _outputView = new BuildOutputView();
+ tabs.AddChild(_outputView);
- _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);
+ UpdateTheme();
- BuildOutputView = new BuildOutputView();
- AddChild(BuildOutputView);
+ AddBuildEventListeners();
}
public override void _Notification(int what)
@@ -183,13 +256,49 @@ public override void _Notification(int what)
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.
+ }
}
}
diff --git a/modules/mono/editor/GodotTools/GodotTools/GodotSharpEditor.cs b/modules/mono/editor/GodotTools/GodotTools/GodotSharpEditor.cs
index e186c0302bb1..48e654c2866f 100644
--- a/modules/mono/editor/GodotTools/GodotTools/GodotSharpEditor.cs
+++ b/modules/mono/editor/GodotTools/GodotTools/GodotSharpEditor.cs
@@ -30,6 +30,7 @@ public static class Settings
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 @@ private void ApplyNecessaryChangesToSolution()
private void BuildStateChanged()
{
if (_bottomPanelBtn != null)
- _bottomPanelBtn.Icon = MSBuildPanel.BuildOutputView.BuildStateIcon;
+ _bottomPanelBtn.Icon = MSBuildPanel.GetBuildStateIcon();
}
public override void _EnablePlugin()
@@ -489,8 +490,7 @@ public override void _EnablePlugin()
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 @@ public override void _EnablePlugin()
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 @@ public override void _EnablePlugin()
["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;