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