|
@@ -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");
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|