diff --git a/Bonsai.Core/Bonsai.Core.csproj b/Bonsai.Core/Bonsai.Core.csproj
index 294af70cd..8a5256b6d 100644
--- a/Bonsai.Core/Bonsai.Core.csproj
+++ b/Bonsai.Core/Bonsai.Core.csproj
@@ -6,7 +6,7 @@
Bonsai Rx Reactive Extensions
net462;netstandard2.0;net6.0
Bonsai
- 2.7.3
+ 2.8.0
diff --git a/Bonsai.Core/Expressions/AnnotationBuilder.cs b/Bonsai.Core/Expressions/AnnotationBuilder.cs
new file mode 100644
index 000000000..98a592e4d
--- /dev/null
+++ b/Bonsai.Core/Expressions/AnnotationBuilder.cs
@@ -0,0 +1,66 @@
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Linq.Expressions;
+using System.Xml;
+using System.Xml.Serialization;
+using Bonsai.Dag;
+
+namespace Bonsai.Expressions
+{
+ ///
+ /// Represents a written explanation or critical comment added to the workflow.
+ ///
+ [DefaultProperty(nameof(Name))]
+ [WorkflowElementCategory(ElementCategory.Property)]
+ [XmlType("Annotation", Namespace = Constants.XmlNamespace)]
+ [Editor("Bonsai.Design.AnnotationBuilderEditor, Bonsai.Design", typeof(ComponentEditor))]
+ [Description("Represents a written explanation or critical comment added to the workflow.")]
+ public class AnnotationBuilder : ExpressionBuilder, INamedElement, IArgumentBuilder
+ {
+ static readonly XmlDocument cdataFactory = new XmlDocument();
+ static readonly Range argumentRange = Range.Create(lowerBound: 0, upperBound: 0);
+
+ ///
+ /// Gets or sets the name of the annotation node in the workflow.
+ ///
+ [Externalizable(false)]
+ [Category(nameof(CategoryAttribute.Design))]
+ [Description("The name of the annotation node in the workflow.")]
+ public string Name { get; set; }
+
+ ///
+ /// Gets or sets the text associated with this annotation.
+ ///
+ [XmlIgnore]
+ [Externalizable(false)]
+ [Category(nameof(CategoryAttribute.Design))]
+ [Description("The text associated with this annotation.")]
+ public string Text { get; set; }
+
+ ///
+ /// Gets or sets a CDATA section representing the annotation for serialization.
+ ///
+ [Browsable(false)]
+ [XmlElement(nameof(Text))]
+ public XmlCDataSection TextCData
+ {
+ get { return cdataFactory.CreateCDataSection(Text); }
+ set { Text = value?.Data; }
+ }
+
+ ///
+ public override Range ArgumentRange => argumentRange;
+
+ ///
+ public override Expression Build(IEnumerable arguments)
+ {
+ return EmptyExpression.Instance;
+ }
+
+ bool IArgumentBuilder.BuildArgument(Expression source, Edge successor, out Expression argument)
+ {
+ argument = source;
+ return false;
+ }
+ }
+}
diff --git a/Bonsai.Design/AnnotationBuilderEditor.cs b/Bonsai.Design/AnnotationBuilderEditor.cs
new file mode 100644
index 000000000..4fcbd41ad
--- /dev/null
+++ b/Bonsai.Design/AnnotationBuilderEditor.cs
@@ -0,0 +1,35 @@
+using System;
+using System.ComponentModel;
+using System.Windows.Forms;
+using Bonsai.Expressions;
+
+namespace Bonsai.Design
+{
+ ///
+ /// Provides a user interface editor that displays a dialog box for editing
+ /// a workflow annotation.
+ ///
+ public class AnnotationBuilderEditor : WorkflowComponentEditor
+ {
+ ///
+ public override bool EditComponent(ITypeDescriptorContext context, object component, IServiceProvider provider, IWin32Window owner)
+ {
+ if (provider != null)
+ {
+ var editorState = (IWorkflowEditorState)provider.GetService(typeof(IWorkflowEditorState));
+ if (editorState != null && !editorState.WorkflowRunning && component is AnnotationBuilder annotationBuilder)
+ {
+ using var editorDialog = new AnnotationBuilderEditorDialog();
+ editorDialog.Annotation = annotationBuilder.Text;
+ if (editorDialog.ShowDialog(owner) == DialogResult.OK)
+ {
+ annotationBuilder.Text = editorDialog.Annotation;
+ }
+ return true;
+ }
+ }
+
+ return false;
+ }
+ }
+}
diff --git a/Bonsai.Design/AnnotationBuilderEditorDialog.Designer.cs b/Bonsai.Design/AnnotationBuilderEditorDialog.Designer.cs
new file mode 100644
index 000000000..6c635165a
--- /dev/null
+++ b/Bonsai.Design/AnnotationBuilderEditorDialog.Designer.cs
@@ -0,0 +1,96 @@
+namespace Bonsai.Design
+{
+ partial class AnnotationBuilderEditorDialog
+ {
+ ///
+ /// Required designer variable.
+ ///
+ private System.ComponentModel.IContainer components = null;
+
+ ///
+ /// Clean up any resources being used.
+ ///
+ /// true if managed resources should be disposed; otherwise, false.
+ protected override void Dispose(bool disposing)
+ {
+ if (disposing && (components != null))
+ {
+ components.Dispose();
+ }
+ base.Dispose(disposing);
+ }
+
+ #region Windows Form Designer generated code
+
+ ///
+ /// Required method for Designer support - do not modify
+ /// the contents of this method with the code editor.
+ ///
+ private void InitializeComponent()
+ {
+ this.scintilla = new ScintillaNET.Scintilla();
+ this.okButton = new System.Windows.Forms.Button();
+ this.cancelButton = new System.Windows.Forms.Button();
+ this.SuspendLayout();
+ //
+ // scintilla
+ //
+ this.scintilla.Anchor = ((System.Windows.Forms.AnchorStyles)((((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom)
+ | System.Windows.Forms.AnchorStyles.Left)
+ | System.Windows.Forms.AnchorStyles.Right)));
+ this.scintilla.Location = new System.Drawing.Point(12, 12);
+ this.scintilla.Name = "scintilla";
+ this.scintilla.Size = new System.Drawing.Size(600, 229);
+ this.scintilla.TabIndex = 3;
+ this.scintilla.TabWidth = 2;
+ this.scintilla.UseTabs = false;
+ this.scintilla.WrapMode = ScintillaNET.WrapMode.Word;
+ this.scintilla.KeyDown += new System.Windows.Forms.KeyEventHandler(this.scintilla_KeyDown);
+ this.scintilla.TextChanged += new System.EventHandler(this.scintilla_TextChanged);
+ //
+ // okButton
+ //
+ this.okButton.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right)));
+ this.okButton.DialogResult = System.Windows.Forms.DialogResult.OK;
+ this.okButton.Location = new System.Drawing.Point(456, 247);
+ this.okButton.Name = "okButton";
+ this.okButton.Size = new System.Drawing.Size(75, 23);
+ this.okButton.TabIndex = 1;
+ this.okButton.Text = "OK";
+ this.okButton.UseVisualStyleBackColor = true;
+ //
+ // cancelButton
+ //
+ this.cancelButton.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right)));
+ this.cancelButton.DialogResult = System.Windows.Forms.DialogResult.Cancel;
+ this.cancelButton.Location = new System.Drawing.Point(537, 247);
+ this.cancelButton.Name = "cancelButton";
+ this.cancelButton.Size = new System.Drawing.Size(75, 23);
+ this.cancelButton.TabIndex = 2;
+ this.cancelButton.Text = "Cancel";
+ this.cancelButton.UseVisualStyleBackColor = true;
+ //
+ // ExpressionScriptEditorDialog
+ //
+ this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F);
+ this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
+ this.ClientSize = new System.Drawing.Size(624, 282);
+ this.Controls.Add(this.cancelButton);
+ this.Controls.Add(this.okButton);
+ this.Controls.Add(this.scintilla);
+ this.KeyPreview = true;
+ this.MinimumSize = new System.Drawing.Size(640, 320);
+ this.Name = "AnnotationBuilderEditorDialog";
+ this.ShowIcon = false;
+ this.Text = "Annotation";
+ this.ResumeLayout(false);
+
+ }
+
+ #endregion
+
+ private ScintillaNET.Scintilla scintilla;
+ private System.Windows.Forms.Button okButton;
+ private System.Windows.Forms.Button cancelButton;
+ }
+}
diff --git a/Bonsai.Design/AnnotationBuilderEditorDialog.cs b/Bonsai.Design/AnnotationBuilderEditorDialog.cs
new file mode 100644
index 000000000..5a9d8037c
--- /dev/null
+++ b/Bonsai.Design/AnnotationBuilderEditorDialog.cs
@@ -0,0 +1,88 @@
+using ScintillaNET;
+using System;
+using System.Drawing;
+using System.Windows.Forms;
+
+namespace Bonsai.Design
+{
+ internal partial class AnnotationBuilderEditorDialog : Form
+ {
+ public AnnotationBuilderEditorDialog()
+ {
+ InitializeComponent();
+ scintilla.StyleResetDefault();
+ scintilla.Styles[Style.Default].Font = "Consolas";
+ scintilla.Styles[Style.Default].Size = 10;
+ scintilla.StyleClearAll();
+
+ scintilla.CaretLineBackColor = ColorTranslator.FromHtml("#feefff");
+ scintilla.Styles[Style.Markdown.Default].ForeColor = Color.Black;
+ scintilla.Styles[Style.Markdown.Link].Underline = true;
+ scintilla.Styles[Style.Markdown.Em1].Italic = true;
+ scintilla.Styles[Style.Markdown.Em2].Italic = true;
+ scintilla.Styles[Style.Markdown.Strong1].Bold = true;
+ scintilla.Styles[Style.Markdown.Strong2].Bold = true;
+ scintilla.Styles[Style.Markdown.Header1].Bold = true;
+ scintilla.Styles[Style.Markdown.Header2].Bold = true;
+ scintilla.Styles[Style.Markdown.Header3].Bold = true;
+ scintilla.Styles[Style.Markdown.Header4].Bold = true;
+ scintilla.Styles[Style.Markdown.Header5].Bold = true;
+ scintilla.Styles[Style.Markdown.Header6].Bold = true;
+ scintilla.Styles[Style.Markdown.Strong1].ForeColor = ColorTranslator.FromHtml("#2b91af");
+ scintilla.Styles[Style.Markdown.Strong2].ForeColor = ColorTranslator.FromHtml("#2b91af");
+ scintilla.Styles[Style.Markdown.Header1].ForeColor = ColorTranslator.FromHtml("#2b91af");
+ scintilla.Styles[Style.Markdown.Header2].ForeColor = ColorTranslator.FromHtml("#2b91af");
+ scintilla.Styles[Style.Markdown.Header3].ForeColor = ColorTranslator.FromHtml("#2b91af");
+ scintilla.Styles[Style.Markdown.Header4].ForeColor = ColorTranslator.FromHtml("#2b91af");
+ scintilla.Styles[Style.Markdown.Header5].ForeColor = ColorTranslator.FromHtml("#2b91af");
+ scintilla.Styles[Style.Markdown.Header6].ForeColor = ColorTranslator.FromHtml("#2b91af");
+ scintilla.Styles[Style.Markdown.UListItem].ForeColor = ColorTranslator.FromHtml("#2b91af");
+ scintilla.Styles[Style.Markdown.OListItem].ForeColor = ColorTranslator.FromHtml("#2b91af");
+ scintilla.Styles[Style.Markdown.HRule].ForeColor = ColorTranslator.FromHtml("#2b91af");
+ scintilla.Styles[Style.Markdown.Code].ForeColor = ColorTranslator.FromHtml("#a31515");
+ scintilla.Styles[Style.Markdown.Code2].ForeColor = ColorTranslator.FromHtml("#a31515");
+ scintilla.Styles[Style.Markdown.CodeBk].ForeColor = ColorTranslator.FromHtml("#a31515");
+ scintilla.Styles[Style.Markdown.BlockQuote].ForeColor = ColorTranslator.FromHtml("#a31515");
+ scintilla.Lexer = Lexer.Markdown;
+ }
+
+ public string Annotation { get; set; }
+
+ protected override void OnLoad(EventArgs e)
+ {
+ scintilla.Text = Annotation;
+ scintilla.EmptyUndoBuffer();
+ if (Owner != null)
+ {
+ Icon = Owner.Icon;
+ ShowIcon = true;
+ }
+
+ base.OnLoad(e);
+ }
+
+ protected override void OnKeyDown(KeyEventArgs e)
+ {
+ if (e.KeyCode == Keys.Escape && !e.Handled)
+ {
+ Close();
+ e.Handled = true;
+ }
+
+ base.OnKeyDown(e);
+ }
+
+ private void scintilla_KeyDown(object sender, KeyEventArgs e)
+ {
+ if (e.KeyCode == Keys.Enter && e.Modifiers == Keys.Control)
+ {
+ okButton.PerformClick();
+ }
+ }
+
+ private void scintilla_TextChanged(object sender, EventArgs e)
+ {
+ Annotation = scintilla.Text;
+ }
+ }
+}
diff --git a/Bonsai.Design/AnnotationBuilderEditorDialog.resx b/Bonsai.Design/AnnotationBuilderEditorDialog.resx
new file mode 100644
index 000000000..1af7de150
--- /dev/null
+++ b/Bonsai.Design/AnnotationBuilderEditorDialog.resx
@@ -0,0 +1,120 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ text/microsoft-resx
+
+
+ 2.0
+
+
+ System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
\ No newline at end of file
diff --git a/Bonsai.Design/Bonsai.Design.csproj b/Bonsai.Design/Bonsai.Design.csproj
index 11a3c682e..7ea1129d8 100644
--- a/Bonsai.Design/Bonsai.Design.csproj
+++ b/Bonsai.Design/Bonsai.Design.csproj
@@ -6,8 +6,11 @@
Bonsai Design Rx Reactive Extensions
true
net462
- 2.7.1
+ 2.8.0
+
+
+
diff --git a/Bonsai.Editor/Bonsai.Editor.csproj b/Bonsai.Editor/Bonsai.Editor.csproj
index 031451a74..7aac65a60 100644
--- a/Bonsai.Editor/Bonsai.Editor.csproj
+++ b/Bonsai.Editor/Bonsai.Editor.csproj
@@ -7,7 +7,7 @@
true
false
net472
- 2.7.2
+ 2.8.0
@@ -15,6 +15,8 @@
+
+
diff --git a/Bonsai.Editor/ExportHelper.cs b/Bonsai.Editor/ExportHelper.cs
index fb81ead80..d8be25913 100644
--- a/Bonsai.Editor/ExportHelper.cs
+++ b/Bonsai.Editor/ExportHelper.cs
@@ -19,7 +19,7 @@ static GraphViewControl CreateGraphView(
SvgRendererFactory iconRenderer,
Image graphicsProvider)
{
- var selectedLayout = workflow.ConnectedComponentLayering().ToList();
+ var selectedLayout = workflow.ConnectedComponentLayering();
return new GraphViewControl
{
GraphicsProvider = graphicsProvider,
diff --git a/Bonsai.Editor/GraphModel/GraphNode.cs b/Bonsai.Editor/GraphModel/GraphNode.cs
index 46b71c181..dcfe71c63 100644
--- a/Bonsai.Editor/GraphModel/GraphNode.cs
+++ b/Bonsai.Editor/GraphModel/GraphNode.cs
@@ -26,6 +26,7 @@ public GraphNode(ExpressionBuilder value, int layer, IEnumerable succ
var elementCategoryAttribute = (WorkflowElementCategoryAttribute)elementAttributes[typeof(WorkflowElementCategoryAttribute)];
var obsolete = (ObsoleteAttribute)elementAttributes[typeof(ObsoleteAttribute)] != null;
if (expressionBuilder is DisableBuilder) Flags |= NodeFlags.Disabled;
+ if (expressionBuilder is AnnotationBuilder) Flags |= NodeFlags.Annotation;
var workflowElement = ExpressionBuilder.GetWorkflowElement(expressionBuilder);
if (workflowElement != expressionBuilder)
@@ -42,7 +43,7 @@ public GraphNode(ExpressionBuilder value, int layer, IEnumerable succ
if (obsolete) Flags |= NodeFlags.Obsolete;
Category = elementCategoryAttribute.Category;
- BuildDependency = expressionBuilder.IsBuildDependency();
+ IsBuildDependency = expressionBuilder.IsBuildDependency();
Icon = new ElementIcon(workflowElement);
if (workflowElement is IWorkflowExpressionBuilder)
{
@@ -64,7 +65,7 @@ void InitializeDummySuccessors()
{
if (successor.Node.Value == null)
{
- successor.Node.BuildDependency = BuildDependency;
+ successor.Node.IsBuildDependency = IsBuildDependency;
successor.Node.InitializeDummySuccessors();
}
}
@@ -131,7 +132,9 @@ public ElementCategory? NestedCategory
public ElementIcon Icon { get; private set; }
- public bool BuildDependency { get; private set; }
+ public bool IsBuildDependency { get; private set; }
+
+ public bool IsAnnotation => (Flags & NodeFlags.Annotation) != 0;
public string Text
{
@@ -168,7 +171,8 @@ enum NodeFlags
Obsolete = 0x2,
Disabled = 0x4,
NestedScope = 0x8,
- NestedGroup = 0x10
+ NestedGroup = 0x10,
+ Annotation = 0x20
}
static class CategoryColors
diff --git a/Bonsai.Editor/GraphModel/LayeredGraphExtensions.cs b/Bonsai.Editor/GraphModel/LayeredGraphExtensions.cs
index 35ade37b3..123e6e876 100644
--- a/Bonsai.Editor/GraphModel/LayeredGraphExtensions.cs
+++ b/Bonsai.Editor/GraphModel/LayeredGraphExtensions.cs
@@ -285,11 +285,13 @@ public static IEnumerable ConnectedComponentLayering(this Exp
.ToList();
if (component.Count == 1)
{
- if (singletonLayer == null) singletonLayer = layeredComponent[0];
+ var layer = layeredComponent[0];
+ if (singletonLayer == null) singletonLayer = layer;
+ else if (!layer[0].IsAnnotation) singletonLayer.Add(layer[0]);
else
{
- var layer = layeredComponent[0];
- singletonLayer.Add(layer[0]);
+ MergeSingletonComponentLayers(ref singletonLayer, layers, ref layerOffset);
+ singletonLayer = layer;
}
continue;
}
diff --git a/Bonsai.Editor/GraphModel/WorkflowEditor.cs b/Bonsai.Editor/GraphModel/WorkflowEditor.cs
index e1a7b2dfc..e26368dc1 100644
--- a/Bonsai.Editor/GraphModel/WorkflowEditor.cs
+++ b/Bonsai.Editor/GraphModel/WorkflowEditor.cs
@@ -256,7 +256,7 @@ GraphCommand GetInsertGraphNodeCommands(
index++;
}
}
- else if (!validate || sourceNode.Value.ArgumentRange.UpperBound > 0)
+ else if (!validate || sourceNode.Value.ArgumentRange.UpperBound > 0 || targetNodes.All(node => node.Value.IsBuildDependency()))
{
var index = 0;
foreach (var node in targetNodes)
@@ -884,8 +884,11 @@ public void InsertGraphNode(string typeName, ElementCategory elementCategory, Cr
builder = CreateBuilder(typeName, elementCategory, group);
ConfigureBuilder(builder, selectedNode, arguments);
- var externalizedMapping = typeName == typeof(ExternalizedMappingBuilder).AssemblyQualifiedName;
- if (externalizedMapping) nodeType = CreateGraphNodeType.Predecessor;
+ if (typeName == typeof(ExternalizedMappingBuilder).AssemblyQualifiedName ||
+ typeName == typeof(AnnotationBuilder).AssemblyQualifiedName)
+ {
+ nodeType = CreateGraphNodeType.Predecessor;
+ }
var commands = GetCreateGraphNodeCommands(builder, selectedNodes.Select(GetGraphNodeTag), nodeType, branch);
commandExecutor.BeginCompositeCommand();
commandExecutor.Execute(EmptyAction, commands.Item2.Undo);
@@ -1015,7 +1018,6 @@ Tuple GetCreateGraphNodeCommands(
ConfigureWorkflowBuilder(workflowBuilder, targetNodes, workflow, nodeType);
}
- var validateInsert = validate && !(nodeType == CreateGraphNodeType.Predecessor && builder.IsBuildDependency());
if (validate && !branch && targetNodes.Length > 1 &&
((nodeType == CreateGraphNodeType.Successor && targetNodes.Skip(1).Any(node => targetNodes[0].DepthFirstSearch().Contains(node))) ||
(nodeType == CreateGraphNodeType.Predecessor && targetNodes.Skip(1).Any(node => node.DepthFirstSearch().Contains(targetNodes[0])))))
@@ -1023,6 +1025,15 @@ Tuple GetCreateGraphNodeCommands(
throw new InvalidOperationException(Resources.InsertValidation_Error);
}
+ var validateInsert = validate && !(
+ nodeType == CreateGraphNodeType.Predecessor &&
+ builder.IsBuildDependency() &&
+ !targetNodes.Any(node => ExpressionBuilder.Unwrap(node.Value) switch
+ {
+ AnnotationBuilder or ExternalizedMappingBuilder => true,
+ _ => false
+ }));
+
var updateGraphLayout = CreateUpdateGraphLayoutDelegate();
var updateSelectedNode = CreateUpdateSelectionDelegate(builder);
var insertCommands = GetInsertGraphNodeCommands(inspectNode, inspectNode, targetNodes, nodeType, branch, validateInsert);
diff --git a/Bonsai.Editor/GraphView/GraphViewControl.cs b/Bonsai.Editor/GraphView/GraphViewControl.cs
index 19a342a55..2bbfd61fd 100644
--- a/Bonsai.Editor/GraphView/GraphViewControl.cs
+++ b/Bonsai.Editor/GraphView/GraphViewControl.cs
@@ -1121,7 +1121,7 @@ private void DrawDummyNode(IGraphics graphics, LayoutNode layout, Size offset)
if (layout.Node.Tag != null)
{
graphics.DrawLine(
- layout.Node.BuildDependency ? DashPen : SolidPen,
+ layout.Node.IsBuildDependency ? DashPen : SolidPen,
PointF.Add(layout.EntryPoint, offset),
PointF.Add(layout.ExitPoint, offset));
}
@@ -1135,7 +1135,7 @@ private void DrawEdges(IGraphics graphics, Size offset)
{
var successorLayout = layoutNodes[successor.Node];
graphics.DrawLine(
- layout.Node.BuildDependency ? DashPen : SolidPen,
+ layout.Node.IsBuildDependency ? DashPen : SolidPen,
PointF.Add(layout.ExitPoint, offset),
PointF.Add(successorLayout.EntryPoint, offset));
}
diff --git a/Bonsai.Editor/GraphView/MarkdownConvert.cs b/Bonsai.Editor/GraphView/MarkdownConvert.cs
new file mode 100644
index 000000000..7d67298cc
--- /dev/null
+++ b/Bonsai.Editor/GraphView/MarkdownConvert.cs
@@ -0,0 +1,34 @@
+using System;
+using System.Drawing;
+using System.IO;
+using Markdig;
+using Markdig.Renderers;
+
+namespace Bonsai.Editor.GraphView
+{
+ class MarkdownConvert
+ {
+ public const string DefaultUrl = "path.localhost";
+
+ public static string ToHtml(Font font, string text)
+ {
+ if (!string.IsNullOrEmpty(text))
+ {
+ using var writer = new StringWriter();
+ var pipeline = new MarkdownPipelineBuilder().UseAdvancedExtensions().Build();
+ var renderer = new HtmlRenderer(writer);
+ renderer.BaseUrl = new Uri($"https://{DefaultUrl}/");
+ pipeline.Setup(renderer);
+
+ var document = Markdown.Parse(text, pipeline);
+ renderer.Render(document);
+ writer.Flush();
+
+ var html = writer.ToString();
+ return $@"{html}
";
+ }
+
+ return string.Empty;
+ }
+ }
+}
diff --git a/Bonsai.Editor/GraphView/WorkflowEditorControl.Designer.cs b/Bonsai.Editor/GraphView/WorkflowEditorControl.Designer.cs
index daafa675c..c1aa4549f 100644
--- a/Bonsai.Editor/GraphView/WorkflowEditorControl.Designer.cs
+++ b/Bonsai.Editor/GraphView/WorkflowEditorControl.Designer.cs
@@ -32,14 +32,22 @@ private void InitializeComponent()
this.tabContextMenuStrip = new System.Windows.Forms.ContextMenuStrip(this.components);
this.closeToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
this.closeAllToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
+ this.splitContainer = new System.Windows.Forms.SplitContainer();
+ this.webView = new Microsoft.Web.WebView2.WinForms.WebView2();
this.tabControl = new Bonsai.Editor.GraphView.WorkflowEditorTabControl();
this.workflowTabPage = new System.Windows.Forms.TabPage();
this.tabContextMenuStrip.SuspendLayout();
+ ((System.ComponentModel.ISupportInitialize)(this.splitContainer)).BeginInit();
+ this.splitContainer.Panel1.SuspendLayout();
+ this.splitContainer.Panel2.SuspendLayout();
+ this.splitContainer.SuspendLayout();
+ ((System.ComponentModel.ISupportInitialize)(this.webView)).BeginInit();
this.tabControl.SuspendLayout();
this.SuspendLayout();
//
// tabContextMenuStrip
//
+ this.tabContextMenuStrip.ImageScalingSize = new System.Drawing.Size(24, 24);
this.tabContextMenuStrip.Items.AddRange(new System.Windows.Forms.ToolStripItem[] {
this.closeToolStripMenuItem,
this.closeAllToolStripMenuItem});
@@ -62,8 +70,44 @@ private void InitializeComponent()
this.closeAllToolStripMenuItem.Text = "Close All";
this.closeAllToolStripMenuItem.Click += new System.EventHandler(this.closeAllToolStripMenuItem_Click);
//
+ // splitContainer
+ //
+ this.splitContainer.Dock = System.Windows.Forms.DockStyle.Fill;
+ this.splitContainer.Location = new System.Drawing.Point(0, 0);
+ this.splitContainer.Margin = new System.Windows.Forms.Padding(2);
+ this.splitContainer.Name = "splitContainer";
+ this.splitContainer.Orientation = System.Windows.Forms.Orientation.Horizontal;
+ //
+ // splitContainer.Panel1
+ //
+ this.splitContainer.Panel1.Controls.Add(this.tabControl);
+ //
+ // splitContainer.Panel2
+ //
+ this.splitContainer.Panel2.Controls.Add(this.webView);
+ this.splitContainer.Panel2Collapsed = true;
+ this.splitContainer.Size = new System.Drawing.Size(300, 200);
+ this.splitContainer.SplitterDistance = 100;
+ this.splitContainer.SplitterWidth = 3;
+ this.splitContainer.TabIndex = 1;
+ //
+ // webView
+ //
+ this.webView.AllowExternalDrop = true;
+ this.webView.CreationProperties = null;
+ this.webView.DefaultBackgroundColor = System.Drawing.Color.White;
+ this.webView.Dock = System.Windows.Forms.DockStyle.Fill;
+ this.webView.Location = new System.Drawing.Point(0, 0);
+ this.webView.Margin = new System.Windows.Forms.Padding(2);
+ this.webView.Name = "webView";
+ this.webView.Size = new System.Drawing.Size(300, 97);
+ this.webView.TabIndex = 0;
+ this.webView.ZoomFactor = 1D;
+ this.webView.KeyDown += new System.Windows.Forms.KeyEventHandler(this.webView_KeyDown);
+ //
// tabControl
//
+ this.tabControl.AdjustRectangle = new System.Windows.Forms.Padding(0);
this.tabControl.Controls.Add(this.workflowTabPage);
this.tabControl.Dock = System.Windows.Forms.DockStyle.Fill;
this.tabControl.Location = new System.Drawing.Point(0, 0);
@@ -72,17 +116,17 @@ private void InitializeComponent()
this.tabControl.SelectedIndex = 0;
this.tabControl.Size = new System.Drawing.Size(300, 200);
this.tabControl.TabIndex = 0;
- this.tabControl.Selected += new System.Windows.Forms.TabControlEventHandler(this.tabControl_Selected);
this.tabControl.Selecting += new System.Windows.Forms.TabControlCancelEventHandler(this.tabControl_Selecting);
+ this.tabControl.Selected += new System.Windows.Forms.TabControlEventHandler(this.tabControl_Selected);
this.tabControl.KeyDown += new System.Windows.Forms.KeyEventHandler(this.tabControl_KeyDown);
this.tabControl.MouseUp += new System.Windows.Forms.MouseEventHandler(this.tabControl_MouseUp);
//
// workflowTabPage
//
- this.workflowTabPage.Location = new System.Drawing.Point(-1, 24);
+ this.workflowTabPage.Location = new System.Drawing.Point(4, 28);
this.workflowTabPage.Name = "workflowTabPage";
this.workflowTabPage.Padding = new System.Windows.Forms.Padding(3);
- this.workflowTabPage.Size = new System.Drawing.Size(301, 176);
+ this.workflowTabPage.Size = new System.Drawing.Size(292, 168);
this.workflowTabPage.TabIndex = 0;
this.workflowTabPage.Text = "Workflow";
this.workflowTabPage.UseVisualStyleBackColor = true;
@@ -91,11 +135,16 @@ private void InitializeComponent()
//
this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F);
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
- this.Controls.Add(this.tabControl);
+ this.Controls.Add(this.splitContainer);
this.MinimumSize = new System.Drawing.Size(250, 125);
this.Name = "WorkflowEditorControl";
this.Size = new System.Drawing.Size(300, 200);
this.tabContextMenuStrip.ResumeLayout(false);
+ this.splitContainer.Panel1.ResumeLayout(false);
+ this.splitContainer.Panel2.ResumeLayout(false);
+ ((System.ComponentModel.ISupportInitialize)(this.splitContainer)).EndInit();
+ this.splitContainer.ResumeLayout(false);
+ ((System.ComponentModel.ISupportInitialize)(this.webView)).EndInit();
this.tabControl.ResumeLayout(false);
this.ResumeLayout(false);
@@ -108,5 +157,7 @@ private void InitializeComponent()
private System.Windows.Forms.ContextMenuStrip tabContextMenuStrip;
private System.Windows.Forms.ToolStripMenuItem closeToolStripMenuItem;
private System.Windows.Forms.ToolStripMenuItem closeAllToolStripMenuItem;
+ private System.Windows.Forms.SplitContainer splitContainer;
+ private Microsoft.Web.WebView2.WinForms.WebView2 webView;
}
}
diff --git a/Bonsai.Editor/GraphView/WorkflowEditorControl.cs b/Bonsai.Editor/GraphView/WorkflowEditorControl.cs
index a703a4d08..34abc3917 100644
--- a/Bonsai.Editor/GraphView/WorkflowEditorControl.cs
+++ b/Bonsai.Editor/GraphView/WorkflowEditorControl.cs
@@ -4,6 +4,8 @@
using System.Windows.Forms;
using Bonsai.Expressions;
using Bonsai.Design;
+using Microsoft.Web.WebView2.WinForms;
+using Microsoft.Web.WebView2.Core;
namespace Bonsai.Editor.GraphView
{
@@ -13,6 +15,7 @@ partial class WorkflowEditorControl : UserControl
readonly IWorkflowEditorService editorService;
readonly TabPageController workflowTab;
Padding? adjustMargin;
+ bool webViewInitialized;
public WorkflowEditorControl(IServiceProvider provider)
: this(provider, false)
@@ -21,16 +24,20 @@ public WorkflowEditorControl(IServiceProvider provider)
public WorkflowEditorControl(IServiceProvider provider, bool readOnly)
{
- if (provider == null)
- {
- throw new ArgumentNullException(nameof(provider));
- }
-
InitializeComponent();
- serviceProvider = provider;
+ serviceProvider = provider ?? throw new ArgumentNullException(nameof(provider));
editorService = (IWorkflowEditorService)provider.GetService(typeof(IWorkflowEditorService));
workflowTab = InitializeTab(workflowTabPage, readOnly, null);
InitializeTheme(workflowTabPage);
+ webView.CoreWebView2InitializationCompleted += (sender, e) =>
+ {
+ webViewInitialized = true;
+ webView.CoreWebView2.ContextMenuRequested += CoreWebView2_ContextMenuRequested;
+ webView.CoreWebView2.SetVirtualHostNameToFolderMapping(
+ MarkdownConvert.DefaultUrl,
+ Environment.CurrentDirectory,
+ CoreWebView2HostResourceAccessKind.Allow);
+ };
}
public WorkflowGraphView WorkflowGraphView
@@ -38,6 +45,21 @@ public WorkflowGraphView WorkflowGraphView
get { return workflowTab.WorkflowGraphView; }
}
+ public WebView2 WebView
+ {
+ get { return webView; }
+ }
+
+ public bool WebViewInitialized
+ {
+ get { return webViewInitialized; }
+ }
+
+ public bool WebViewCollapsed
+ {
+ get { return splitContainer.Panel2Collapsed; }
+ }
+
public VisualizerLayout VisualizerLayout
{
get { return WorkflowGraphView.VisualizerLayout; }
@@ -50,6 +72,17 @@ public ExpressionBuilderGraph Workflow
set { WorkflowGraphView.Workflow = value; }
}
+ public void ExpandWebView()
+ {
+ splitContainer.Panel2Collapsed = false;
+ }
+
+ public void CollapseWebView()
+ {
+ splitContainer.Panel2Collapsed = true;
+ webView.Tag = null;
+ }
+
public void UpdateVisualizerLayout()
{
WorkflowGraphView.UpdateVisualizerLayout();
@@ -215,6 +248,7 @@ protected override void OnFontChanged(EventArgs e)
protected override void OnLoad(EventArgs e)
{
ActivateTab(workflowTabPage);
+ webView.EnsureCoreWebView2Async();
base.OnLoad(e);
}
@@ -404,5 +438,30 @@ private void InitializeTheme(TabPage tabPage)
else adjustRectangle.Bottom = adjustRectangle.Left;
tabControl.AdjustRectangle = adjustRectangle;
}
+
+ private void CoreWebView2_ContextMenuRequested(object sender, CoreWebView2ContextMenuRequestedEventArgs e)
+ {
+ var closeMenuItem = webView.CoreWebView2.Environment.CreateContextMenuItem(
+ "Close",
+ iconStream: null,
+ CoreWebView2ContextMenuItemKind.Command);
+ closeMenuItem.CustomItemSelected += delegate { CollapseWebView(); };
+ e.MenuItems.Add(closeMenuItem);
+ }
+
+ private void webView_KeyDown(object sender, KeyEventArgs e)
+ {
+ if (ModifierKeys == Keys.Control)
+ {
+ switch (e.KeyCode)
+ {
+ case Keys.F4: CollapseWebView(); break;
+ case Keys.Back:
+ e.Handled = true;
+ ActiveTab.WorkflowGraphView.Focus();
+ break;
+ }
+ }
+ }
}
}
diff --git a/Bonsai.Editor/GraphView/WorkflowGraphView.cs b/Bonsai.Editor/GraphView/WorkflowGraphView.cs
index b09fae039..206ee7059 100644
--- a/Bonsai.Editor/GraphView/WorkflowGraphView.cs
+++ b/Bonsai.Editor/GraphView/WorkflowGraphView.cs
@@ -411,6 +411,23 @@ private bool HasDefaultEditor(ExpressionBuilder builder)
return false;
}
+ private static bool IsAnnotation(GraphNode node)
+ {
+ return node != null && node.IsAnnotation;
+ }
+
+ private void LaunchDefaultAction(GraphNode node)
+ {
+ if (!editorState.WorkflowRunning && !IsAnnotation(node) || ModifierKeys == Keys.Control)
+ {
+ LaunchDefaultEditor(node);
+ }
+ else
+ {
+ LaunchVisualizer(node);
+ }
+ }
+
private void LaunchDefaultEditor(GraphNode node)
{
var builder = WorkflowEditor.GetGraphNodeBuilder(node);
@@ -462,6 +479,17 @@ private void LaunchDefaultEditor(GraphNode node)
private void LaunchVisualizer(GraphNode node)
{
+ if (IsAnnotation(node) &&
+ EditorControl.WebViewInitialized &&
+ WorkflowEditor.GetGraphNodeBuilder(node) is AnnotationBuilder annotationBuilder)
+ {
+ var html = MarkdownConvert.ToHtml(Font, annotationBuilder.Text);
+ EditorControl.WebView.NavigateToString(html);
+ EditorControl.WebView.Tag = annotationBuilder;
+ EditorControl.ExpandWebView();
+ return;
+ }
+
var visualizerLauncher = GetVisualizerDialogLauncher(node);
if (visualizerLauncher != null)
{
@@ -786,7 +814,7 @@ private void UpdateGraphLayout()
private void UpdateGraphLayout(bool validateWorkflow)
{
- graphView.Nodes = workflow.ConnectedComponentLayering().ToList();
+ graphView.Nodes = workflow.ConnectedComponentLayering();
graphView.Invalidate();
if (validateWorkflow)
{
@@ -794,7 +822,18 @@ private void UpdateGraphLayout(bool validateWorkflow)
}
UpdateVisualizerLayout();
- if (validateWorkflow) EditorControl.SelectTab(this);
+ if (validateWorkflow)
+ {
+ EditorControl.SelectTab(this);
+ if (EditorControl.WebView.Tag is ExpressionBuilder builder)
+ {
+ if (!EditorControl.Workflow.Descendants().Contains(builder))
+ {
+ EditorControl.WebView.NavigateToString(string.Empty);
+ EditorControl.WebView.Tag = null;
+ }
+ }
+ }
UpdateSelection();
}
@@ -1056,16 +1095,9 @@ private void graphView_KeyDown(object sender, KeyEventArgs e)
LaunchDefinition(graphView.SelectedNode);
}
- if (e.KeyCode == Keys.Return && editorState.WorkflowRunning)
+ if (e.KeyCode == Keys.Return && !CanEdit)
{
- if (e.Modifiers == Keys.Control)
- {
- LaunchDefaultEditor(graphView.SelectedNode);
- }
- else
- {
- LaunchVisualizer(graphView.SelectedNode);
- }
+ LaunchDefaultAction(graphView.SelectedNode);
}
if (e.KeyCode == Keys.A && e.Modifiers == Keys.Control)
@@ -1125,7 +1157,7 @@ private void graphView_KeyDown(object sender, KeyEventArgs e)
else Editor.ConnectGraphNodes(graphView.SelectedNodes, graphView.CursorNode);
}
}
- else LaunchDefaultEditor(graphView.SelectedNode);
+ else LaunchDefaultAction(graphView.SelectedNode);
}
if (e.KeyCode == Keys.Delete)
@@ -1177,14 +1209,7 @@ private void graphView_SelectedNodeChanged(object sender, EventArgs e)
private void graphView_NodeMouseDoubleClick(object sender, GraphNodeMouseEventArgs e)
{
- if (!editorState.WorkflowRunning || Control.ModifierKeys == Keys.Control)
- {
- LaunchDefaultEditor(e.Node);
- }
- else
- {
- LaunchVisualizer(e.Node);
- }
+ LaunchDefaultAction(e.Node);
}
private void graphView_MouseDown(object sender, MouseEventArgs e)
diff --git a/Bonsai.Editor/Resources/Expressions/AnnotationBuilder.svg b/Bonsai.Editor/Resources/Expressions/AnnotationBuilder.svg
new file mode 100644
index 000000000..a42995d04
--- /dev/null
+++ b/Bonsai.Editor/Resources/Expressions/AnnotationBuilder.svg
@@ -0,0 +1,4 @@
+
+
diff --git a/Bonsai/Bonsai.csproj b/Bonsai/Bonsai.csproj
index 92ebd6765..b9b5782a4 100644
--- a/Bonsai/Bonsai.csproj
+++ b/Bonsai/Bonsai.csproj
@@ -7,7 +7,7 @@
false
true
net472
- 2.7.2
+ 2.8.0
Exe
true
..\Bonsai.Editor\Bonsai.ico
diff --git a/Directory.Build.props b/Directory.Build.props
index 6378fb027..72537679f 100644
--- a/Directory.Build.props
+++ b/Directory.Build.props
@@ -8,13 +8,13 @@
https://bonsai-rx.org/assets/images/bonsai.png
..\bin\$(Configuration)
true
+ true
true
snupkg
- true
https://github.com/bonsai-rx/bonsai.git
git
- 8.0
+ 9.0
strict
\ No newline at end of file