From 7610ffaf381e76bb45ff2d82ea4a9f6437f038d1 Mon Sep 17 00:00:00 2001 From: Neil Brommer Date: Wed, 2 Aug 2023 16:30:24 -0700 Subject: [PATCH] Add a popover with a list of diagnostics to the diagnostics toolbar buttons --- Cauldron.Macos/Cauldron.Macos.csproj | 5 + .../DiagnosticsPopoverController.cs | 69 +++++ .../DiagnosticsPopoverController.designer.cs | 26 ++ Cauldron.Macos/Main.storyboard | 128 +++++++++- Cauldron.Macos/MainWindow.cs | 46 ++-- Cauldron.Macos/MainWindow.designer.cs | 19 +- .../SourceList/SourceListDataSource.cs | 96 +++++++ .../SourceList/SourceListDelegate.cs | 112 ++++++++ Cauldron.Macos/SourceList/SourceListItem.cs | 241 ++++++++++++++++++ Cauldron.Macos/SourceList/SourceListView.cs | 69 +++++ 10 files changed, 769 insertions(+), 42 deletions(-) create mode 100644 Cauldron.Macos/DiagnosticsPopoverController.cs create mode 100644 Cauldron.Macos/DiagnosticsPopoverController.designer.cs create mode 100644 Cauldron.Macos/SourceList/SourceListDataSource.cs create mode 100644 Cauldron.Macos/SourceList/SourceListDelegate.cs create mode 100644 Cauldron.Macos/SourceList/SourceListItem.cs create mode 100644 Cauldron.Macos/SourceList/SourceListView.cs diff --git a/Cauldron.Macos/Cauldron.Macos.csproj b/Cauldron.Macos/Cauldron.Macos.csproj index b6448b5..deae7bc 100644 --- a/Cauldron.Macos/Cauldron.Macos.csproj +++ b/Cauldron.Macos/Cauldron.Macos.csproj @@ -44,6 +44,9 @@ AppDelegate.cs + + DiagnosticsPopoverController.cs + @@ -56,10 +59,12 @@ + + diff --git a/Cauldron.Macos/DiagnosticsPopoverController.cs b/Cauldron.Macos/DiagnosticsPopoverController.cs new file mode 100644 index 0000000..3dadfab --- /dev/null +++ b/Cauldron.Macos/DiagnosticsPopoverController.cs @@ -0,0 +1,69 @@ +using AppKit; +using Microsoft.CodeAnalysis; +using System.Collections.Immutable; +using System.Linq; +using Cauldron.Macos.SourceList; +using Foundation; + +namespace Cauldron.Macos +{ + public partial class DiagnosticsPopoverController : NSViewController + { + public DiagnosticSeverity Severity { get; set; } + + public DiagnosticsPopoverController (ObjCRuntime.NativeHandle handle) : base (handle) { } + + public override void ViewWillAppear() + { + base.ViewWillAppear(); + + // Build the list of diagnostics info + + MainWindow window = NSApplication.SharedApplication.KeyWindow.WindowController + as MainWindow; + + ImmutableArray diagnostics = window.Diagnostics; + + this.DiagnosticsOutlineView.Initialize(); + + SourceListItem errors = new("Errors") + { + IsHeader = true + }; + SourceListItem warnings = new("Warnings") + { + IsHeader = true + }; + SourceListItem infos = new("Information") + { + IsHeader = true + }; + + foreach (var diagnostic in diagnostics) + { + SourceListItem item = new($"{diagnostic.Id} {diagnostic.GetMessage()}\n{diagnostic.Location}", "", + () => + { + window.ScriptEditorTextBox.SetSelectedRange( + new NSRange(diagnostic.Location.SourceSpan.Start, diagnostic.Location.SourceSpan.End)); + this.DismissController(this); + }); + + if (diagnostic.Severity == DiagnosticSeverity.Error) + errors.AddItem(item); + else if (diagnostic.Severity == DiagnosticSeverity.Warning) + warnings.AddItem(item); + else if (diagnostic.Severity == DiagnosticSeverity.Info) + infos.AddItem(item); + } + + this.DiagnosticsOutlineView.AddItem(errors); + this.DiagnosticsOutlineView.AddItem(warnings); + this.DiagnosticsOutlineView.AddItem(infos); + + this.DiagnosticsOutlineView.ReloadData(); + this.DiagnosticsOutlineView.ExpandItem(null, true); + this.DiagnosticsOutlineView.UsesAutomaticRowHeights = true; + } + } +} diff --git a/Cauldron.Macos/DiagnosticsPopoverController.designer.cs b/Cauldron.Macos/DiagnosticsPopoverController.designer.cs new file mode 100644 index 0000000..f2cb638 --- /dev/null +++ b/Cauldron.Macos/DiagnosticsPopoverController.designer.cs @@ -0,0 +1,26 @@ +// WARNING +// +// This file has been generated automatically by Visual Studio to store outlets and +// actions made in the UI designer. If it is removed, they will be lost. +// Manual changes to this file may not be handled correctly. +// +using Foundation; +using System.CodeDom.Compiler; + +namespace Cauldron.Macos +{ + [Register ("DiagnosticsPopoverController")] + partial class DiagnosticsPopoverController + { + [Outlet] + Cauldron.Macos.SourceList.SourceListView DiagnosticsOutlineView { get; set; } + + void ReleaseDesignerOutlets () + { + if (DiagnosticsOutlineView != null) { + DiagnosticsOutlineView.Dispose (); + DiagnosticsOutlineView = null; + } + } + } +} diff --git a/Cauldron.Macos/Main.storyboard b/Cauldron.Macos/Main.storyboard index d998ed9..0634e53 100644 --- a/Cauldron.Macos/Main.storyboard +++ b/Cauldron.Macos/Main.storyboard @@ -411,6 +411,104 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -435,25 +533,30 @@ - + - + + + - + - + - + + + + @@ -478,7 +581,7 @@ - + @@ -531,18 +634,18 @@ - + - + - + - + @@ -585,7 +688,7 @@ - + @@ -599,7 +702,7 @@ - + @@ -638,6 +741,7 @@ + diff --git a/Cauldron.Macos/MainWindow.cs b/Cauldron.Macos/MainWindow.cs index eb02b26..b5ec0d0 100644 --- a/Cauldron.Macos/MainWindow.cs +++ b/Cauldron.Macos/MainWindow.cs @@ -55,6 +55,7 @@ public partial class MainWindow : NSWindowController #region Shared properties public CancellationTokenSource ScriptCancellationTokenSource { get; set; } + public ImmutableArray Diagnostics { get; set; } = ImmutableArray.Empty; #endregion @@ -92,6 +93,24 @@ public partial class MainWindow : NSWindowController this.SetDocumentEdited(this.ScriptDocument.IsDocumentEdited); } + public override void PrepareForSegue(NSStoryboardSegue segue, NSObject sender) + { + base.PrepareForSegue(segue, sender); + + if (sender is NSSegmentedControl segmentedControl + && segmentedControl.Identifier == "DiagnosticsButtons" + && segue.DestinationController is DiagnosticsPopoverController diagPopover) + { + diagPopover.Severity = segmentedControl.SelectedSegment switch + { + 0 => DiagnosticSeverity.Info, + 1 => DiagnosticSeverity.Warning, + 2 => DiagnosticSeverity.Error, + _ => DiagnosticSeverity.Info + }; + } + } + public void UpdateDocument(object sender, EventArgs args) { this.ScriptDocument.ScriptText = new NSString(this.ScriptText); @@ -113,13 +132,10 @@ public partial class MainWindow : NSWindowController this.ScriptCancellationTokenSource?.Cancel(); } - partial void NewTabMenuItemClicked(AppKit.NSMenuItem sender) - { - this.CreateNewTab(); - } - public void UpdateScriptDiagnostics(ImmutableArray diagnostics) { + this.Diagnostics = diagnostics; + ImmutableList infoDiagnostics = diagnostics .Where(d => d.Severity == DiagnosticSeverity.Info) .ToImmutableList(); @@ -131,9 +147,15 @@ public partial class MainWindow : NSWindowController .ToImmutableList(); this.DiagnosticsToolbarGroup.SetLabel(infoDiagnostics.Count.ToString(), 0); - this.DiagnosticsToolbarGroup.SetLabel(warningDiagnostics.Count.ToString(), 1); - this.DiagnosticsToolbarGroup.SetLabel(errorDiagnostics.Count.ToString(), 2); + this.DiagnosticsToolbarGroup.SetEnabled(infoDiagnostics.Count != 0, 0); + this.DiagnosticsToolbarGroup.SetLabel(warningDiagnostics.Count.ToString(), 1); + this.DiagnosticsToolbarGroup.SetEnabled(warningDiagnostics.Count != 0, 1); + + this.DiagnosticsToolbarGroup.SetLabel(errorDiagnostics.Count.ToString(), 2); + this.DiagnosticsToolbarGroup.SetEnabled(errorDiagnostics.Count != 0, 2); + + // Mark text in the foreach (Diagnostic diagnostic in diagnostics) { int start = diagnostic.Location.SourceSpan.Start; @@ -161,7 +183,7 @@ public partial class MainWindow : NSWindowController range); else if (diagnostic.Severity == DiagnosticSeverity.Warning) this.ScriptEditorTextBox.LayoutManager - .AddTemporaryAttribute(NSStringAttributeKey.UnderlineColor, NSColor.SystemGreen, + .AddTemporaryAttribute(NSStringAttributeKey.UnderlineColor, NSColor.SystemYellow, range); else if (diagnostic.Severity == DiagnosticSeverity.Info) this.ScriptEditorTextBox.LayoutManager @@ -181,12 +203,4 @@ public partial class MainWindow : NSWindowController this.RunScriptToolbarButton.Enabled = true; } } - - public void CreateNewTab() - { - MainWindow newWindow = this.Storyboard.InstantiateInitialController() - as MainWindow; - this.Window.AddTabbedWindow(newWindow.Window, NSWindowOrderingMode.Above); - this.Window.SelectNextTab(this); - } } diff --git a/Cauldron.Macos/MainWindow.designer.cs b/Cauldron.Macos/MainWindow.designer.cs index f24b85d..a0090fc 100644 --- a/Cauldron.Macos/MainWindow.designer.cs +++ b/Cauldron.Macos/MainWindow.designer.cs @@ -20,27 +20,18 @@ namespace Cauldron.Macos [Action ("BtnRunScriptClicked:")] partial void BtnRunScriptClicked (AppKit.NSToolbarItem sender); - - [Action ("NewTabClicked:")] - partial void NewTabClicked (AppKit.NSToolbarItem sender); - - [Action ("NewTabMenuItemClicked:")] - partial void NewTabMenuItemClicked (AppKit.NSMenuItem sender); - - [Action ("NewTabMenuItemClicked2:")] - partial void NewTabMenuItemClicked2 (AppKit.NSMenuItem sender); void ReleaseDesignerOutlets () { - if (RunScriptToolbarButton != null) { - RunScriptToolbarButton.Dispose (); - RunScriptToolbarButton = null; - } - if (DiagnosticsToolbarGroup != null) { DiagnosticsToolbarGroup.Dispose (); DiagnosticsToolbarGroup = null; } + + if (RunScriptToolbarButton != null) { + RunScriptToolbarButton.Dispose (); + RunScriptToolbarButton = null; + } } } } diff --git a/Cauldron.Macos/SourceList/SourceListDataSource.cs b/Cauldron.Macos/SourceList/SourceListDataSource.cs new file mode 100644 index 0000000..6f827d8 --- /dev/null +++ b/Cauldron.Macos/SourceList/SourceListDataSource.cs @@ -0,0 +1,96 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using AppKit; +using Foundation; + +namespace Cauldron.Macos.SourceList +{ + public class SourceListDataSource : NSOutlineViewDataSource + { + #region Private Variables + + private SourceListView _controller; + + #endregion + + #region Public Variables + + public List Items = new(); + + #endregion + + #region Constructors + + public SourceListDataSource(SourceListView controller) + { + // Initialize + this._controller = controller; + } + + #endregion + + #region Override Properties + + public override nint GetChildrenCount(NSOutlineView outlineView, Foundation.NSObject item) + { + if (item == null) + { + return Items.Count; + } + else + { + return ((SourceListItem)item).Count; + } + } + + public override bool ItemExpandable(NSOutlineView outlineView, Foundation.NSObject item) + { + return ((SourceListItem)item).HasChildren; + } + + public override NSObject GetChild(NSOutlineView outlineView, nint childIndex, Foundation.NSObject item) + { + if (item == null) + { + return Items[(int)childIndex]; + } + else + { + return ((SourceListItem)item)[(int)childIndex]; + } + } + + public override NSObject GetObjectValue(NSOutlineView outlineView, NSTableColumn tableColumn, NSObject item) + { + return new NSString(((SourceListItem)item).Title); + } + + #endregion + + #region Internal Methods + + internal SourceListItem ItemForRow(int row) + { + int index = 0; + + // Look at each group + foreach (SourceListItem item in Items) + { + // Is the row inside this group? + if (row >= index && row <= (index + item.Count)) + { + return item[row - index - 1]; + } + + // Move index + index += item.Count + 1; + } + + // Not found + return null; + } + + #endregion + } +} \ No newline at end of file diff --git a/Cauldron.Macos/SourceList/SourceListDelegate.cs b/Cauldron.Macos/SourceList/SourceListDelegate.cs new file mode 100644 index 0000000..1f1d9ab --- /dev/null +++ b/Cauldron.Macos/SourceList/SourceListDelegate.cs @@ -0,0 +1,112 @@ +using AppKit; +using CoreGraphics; +using Foundation; + +namespace Cauldron.Macos.SourceList +{ + public class SourceListDelegate : NSOutlineViewDelegate + { + #region Private variables + + private SourceListView _controller; + + #endregion + + #region Constructors + + public SourceListDelegate(SourceListView controller) + { + this._controller = controller; + } + + #endregion + + #region Override Methods + + public override bool ShouldEditTableColumn(NSOutlineView outlineView, + NSTableColumn tableColumn, NSObject item) + { + return false; + } + + public override NSCell GetCell(NSOutlineView outlineView, NSTableColumn tableColumn, + NSObject item) + { + nint row = outlineView.RowForItem(item); + return tableColumn.DataCellForRow(row); + } + + public override bool IsGroupItem(NSOutlineView outlineView, NSObject item) + { + return ((SourceListItem)item).HasChildren; + } + + public override NSView GetView(NSOutlineView outlineView, NSTableColumn tableColumn, + NSObject item) + { + NSTableCellView view; + + // Is this a group item? + if (((SourceListItem)item).IsHeader) + { + view = (NSTableCellView)outlineView.MakeView("HeaderCell", this); + } + else + { + view = (NSTableCellView)outlineView.MakeView("DataCell", this); + view.ImageView.Image = ((SourceListItem)item).Icon; + view.TextField.LineBreakMode = NSLineBreakMode.CharWrapping; + view.TextField.UsesSingleLineMode = false; + view.TextField.MaximumNumberOfLines = 0; + } + + view.TextField.StringValue = ((SourceListItem)item).Title; + view.TextField.SetBoundsSize(CalculateTextFieldHeight(view)); + + return view; + } + + public override bool ShouldSelectItem(NSOutlineView outlineView, NSObject item) + { + return (outlineView.GetParent(item) != null); + } + + public override void SelectionDidChange(NSNotification notification) + { + NSIndexSet selectedIndexes = _controller.SelectedRows; + + // More than one item selected? + if (selectedIndexes.Count > 1) + { + // Not handling this case + } + else + { + // Grab the item + var item = _controller.Data.ItemForRow((int)selectedIndexes.FirstIndex); + + // Was an item found? + if (item != null) + { + // Fire the clicked event for the item + item.RaiseClickedEvent(); + + // Inform caller of selection + _controller.RaiseItemSelected(item); + } + } + } + + private static CGSize CalculateTextFieldHeight(NSTableCellView cell) + { + CGRect rect = new(0, 0, cell.TextField.Bounds.Width, double.MaxValue); + NSString str = new(cell.TextField.StringValue); + CGRect bounds = str.BoundingRectWithSize(rect.Size, 0, + new NSDictionary(NSStringAttributeKey.Font, cell.TextField.Font)); + + return new CGSize(cell.TextField.Bounds.Width, bounds.Size.Height); + } + + #endregion + } +} diff --git a/Cauldron.Macos/SourceList/SourceListItem.cs b/Cauldron.Macos/SourceList/SourceListItem.cs new file mode 100644 index 0000000..a74dbf6 --- /dev/null +++ b/Cauldron.Macos/SourceList/SourceListItem.cs @@ -0,0 +1,241 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using AppKit; +using Foundation; + +namespace Cauldron.Macos.SourceList +{ + public class SourceListItem : NSObject, IEnumerator, IEnumerable + { + #region Private Properties + + private string _title; + private NSImage _icon; + private string _tag; + private bool _isHeader = false; + private List _items = new(); + + #endregion + + #region Computed Properties + + public string Title + { + get { return _title; } + set { _title = value; } + } + + public NSImage Icon + { + get { return _icon; } + set { _icon = value; } + } + + public string Tag + { + get { return _tag; } + set { _tag = value; } + } + + public bool IsHeader + { + get => this._isHeader; + set => this._isHeader = value; + } + + #endregion + + #region Indexer + + public SourceListItem this[int index] + { + get + { + return _items[index]; + } + + set + { + _items[index] = value; + } + } + + public int Count + { + get { return _items.Count; } + } + + public bool HasChildren + { + get { return (Count > 0); } + } + + #endregion + + #region Enumerable Routines + + private int _position = -1; + + public IEnumerator GetEnumerator() + { + _position = -1; + return (IEnumerator)this; + } + + public bool MoveNext() + { + _position++; + return (_position < _items.Count); + } + + public void Reset() + { _position = -1; } + + public object Current + { + get + { + try + { + return _items[_position]; + } + + catch (IndexOutOfRangeException) + { + throw new InvalidOperationException(); + } + } + } + + #endregion + + #region Constructors + + public SourceListItem() { } + + public SourceListItem(string title) + { + this._title = title; + } + + public SourceListItem(string title, string icon) + { + this._title = title; + this._icon = NSImage.ImageNamed(icon); + } + + public SourceListItem(string title, string icon, ClickedDelegate clicked) + { + this._title = title; + this._icon = NSImage.ImageNamed(icon); + this.Clicked = clicked; + } + + public SourceListItem(string title, NSImage icon) + { + this._title = title; + this._icon = icon; + } + + public SourceListItem(string title, NSImage icon, ClickedDelegate clicked) + { + this._title = title; + this._icon = icon; + this.Clicked = clicked; + } + + public SourceListItem(string title, NSImage icon, string tag) + { + this._title = title; + this._icon = icon; + this._tag = tag; + } + + public SourceListItem(string title, NSImage icon, string tag, ClickedDelegate clicked) + { + this._title = title; + this._icon = icon; + this._tag = tag; + this.Clicked = clicked; + } + + #endregion + + #region Public Methods + + public void AddItem(SourceListItem item) + { + _items.Add(item); + } + + public void AddItem(string title) + { + _items.Add(new SourceListItem(title)); + } + + public void AddItem(string title, string icon) + { + _items.Add(new SourceListItem(title, icon)); + } + + public void AddItem(string title, string icon, ClickedDelegate clicked) + { + _items.Add(new SourceListItem(title, icon, clicked)); + } + + public void AddItem(string title, NSImage icon) + { + _items.Add(new SourceListItem(title, icon)); + } + + public void AddItem(string title, NSImage icon, ClickedDelegate clicked) + { + _items.Add(new SourceListItem(title, icon, clicked)); + } + + public void AddItem(string title, NSImage icon, string tag) + { + _items.Add(new SourceListItem(title, icon, tag)); + } + + public void AddItem(string title, NSImage icon, string tag, ClickedDelegate clicked) + { + _items.Add(new SourceListItem(title, icon, tag, clicked)); + } + + public void Insert(int n, SourceListItem item) + { + _items.Insert(n, item); + } + + public void RemoveItem(SourceListItem item) + { + _items.Remove(item); + } + + public void RemoveItem(int n) + { + _items.RemoveAt(n); + } + + public void Clear() + { + _items.Clear(); + } + + #endregion + + #region Events + + public delegate void ClickedDelegate(); + public event ClickedDelegate Clicked; + + internal void RaiseClickedEvent() + { + this.Clicked?.Invoke(); + } + + #endregion + } +} \ No newline at end of file diff --git a/Cauldron.Macos/SourceList/SourceListView.cs b/Cauldron.Macos/SourceList/SourceListView.cs new file mode 100644 index 0000000..b361add --- /dev/null +++ b/Cauldron.Macos/SourceList/SourceListView.cs @@ -0,0 +1,69 @@ +using System; +using AppKit; +using Foundation; + +namespace Cauldron.Macos.SourceList +{ + [Register("SourceListView")] + public class SourceListView : NSOutlineView + { + #region Computed Properties + + public SourceListDataSource Data + { + get { return (SourceListDataSource)this.DataSource; } + } + + #endregion + + #region Constructors + + public SourceListView() { } + + public SourceListView(IntPtr handle) : base(handle) { } + + public SourceListView(NSCoder coder) : base(coder) { } + + public SourceListView(NSObjectFlag t) : base(t) { } + + public SourceListView(ObjCRuntime.NativeHandle handle) : base(handle) { } + + #endregion + + #region Override Methods + + public override void AwakeFromNib() + { + base.AwakeFromNib(); + } + + #endregion + + #region Public Methods + + public void Initialize() + { + this.DataSource = new SourceListDataSource(this); + this.Delegate = new SourceListDelegate(this); + } + + public void AddItem(SourceListItem item) + { + Data?.Items.Add(item); + } + + #endregion + + #region Events + + public delegate void ItemSelectedDelegate(SourceListItem item); + public event ItemSelectedDelegate ItemSelected; + + internal void RaiseItemSelected(SourceListItem item) + { + this.ItemSelected?.Invoke(item); + } + + #endregion + } +}