From 3c2a3049d64cfd96ec093ecc3ac78a49e48701a4 Mon Sep 17 00:00:00 2001 From: glopesdev Date: Wed, 26 Jun 2024 13:15:03 +0100 Subject: [PATCH 01/32] Avoid serializing default property values --- Bonsai.Editor/Layout/VisualizerDialogSettings.cs | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/Bonsai.Editor/Layout/VisualizerDialogSettings.cs b/Bonsai.Editor/Layout/VisualizerDialogSettings.cs index a6aaa0623..9b8f99795 100644 --- a/Bonsai.Editor/Layout/VisualizerDialogSettings.cs +++ b/Bonsai.Editor/Layout/VisualizerDialogSettings.cs @@ -38,9 +38,12 @@ public Rectangle Bounds // [Obsolete] public Collection Mashups { get; } = new Collection(); - public bool MashupsSpecified - { - get { return false; } - } + public bool LocationSpecified => !Location.IsEmpty; + + public bool SizeSpecified => !Size.IsEmpty; + + public bool WindowStateSpecified => WindowState != FormWindowState.Normal; + + public bool MashupsSpecified => false; } } From a98348ac1830eab1600af5a7396ac90d3def06c8 Mon Sep 17 00:00:00 2001 From: glopesdev Date: Mon, 1 Jul 2024 12:36:35 +0100 Subject: [PATCH 02/32] Add explorer tree view control and path navigation --- Bonsai.Editor/EditorForm.cs | 12 ++ Bonsai.Editor/ExplorerTreeView.cs | 107 +++++++++++ .../GraphModel/WorkflowEditorPath.cs | 100 ++++++++++ .../GraphView/WorkflowPathMouseEventArgs.cs | 16 ++ .../WorkflowPathNavigationControl.Designer.cs | 60 ++++++ .../WorkflowPathNavigationControl.cs | 175 ++++++++++++++++++ .../WorkflowPathNavigationControl.resx | 120 ++++++++++++ Bonsai.Editor/IWorkflowEditorService.cs | 2 + 8 files changed, 592 insertions(+) create mode 100644 Bonsai.Editor/ExplorerTreeView.cs create mode 100644 Bonsai.Editor/GraphModel/WorkflowEditorPath.cs create mode 100644 Bonsai.Editor/GraphView/WorkflowPathMouseEventArgs.cs create mode 100644 Bonsai.Editor/GraphView/WorkflowPathNavigationControl.Designer.cs create mode 100644 Bonsai.Editor/GraphView/WorkflowPathNavigationControl.cs create mode 100644 Bonsai.Editor/GraphView/WorkflowPathNavigationControl.resx diff --git a/Bonsai.Editor/EditorForm.cs b/Bonsai.Editor/EditorForm.cs index 48f0799af..07d253818 100644 --- a/Bonsai.Editor/EditorForm.cs +++ b/Bonsai.Editor/EditorForm.cs @@ -1582,6 +1582,13 @@ private void GetSelectionDescription(object[] selectedObjects, out string displa description = objectDescriptions.Length == 1 ? objectDescriptions[0] : string.Empty; } + private string GetProjectDisplayName() + { + return !string.IsNullOrEmpty(saveWorkflowDialog.FileName) + ? Path.GetFileNameWithoutExtension(saveWorkflowDialog.FileName) + : "Workflow"; + } + private void UpdatePropertyGrid() { var selectedObjects = selectionModel.SelectedNodes.Select(node => @@ -2494,6 +2501,11 @@ public bool DesignMode public string Name { get; set; } + public string GetProjectDisplayName() + { + return siteForm.GetProjectDisplayName(); + } + public object GetService(Type serviceType) { if (serviceType == typeof(ExpressionBuilderGraph)) diff --git a/Bonsai.Editor/ExplorerTreeView.cs b/Bonsai.Editor/ExplorerTreeView.cs new file mode 100644 index 000000000..7f174d50e --- /dev/null +++ b/Bonsai.Editor/ExplorerTreeView.cs @@ -0,0 +1,107 @@ +using System.Drawing; +using System.Windows.Forms; +using Bonsai.Editor.GraphModel; +using Bonsai.Editor.Properties; +using Bonsai.Expressions; + +namespace Bonsai.Editor +{ + class ExplorerTreeView : ToolboxTreeView + { + bool activeDoubleClick; + readonly ImageList iconList; + + public ExplorerTreeView() + { + iconList = new() + { + ColorDepth = ColorDepth.Depth8Bit, + ImageSize = new Size(16, 16), + TransparentColor = Color.Transparent + }; + ImageList = iconList; + HideSelection = false; + } + + protected override void ScaleControl(SizeF factor, BoundsSpecified specified) + { + iconList.Images.Clear(); + iconList.Images.Add(Resources.StatusReadyImage); + iconList.Images.Add(Resources.StatusBlockedImage); + base.ScaleControl(factor, specified); + } + + protected override void OnBeforeCollapse(TreeViewCancelEventArgs e) + { + if (activeDoubleClick && e.Action == TreeViewAction.Collapse) + e.Cancel = true; + activeDoubleClick = false; + base.OnBeforeCollapse(e); + } + + protected override void OnBeforeExpand(TreeViewCancelEventArgs e) + { + if (activeDoubleClick && e.Action == TreeViewAction.Expand) + e.Cancel = true; + activeDoubleClick = false; + base.OnBeforeExpand(e); + } + + protected override void OnMouseDown(MouseEventArgs e) + { + activeDoubleClick = e.Clicks > 1; + base.OnMouseDown(e); + } + + public void UpdateWorkflow(string name, WorkflowBuilder workflowBuilder) + { + BeginUpdate(); + + Nodes.Clear(); + var rootNode = Nodes.Add(name); + AddWorkflow(rootNode.Nodes, null, workflowBuilder.Workflow); + void AddWorkflow(TreeNodeCollection nodes, WorkflowEditorPath basePath, ExpressionBuilderGraph workflow) + { + for (int i = 0; i < workflow.Count; i++) + { + var builder = workflow[i].Value; + if (ExpressionBuilder.Unwrap(builder) is IWorkflowExpressionBuilder workflowBuilder && + workflowBuilder.Workflow != null) + { + var displayName = ExpressionBuilder.GetElementDisplayName(builder); + var builderPath = new WorkflowEditorPath(i, basePath); + var node = nodes.Add(displayName); + node.Tag = builderPath; + AddWorkflow(node.Nodes, builderPath, workflowBuilder.Workflow); + } + } + } + + rootNode.Expand(); + EndUpdate(); + } + + public void SelectNode(WorkflowEditorPath path) + { + SelectNode(Nodes, path); + } + + bool SelectNode(TreeNodeCollection nodes, WorkflowEditorPath path) + { + foreach (TreeNode node in nodes) + { + var nodePath = (WorkflowEditorPath)node.Tag; + if (nodePath == path) + { + SelectedNode = node; + return true; + } + + var selected = SelectNode(node.Nodes, path); + if (selected) break; + } + + return false; + } + } +} diff --git a/Bonsai.Editor/GraphModel/WorkflowEditorPath.cs b/Bonsai.Editor/GraphModel/WorkflowEditorPath.cs new file mode 100644 index 000000000..006c7332e --- /dev/null +++ b/Bonsai.Editor/GraphModel/WorkflowEditorPath.cs @@ -0,0 +1,100 @@ +using System; +using System.Collections.Generic; +using Bonsai.Expressions; + +namespace Bonsai.Editor.GraphModel +{ + class WorkflowEditorPath : IEquatable + { + public WorkflowEditorPath() + { + } + + public WorkflowEditorPath(int index, WorkflowEditorPath parent) + { + Index = index; + Parent = parent; + } + + public int Index { get; } + + public WorkflowEditorPath Parent { get; } + + public IEnumerable GetPathElements() + { + var stack = new Stack(); + var pathElement = this; + while (pathElement != null) + { + stack.Push(pathElement); + pathElement = pathElement.Parent; + } + + foreach (var element in stack) + { + yield return element; + } + } + + public ExpressionBuilder Resolve(WorkflowBuilder workflowBuilder) + { + var builder = default(ExpressionBuilder); + var workflow = workflowBuilder.Workflow; + foreach (var pathElement in GetPathElements()) + { + if (workflow == null) + { + throw new ArgumentException($"Unable to resolve workflow editor path.", nameof(workflowBuilder)); + } + + builder = workflow[pathElement.Index].Value; + if (ExpressionBuilder.Unwrap(builder) is IWorkflowExpressionBuilder nestedWorkflowBuilder) + { + workflow = nestedWorkflowBuilder.Workflow; + } + else workflow = null; + } + + return builder; + } + + public override int GetHashCode() + { + var hash = 107; + hash += Index.GetHashCode() * 13; + hash += (Parent?.GetHashCode()).GetValueOrDefault() * 13; + return hash; + } + + public override bool Equals(object obj) + { + if (obj is WorkflowEditorPath path) + return Equals(path); + + return false; + } + + public bool Equals(WorkflowEditorPath other) + { + if (ReferenceEquals(this, other)) + return true; + + if (other == null) + return false; + + return Index == other.Index && Parent == other.Parent; + } + + public static bool operator ==(WorkflowEditorPath left, WorkflowEditorPath right) + { + if (left is not null) return left.Equals(right); + else return right is null; + } + + public static bool operator !=(WorkflowEditorPath left, WorkflowEditorPath right) + { + if (left is not null) return !left.Equals(right); + else return right is not null; + } + } +} diff --git a/Bonsai.Editor/GraphView/WorkflowPathMouseEventArgs.cs b/Bonsai.Editor/GraphView/WorkflowPathMouseEventArgs.cs new file mode 100644 index 000000000..d70832b1c --- /dev/null +++ b/Bonsai.Editor/GraphView/WorkflowPathMouseEventArgs.cs @@ -0,0 +1,16 @@ +using System.Windows.Forms; +using Bonsai.Editor.GraphModel; + +namespace Bonsai.Editor.GraphView +{ + internal class WorkflowPathMouseEventArgs : MouseEventArgs + { + public WorkflowPathMouseEventArgs(WorkflowEditorPath path, MouseButtons button, int clicks, int x, int y, int delta) + : base(button, clicks, x, y, delta) + { + Path = path; + } + + public WorkflowEditorPath Path { get; } + } +} diff --git a/Bonsai.Editor/GraphView/WorkflowPathNavigationControl.Designer.cs b/Bonsai.Editor/GraphView/WorkflowPathNavigationControl.Designer.cs new file mode 100644 index 000000000..01368109d --- /dev/null +++ b/Bonsai.Editor/GraphView/WorkflowPathNavigationControl.Designer.cs @@ -0,0 +1,60 @@ +namespace Bonsai.Editor.GraphView +{ + partial class WorkflowPathNavigationControl + { + /// + /// 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 Component Designer generated code + + /// + /// Required method for Designer support - do not modify + /// the contents of this method with the code editor. + /// + private void InitializeComponent() + { + this.flowLayoutPanel = new System.Windows.Forms.FlowLayoutPanel(); + this.SuspendLayout(); + // + // flowLayoutPanel + // + this.flowLayoutPanel.AutoSize = true; + this.flowLayoutPanel.Dock = System.Windows.Forms.DockStyle.Fill; + this.flowLayoutPanel.Location = new System.Drawing.Point(0, 0); + this.flowLayoutPanel.Name = "flowLayoutPanel"; + this.flowLayoutPanel.Size = new System.Drawing.Size(452, 29); + this.flowLayoutPanel.TabIndex = 0; + // + // EditorPathNavigationControl + // + this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F); + this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; + this.AutoSize = true; + this.Controls.Add(this.flowLayoutPanel); + this.Name = "EditorPathNavigationControl"; + this.Size = new System.Drawing.Size(452, 29); + this.ResumeLayout(false); + this.PerformLayout(); + + } + + #endregion + + private System.Windows.Forms.FlowLayoutPanel flowLayoutPanel; + } +} diff --git a/Bonsai.Editor/GraphView/WorkflowPathNavigationControl.cs b/Bonsai.Editor/GraphView/WorkflowPathNavigationControl.cs new file mode 100644 index 000000000..bdc27c77a --- /dev/null +++ b/Bonsai.Editor/GraphView/WorkflowPathNavigationControl.cs @@ -0,0 +1,175 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Windows.Forms; +using Bonsai.Editor.GraphModel; +using Bonsai.Editor.Themes; +using Bonsai.Expressions; + +namespace Bonsai.Editor.GraphView +{ + partial class WorkflowPathNavigationControl : UserControl + { + static readonly object WorkflowPathMouseClickEvent = new(); + readonly IServiceProvider serviceProvider; + readonly IWorkflowEditorService editorService; + readonly ThemeRenderer themeRenderer; + WorkflowEditorPath workflowPath; + + public WorkflowPathNavigationControl(IServiceProvider provider) + { + InitializeComponent(); + serviceProvider = provider; + themeRenderer = (ThemeRenderer)provider.GetService(typeof(ThemeRenderer)); + themeRenderer.ThemeChanged += ThemeRenderer_ThemeChanged; + editorService = (IWorkflowEditorService)provider.GetService(typeof(IWorkflowEditorService)); + } + + public string DisplayName + { + get + { + return flowLayoutPanel.Controls.Count > 0 + ? flowLayoutPanel.Controls[flowLayoutPanel.Controls.Count - 1].Text + : editorService.GetProjectDisplayName(); + } + } + + public WorkflowEditorPath WorkflowPath + { + get { return workflowPath; } + set + { + workflowPath = value; + var workflowBuilder = (WorkflowBuilder)serviceProvider.GetService(typeof(WorkflowBuilder)); + var pathElements = GetPathElements(workflowPath, workflowBuilder); + SetPath(pathElements); + } + } + + public event EventHandler WorkflowPathMouseClick + { + add { Events.AddHandler(WorkflowPathMouseClickEvent, value); } + remove { Events.RemoveHandler(WorkflowPathMouseClickEvent, value); } + } + + private void OnWorkflowPathMouseClick(WorkflowPathMouseEventArgs e) + { + (Events[WorkflowPathMouseClickEvent] as EventHandler)?.Invoke(this, e); + } + + static IEnumerable> GetPathElements(WorkflowEditorPath workflowPath, WorkflowBuilder workflowBuilder) + { + var workflow = workflowBuilder.Workflow; + foreach (var pathElement in workflowPath?.GetPathElements() ?? Enumerable.Empty()) + { + var builder = workflow[pathElement.Index].Value; + if (ExpressionBuilder.Unwrap(builder) is IWorkflowExpressionBuilder nestedWorkflowBuilder) + { + workflow = nestedWorkflowBuilder.Workflow; + } + + yield return new( + key: ExpressionBuilder.GetElementDisplayName(builder), + value: pathElement); + } + } + + private void SetPath(IEnumerable> pathElements) + { + SuspendLayout(); + var rootButton = CreateButton(editorService.GetProjectDisplayName(), null); + flowLayoutPanel.Controls.Clear(); + flowLayoutPanel.Controls.Add(rootButton); + foreach (var path in pathElements) + { + var separator = CreateButton(">", null, createEvent: false); + var pathButton = CreateButton(path.Key, path.Value); + flowLayoutPanel.Controls.Add(separator); + flowLayoutPanel.Controls.Add(pathButton); + } + ResumeLayout(true); + } + + private Button CreateButton(string text, WorkflowEditorPath path, bool createEvent = true) + { + var breadcrumbButton = new BreadcrumbButtton + { + AutoSize = true, + Locked = !createEvent, + AutoSizeMode = AutoSizeMode.GrowAndShrink, + Text = text, + Tag = path + }; + if (createEvent) + breadcrumbButton.MouseClick += BreadcrumbButton_MouseClick; + SetBreadcrumbTheme(breadcrumbButton, themeRenderer); + return breadcrumbButton; + } + + private void BreadcrumbButton_MouseClick(object sender, MouseEventArgs e) + { + var button = (Button)sender; + var path = (WorkflowEditorPath)button.Tag; + OnWorkflowPathMouseClick(new WorkflowPathMouseEventArgs(path, e.Button, e.Clicks, e.X, e.Y, e.Delta)); + } + + protected override void OnHandleDestroyed(EventArgs e) + { + themeRenderer.ThemeChanged -= ThemeRenderer_ThemeChanged; + base.OnHandleDestroyed(e); + } + + private void ThemeRenderer_ThemeChanged(object sender, EventArgs e) + { + InitializeTheme(); + } + + internal void InitializeTheme() + { + foreach (Button button in flowLayoutPanel.Controls) + { + SetBreadcrumbTheme(button, themeRenderer); + } + } + + private static void SetBreadcrumbTheme(Button button, ThemeRenderer themeRenderer) + { + if (themeRenderer == null) + return; + + var colorTable = themeRenderer.ToolStripRenderer.ColorTable; + button.BackColor = colorTable.WindowBackColor; + button.ForeColor = colorTable.WindowText; + } + + class BreadcrumbButtton : Button + { + bool locked; + + public bool Locked + { + get => locked; + set + { + locked = value; + SetStyle(ControlStyles.Selectable, !locked); + } + } + + protected override void OnMouseEnter(EventArgs e) + { + if (Locked) + return; + base.OnMouseEnter(e); + } + + protected override void OnMouseDown(MouseEventArgs mevent) + { + if (Locked) + return; + base.OnMouseDown(mevent); + } + } + } +} diff --git a/Bonsai.Editor/GraphView/WorkflowPathNavigationControl.resx b/Bonsai.Editor/GraphView/WorkflowPathNavigationControl.resx new file mode 100644 index 000000000..1af7de150 --- /dev/null +++ b/Bonsai.Editor/GraphView/WorkflowPathNavigationControl.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.Editor/IWorkflowEditorService.cs b/Bonsai.Editor/IWorkflowEditorService.cs index ffb1c4aca..e831ae9dd 100644 --- a/Bonsai.Editor/IWorkflowEditorService.cs +++ b/Bonsai.Editor/IWorkflowEditorService.cs @@ -6,6 +6,8 @@ namespace Bonsai.Editor { interface IWorkflowEditorService { + string GetProjectDisplayName(); + void OnKeyDown(KeyEventArgs e); void OnKeyPress(KeyPressEventArgs e); From 337a825f2facacd31f5f6ec7877df56fb20055ce Mon Sep 17 00:00:00 2001 From: glopesdev Date: Mon, 1 Jul 2024 12:36:58 +0100 Subject: [PATCH 03/32] Add explorer support for highlighting node paths --- Bonsai.Editor/ExplorerTreeView.cs | 69 +++++++++++++++++-- .../GraphModel/WorkflowEditorPath.cs | 64 +++++++++++++++++ 2 files changed, 129 insertions(+), 4 deletions(-) diff --git a/Bonsai.Editor/ExplorerTreeView.cs b/Bonsai.Editor/ExplorerTreeView.cs index 7f174d50e..b941cec35 100644 --- a/Bonsai.Editor/ExplorerTreeView.cs +++ b/Bonsai.Editor/ExplorerTreeView.cs @@ -1,4 +1,7 @@ -using System.Drawing; +using System; +using System.Collections.Generic; +using System.Drawing; +using System.Linq; using System.Windows.Forms; using Bonsai.Editor.GraphModel; using Bonsai.Editor.Properties; @@ -20,7 +23,6 @@ public ExplorerTreeView() TransparentColor = Color.Transparent }; ImageList = iconList; - HideSelection = false; } protected override void ScaleControl(SizeF factor, BoundsSpecified specified) @@ -56,11 +58,12 @@ protected override void OnMouseDown(MouseEventArgs e) public void UpdateWorkflow(string name, WorkflowBuilder workflowBuilder) { BeginUpdate(); - Nodes.Clear(); + var rootNode = Nodes.Add(name); AddWorkflow(rootNode.Nodes, null, workflowBuilder.Workflow); - void AddWorkflow(TreeNodeCollection nodes, WorkflowEditorPath basePath, ExpressionBuilderGraph workflow) + + static void AddWorkflow(TreeNodeCollection nodes, WorkflowEditorPath basePath, ExpressionBuilderGraph workflow) { for (int i = 0; i < workflow.Count; i++) { @@ -103,5 +106,63 @@ bool SelectNode(TreeNodeCollection nodes, WorkflowEditorPath path) return false; } + + private static int GetImageIndex(ExplorerNodeStatus status) + { + return status switch + { + ExplorerNodeStatus.Ready => 0, + ExplorerNodeStatus.Blocked => 1, + _ => throw new ArgumentException("Invalid node status.", nameof(status)) + }; + } + + public void SetNodeStatus(ExplorerNodeStatus status) + { + var imageIndex = GetImageIndex(status); + SetNodeImageIndex(Nodes, imageIndex); + + static void SetNodeImageIndex(TreeNodeCollection nodes, int index) + { + foreach (TreeNode node in nodes) + { + if (node.ImageIndex == index) + continue; + + node.ImageIndex = node.SelectedImageIndex = index; + SetNodeImageIndex(node.Nodes, index); + } + } + } + + public void SetNodeStatus(IEnumerable pathElements, ExplorerNodeStatus status) + { + var nodes = Nodes; + var imageIndex = GetImageIndex(status); + foreach (var path in pathElements.Prepend(null)) + { + var found = false; + for (int n = 0; n < nodes.Count; n++) + { + var groupNode = nodes[n]; + if ((WorkflowEditorPath)groupNode.Tag == path) + { + groupNode.ImageIndex = groupNode.SelectedImageIndex = imageIndex; + nodes = groupNode.Nodes; + found = true; + break; + } + } + + if (!found) + break; + } + } + } + + enum ExplorerNodeStatus + { + Ready, + Blocked } } diff --git a/Bonsai.Editor/GraphModel/WorkflowEditorPath.cs b/Bonsai.Editor/GraphModel/WorkflowEditorPath.cs index 006c7332e..02ab49f27 100644 --- a/Bonsai.Editor/GraphModel/WorkflowEditorPath.cs +++ b/Bonsai.Editor/GraphModel/WorkflowEditorPath.cs @@ -58,6 +58,70 @@ public ExpressionBuilder Resolve(WorkflowBuilder workflowBuilder) return builder; } + public static WorkflowEditorPath GetExceptionPath(WorkflowBuilder workflowBuilder, WorkflowException ex) + { + return GetExceptionPath(workflowBuilder.Workflow, ex, null); + } + + static WorkflowEditorPath GetExceptionPath(ExpressionBuilderGraph workflow, WorkflowException ex, WorkflowEditorPath parent) + { + for (int i = 0; i < workflow.Count; i++) + { + var builder = workflow[i].Value; + if (builder == ex.Builder) + { + var path = new WorkflowEditorPath(i, parent); + if (ex.InnerException is WorkflowException nestedEx && + ExpressionBuilder.Unwrap(ex.Builder) is IWorkflowExpressionBuilder workflowBuilder) + { + return GetExceptionPath(workflowBuilder.Workflow, nestedEx, path); + } + else return path; + } + } + + return null; + } + + public static WorkflowEditorPath GetBuilderPath(WorkflowBuilder workflowBuilder, ExpressionBuilder builder) + { + return GetBuilderPath(workflowBuilder.Workflow, ExpressionBuilder.Unwrap(builder), new List()); + } + + static WorkflowEditorPath GetBuilderPath(ExpressionBuilderGraph workflow, ExpressionBuilder target, List pathElements) + { + for (int i = 0; i < workflow.Count; i++) + { + var builder = ExpressionBuilder.Unwrap(workflow[i].Value); + if (builder == target) + { + pathElements.Add(i); + return GetBuilderPath(pathElements); + } + + if (builder is IWorkflowExpressionBuilder workflowBuilder) + { + pathElements.Add(i); + var path = GetBuilderPath(workflowBuilder.Workflow, target, pathElements); + if (path is not null) + return path; + pathElements.RemoveAt(pathElements.Count - 1); + } + } + + return null; + } + + static WorkflowEditorPath GetBuilderPath(List pathElements) + { + WorkflowEditorPath path = null; + foreach (var index in pathElements) + { + path = new WorkflowEditorPath(index, path); + } + return path; + } + public override int GetHashCode() { var hash = 107; From 6e816dab1532fd9bf0ba2018596110bd7f31a68c Mon Sep 17 00:00:00 2001 From: glopesdev Date: Tue, 2 Jul 2024 08:27:46 +0100 Subject: [PATCH 04/32] Add theme renderer support to custom tree views --- Bonsai.Editor/EditorForm.cs | 28 +----------------- Bonsai.Editor/ToolboxTreeView.cs | 51 ++++++++++++++++++++++++++++++++ 2 files changed, 52 insertions(+), 27 deletions(-) diff --git a/Bonsai.Editor/EditorForm.cs b/Bonsai.Editor/EditorForm.cs index 07d253818..5416d312e 100644 --- a/Bonsai.Editor/EditorForm.cs +++ b/Bonsai.Editor/EditorForm.cs @@ -1823,19 +1823,6 @@ void UpdateTreeViewDescription() else UpdateDescriptionTextBox(string.Empty, string.Empty, toolboxDescriptionTextBox); } - void UpdateTreeViewSelection(bool focused) - { - var selectedNode = toolboxTreeView.SelectedNode; - if (toolboxTreeView.Tag != selectedNode) - { - if (toolboxTreeView.Tag is TreeNode previousNode) previousNode.BackColor = Color.Empty; - toolboxTreeView.Tag = selectedNode; - } - - if (selectedNode == null) return; - selectedNode.BackColor = focused ? Color.Empty : themeRenderer.ToolStripRenderer.ColorTable.InactiveCaption; - } - void SelectTreeViewSubjectNode(string subjectName) { var subjectCategory = toolboxCategories[SubjectCategoryName]; @@ -1984,7 +1971,6 @@ private void toolboxTreeView_NodeMouseDoubleClick(object sender, TreeNodeMouseCl private void toolboxTreeView_AfterSelect(object sender, TreeViewEventArgs e) { UpdateTreeViewDescription(); - UpdateTreeViewSelection(toolboxTreeView.Focused); } private void toolboxTreeView_MouseUp(object sender, MouseEventArgs e) @@ -2037,16 +2023,6 @@ private void toolboxTreeView_MouseUp(object sender, MouseEventArgs e) } } - private void toolboxTreeView_Enter(object sender, EventArgs e) - { - UpdateTreeViewSelection(true); - } - - private void toolboxTreeView_Leave(object sender, EventArgs e) - { - UpdateTreeViewSelection(false); - } - private void insertAfterToolStripMenuItem_Click(object sender, EventArgs e) { toolboxTreeView_KeyDown(sender, new KeyEventArgs(Keys.Return)); @@ -3049,8 +3025,7 @@ private void InitializeTheme() toolboxSplitContainer.BackColor = panelColor; toolboxLabel.BackColor = colorTable.SeparatorDark; toolboxLabel.ForeColor = ForeColor; - toolboxTreeView.BackColor = panelColor; - toolboxTreeView.ForeColor = windowText; + toolboxTreeView.Renderer = themeRenderer.ToolStripRenderer; toolboxDescriptionTextBox.BackColor = panelColor; toolboxDescriptionTextBox.ForeColor = ForeColor; propertiesDescriptionTextBox.BackColor = panelColor; @@ -3069,7 +3044,6 @@ private void InitializeTheme() } propertiesLayoutPanel.RowStyles[0].Height -= labelOffset; toolboxLayoutPanel.RowStyles[0].Height -= labelOffset; - UpdateTreeViewSelection(toolboxTreeView.Focused); propertyGrid.Refresh(); } diff --git a/Bonsai.Editor/ToolboxTreeView.cs b/Bonsai.Editor/ToolboxTreeView.cs index 0c89e4f39..8f3e16136 100644 --- a/Bonsai.Editor/ToolboxTreeView.cs +++ b/Bonsai.Editor/ToolboxTreeView.cs @@ -1,11 +1,24 @@ using System; using System.Drawing; using System.Windows.Forms; +using Bonsai.Editor.Themes; namespace Bonsai.Editor { class ToolboxTreeView : TreeView { + private ToolStripExtendedRenderer renderer; + + public ToolStripExtendedRenderer Renderer + { + get => renderer; + set + { + renderer = value; + UpdateTreeViewSelection(Focused); + } + } + protected override void OnEnabledChanged(EventArgs e) { base.OnEnabledChanged(e); @@ -23,5 +36,43 @@ void SetNodeEnabled(TreeNode node) SetNodeEnabled(child); } } + + protected override void OnEnter(EventArgs e) + { + UpdateTreeViewSelection(true); + base.OnEnter(e); + } + + protected override void OnLeave(EventArgs e) + { + UpdateTreeViewSelection(false); + base.OnLeave(e); + } + + protected override void OnAfterSelect(TreeViewEventArgs e) + { + UpdateTreeViewSelection(Focused); + base.OnAfterSelect(e); + } + + void UpdateTreeViewSelection(bool focused) + { + if (Renderer == null) + return; + + var colorTable = Renderer.ColorTable; + BackColor = colorTable.ContentPanelBackColor; + ForeColor = colorTable.WindowText; + + var selectedNode = SelectedNode; + if (Tag != selectedNode) + { + if (Tag is TreeNode previousNode) previousNode.BackColor = Color.Empty; + Tag = selectedNode; + } + + if (selectedNode == null) return; + selectedNode.BackColor = focused ? Color.Empty : colorTable.InactiveCaption; + } } } From c7eb45564204134bee0d8e2419c9375aa9c38948 Mon Sep 17 00:00:00 2001 From: glopesdev Date: Tue, 2 Jul 2024 09:58:12 +0100 Subject: [PATCH 05/32] Use state image list for flexible icon rendering --- Bonsai.Editor/ExplorerTreeView.cs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/Bonsai.Editor/ExplorerTreeView.cs b/Bonsai.Editor/ExplorerTreeView.cs index b941cec35..9bdf637fc 100644 --- a/Bonsai.Editor/ExplorerTreeView.cs +++ b/Bonsai.Editor/ExplorerTreeView.cs @@ -22,7 +22,7 @@ public ExplorerTreeView() ImageSize = new Size(16, 16), TransparentColor = Color.Transparent }; - ImageList = iconList; + StateImageList = iconList; } protected override void ScaleControl(SizeF factor, BoundsSpecified specified) @@ -80,6 +80,7 @@ static void AddWorkflow(TreeNodeCollection nodes, WorkflowEditorPath basePath, E } } + SetNodeStatus(ExplorerNodeStatus.Ready); rootNode.Expand(); EndUpdate(); } @@ -126,10 +127,10 @@ static void SetNodeImageIndex(TreeNodeCollection nodes, int index) { foreach (TreeNode node in nodes) { - if (node.ImageIndex == index) + if (node.StateImageIndex == index) continue; - node.ImageIndex = node.SelectedImageIndex = index; + node.StateImageIndex = index; SetNodeImageIndex(node.Nodes, index); } } @@ -147,7 +148,7 @@ public void SetNodeStatus(IEnumerable pathElements, Explorer var groupNode = nodes[n]; if ((WorkflowEditorPath)groupNode.Tag == path) { - groupNode.ImageIndex = groupNode.SelectedImageIndex = imageIndex; + groupNode.StateImageIndex = imageIndex; nodes = groupNode.Nodes; found = true; break; From 18eb816e735ba00bed5f52db3e59abacb5e4a0a1 Mon Sep 17 00:00:00 2001 From: glopesdev Date: Tue, 2 Jul 2024 12:09:49 +0100 Subject: [PATCH 06/32] Allow resolution to determine if path is read-only --- Bonsai.Editor/GraphModel/WorkflowEditorPath.cs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/Bonsai.Editor/GraphModel/WorkflowEditorPath.cs b/Bonsai.Editor/GraphModel/WorkflowEditorPath.cs index 02ab49f27..96c91e3a5 100644 --- a/Bonsai.Editor/GraphModel/WorkflowEditorPath.cs +++ b/Bonsai.Editor/GraphModel/WorkflowEditorPath.cs @@ -38,6 +38,12 @@ public IEnumerable GetPathElements() public ExpressionBuilder Resolve(WorkflowBuilder workflowBuilder) { + return Resolve(workflowBuilder, out _); + } + + public ExpressionBuilder Resolve(WorkflowBuilder workflowBuilder, out bool isReadOnly) + { + isReadOnly = false; var builder = default(ExpressionBuilder); var workflow = workflowBuilder.Workflow; foreach (var pathElement in GetPathElements()) @@ -51,6 +57,7 @@ public ExpressionBuilder Resolve(WorkflowBuilder workflowBuilder) if (ExpressionBuilder.Unwrap(builder) is IWorkflowExpressionBuilder nestedWorkflowBuilder) { workflow = nestedWorkflowBuilder.Workflow; + isReadOnly |= nestedWorkflowBuilder is IncludeWorkflowBuilder; } else workflow = null; } From 3eda277e7af1844f694386f1527c5d4b8fc6aacd Mon Sep 17 00:00:00 2001 From: glopesdev Date: Tue, 2 Jul 2024 13:39:59 +0100 Subject: [PATCH 07/32] Rewrite editor navigation model - Add workflow explorer treeview - Allow navigating to any level without opening previous levels - Allow visualizers to run uncoupled from the workflow view - Highlight build errors in explorer treeview --- Bonsai.Editor.Tests/MockGraphView.cs | 1 + Bonsai.Editor.Tests/WorkflowEditorTests.cs | 1 - Bonsai.Editor/EditorForm.Designer.cs | 127 +++- Bonsai.Editor/EditorForm.cs | 268 ++++---- .../GraphModel/WorkflowBuilderExtensions.cs | 41 -- Bonsai.Editor/GraphModel/WorkflowEditor.cs | 58 +- .../GraphView/WorkflowEditorControl.cs | 122 +--- Bonsai.Editor/GraphView/WorkflowGraphView.cs | 623 ++++-------------- Bonsai.Editor/IWorkflowEditorService.cs | 3 + Bonsai.Editor/Layout/LayoutHelper.cs | 156 +---- .../Layout/VisualizerDialogLauncher.cs | 86 ++- Bonsai.Editor/Layout/VisualizerDialogMap.cs | 75 +++ .../Layout/VisualizerDialogSettings.cs | 6 + Bonsai.Editor/Layout/VisualizerLayoutMap.cs | 201 ++++++ .../Layout/WorkflowEditorLauncher.cs | 188 ------ .../Layout/WorkflowEditorSettings.cs | 5 +- .../Properties/Resources.Designer.cs | 9 + Bonsai.Editor/Properties/Resources.resx | 21 +- Bonsai.Editor/WorkflowRunner.cs | 34 +- Bonsai/DependencyInspector.cs | 5 +- 20 files changed, 838 insertions(+), 1192 deletions(-) create mode 100644 Bonsai.Editor/Layout/VisualizerDialogMap.cs create mode 100644 Bonsai.Editor/Layout/VisualizerLayoutMap.cs delete mode 100644 Bonsai.Editor/Layout/WorkflowEditorLauncher.cs diff --git a/Bonsai.Editor.Tests/MockGraphView.cs b/Bonsai.Editor.Tests/MockGraphView.cs index 296a11c2c..e67e10448 100644 --- a/Bonsai.Editor.Tests/MockGraphView.cs +++ b/Bonsai.Editor.Tests/MockGraphView.cs @@ -18,6 +18,7 @@ public MockGraphView(ExpressionBuilderGraph workflow = null) Workflow = workflow ?? new ExpressionBuilderGraph(); CommandExecutor = new CommandExecutor(); var serviceContainer = new ServiceContainer(); + serviceContainer.AddService(typeof(WorkflowBuilder), new WorkflowBuilder(Workflow)); serviceContainer.AddService(typeof(CommandExecutor), CommandExecutor); ServiceProvider = serviceContainer; } diff --git a/Bonsai.Editor.Tests/WorkflowEditorTests.cs b/Bonsai.Editor.Tests/WorkflowEditorTests.cs index 4b4930d04..b181e19d5 100644 --- a/Bonsai.Editor.Tests/WorkflowEditorTests.cs +++ b/Bonsai.Editor.Tests/WorkflowEditorTests.cs @@ -51,7 +51,6 @@ static string ToString(IEnumerable sequence) var editor = new WorkflowEditor(graphView.ServiceProvider, graphView); editor.UpdateLayout.Subscribe(graphView.UpdateGraphLayout); editor.UpdateSelection.Subscribe(graphView.UpdateSelection); - editor.Workflow = graphView.Workflow; var nodeSequence = editor.GetGraphValues().ToArray(); return (editor, assertIsReversible: () => diff --git a/Bonsai.Editor/EditorForm.Designer.cs b/Bonsai.Editor/EditorForm.Designer.cs index 3c99e6e7b..52cc5ec1b 100644 --- a/Bonsai.Editor/EditorForm.Designer.cs +++ b/Bonsai.Editor/EditorForm.Designer.cs @@ -113,9 +113,9 @@ private void InitializeComponent() this.editExtensionsToolStripButton = new System.Windows.Forms.ToolStripButton(); this.reloadExtensionsToolStripButton = new System.Windows.Forms.ToolStripButton(); this.statusStrip = new System.Windows.Forms.StatusStrip(); + this.statusImageLabel = new System.Windows.Forms.ToolStripStatusLabel(); this.statusContextMenuStrip = new System.Windows.Forms.ContextMenuStrip(this.components); this.statusCopyToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); - this.statusImageLabel = new System.Windows.Forms.ToolStripStatusLabel(); this.toolboxSplitContainer = new Bonsai.Editor.SelectableSplitContainer(); this.toolboxTableLayoutPanel = new Bonsai.Editor.TableLayoutPanel(); this.searchTextBox = new Bonsai.Editor.CueBannerTextBox(); @@ -151,6 +151,10 @@ private void InitializeComponent() this.commandExecutor = new Bonsai.Design.CommandExecutor(); this.workflowFileWatcher = new System.IO.FileSystemWatcher(); this.exportImageDialog = new System.Windows.Forms.SaveFileDialog(); + this.explorerSplitContainer = new System.Windows.Forms.SplitContainer(); + this.explorerLayoutPanel = new Bonsai.Editor.TableLayoutPanel(); + this.explorerLabel = new Bonsai.Editor.Label(); + this.explorerTreeView = new Bonsai.Editor.ExplorerTreeView(); this.menuStrip.SuspendLayout(); this.toolStrip.SuspendLayout(); this.statusStrip.SuspendLayout(); @@ -178,6 +182,11 @@ private void InitializeComponent() this.propertiesLayoutPanel.SuspendLayout(); this.toolboxContextMenuStrip.SuspendLayout(); ((System.ComponentModel.ISupportInitialize)(this.workflowFileWatcher)).BeginInit(); + ((System.ComponentModel.ISupportInitialize)(this.explorerSplitContainer)).BeginInit(); + this.explorerSplitContainer.Panel1.SuspendLayout(); + this.explorerSplitContainer.Panel2.SuspendLayout(); + this.explorerSplitContainer.SuspendLayout(); + this.explorerLayoutPanel.SuspendLayout(); this.SuspendLayout(); // // menuStrip @@ -606,7 +615,7 @@ private void InitializeComponent() // welcomeToolStripMenuItem // this.welcomeToolStripMenuItem.Name = "welcomeToolStripMenuItem"; - this.welcomeToolStripMenuItem.Size = new System.Drawing.Size(152, 22); + this.welcomeToolStripMenuItem.Size = new System.Drawing.Size(180, 22); this.welcomeToolStripMenuItem.Text = "&Welcome..."; this.welcomeToolStripMenuItem.Click += new System.EventHandler(this.welcomeToolStripMenuItem_Click); // @@ -615,7 +624,7 @@ private void InitializeComponent() this.docsToolStripMenuItem.Image = ((System.Drawing.Image)(resources.GetObject("docsToolStripMenuItem.Image"))); this.docsToolStripMenuItem.Name = "docsToolStripMenuItem"; this.docsToolStripMenuItem.ShortcutKeys = System.Windows.Forms.Keys.F1; - this.docsToolStripMenuItem.Size = new System.Drawing.Size(152, 22); + this.docsToolStripMenuItem.Size = new System.Drawing.Size(180, 22); this.docsToolStripMenuItem.Text = "&View Help"; this.docsToolStripMenuItem.Click += new System.EventHandler(this.docsToolStripMenuItem_Click); // @@ -623,7 +632,7 @@ private void InitializeComponent() // this.forumToolStripMenuItem.Image = ((System.Drawing.Image)(resources.GetObject("forumToolStripMenuItem.Image"))); this.forumToolStripMenuItem.Name = "forumToolStripMenuItem"; - this.forumToolStripMenuItem.Size = new System.Drawing.Size(152, 22); + this.forumToolStripMenuItem.Size = new System.Drawing.Size(180, 22); this.forumToolStripMenuItem.Text = "Bonsai &Forums"; this.forumToolStripMenuItem.Click += new System.EventHandler(this.forumToolStripMenuItem_Click); // @@ -631,19 +640,19 @@ private void InitializeComponent() // this.reportBugToolStripMenuItem.Image = ((System.Drawing.Image)(resources.GetObject("reportBugToolStripMenuItem.Image"))); this.reportBugToolStripMenuItem.Name = "reportBugToolStripMenuItem"; - this.reportBugToolStripMenuItem.Size = new System.Drawing.Size(152, 22); + this.reportBugToolStripMenuItem.Size = new System.Drawing.Size(180, 22); this.reportBugToolStripMenuItem.Text = "&Report a Bug"; this.reportBugToolStripMenuItem.Click += new System.EventHandler(this.reportBugToolStripMenuItem_Click); // // toolStripSeparator5 // this.toolStripSeparator5.Name = "toolStripSeparator5"; - this.toolStripSeparator5.Size = new System.Drawing.Size(149, 6); + this.toolStripSeparator5.Size = new System.Drawing.Size(177, 6); // // aboutToolStripMenuItem // this.aboutToolStripMenuItem.Name = "aboutToolStripMenuItem"; - this.aboutToolStripMenuItem.Size = new System.Drawing.Size(152, 22); + this.aboutToolStripMenuItem.Size = new System.Drawing.Size(180, 22); this.aboutToolStripMenuItem.Text = "&About..."; this.aboutToolStripMenuItem.Click += new System.EventHandler(this.aboutToolStripMenuItem_Click); // @@ -880,6 +889,14 @@ private void InitializeComponent() this.statusStrip.TabIndex = 2; this.statusStrip.Text = "statusStrip"; // + // statusImageLabel + // + this.statusImageLabel.DisplayStyle = System.Windows.Forms.ToolStripItemDisplayStyle.Image; + this.statusImageLabel.Image = global::Bonsai.Editor.Properties.Resources.StatusReadyImage; + this.statusImageLabel.Name = "statusImageLabel"; + this.statusImageLabel.Size = new System.Drawing.Size(16, 17); + this.statusImageLabel.Text = "statusImageLabel"; + // // statusContextMenuStrip // this.statusContextMenuStrip.Items.AddRange(new System.Windows.Forms.ToolStripItem[] { @@ -897,14 +914,6 @@ private void InitializeComponent() this.statusCopyToolStripMenuItem.Text = "&Copy"; this.statusCopyToolStripMenuItem.Click += new System.EventHandler(this.statusCopyToolStripMenuItem_Click); // - // statusImageLabel - // - this.statusImageLabel.DisplayStyle = System.Windows.Forms.ToolStripItemDisplayStyle.Image; - this.statusImageLabel.Image = global::Bonsai.Editor.Properties.Resources.StatusReadyImage; - this.statusImageLabel.Name = "statusImageLabel"; - this.statusImageLabel.Size = new System.Drawing.Size(16, 17); - this.statusImageLabel.Text = "statusImageLabel"; - // // toolboxSplitContainer // this.toolboxSplitContainer.Dock = System.Windows.Forms.DockStyle.Fill; @@ -922,8 +931,8 @@ private void InitializeComponent() // this.toolboxSplitContainer.Panel2.Controls.Add(this.toolboxDescriptionPanel); this.toolboxSplitContainer.Selectable = true; - this.toolboxSplitContainer.Size = new System.Drawing.Size(197, 308); - this.toolboxSplitContainer.SplitterDistance = 245; + this.toolboxSplitContainer.Size = new System.Drawing.Size(197, 138); + this.toolboxSplitContainer.SplitterDistance = 75; this.toolboxSplitContainer.TabIndex = 1; this.toolboxSplitContainer.TabStop = false; // @@ -939,7 +948,7 @@ private void InitializeComponent() this.toolboxTableLayoutPanel.RowCount = 2; this.toolboxTableLayoutPanel.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Absolute, 23F)); this.toolboxTableLayoutPanel.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 100F)); - this.toolboxTableLayoutPanel.Size = new System.Drawing.Size(197, 245); + this.toolboxTableLayoutPanel.Size = new System.Drawing.Size(197, 75); this.toolboxTableLayoutPanel.TabIndex = 2; // // searchTextBox @@ -980,15 +989,13 @@ private void InitializeComponent() treeNode4, treeNode5, treeNode6}); - this.toolboxTreeView.Size = new System.Drawing.Size(197, 222); + this.toolboxTreeView.Size = new System.Drawing.Size(197, 52); this.toolboxTreeView.TabIndex = 0; - this.toolboxTreeView.ItemDrag += new System.Windows.Forms.ItemDragEventHandler(this.toolboxTreeView_ItemDrag); this.toolboxTreeView.AfterLabelEdit += new System.Windows.Forms.NodeLabelEditEventHandler(this.toolboxTreeView_AfterLabelEdit); + this.toolboxTreeView.ItemDrag += new System.Windows.Forms.ItemDragEventHandler(this.toolboxTreeView_ItemDrag); this.toolboxTreeView.AfterSelect += new System.Windows.Forms.TreeViewEventHandler(this.toolboxTreeView_AfterSelect); this.toolboxTreeView.NodeMouseDoubleClick += new System.Windows.Forms.TreeNodeMouseClickEventHandler(this.toolboxTreeView_NodeMouseDoubleClick); - this.toolboxTreeView.Enter += new System.EventHandler(this.toolboxTreeView_Enter); this.toolboxTreeView.KeyDown += new System.Windows.Forms.KeyEventHandler(this.toolboxTreeView_KeyDown); - this.toolboxTreeView.Leave += new System.EventHandler(this.toolboxTreeView_Leave); this.toolboxTreeView.MouseUp += new System.Windows.Forms.MouseEventHandler(this.toolboxTreeView_MouseUp); // // toolboxDescriptionPanel @@ -1068,9 +1075,9 @@ private void InitializeComponent() this.propertyGrid.Name = "propertyGrid"; this.propertyGrid.Size = new System.Drawing.Size(193, 245); this.propertyGrid.TabIndex = 0; + this.propertyGrid.Refreshed += new System.EventHandler(this.propertyGrid_Refreshed); this.propertyGrid.DragDrop += new System.Windows.Forms.DragEventHandler(this.propertyGrid_DragDrop); this.propertyGrid.DragEnter += new System.Windows.Forms.DragEventHandler(this.propertyGrid_DragEnter); - this.propertyGrid.Refreshed += new System.EventHandler(this.propertyGrid_Refreshed); this.propertyGrid.Validated += new System.EventHandler(this.propertyGrid_Validated); // // propertyGridContextMenuStrip @@ -1108,7 +1115,7 @@ private void InitializeComponent() // // panelSplitContainer.Panel1 // - this.panelSplitContainer.Panel1.Controls.Add(this.toolboxLayoutPanel); + this.panelSplitContainer.Panel1.Controls.Add(this.explorerSplitContainer); // // panelSplitContainer.Panel2 // @@ -1131,7 +1138,7 @@ private void InitializeComponent() this.toolboxLayoutPanel.RowCount = 2; this.toolboxLayoutPanel.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Absolute, 23F)); this.toolboxLayoutPanel.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 100F)); - this.toolboxLayoutPanel.Size = new System.Drawing.Size(200, 340); + this.toolboxLayoutPanel.Size = new System.Drawing.Size(200, 170); this.toolboxLayoutPanel.TabIndex = 1; // // toolboxLabel @@ -1207,7 +1214,7 @@ private void InitializeComponent() this.findPreviousToolStripMenuItem, this.goToDefinitionToolStripMenuItem}); this.toolboxContextMenuStrip.Name = "toolboxContextMenuStrip"; - this.toolboxContextMenuStrip.Size = new System.Drawing.Size(207, 290); + this.toolboxContextMenuStrip.Size = new System.Drawing.Size(207, 268); // // toolboxDocsToolStripMenuItem // @@ -1327,6 +1334,64 @@ private void InitializeComponent() "*.tiff)|*.tif;*.tiff|PNG (*.png)|*.png|SVG (*.svg)|*.svg"; this.exportImageDialog.FilterIndex = 6; // + // explorerSplitContainer + // + this.explorerSplitContainer.Dock = System.Windows.Forms.DockStyle.Fill; + this.explorerSplitContainer.Location = new System.Drawing.Point(0, 0); + this.explorerSplitContainer.Name = "explorerSplitContainer"; + this.explorerSplitContainer.Orientation = System.Windows.Forms.Orientation.Horizontal; + // + // explorerSplitContainer.Panel1 + // + this.explorerSplitContainer.Panel1.Controls.Add(this.toolboxLayoutPanel); + // + // explorerSplitContainer.Panel2 + // + this.explorerSplitContainer.Panel2.Controls.Add(this.explorerLayoutPanel); + this.explorerSplitContainer.Size = new System.Drawing.Size(200, 340); + this.explorerSplitContainer.SplitterDistance = 170; + this.explorerSplitContainer.TabIndex = 2; + // + // explorerLayoutPanel + // + this.explorerLayoutPanel.ColumnCount = 1; + this.explorerLayoutPanel.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Percent, 100F)); + this.explorerLayoutPanel.Controls.Add(this.explorerTreeView, 0, 1); + this.explorerLayoutPanel.Controls.Add(this.explorerLabel, 0, 0); + this.explorerLayoutPanel.Dock = System.Windows.Forms.DockStyle.Fill; + this.explorerLayoutPanel.Location = new System.Drawing.Point(0, 0); + this.explorerLayoutPanel.Name = "explorerLayoutPanel"; + this.explorerLayoutPanel.Padding = new System.Windows.Forms.Padding(0, 6, 0, 0); + this.explorerLayoutPanel.RowCount = 2; + this.explorerLayoutPanel.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Absolute, 23F)); + this.explorerLayoutPanel.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 100F)); + this.explorerLayoutPanel.Size = new System.Drawing.Size(200, 166); + this.explorerLayoutPanel.TabIndex = 2; + // + // explorerLabel + // + this.explorerLabel.AutoSize = true; + this.explorerLabel.BackColor = System.Drawing.SystemColors.ScrollBar; + this.explorerLabel.Dock = System.Windows.Forms.DockStyle.Fill; + this.explorerLabel.Location = new System.Drawing.Point(3, 6); + this.explorerLabel.Margin = new System.Windows.Forms.Padding(3, 0, 0, 0); + this.explorerLabel.Name = "explorerLabel"; + this.explorerLabel.Size = new System.Drawing.Size(197, 23); + this.explorerLabel.TabIndex = 2; + this.explorerLabel.Text = "Explorer"; + this.explorerLabel.TextAlign = System.Drawing.ContentAlignment.MiddleLeft; + // + // explorerTreeView + // + this.explorerTreeView.BorderStyle = System.Windows.Forms.BorderStyle.None; + this.explorerTreeView.Dock = System.Windows.Forms.DockStyle.Fill; + this.explorerTreeView.Location = new System.Drawing.Point(0, 29); + this.explorerTreeView.Margin = new System.Windows.Forms.Padding(3, 0, 0, 3); + this.explorerTreeView.Name = "explorerTreeView"; + this.explorerTreeView.Size = new System.Drawing.Size(200, 137); + this.explorerTreeView.TabIndex = 3; + this.explorerTreeView.NodeMouseDoubleClick += new System.Windows.Forms.TreeNodeMouseClickEventHandler(explorerTreeView_NodeMouseDoubleClick); + // // EditorForm // this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F); @@ -1378,6 +1443,12 @@ private void InitializeComponent() this.propertiesLayoutPanel.PerformLayout(); this.toolboxContextMenuStrip.ResumeLayout(false); ((System.ComponentModel.ISupportInitialize)(this.workflowFileWatcher)).EndInit(); + this.explorerSplitContainer.Panel1.ResumeLayout(false); + this.explorerSplitContainer.Panel2.ResumeLayout(false); + ((System.ComponentModel.ISupportInitialize)(this.explorerSplitContainer)).EndInit(); + this.explorerSplitContainer.ResumeLayout(false); + this.explorerLayoutPanel.ResumeLayout(false); + this.explorerLayoutPanel.PerformLayout(); this.ResumeLayout(false); this.PerformLayout(); @@ -1500,6 +1571,10 @@ private void InitializeComponent() private System.Windows.Forms.ContextMenuStrip statusContextMenuStrip; private System.Windows.Forms.ToolStripMenuItem statusCopyToolStripMenuItem; private System.Windows.Forms.ToolStripMenuItem toolboxDocsToolStripMenuItem; + private System.Windows.Forms.SplitContainer explorerSplitContainer; + private Bonsai.Editor.TableLayoutPanel explorerLayoutPanel; + private Bonsai.Editor.Label explorerLabel; + private Bonsai.Editor.ExplorerTreeView explorerTreeView; } } diff --git a/Bonsai.Editor/EditorForm.cs b/Bonsai.Editor/EditorForm.cs index 5416d312e..48b1a34d5 100644 --- a/Bonsai.Editor/EditorForm.cs +++ b/Bonsai.Editor/EditorForm.cs @@ -43,6 +43,7 @@ public partial class EditorForm : Form static readonly char[] ToolboxArgumentSeparator = new[] { ' ' }; static readonly object ExtensionsDirectoryChanged = new object(); static readonly object WorkflowValidating = new object(); + static readonly object WorkflowValidated = new object(); int version; int saveVersion; @@ -69,6 +70,7 @@ public partial class EditorForm : Form readonly BehaviorSubject updatesAvailable; readonly FormScheduler formScheduler; readonly TypeVisualizerMap typeVisualizers; + readonly VisualizerLayoutMap visualizerSettings; readonly List workflowElements; readonly List workflowExtensions; readonly WorkflowRuntimeExceptionCache exceptionCache; @@ -76,6 +78,7 @@ public partial class EditorForm : Form AttributeCollection browsableAttributes; DirectoryInfo extensionsPath; WorkflowBuilder workflowBuilder; + VisualizerDialogMap visualizerDialogs; WorkflowException workflowError; IDisposable running; bool debugging; @@ -154,6 +157,7 @@ public EditorForm( regularFont = new Font(toolboxDescriptionTextBox.Font, FontStyle.Regular); selectionFont = new Font(toolboxDescriptionTextBox.Font, FontStyle.Bold); typeVisualizers = new TypeVisualizerMap(); + visualizerSettings = new VisualizerLayoutMap(typeVisualizers); workflowElements = new List(); workflowExtensions = new List(); exceptionCache = new WorkflowRuntimeExceptionCache(); @@ -168,7 +172,7 @@ public EditorForm( definitionsPath = Path.Combine(Path.GetTempPath(), DefinitionsDirectory + "." + GuidHelper.GetProcessGuid().ToString()); editorControl = new WorkflowEditorControl(editorSite); editorControl.Enter += new EventHandler(editorControl_Enter); - editorControl.Workflow = workflowBuilder.Workflow; + editorControl.WorkflowPath = null; editorControl.Dock = DockStyle.Fill; workflowSplitContainer.Panel1.Controls.Add(editorControl); propertyGrid.BrowsableAttributes = browsableAttributes = DesignTimeAttributes; @@ -296,6 +300,7 @@ protected override void OnLoad(EventArgs e) handler => FormClosed -= handler); InitializeSubjectSources().TakeUntil(formClosed).Subscribe(); InitializeWorkflowFileWatcher().TakeUntil(formClosed).Subscribe(); + InitializeWorkflowExplorerWatcher().TakeUntil(formClosed).Subscribe(); updatesAvailable.TakeUntil(formClosed).ObserveOn(formScheduler).Subscribe(HandleUpdatesAvailable); var currentDirectory = Path.GetFullPath(Environment.CurrentDirectory).TrimEnd('\\'); @@ -324,7 +329,10 @@ protected override void OnLoad(EventArgs e) InitializeEditorToolboxTypes(); var shutdown = ShutdownSequence(); - var initialization = InitializeToolbox().Merge(InitializeTypeVisualizers()).TakeLast(1).Finally(shutdown.Dispose).ObserveOn(Scheduler.Default); + var initialization = InitializeToolbox().Merge(InitializeTypeVisualizers()) + .TakeLast(1) + .Finally(shutdown.Dispose) + .ObserveOn(Scheduler.Default); if (validFileName && OpenWorkflow(initialFileName, false)) { foreach (var assignment in propertyAssignments) @@ -440,7 +448,7 @@ IEnumerable EditorToolboxTypes() IObservable InitializeSubjectSources() { - var selectionChanged = Observable.FromEventPattern( + var selectedViewChanged = Observable.FromEventPattern( handler => selectionModel.SelectionChanged += handler, handler => selectionModel.SelectionChanged -= handler) .Select(evt => selectionModel.SelectedView) @@ -450,7 +458,7 @@ IObservable InitializeSubjectSources() handler => Events.RemoveHandler(WorkflowValidating, handler)) .Select(evt => selectionModel.SelectedView); return Observable - .Merge(selectionChanged, workflowValidating) + .Merge(selectedViewChanged, workflowValidating) .Do(view => { toolboxTreeView.BeginUpdate(); @@ -481,6 +489,34 @@ IObservable InitializeSubjectSources() .Select(xs => Unit.Default); } + IObservable InitializeWorkflowExplorerWatcher() + { + var selectedViewChanged = Observable.FromEventPattern( + handler => selectionModel.SelectionChanged += handler, + handler => selectionModel.SelectionChanged -= handler) + .Select(evt => selectionModel.SelectedView.WorkflowPath) + .DistinctUntilChanged() + .Do(view => explorerTreeView.SelectNode(editorControl.WorkflowGraphView.WorkflowPath)) + .IgnoreElements() + .Select(xs => Unit.Default); + + var workflowValidated = Observable.FromEventPattern( + handler => Events.AddHandler(WorkflowValidated, handler), + handler => Events.RemoveHandler(WorkflowValidated, handler)) + .Select(evt => selectionModel.SelectedView); + return Observable.Merge(selectedViewChanged, workflowValidated.Do(view => + { + if (workflowBuilder.Workflow == null) + return; + + explorerTreeView.UpdateWorkflow( + GetProjectDisplayName(), + workflowBuilder); + }) + .IgnoreElements() + .Select(xs => Unit.Default)); + } + IObservable InitializeWorkflowFileWatcher() { var extensionsDirectoryChanged = Observable.FromEventPattern( @@ -803,8 +839,8 @@ void ClearWorkflow() ClearWorkflowError(); saveWorkflowDialog.FileName = null; workflowBuilder.Workflow.Clear(); - editorControl.VisualizerLayout = null; - editorControl.Workflow = workflowBuilder.Workflow; + editorControl.WorkflowPath = null; + visualizerSettings.Clear(); ResetProjectStatus(); UpdateTitle(); } @@ -835,7 +871,7 @@ bool OpenWorkflow(string fileName, bool setWorkingDirectory) UpdateWorkflowDirectory(fileName, setWorkingDirectory); if (EditorResult == EditorResult.ReloadEditor) return false; - editorControl.Workflow = workflowBuilder.Workflow; + editorControl.WorkflowPath = null; if (workflowBuilder.Workflow.Count > 0 && !editorControl.WorkflowGraphView.GraphView.Nodes.Any()) { try { workflowBuilder.Workflow.Build(); } @@ -849,8 +885,7 @@ bool OpenWorkflow(string fileName, bool setWorkingDirectory) } workflowBuilder = PrepareWorkflow(workflowBuilder, workflowVersion, out bool upgraded); - editorControl.VisualizerLayout = null; - editorControl.Workflow = workflowBuilder.Workflow; + editorControl.WorkflowPath = null; editorSite.ValidateWorkflow(); var layoutPath = LayoutHelper.GetLayoutPath(fileName); @@ -858,7 +893,11 @@ bool OpenWorkflow(string fileName, bool setWorkingDirectory) { using (var reader = XmlReader.Create(layoutPath)) { - try { editorControl.VisualizerLayout = (VisualizerLayout)VisualizerLayout.Serializer.Deserialize(reader); } + try + { + var visualizerLayout = (VisualizerLayout)VisualizerLayout.Serializer.Deserialize(reader); + visualizerSettings.SetVisualizerLayout(workflowBuilder, visualizerLayout); + } catch (InvalidOperationException) { } } } @@ -912,11 +951,11 @@ bool SaveWorkflow(string fileName) if (!SaveWorkflowBuilder(fileName, serializerWorkflowBuilder)) return false; saveVersion = version; - editorControl.UpdateVisualizerLayout(); - if (editorControl.VisualizerLayout != null) + var visualizerLayout = visualizerSettings.GetVisualizerLayout(workflowBuilder); + if (visualizerLayout != null) { var layoutPath = LayoutHelper.GetLayoutPath(fileName); - SaveVisualizerLayout(layoutPath, editorControl.VisualizerLayout); + SaveVisualizerLayout(layoutPath, visualizerLayout); } UpdateWorkflowDirectory(fileName); @@ -979,6 +1018,11 @@ void OnWorkflowValidating(EventArgs e) (Events[WorkflowValidating] as EventHandler)?.Invoke(this, e); } + void OnWorkflowValidated(EventArgs e) + { + (Events[WorkflowValidated] as EventHandler)?.Invoke(this, e); + } + void OnExtensionsDirectoryChanged(EventArgs e) { (Events[ExtensionsDirectoryChanged] as EventHandler)?.Invoke(this, e); @@ -1196,7 +1240,11 @@ IDisposable ShutdownSequence() running = null; building = false; - editorControl.UpdateVisualizerLayout(); + if (visualizerDialogs != null) + { + visualizerSettings.Update(visualizerDialogs); + visualizerDialogs = null; + } UpdateTitle(); })); } @@ -1208,10 +1256,11 @@ void StartWorkflow(bool debug) building = true; debugging = debug; ClearWorkflowError(); + visualizerDialogs = visualizerSettings.CreateVisualizerDialogs(workflowBuilder); LayoutHelper.SetWorkflowNotifications(workflowBuilder.Workflow, debug); - if (!debug && editorControl.VisualizerLayout != null) + if (!debug) { - LayoutHelper.SetLayoutNotifications(editorControl.VisualizerLayout); + LayoutHelper.SetLayoutNotifications(workflowBuilder.Workflow, visualizerDialogs); } running = Observable.Using( @@ -1225,6 +1274,7 @@ void StartWorkflow(bool debug) { statusTextLabel.Text = Resources.RunningStatus; statusImageLabel.Image = statusRunningImage; + visualizerDialogs.Show(visualizerSettings, editorSite, this); editorSite.OnWorkflowStarted(EventArgs.Empty); Activate(); })); @@ -1299,7 +1349,10 @@ void ClearWorkflowError() { if (workflowError != null) { - ClearExceptionBuilderNode(editorControl.WorkflowGraphView, workflowError); + statusStrip.ContextMenuStrip = null; + statusTextLabel.Text = Resources.ReadyStatus; + statusImageLabel.Image = Resources.StatusReadyImage; + explorerTreeView.SetNodeStatus(ExplorerNodeStatus.Ready); } exceptionCache.Clear(); @@ -1314,91 +1367,22 @@ void HighlightWorkflowError() } } - void ClearExceptionBuilderNode(WorkflowGraphView workflowView, WorkflowException e) - { - GraphNode graphNode = null; - if (workflowView != null) - { - graphNode = workflowView.FindGraphNode(e.Builder); - if (graphNode != null) - { - workflowView.GraphView.Invalidate(graphNode); - graphNode.Highlight = false; - } - } - - if (e.InnerException is WorkflowException nestedException) - { - WorkflowGraphView nestedEditor = null; - if (workflowView != null) - { - var editorLauncher = workflowView.GetWorkflowEditorLauncher(graphNode); - nestedEditor = editorLauncher != null && editorLauncher.Visible ? editorLauncher.WorkflowGraphView : null; - } - - ClearExceptionBuilderNode(nestedEditor, nestedException); - } - else - { - statusStrip.ContextMenuStrip = null; - statusTextLabel.Text = Resources.ReadyStatus; - statusImageLabel.Image = Resources.StatusReadyImage; - } - } - void HighlightExceptionBuilderNode(WorkflowException ex, bool showMessageBox) { - HighlightExceptionBuilderNode(editorControl.WorkflowGraphView, ex, showMessageBox); - } - - void HighlightExceptionBuilderNode(WorkflowGraphView workflowView, WorkflowException ex, bool showMessageBox) - { - GraphNode graphNode = null; - if (workflowView != null) - { - graphNode = workflowView.FindGraphNode(ex.Builder); - if (graphNode == null) - { - throw new InvalidOperationException(Resources.ExceptionNodeNotFound_Error); - } - - workflowView.GraphView.Invalidate(graphNode); - if (showMessageBox) workflowView.GraphView.SelectedNode = graphNode; - graphNode.Highlight = true; - } - - var nestedException = ex.InnerException as WorkflowException; - if (nestedException != null) - { - WorkflowGraphView nestedEditor = null; - if (workflowView != null) - { - var editorLauncher = workflowView.GetWorkflowEditorLauncher(graphNode); - if (editorLauncher != null) - { - if (building && editorLauncher.Visible) workflowView.LaunchWorkflowView(graphNode); - nestedEditor = editorLauncher.WorkflowGraphView; - } - } + var workflowPath = WorkflowEditorPath.GetExceptionPath(workflowBuilder, ex); + var pathElements = workflowPath.GetPathElements(); + var selectedView = selectionModel.SelectedView; + selectedView.HighlightGraphNode(workflowPath, showMessageBox); - HighlightExceptionBuilderNode(nestedEditor, nestedException, showMessageBox); - } - else + var buildException = ex is WorkflowBuildException; + var errorCaption = buildException ? Resources.BuildError_Caption : Resources.RuntimeError_Caption; + statusTextLabel.Text = ex.Message; + statusStrip.ContextMenuStrip = statusContextMenuStrip; + statusImageLabel.Image = buildException ? Resources.StatusBlockedImage : Resources.StatusCriticalImage; + explorerTreeView.SetNodeStatus(pathElements, ExplorerNodeStatus.Blocked); + if (showMessageBox) { - if (workflowView != null) - { - workflowView.GraphView.Select(); - } - - var buildException = ex is WorkflowBuildException; - var errorCaption = buildException ? Resources.BuildError_Caption : Resources.RuntimeError_Caption; - statusTextLabel.Text = ex.Message; - statusStrip.ContextMenuStrip = statusContextMenuStrip; - statusImageLabel.Image = buildException ? Resources.StatusBlockedImage : Resources.StatusCriticalImage; - if (showMessageBox) - { - editorSite.ShowError(ex.Message, errorCaption); - } + editorSite.ShowError(ex.Message, errorCaption); } } @@ -1433,33 +1417,21 @@ void HandleWorkflowCompleted() else clearErrors(); } - void HighlightExpression(WorkflowGraphView workflowView, ExpressionScope scope) + void SelectBuilderNode(ExpressionBuilder builder) { - if (workflowView == null) + var builderPath = WorkflowEditorPath.GetBuilderPath(workflowBuilder, builder); + if (builderPath != null) { - throw new ArgumentNullException(nameof(workflowView)); - } + var selectedView = selectionModel.SelectedView; + selectedView.WorkflowPath = builderPath.Parent; - var graphNode = workflowView.FindGraphNode(scope.Value); - if (graphNode != null) - { - workflowView.GraphView.SelectedNode = graphNode; - var innerScope = scope.InnerScope; - if (innerScope != null) - { - workflowView.LaunchWorkflowView(graphNode); - var editorLauncher = workflowView.GetWorkflowEditorLauncher(graphNode); - if (editorLauncher != null) - { - HighlightExpression(editorLauncher.WorkflowGraphView, innerScope); - } - } - else + var graphNode = selectedView.FindGraphNode(builderPath.Resolve(workflowBuilder)); + if (graphNode == null) { - var ownerForm = workflowView.EditorControl.ParentForm; - if (ownerForm != null) ownerForm.Activate(); - workflowView.SelectGraphNode(graphNode); + throw new InvalidOperationException(Resources.ExceptionNodeNotFound_Error); } + + selectedView.SelectGraphNode(graphNode); } } @@ -1557,13 +1529,13 @@ selectedNode.Tag is not null && void editorControl_Enter(object sender, EventArgs e) { var selectedView = selectionModel.SelectedView; - if (selectedView != null && selectedView.Launcher != null) + if (selectedView != null) { var container = selectedView.EditorControl; if (container != null && container != editorControl && hotKeys.TabState) { container.ParentForm.Activate(); - var forward = Form.ModifierKeys.HasFlag(Keys.Shift); + var forward = ModifierKeys.HasFlag(Keys.Shift); container.SelectNextControl(container.ActiveControl, forward, true, true, false); } } @@ -1623,18 +1595,16 @@ private void UpdatePropertyGrid() if (!hasSelectedObjects && selectedView != null) { // Select externalized properties - var launcher = selectedView.Launcher; - if (launcher != null) + if (selectedView.WorkflowPath != null) { - displayName = ElementHelper.GetElementName(launcher.Builder); - description = ElementHelper.GetElementDescription(launcher.Builder); + var builder = ExpressionBuilder.Unwrap(selectedView.WorkflowPath.Resolve(workflowBuilder)); + displayName = ElementHelper.GetElementName(builder); + description = ElementHelper.GetElementDescription(builder); } else { description = workflowBuilder.Description ?? Resources.WorkflowPropertiesDescription; - displayName = !string.IsNullOrEmpty(saveWorkflowDialog.FileName) - ? Path.GetFileNameWithoutExtension(saveWorkflowDialog.FileName) - : editorControl.ActiveTab.TabPage.Text; + displayName = GetProjectDisplayName(); } propertyGrid.SelectedObject = selectedView.Workflow; @@ -1883,11 +1853,16 @@ void FindNextMatch(Func predicate, ExpressionBuilder cu var match = workflowBuilder.Find(predicate, current, findPrevious); if (match != null) { - var scope = workflowBuilder.GetExpressionScope(match); - HighlightExpression(editorControl.WorkflowGraphView, scope); + SelectBuilderNode(match); } } + private void explorerTreeView_NodeMouseDoubleClick(object sender, TreeNodeMouseClickEventArgs e) + { + var workflowPath = (WorkflowEditorPath)e.Node?.Tag; + editorControl.WorkflowGraphView.WorkflowPath = workflowPath; + } + private void toolboxTreeView_KeyDown(object sender, KeyEventArgs e) { var selectedNode = toolboxTreeView.SelectedNode; @@ -1930,8 +1905,7 @@ private void toolboxTreeView_KeyDown(object sender, KeyEventArgs e) } else { - var scope = workflowBuilder.GetExpressionScope(definition.Subject); - HighlightExpression(editorControl.WorkflowGraphView, scope); + SelectBuilderNode(definition.Subject); } } } @@ -2509,6 +2483,16 @@ public object GetService(Type serviceType) return siteForm.typeVisualizers; } + if (serviceType == typeof(VisualizerLayoutMap)) + { + return siteForm.visualizerSettings; + } + + if (serviceType == typeof(VisualizerDialogMap)) + { + return siteForm.visualizerDialogs; + } + if (serviceType == typeof(ThemeRenderer)) { return siteForm.themeRenderer; @@ -2521,11 +2505,10 @@ public object GetService(Type serviceType) if (serviceType == typeof(DialogTypeVisualizer)) { - var selectedView = siteForm.selectionModel.SelectedView; var selectedNode = siteForm.selectionModel.SelectedNodes.FirstOrDefault(); - if (selectedNode != null) + if (selectedNode != null && selectedNode.Value is InspectBuilder builder && + siteForm.visualizerDialogs.TryGetValue(builder, out VisualizerDialogLauncher visualizerDialog)) { - var visualizerDialog = selectedView.GetVisualizerDialogLauncher(selectedNode); var visualizer = visualizerDialog.Visualizer; if (visualizer.IsValueCreated) { @@ -2676,6 +2659,11 @@ public void SelectNextControl(bool forward) siteForm.Activate(); } + public void SelectBuilderNode(ExpressionBuilder builder) + { + siteForm.SelectBuilderNode(builder); + } + public bool ValidateWorkflow() { if (siteForm.running == null) @@ -2685,6 +2673,7 @@ public bool ValidateWorkflow() siteForm.OnWorkflowValidating(EventArgs.Empty); siteForm.ClearWorkflowError(); siteForm.workflowBuilder.Workflow.Build(); + siteForm.OnWorkflowValidated(EventArgs.Empty); } catch (WorkflowBuildException ex) { @@ -2750,8 +2739,7 @@ public void ShowDefinition(object component) var definition = siteForm.workflowBuilder.GetSubjectDefinition(model.Workflow, namedElement.Name); if (definition != null) { - var scope = siteForm.workflowBuilder.GetExpressionScope(definition.Subject); - siteForm.HighlightExpression(siteForm.editorControl.WorkflowGraphView, scope); + siteForm.SelectBuilderNode(definition.Subject); return; } } @@ -3028,6 +3016,9 @@ private void InitializeTheme() toolboxTreeView.Renderer = themeRenderer.ToolStripRenderer; toolboxDescriptionTextBox.BackColor = panelColor; toolboxDescriptionTextBox.ForeColor = ForeColor; + explorerTreeView.Renderer = themeRenderer.ToolStripRenderer; + explorerLabel.BackColor = colorTable.SeparatorDark; + explorerLabel.ForeColor = ForeColor; propertiesDescriptionTextBox.BackColor = panelColor; propertiesDescriptionTextBox.ForeColor = ForeColor; menuStrip.ForeColor = SystemColors.ControlText; @@ -3044,6 +3035,7 @@ private void InitializeTheme() } propertiesLayoutPanel.RowStyles[0].Height -= labelOffset; toolboxLayoutPanel.RowStyles[0].Height -= labelOffset; + explorerLayoutPanel.RowStyles[0].Height -= labelOffset; propertyGrid.Refresh(); } diff --git a/Bonsai.Editor/GraphModel/WorkflowBuilderExtensions.cs b/Bonsai.Editor/GraphModel/WorkflowBuilderExtensions.cs index b6b8b8f0a..3b3750a6b 100644 --- a/Bonsai.Editor/GraphModel/WorkflowBuilderExtensions.cs +++ b/Bonsai.Editor/GraphModel/WorkflowBuilderExtensions.cs @@ -91,47 +91,6 @@ public static IEnumerable GetDependentExpressions(this SubjectD } } } - - public static ExpressionScope GetExpressionScope(this WorkflowBuilder source, ExpressionBuilder target) - { - return GetExpressionScope(source.Workflow, target); - } - - static ExpressionScope GetExpressionScope(ExpressionBuilderGraph source, ExpressionBuilder target) - { - foreach (var node in source) - { - var builder = ExpressionBuilder.Unwrap(node.Value); - if (builder == target) - { - return new ExpressionScope(node.Value, innerScope: null); - } - - if (builder is IWorkflowExpressionBuilder workflowBuilder) - { - var innerScope = GetExpressionScope(workflowBuilder.Workflow, target); - if (innerScope != null) - { - return new ExpressionScope(node.Value, innerScope); - } - } - } - - return null; - } - } - - class ExpressionScope - { - public ExpressionScope(ExpressionBuilder value, ExpressionScope innerScope) - { - Value = value; - InnerScope = innerScope; - } - - public ExpressionBuilder Value { get; } - - public ExpressionScope InnerScope { get; } } class SubjectDefinition diff --git a/Bonsai.Editor/GraphModel/WorkflowEditor.cs b/Bonsai.Editor/GraphModel/WorkflowEditor.cs index 8453da7a6..c7fd40b15 100644 --- a/Bonsai.Editor/GraphModel/WorkflowEditor.cs +++ b/Bonsai.Editor/GraphModel/WorkflowEditor.cs @@ -20,10 +20,10 @@ class WorkflowEditor readonly IGraphView graphView; readonly Subject error; readonly Subject updateLayout; - readonly Subject updateParentLayout; readonly Subject invalidateLayout; readonly Subject> updateSelection; readonly Subject closeWorkflowEditor; + WorkflowEditorPath workflowPath; public WorkflowEditor(IServiceProvider provider, IGraphView view) { @@ -32,20 +32,47 @@ public WorkflowEditor(IServiceProvider provider, IGraphView view) commandExecutor = (CommandExecutor)provider.GetService(typeof(CommandExecutor)); error = new Subject(); updateLayout = new Subject(); - updateParentLayout = new Subject(); invalidateLayout = new Subject(); updateSelection = new Subject>(); closeWorkflowEditor = new Subject(); + WorkflowPath = null; } - public ExpressionBuilderGraph Workflow { get; set; } + public ExpressionBuilderGraph Workflow { get; private set; } + + public bool IsReadOnly { get; private set; } + + public WorkflowEditorPath WorkflowPath + { + get { return workflowPath; } + set + { + workflowPath = value; + var workflowBuilder = (WorkflowBuilder)serviceProvider.GetService(typeof(WorkflowBuilder)); + if (workflowPath != null) + { + var builder = ExpressionBuilder.Unwrap(workflowPath.Resolve(workflowBuilder, out bool isReadOnly)); + if (builder is not IWorkflowExpressionBuilder workflowExpressionBuilder) + { + throw new ArgumentException(Resources.InvalidWorkflowPath_Error, nameof(value)); + } + + Workflow = workflowExpressionBuilder.Workflow; + IsReadOnly = isReadOnly; + } + else + { + Workflow = workflowBuilder.Workflow; + IsReadOnly = false; + } + updateLayout.OnNext(false); + } + } public IObservable Error => error; public IObservable UpdateLayout => updateLayout; - public IObservable UpdateParentLayout => updateParentLayout; - public IObservable InvalidateLayout => invalidateLayout; public IObservable> UpdateSelection => updateSelection; @@ -170,8 +197,6 @@ private void AddWorkflowInput(ExpressionBuilderGraph workflow, Node layer).FirstOrDefault(n => n.Value == value); } + + public void NavigateTo(WorkflowEditorPath path) + { + if (path == workflowPath) + return; + + var previousPath = workflowPath; + var selectedNodes = graphView.SelectedNodes.ToArray(); + var restoreSelectedNodes = CreateUpdateSelectionDelegate(selectedNodes); + commandExecutor.Execute( + () => WorkflowPath = path, + () => + { + WorkflowPath = previousPath; + restoreSelectedNodes(); + }); + } } enum CreateGraphNodeType diff --git a/Bonsai.Editor/GraphView/WorkflowEditorControl.cs b/Bonsai.Editor/GraphView/WorkflowEditorControl.cs index 36c5234e8..b3344be6a 100644 --- a/Bonsai.Editor/GraphView/WorkflowEditorControl.cs +++ b/Bonsai.Editor/GraphView/WorkflowEditorControl.cs @@ -3,7 +3,6 @@ using System.Drawing; using System.Windows.Forms; using Bonsai.Expressions; -using Bonsai.Design; using Bonsai.Editor.Themes; using Bonsai.Editor.GraphModel; @@ -18,17 +17,12 @@ partial class WorkflowEditorControl : UserControl Padding? adjustMargin; public WorkflowEditorControl(IServiceProvider provider) - : this(provider, false) - { - } - - public WorkflowEditorControl(IServiceProvider provider, bool readOnly) { InitializeComponent(); serviceProvider = provider ?? throw new ArgumentNullException(nameof(provider)); editorService = (IWorkflowEditorService)provider.GetService(typeof(IWorkflowEditorService)); themeRenderer = (ThemeRenderer)provider.GetService(typeof(ThemeRenderer)); - workflowTab = InitializeTab(workflowTabPage, readOnly, null); + workflowTab = InitializeTab(workflowTabPage); annotationPanel.ThemeRenderer = themeRenderer; annotationPanel.LinkClicked += (sender, e) => { EditorDialog.OpenUrl(e.LinkText); }; annotationPanel.CloseRequested += delegate { CollapseAnnotationPanel(); }; @@ -60,16 +54,15 @@ public int AnnotationPanelSize } } - public VisualizerLayout VisualizerLayout + public WorkflowEditorPath WorkflowPath { - get { return WorkflowGraphView.VisualizerLayout; } - set { WorkflowGraphView.VisualizerLayout = value; } + get { return WorkflowGraphView.WorkflowPath; } + set { WorkflowGraphView.WorkflowPath = value; } } public ExpressionBuilderGraph Workflow { get { return WorkflowGraphView.Workflow; } - set { WorkflowGraphView.Workflow = value; } } public void ExpandAnnotationPanel(ExpressionBuilder builder) @@ -91,11 +84,6 @@ public void CollapseAnnotationPanel() annotationPanel.Tag = null; } - public void UpdateVisualizerLayout() - { - WorkflowGraphView.UpdateVisualizerLayout(); - } - public TabPageController ActiveTab { get; private set; } public int ItemHeight @@ -103,9 +91,9 @@ public int ItemHeight get { return tabControl.DisplayRectangle.Y; } } - TabPageController InitializeTab(TabPage tabPage, bool readOnly, Control container) + TabPageController InitializeTab(TabPage tabPage) { - var workflowGraphView = new WorkflowGraphView(serviceProvider, this, readOnly); + var workflowGraphView = new WorkflowGraphView(serviceProvider, this); workflowGraphView.BackColorChanged += (sender, e) => { tabPage.BackColor = workflowGraphView.BackColor; @@ -118,42 +106,43 @@ TabPageController InitializeTab(TabPage tabPage, bool readOnly, Control containe var tabState = new TabPageController(tabPage, workflowGraphView); tabPage.Tag = tabState; tabPage.SuspendLayout(); - if (container != null) + + var breadcrumbs = new WorkflowPathNavigationControl(serviceProvider); + breadcrumbs.WorkflowPath = null; + breadcrumbs.WorkflowPathMouseClick += (sender, e) => workflowGraphView.WorkflowPath = e.Path; + workflowGraphView.WorkflowPathChanged += (sender, e) => { - container.TextChanged += (sender, e) => tabState.Text = container.Text; - container.Controls.Add(workflowGraphView); - tabPage.Controls.Add(container); - } - else tabPage.Controls.Add(workflowGraphView); + breadcrumbs.WorkflowPath = workflowGraphView.WorkflowPath; + tabState.Text = breadcrumbs.DisplayName; + }; + + var navigationPanel = new TableLayoutPanel(); + navigationPanel.Dock = DockStyle.Fill; + navigationPanel.ColumnCount = 1; + navigationPanel.RowCount = 2; + navigationPanel.RowStyles.Add(new RowStyle(SizeType.Absolute, breadcrumbs.Height)); + navigationPanel.RowStyles.Add(new RowStyle(SizeType.AutoSize)); + navigationPanel.Controls.Add(breadcrumbs); + navigationPanel.Controls.Add(workflowGraphView); + tabPage.Controls.Add(navigationPanel); tabPage.BackColor = workflowGraphView.BackColor; tabPage.ResumeLayout(false); tabPage.PerformLayout(); return tabState; } - public TabPageController CreateTab(IWorkflowExpressionBuilder builder, bool readOnly, Control owner) + public TabPageController CreateTab(WorkflowEditorPath workflowPath) { var tabPage = new TabPage(); tabPage.Padding = workflowTabPage.Padding; tabPage.UseVisualStyleBackColor = workflowTabPage.UseVisualStyleBackColor; - var tabState = InitializeTab(tabPage, readOnly || builder is IncludeWorkflowBuilder, owner); - tabState.Text = ExpressionBuilder.GetElementDisplayName(builder); - tabState.WorkflowGraphView.Workflow = builder.Workflow; - tabState.Builder = builder; + var tabState = InitializeTab(tabPage); + tabState.WorkflowGraphView.WorkflowPath = workflowPath; tabControl.TabPages.Add(tabPage); return tabState; } - public void SelectTab(IWorkflowExpressionBuilder builder) - { - var tabPage = FindTab(builder); - if (tabPage != null) - { - tabControl.SelectTab(tabPage); - } - } - public void SelectTab(WorkflowGraphView workflowGraphView) { var tabPage = (TabPage)workflowGraphView.Tag; @@ -164,23 +153,10 @@ public void SelectTab(WorkflowGraphView workflowGraphView) } } - public void CloseTab(IWorkflowExpressionBuilder builder) - { - var tabPage = FindTab(builder); - if (tabPage != null) - { - var tabState = (TabPageController)tabPage.Tag; - CloseTab(tabState); - } - } - void CloseTab(TabPage tabPage) { var tabState = (TabPageController)tabPage.Tag; - if (tabState.Builder != null) - { - CloseTab(tabState); - } + CloseTab(tabState); } void CloseTab(TabPageController tabState) @@ -202,47 +178,12 @@ void CloseTab(TabPageController tabState) } } - public void RefreshTab(IWorkflowExpressionBuilder builder) - { - var tabPage = FindTab(builder); - if (tabPage != null) - { - var tabState = (TabPageController)tabPage.Tag; - RefreshTab(tabState); - } - } - - void RefreshTab(TabPageController tabState) - { - var builder = tabState.Builder; - var workflowGraphView = tabState.WorkflowGraphView; - if (builder != null && builder.Workflow != workflowGraphView.Workflow) - { - CloseTab(tabState); - } - } - - TabPage FindTab(IWorkflowExpressionBuilder builder) - { - foreach (TabPage tabPage in tabControl.TabPages) - { - var tabState = (TabPageController)tabPage.Tag; - if (tabState.Builder == builder) - { - return tabPage; - } - } - - return null; - } - void ActivateTab(TabPage tabPage) { var tabState = tabPage != null ? (TabPageController)tabPage.Tag : null; if (tabState != null && ActiveTab != tabState) { ActiveTab = tabState; - RefreshTab(ActiveTab); ActiveTab.UpdateSelection(); } } @@ -285,8 +226,6 @@ public TabPageController(TabPage tabPage, WorkflowGraphView graphView) public TabPage TabPage { get; private set; } - public IWorkflowExpressionBuilder Builder { get; set; } - public WorkflowGraphView WorkflowGraphView { get; private set; } public string Text @@ -301,7 +240,8 @@ public string Text void UpdateDisplayText() { - TabPage.Text = displayText + (WorkflowGraphView.ReadOnly ? ReadOnlySuffix : string.Empty) + CloseSuffix; + //TabPage.Text = displayText + (WorkflowGraphView.ReadOnly ? ReadOnlySuffix : string.Empty) + CloseSuffix; + TabPage.Text = displayText + (WorkflowGraphView.IsReadOnly ? ReadOnlySuffix : string.Empty); } public void UpdateSelection() @@ -361,7 +301,7 @@ void tabControl_MouseUp(object sender, MouseEventArgs e) var tabState = (TabPageController)selectedTab.Tag; var tabRect = tabControl.GetTabRect(tabControl.SelectedIndex); - if (tabState.Builder != null && tabRect.Contains(e.Location)) + if (selectedTab != workflowTabPage && tabRect.Contains(e.Location)) { using (var graphics = selectedTab.CreateGraphics()) { diff --git a/Bonsai.Editor/GraphView/WorkflowGraphView.cs b/Bonsai.Editor/GraphView/WorkflowGraphView.cs index 539aacea4..fb59d0468 100644 --- a/Bonsai.Editor/GraphView/WorkflowGraphView.cs +++ b/Bonsai.Editor/GraphView/WorkflowGraphView.cs @@ -21,10 +21,10 @@ namespace Bonsai.Editor.GraphView { partial class WorkflowGraphView : UserControl { - static readonly Action EmptyAction = () => { }; static readonly Cursor InvalidSelectionCursor = Cursors.No; static readonly Cursor MoveSelectionCursor = Cursors.SizeAll; static readonly Cursor AlternateSelectionCursor = Cursors.UpArrow; + static readonly object WorkflowPathChangedEvent = new(); const int RightMouseButton = 0x2; const int ShiftModifier = 0x4; @@ -36,7 +36,6 @@ partial class WorkflowGraphView : UserControl public const string BonsaiExtension = ".bonsai"; int dragKeyState; - bool editorLaunching; bool isContextMenuSource; GraphNode dragHighlight; IEnumerable dragSelection; @@ -45,31 +44,17 @@ partial class WorkflowGraphView : UserControl readonly IWorkflowEditorState editorState; readonly IWorkflowEditorService editorService; readonly TypeVisualizerMap typeVisualizerMap; - readonly Dictionary workflowEditorMapping; + readonly VisualizerLayoutMap visualizerSettings; readonly IServiceProvider serviceProvider; readonly IUIService uiService; readonly ThemeRenderer themeRenderer; readonly IDefinitionProvider definitionProvider; - Dictionary visualizerMapping; - VisualizerLayout visualizerLayout; - ExpressionBuilderGraph workflow; - public WorkflowGraphView(IServiceProvider provider, WorkflowEditorControl owner, bool readOnly) + public WorkflowGraphView(IServiceProvider provider, WorkflowEditorControl owner) { - if (provider == null) - { - throw new ArgumentNullException(nameof(provider)); - } - - if (owner == null) - { - throw new ArgumentNullException(nameof(owner)); - } - + EditorControl = owner ?? throw new ArgumentNullException(nameof(owner)); + serviceProvider = provider ?? throw new ArgumentNullException(nameof(provider)); InitializeComponent(); - EditorControl = owner; - serviceProvider = provider; - ReadOnly = readOnly; Editor = new WorkflowEditor(provider, graphView); uiService = (IUIService)provider.GetService(typeof(IUIService)); themeRenderer = (ThemeRenderer)provider.GetService(typeof(ThemeRenderer)); @@ -79,11 +64,10 @@ public WorkflowGraphView(IServiceProvider provider, WorkflowEditorControl owner, selectionModel = (WorkflowSelectionModel)provider.GetService(typeof(WorkflowSelectionModel)); editorService = (IWorkflowEditorService)provider.GetService(typeof(IWorkflowEditorService)); typeVisualizerMap = (TypeVisualizerMap)provider.GetService(typeof(TypeVisualizerMap)); + visualizerSettings = (VisualizerLayoutMap)provider.GetService(typeof(VisualizerLayoutMap)); editorState = (IWorkflowEditorState)provider.GetService(typeof(IWorkflowEditorState)); - workflowEditorMapping = new Dictionary(); graphView.HandleDestroyed += graphView_HandleDestroyed; - editorState.WorkflowStarted += editorService_WorkflowStarted; themeRenderer.ThemeChanged += themeRenderer_ThemeChanged; InitializeTheme(); InitializeViewBindings(); @@ -91,15 +75,16 @@ public WorkflowGraphView(IServiceProvider provider, WorkflowEditorControl owner, internal WorkflowEditor Editor { get; } - internal WorkflowEditorLauncher Launcher { get; set; } - internal WorkflowEditorControl EditorControl { get; } - internal bool ReadOnly { get; } + internal bool IsReadOnly + { + get { return Editor.IsReadOnly; } + } internal bool CanEdit { - get { return !ReadOnly && !editorState.WorkflowRunning; } + get { return !Editor.IsReadOnly && !editorState.WorkflowRunning; } } public GraphViewControl GraphView @@ -107,138 +92,46 @@ public GraphViewControl GraphView get { return graphView; } } - public VisualizerLayout VisualizerLayout + public WorkflowEditorPath WorkflowPath { - get { return visualizerLayout; } - set { SetVisualizerLayout(value); } - } - - public ExpressionBuilderGraph Workflow - { - get { return workflow; } + get { return Editor.WorkflowPath; } set { - ClearEditorMapping(); - workflow = value; - Editor.Workflow = value; - UpdateEditorWorkflow(); + Editor.WorkflowPath = value; + UpdateSelection(forceUpdate: true); + OnWorkflowPathChanged(EventArgs.Empty); } } - public static ElementCategory GetToolboxElementCategory(TreeNode typeNode) + public event EventHandler WorkflowPathChanged { - var elementCategories = (ElementCategory[])typeNode.Tag; - for (int i = 0; i < elementCategories.Length; i++) - { - if (elementCategories[i] == ElementCategory.Nested) continue; - return elementCategories[i]; - } - - return ElementCategory.Combinator; + add { Events.AddHandler(WorkflowPathChangedEvent, value); } + remove { Events.RemoveHandler(WorkflowPathChangedEvent, value); } } - private Func CreateWindowOwnerSelectorDelegate() - { - return Launcher != null ? (Func)(() => Launcher.Owner) : () => graphView; - } - - private Action CreateUpdateEditorMappingDelegate(Action> action) + public ExpressionBuilderGraph Workflow { - return Launcher != null - ? (Action)(() => action(Launcher.WorkflowGraphView.workflowEditorMapping)) - : () => action(workflowEditorMapping); - } - - #region Model - - private void HideWorkflowEditorLauncher(WorkflowEditorLauncher editorLauncher) - { - var visible = editorLauncher.Visible; - var serviceProvider = this.serviceProvider; - var windowSelector = CreateWindowOwnerSelectorDelegate(); - var activeTabClosing = editorLauncher.Container != null && - editorLauncher.Container.ActiveTab != null && - editorLauncher.Container.ActiveTab.WorkflowGraphView == editorLauncher.WorkflowGraphView; - commandExecutor.Execute( - editorLauncher.Hide, - () => - { - if (visible && editorLauncher.Builder.Workflow != null) - { - editorLauncher.Show(windowSelector(), serviceProvider); - if (editorLauncher.Container != null && activeTabClosing) - { - editorLauncher.Container.SelectTab(editorLauncher.WorkflowGraphView); - } - } - }); + get { return Editor.Workflow; } } - private void UpdateEditorWorkflow() + private void OnWorkflowPathChanged(EventArgs e) { - UpdateGraphLayout(validateWorkflow: false); - if (editorState.WorkflowRunning) - { - InitializeVisualizerMapping(); - } + (Events[WorkflowPathChangedEvent] as EventHandler)?.Invoke(this, e); } - internal void HideEditorMapping() + public static ElementCategory GetToolboxElementCategory(TreeNode typeNode) { - foreach (var mapping in workflowEditorMapping) + var elementCategories = (ElementCategory[])typeNode.Tag; + for (int i = 0; i < elementCategories.Length; i++) { - mapping.Value.Hide(); + if (elementCategories[i] == ElementCategory.Nested) continue; + return elementCategories[i]; } - } - private void ClearEditorMapping() - { - HideEditorMapping(); - workflowEditorMapping.Clear(); - } - - private void InitializeVisualizerMapping() - { - if (workflow == null) return; - visualizerMapping = LayoutHelper.CreateVisualizerMapping( - workflow, - visualizerLayout, - typeVisualizerMap, - serviceProvider, - graphView, - this); - } - - private void CloseWorkflowEditorLauncher(IWorkflowExpressionBuilder workflowExpressionBuilder) - { - CloseWorkflowEditorLauncher(workflowExpressionBuilder, true); + return ElementCategory.Combinator; } - private void CloseWorkflowEditorLauncher(IWorkflowExpressionBuilder workflowExpressionBuilder, bool removeEditorMapping) - { - if (workflowEditorMapping.TryGetValue(workflowExpressionBuilder, out WorkflowEditorLauncher editorLauncher)) - { - if (editorLauncher.Visible) - { - var workflowGraphView = editorLauncher.WorkflowGraphView; - foreach (var node in workflowGraphView.workflow) - { - var nestedBuilder = ExpressionBuilder.Unwrap(node.Value) as IWorkflowExpressionBuilder; - if (nestedBuilder != null) - { - workflowGraphView.CloseWorkflowEditorLauncher(nestedBuilder, removeEditorMapping); - } - } - } - - HideWorkflowEditorLauncher(editorLauncher); - var removeMapping = removeEditorMapping - ? CreateUpdateEditorMappingDelegate(editorMapping => editorMapping.Remove(workflowExpressionBuilder)) - : EmptyAction; - var addMapping = CreateUpdateEditorMappingDelegate(editorMapping => editorMapping[workflowExpressionBuilder] = editorLauncher); - commandExecutor.Execute(removeMapping, addMapping); - } - } + #region Model private void InsertWorkflow(ExpressionBuilderGraph workflow) { @@ -385,6 +278,32 @@ internal void SelectGraphNode(GraphNode node) UpdateSelection(); } + internal void HighlightGraphNode(WorkflowEditorPath path, bool selectNode) + { + if (selectNode) + WorkflowPath = path?.Parent; + + while (path != null) + { + if (path.Parent == WorkflowPath) + { + var builder = Workflow[path.Index].Value; + var graphNode = FindGraphNode(builder); + if (graphNode == null) + { + throw new InvalidOperationException(Resources.ExceptionNodeNotFound_Error); + } + + GraphView.Invalidate(graphNode); + if (selectNode) GraphView.SelectedNode = graphNode; + graphNode.Highlight = true; + break; + } + + path = path.Parent; + } + } + private bool HasDefaultEditor(ExpressionBuilder builder) { if (builder is IWorkflowExpressionBuilder) return true; @@ -496,10 +415,18 @@ private void LaunchVisualizer(GraphNode node) return; } - var visualizerLauncher = GetVisualizerDialogLauncher(node); - if (visualizerLauncher != null) + var builder = (InspectBuilder)Workflow[node.Index].Value; + var visualizerDialogs = (VisualizerDialogMap)serviceProvider.GetService(typeof(VisualizerDialogMap)); + if (visualizerDialogs != null) { - visualizerLauncher.Show(graphView, serviceProvider); + if (!visualizerDialogs.TryGetValue(builder, out VisualizerDialogLauncher visualizerLauncher)) + { + visualizerSettings.TryGetValue(builder, out VisualizerDialogSettings dialogSettings); + visualizerLauncher = visualizerDialogs.Add(builder, Workflow, dialogSettings); + } + + var ownerWindow = uiService.GetDialogOwnerWindow(); + visualizerLauncher.Show(ownerWindow, serviceProvider); } } @@ -541,85 +468,22 @@ private void LaunchDefinition(GraphNode node) } public void LaunchWorkflowView(GraphNode node) - { - CreateWorkflowView(node, null, Rectangle.Empty, launch: true, activate: true); - } - - private void CreateWorkflowView(GraphNode node, VisualizerLayout editorLayout, Rectangle bounds, bool launch, bool activate) { var builder = WorkflowEditor.GetGraphNodeBuilder(node); var disableBuilder = builder as DisableBuilder; var workflowExpressionBuilder = (disableBuilder != null ? disableBuilder.Builder : builder) as IWorkflowExpressionBuilder; - if (workflowExpressionBuilder == null || editorLaunching) return; - - editorLaunching = true; - var parentLaunching = Launcher != null && Launcher.ParentView.editorLaunching; - var compositeExecutor = new Lazy(() => - { - if (!parentLaunching) commandExecutor.BeginCompositeCommand(); - return commandExecutor; - }, false); - - try - { - if (!workflowEditorMapping.TryGetValue(workflowExpressionBuilder, out WorkflowEditorLauncher editorLauncher)) - { - Func parentSelector; - Func containerSelector; - if (workflowExpressionBuilder is IncludeWorkflowBuilder || - workflowExpressionBuilder is GroupWorkflowBuilder) - { - containerSelector = () => Launcher != null ? Launcher.WorkflowGraphView.EditorControl : EditorControl; - } - else containerSelector = () => null; - parentSelector = () => Launcher != null ? Launcher.WorkflowGraphView : this; - - editorLauncher = new WorkflowEditorLauncher(workflowExpressionBuilder, parentSelector, containerSelector); - editorLauncher.VisualizerLayout = editorLayout; - editorLauncher.Bounds = bounds; - var addEditorMapping = CreateUpdateEditorMappingDelegate(editorMapping => editorMapping.Add(workflowExpressionBuilder, editorLauncher)); - var removeEditorMapping = CreateUpdateEditorMappingDelegate(editorMapping => editorMapping.Remove(workflowExpressionBuilder)); - compositeExecutor.Value.Execute(addEditorMapping, removeEditorMapping); - } - - if (launch && (!editorLauncher.Visible || activate)) - { - var highlight = node.Highlight; - var visible = editorLauncher.Visible; - var editorService = this.editorService; - var serviceProvider = this.serviceProvider; - var windowSelector = CreateWindowOwnerSelectorDelegate(); - Action launchEditor = () => - { - if (editorLauncher.Builder.Workflow != null) - { - editorLauncher.Show(windowSelector(), serviceProvider); - if (editorLauncher.Container != null && !parentLaunching && activate) - { - editorLauncher.Container.SelectTab(editorLauncher.WorkflowGraphView); - } - - if (highlight && !visible) - { - editorService.RefreshEditor(); - } - } - }; - - if (visible) launchEditor(); - else compositeExecutor.Value.Execute(launchEditor, editorLauncher.Hide); - } - } - finally + if (workflowExpressionBuilder != null) { - if (compositeExecutor.IsValueCreated && !parentLaunching) - { - compositeExecutor.Value.EndCompositeCommand(); - } - editorLaunching = false; + var newPath = new WorkflowEditorPath(node.Index, WorkflowPath); + LaunchWorkflowPath(newPath); } } + private void LaunchWorkflowPath(WorkflowEditorPath path) + { + Editor.NavigateTo(path); + } + internal void UpdateSelection() { UpdateSelection(forceUpdate: false); @@ -635,166 +499,6 @@ internal void UpdateSelection(bool forceUpdate) } } - internal void CloseWorkflowView(IWorkflowExpressionBuilder workflowExpressionBuilder) - { - commandExecutor.BeginCompositeCommand(); - CloseWorkflowEditorLauncher(workflowExpressionBuilder, false); - commandExecutor.EndCompositeCommand(); - } - - public VisualizerDialogLauncher GetVisualizerDialogLauncher(GraphNode node) - { - VisualizerDialogLauncher visualizerDialog = null; - if (visualizerMapping != null && node?.Value is InspectBuilder inspectBuilder) - { - visualizerMapping.TryGetValue(inspectBuilder, out visualizerDialog); - } - - return visualizerDialog; - } - - public WorkflowEditorLauncher GetWorkflowEditorLauncher(GraphNode node) - { - var builder = WorkflowEditor.GetGraphNodeBuilder(node); - var disableBuilder = builder as DisableBuilder; - if (disableBuilder != null) builder = disableBuilder.Builder; - - var workflowExpressionBuilder = builder as IWorkflowExpressionBuilder; - if (workflowExpressionBuilder != null) - { - workflowEditorMapping.TryGetValue(workflowExpressionBuilder, out WorkflowEditorLauncher editorLauncher); - return editorLauncher; - } - - return null; - } - - private VisualizerDialogSettings CreateLayoutSettings(ExpressionBuilder builder) - { - VisualizerDialogSettings dialogSettings; - if (ExpressionBuilder.GetWorkflowElement(builder) is IWorkflowExpressionBuilder workflowExpressionBuilder && - workflowEditorMapping.TryGetValue(workflowExpressionBuilder, out WorkflowEditorLauncher editorLauncher)) - { - if (editorLauncher.Visible) editorLauncher.UpdateEditorLayout(); - dialogSettings = new WorkflowEditorSettings - { - EditorVisualizerLayout = editorLauncher.Visible ? editorLauncher.VisualizerLayout : null, - EditorDialogSettings = new VisualizerDialogSettings - { - Visible = editorLauncher.Visible, - Bounds = editorLauncher.Bounds, - Tag = editorLauncher - } - }; - } - else dialogSettings = new VisualizerDialogSettings(); - dialogSettings.Tag = builder; - return dialogSettings; - } - - private void SetVisualizerLayout(VisualizerLayout layout) - { - if (workflow == null) - { - throw new InvalidOperationException(Resources.VisualizerLayoutOnNullWorkflow_Error); - } - - visualizerLayout = layout ?? new VisualizerLayout(); - foreach (var node in workflow) - { - var layoutSettings = visualizerLayout.GetLayoutSettings(node.Value); - if (layoutSettings == null) - { - layoutSettings = CreateLayoutSettings(node.Value); - visualizerLayout.DialogSettings.Add(layoutSettings); - } - else layoutSettings.Tag = node.Value; - - var graphNode = graphView.Nodes.SelectMany(layer => layer).First(n => n.Value == node.Value); - if (layoutSettings is WorkflowEditorSettings workflowEditorSettings && - workflowEditorSettings.EditorDialogSettings.Tag == null) - { - var editorLayout = workflowEditorSettings.EditorVisualizerLayout; - var editorVisible = workflowEditorSettings.EditorDialogSettings.Visible; - var editorBounds = workflowEditorSettings.EditorDialogSettings.Bounds; - CreateWorkflowView(graphNode, - editorLayout, - editorBounds, - launch: editorVisible, - activate: false); - } - } - } - - public void UpdateVisualizerLayout() - { - var updatedLayout = new VisualizerLayout(); - var topologicalOrder = workflow.TopologicalSort(); - foreach (var node in topologicalOrder) - { - var builder = node.Value; - VisualizerDialogSettings dialogSettings; - if (visualizerMapping != null && - visualizerMapping.TryGetValue(builder as InspectBuilder, out VisualizerDialogLauncher visualizerDialog)) - { - var visible = visualizerDialog.Visible; - if (!editorState.WorkflowRunning) - { - visualizerDialog.Hide(); - } - - var visualizer = visualizerDialog.Visualizer; - dialogSettings = CreateLayoutSettings(builder); - dialogSettings.Visible = visible; - dialogSettings.Bounds = visualizerDialog.Bounds; - dialogSettings.WindowState = visualizerDialog.WindowState; - - if (visualizer.IsValueCreated) - { - var visualizerType = visualizer.Value.GetType(); - if (visualizerType.IsPublic) - { - dialogSettings.VisualizerTypeName = visualizerType.FullName; - dialogSettings.VisualizerSettings = LayoutHelper.SerializeVisualizerSettings( - visualizer.Value, - topologicalOrder); - } - } - } - else - { - dialogSettings = visualizerLayout.GetLayoutSettings(builder); - if (dialogSettings == null) dialogSettings = CreateLayoutSettings(builder); - else - { - if (ExpressionBuilder.Unwrap(builder) is IWorkflowExpressionBuilder workflowExpressionBuilder) - { - var updatedEditorSettings = CreateLayoutSettings(builder); - updatedEditorSettings.Bounds = dialogSettings.Bounds; - updatedEditorSettings.Visible = dialogSettings.Visible; - updatedEditorSettings.WindowState = dialogSettings.WindowState; - updatedEditorSettings.VisualizerTypeName = dialogSettings.VisualizerTypeName; - updatedEditorSettings.VisualizerSettings = dialogSettings.VisualizerSettings; - foreach (var mashup in dialogSettings.Mashups) - { - updatedEditorSettings.Mashups.Add(mashup); - } - - dialogSettings = updatedEditorSettings; - } - } - } - - updatedLayout.DialogSettings.Add(dialogSettings); - } - - visualizerLayout = updatedLayout; - if (!editorState.WorkflowRunning) - { - visualizerMapping = null; - } - } - public void RefreshSelection() { foreach (var node in graphView.SelectedNodes) @@ -812,12 +516,6 @@ void RefreshEditorNode(GraphNode node) { LaunchVisualizer(node); } - - var editor = GetWorkflowEditorLauncher(node); - if (editor != null && editor.Visible) - { - editor.UpdateEditorText(); - } } private void UpdateGraphLayout() @@ -827,14 +525,13 @@ private void UpdateGraphLayout() private void UpdateGraphLayout(bool validateWorkflow) { - graphView.Nodes = workflow.ConnectedComponentLayering(); + graphView.Nodes = Workflow.ConnectedComponentLayering(); graphView.Invalidate(); if (validateWorkflow) { editorService.ValidateWorkflow(); } - UpdateVisualizerLayout(); if (validateWorkflow) { EditorControl.SelectTab(this); @@ -848,16 +545,13 @@ private void UpdateGraphLayout(bool validateWorkflow) } } UpdateSelection(); + editorService.RefreshEditor(); } private void InvalidateGraphLayout(bool validateWorkflow) { graphView.Refresh(); - if (Launcher != null) - { - Launcher.ParentView.InvalidateGraphLayout(validateWorkflow); - } - else if (validateWorkflow) + if (validateWorkflow) { editorService.ValidateWorkflow(); } @@ -928,8 +622,8 @@ private void graphView_DragEnter(object sender, DragEventArgs e) else if (e.Data.GetDataPresent(typeof(GraphNode))) { var graphViewNode = (GraphNode)e.Data.GetData(typeof(GraphNode)); - var node = WorkflowEditor.GetGraphNodeTag(workflow, graphViewNode, false); - if (node != null && workflow.Contains(node)) + var node = WorkflowEditor.GetGraphNodeTag(Workflow, graphViewNode, false); + if (node != null && Workflow.Contains(node)) { dragSelection = graphView.SelectedNodes; dragHighlight = graphViewNode; @@ -1125,22 +819,7 @@ private void graphView_KeyDown(object sender, KeyEventArgs e) if (e.KeyCode == Keys.Back && e.Modifiers == Keys.Control) { - if (Launcher != null && Launcher.ParentView != null) - { - var parentView = Launcher.ParentView; - var parentEditor = parentView.EditorControl; - var parentEditorForm = parentEditor.ParentForm; - if (EditorControl.ParentForm != parentEditorForm) - { - parentEditorForm.Activate(); - } - - var parentNode = parentView.Workflow.FirstOrDefault(node => ExpressionBuilder.Unwrap(node.Value) == Launcher.Builder); - if (parentNode != null) - { - parentView.SelectBuilderNode(parentNode.Value); - } - } + LaunchWorkflowPath(WorkflowPath?.Parent); } if (CanEdit) @@ -1266,15 +945,9 @@ private void graphView_NodeMouseLeave(object sender, GraphNodeMouseEventArgs e) private void graphView_HandleDestroyed(object sender, EventArgs e) { - editorState.WorkflowStarted -= editorService_WorkflowStarted; themeRenderer.ThemeChanged -= themeRenderer_ThemeChanged; } - private void editorService_WorkflowStarted(object sender, EventArgs e) - { - InitializeVisualizerMapping(); - } - private void themeRenderer_ThemeChanged(object sender, EventArgs e) { InitializeTheme(); @@ -1288,31 +961,12 @@ private void InitializeTheme() private void InitializeViewBindings() { - Editor.CloseWorkflowEditor.Subscribe(CloseWorkflowEditorLauncher); Editor.Error.Subscribe(ex => uiService.ShowError(ex)); - Editor.UpdateLayout.Subscribe(validateWorkflow => - { - if (Launcher != null) Launcher.WorkflowGraphView.UpdateGraphLayout(); - else UpdateGraphLayout(); - }); - - Editor.UpdateParentLayout.Subscribe(validateWorkflow => - { - if (Launcher != null) - { - Launcher.ParentView.UpdateGraphLayout(validateWorkflow); - } - }); - - Editor.InvalidateLayout.Subscribe(validateWorkflow => - { - if (Launcher != null) Launcher.WorkflowGraphView.InvalidateGraphLayout(validateWorkflow); - else InvalidateGraphLayout(validateWorkflow); - }); - + Editor.UpdateLayout.Subscribe(UpdateGraphLayout); + Editor.InvalidateLayout.Subscribe(InvalidateGraphLayout); Editor.UpdateSelection.Subscribe(selection => { - var activeView = Launcher != null ? Launcher.WorkflowGraphView.GraphView : graphView; + var activeView = graphView; activeView.SelectedNodes = activeView.Nodes.LayeredNodes() .Where(node => { @@ -1589,7 +1243,7 @@ private ToolStripMenuItem CreateSubjectTypeMenuItem( private HashSet FindMappedProperties(GraphNode node) { var mappedProperties = new HashSet(); - foreach (var predecessor in workflow.Predecessors(WorkflowEditor.GetGraphNodeTag(workflow, node))) + foreach (var predecessor in Workflow.Predecessors(WorkflowEditor.GetGraphNodeTag(Workflow, node))) { var builder = ExpressionBuilder.Unwrap(predecessor.Value); if (builder is ExternalizedProperty externalizedProperty) @@ -1637,7 +1291,7 @@ private ToolStripMenuItem CreateExternalizeMenuItem( var menuItem = new ToolStripMenuItem(text, null, delegate { var mapping = new ExternalizedMapping { Name = memberName, DisplayName = externalizedName }; - var mappingNode = (from predecessor in workflow.Predecessors(WorkflowEditor.GetGraphNodeTag(workflow, selectedNode)) + var mappingNode = (from predecessor in Workflow.Predecessors(WorkflowEditor.GetGraphNodeTag(Workflow, selectedNode)) let builder = ExpressionBuilder.Unwrap(predecessor.Value) as ExternalizedMappingBuilder where builder != null && predecessor.Successors.Count == 1 select new { node = FindGraphNode(predecessor.Value), builder }) @@ -1700,7 +1354,7 @@ private ToolStripMenuItem CreatePropertySourceMenuItem( return menuItem; } - private ToolStripMenuItem CreateVisualizerMenuItem(string typeName, VisualizerDialogSettings layoutSettings, GraphNode selectedNode) + private ToolStripMenuItem CreateVisualizerMenuItem(string typeName, GraphNode selectedNode) { ToolStripMenuItem menuItem = null; var emptyVisualizer = string.IsNullOrEmpty(typeName); @@ -1721,37 +1375,37 @@ private ToolStripMenuItem CreateVisualizerMenuItem(string typeName, VisualizerDi } else if (!menuItem.Checked) { - layoutSettings.VisualizerTypeName = typeName; - layoutSettings.VisualizerSettings = null; - layoutSettings.Visible = !emptyVisualizer; - if (!editorState.WorkflowRunning) + var dialogSettings = emptyVisualizer ? default : new VisualizerDialogSettings { - layoutSettings.Size = Size.Empty; - } - else + Tag = inspectBuilder, + VisualizerTypeName = typeName, + Visible = true, + Bounds = Rectangle.Empty + }; + + if (editorState.WorkflowRunning) { - var visualizerLauncher = visualizerMapping[inspectBuilder]; - var visualizerVisible = visualizerLauncher.Visible; - if (visualizerVisible) + var visualizerDialogs = (VisualizerDialogMap)serviceProvider.GetService(typeof(VisualizerDialogMap)); + if (visualizerDialogs.TryGetValue(inspectBuilder, out VisualizerDialogLauncher visualizerDialog)) { - visualizerLauncher.Hide(); + visualizerDialog.Hide(); + visualizerDialogs.Remove(visualizerDialog); } - var visualizerBounds = visualizerLauncher.Bounds; - visualizerLauncher = LayoutHelper.CreateVisualizerLauncher( - inspectBuilder, - visualizerLayout, - typeVisualizerMap, - workflow, - visualizerLauncher.VisualizerFactory.MashupSources, - workflowGraphView: this); - visualizerLauncher.Bounds = new Rectangle(visualizerBounds.Location, Size.Empty); - visualizerMapping[inspectBuilder] = visualizerLauncher; - if (layoutSettings.Visible) + if (!emptyVisualizer) { - visualizerLauncher.Show(graphView, serviceProvider); + var dialogLauncher = visualizerDialogs.Add(inspectBuilder, Workflow, dialogSettings); + var ownerWindow = uiService.GetDialogOwnerWindow(); + visualizerDialog.Show(ownerWindow, serviceProvider); } } + else + { + if (emptyVisualizer) + visualizerSettings.Remove(inspectBuilder); + else + visualizerSettings[inspectBuilder] = dialogSettings; + } } }); return menuItem; @@ -1860,44 +1514,31 @@ private void contextMenuStrip_Opening(object sender, CancelEventArgs e) createPropertySourceToolStripMenuItem.Enabled = createPropertySourceToolStripMenuItem.DropDownItems.Count > 0; } - var layoutSettings = visualizerLayout.GetLayoutSettings(selectedNode.Value); - if (layoutSettings != null) - { - var activeVisualizer = layoutSettings.VisualizerTypeName; - if (workflowElement is VisualizerMappingBuilder mappingBuilder && - mappingBuilder.VisualizerType != null) - { - activeVisualizer = mappingBuilder.VisualizerType.GetType().GetGenericArguments()[0].FullName; - } + var activeVisualizer = visualizerSettings.TryGetValue(inspectBuilder, out var dialogSettings) + ? dialogSettings.VisualizerTypeName + : null; - if (editorState.WorkflowRunning) - { - if (visualizerMapping.TryGetValue(inspectBuilder, out VisualizerDialogLauncher visualizerLauncher)) - { - var visualizer = visualizerLauncher.Visualizer; - if (visualizer.IsValueCreated) - { - activeVisualizer = visualizer.Value.GetType().FullName; - } - } - } + if (workflowElement is VisualizerMappingBuilder mappingBuilder && + mappingBuilder.VisualizerType != null) + { + activeVisualizer = mappingBuilder.VisualizerType.GetType().GetGenericArguments()[0].FullName; + } - var visualizerElement = ExpressionBuilder.GetVisualizerElement(inspectBuilder); - if (visualizerElement.ObservableType != null && - (!editorState.WorkflowRunning || visualizerElement.PublishNotifications)) + var visualizerElement = ExpressionBuilder.GetVisualizerElement(inspectBuilder); + if (visualizerElement.ObservableType != null && + (!editorState.WorkflowRunning || visualizerElement.PublishNotifications)) + { + var visualizerTypes = Enumerable.Repeat(null, 1); + visualizerTypes = visualizerTypes.Concat(typeVisualizerMap.GetTypeVisualizers(visualizerElement)); + visualizerToolStripMenuItem.Enabled = true; + foreach (var type in visualizerTypes) { - var visualizerTypes = Enumerable.Repeat(null, 1); - visualizerTypes = visualizerTypes.Concat(typeVisualizerMap.GetTypeVisualizers(visualizerElement)); - visualizerToolStripMenuItem.Enabled = true; - foreach (var type in visualizerTypes) - { - var typeName = type != null ? type.FullName : string.Empty; - var menuItem = CreateVisualizerMenuItem(typeName, layoutSettings, selectedNode); - visualizerToolStripMenuItem.DropDownItems.Add(menuItem); - menuItem.Checked = type == null - ? string.IsNullOrEmpty(activeVisualizer) - : typeName == activeVisualizer; - } + var typeName = type?.FullName ?? string.Empty; + var menuItem = CreateVisualizerMenuItem(typeName, selectedNode); + visualizerToolStripMenuItem.DropDownItems.Add(menuItem); + menuItem.Checked = type is null + ? activeVisualizer is null + : typeName == activeVisualizer; } } } diff --git a/Bonsai.Editor/IWorkflowEditorService.cs b/Bonsai.Editor/IWorkflowEditorService.cs index e831ae9dd..823f42788 100644 --- a/Bonsai.Editor/IWorkflowEditorService.cs +++ b/Bonsai.Editor/IWorkflowEditorService.cs @@ -1,6 +1,7 @@ using System; using System.IO; using System.Windows.Forms; +using Bonsai.Expressions; namespace Bonsai.Editor { @@ -24,6 +25,8 @@ interface IWorkflowEditorService void SelectNextControl(bool forward); + void SelectBuilderNode(ExpressionBuilder builder); + bool ValidateWorkflow(); void RefreshEditor(); diff --git a/Bonsai.Editor/Layout/LayoutHelper.cs b/Bonsai.Editor/Layout/LayoutHelper.cs index 3047766f4..491724b4a 100644 --- a/Bonsai.Editor/Layout/LayoutHelper.cs +++ b/Bonsai.Editor/Layout/LayoutHelper.cs @@ -4,7 +4,6 @@ using System.Collections.Generic; using System.IO; using System.Linq; -using System.Windows.Forms; using System.Xml.Linq; using System.Xml.Serialization; @@ -20,40 +19,11 @@ static class LayoutHelper const string MashupSettingsElement = "MashupSettings"; const string MashupSourceElement = "Source"; - public static VisualizerDialogSettings GetLayoutSettings(this VisualizerLayout visualizerLayout, object key) - { - return visualizerLayout?.DialogSettings.FirstOrDefault(xs => xs.Tag == key || xs.Tag == null); - } - public static string GetLayoutPath(string fileName) { return Path.ChangeExtension(fileName, Path.GetExtension(fileName) + LayoutExtension); } - public static void SetLayoutTags(ExpressionBuilderGraph source, VisualizerLayout layout) - { - foreach (var node in source) - { - var builder = node.Value; - var layoutSettings = layout.GetLayoutSettings(builder); - if (layoutSettings == null) - { - layoutSettings = new VisualizerDialogSettings(); - layout.DialogSettings.Add(layoutSettings); - } - layoutSettings.Tag = builder; - - if (layoutSettings is WorkflowEditorSettings editorSettings && - ExpressionBuilder.Unwrap(builder) is IWorkflowExpressionBuilder workflowBuilder && - editorSettings.EditorVisualizerLayout != null && - editorSettings.EditorDialogSettings.Visible && - workflowBuilder.Workflow != null) - { - SetLayoutTags(workflowBuilder.Workflow, editorSettings.EditorVisualizerLayout); - } - } - } - public static void SetWorkflowNotifications(ExpressionBuilderGraph source, bool publishNotifications) { foreach (var builder in from node in source @@ -70,41 +40,15 @@ public static void SetWorkflowNotifications(ExpressionBuilderGraph source, bool } } - public static void SetLayoutNotifications(VisualizerLayout root) + public static void SetLayoutNotifications(ExpressionBuilderGraph source, VisualizerDialogMap lookup) { - foreach (var settings in root.DialogSettings) + foreach (var builder in source.Descendants()) { - SetLayoutNotifications(settings, root, forcePublish: false); - } - } - - static void SetLayoutNotifications(VisualizerDialogSettings settings, VisualizerLayout root, bool forcePublish = false) - { - var inspectBuilder = settings.Tag as InspectBuilder; - while (inspectBuilder != null && !inspectBuilder.PublishNotifications) - { - if (string.IsNullOrEmpty(settings.VisualizerTypeName) && !forcePublish) - { - break; - } - - SetVisualizerNotifications(inspectBuilder); - foreach (var index in settings.Mashups.Concat(settings.VisualizerSettings? - .Descendants(MashupSourceElement) - .Select(m => int.Parse(m.Value)) - .Distinct() ?? Enumerable.Empty())) + var inspectBuilder = (InspectBuilder)builder; + if (lookup.TryGetValue((InspectBuilder)builder, out VisualizerDialogLauncher _)) { - if (index < 0 || index >= root.DialogSettings.Count) continue; - var mashupSource = root.DialogSettings[index]; - SetLayoutNotifications(mashupSource, root, forcePublish: true); + SetVisualizerNotifications(inspectBuilder); } - - inspectBuilder = ExpressionBuilder.GetVisualizerElement(inspectBuilder); - } - - if (settings is WorkflowEditorSettings editorSettings && editorSettings.EditorVisualizerLayout != null) - { - SetLayoutNotifications(editorSettings.EditorVisualizerLayout); } } @@ -117,18 +61,6 @@ static void SetVisualizerNotifications(InspectBuilder inspectBuilder) } } - static IEnumerable GetMashupSources(this VisualizerFactory visualizerFactory) - { - yield return visualizerFactory; - foreach (var source in visualizerFactory.MashupSources) - { - foreach (var nestedSource in source.GetMashupSources()) - { - yield return nestedSource; - } - } - } - internal static Type GetMashupSourceType(Type mashupVisualizerType, Type visualizerType, TypeVisualizerMap typeVisualizerMap) { Type mashupSource = default; @@ -153,11 +85,9 @@ internal static Type GetMashupSourceType(Type mashupVisualizerType, Type visuali public static VisualizerDialogLauncher CreateVisualizerLauncher( InspectBuilder source, - VisualizerLayout visualizerLayout, + VisualizerDialogSettings layoutSettings, TypeVisualizerMap typeVisualizerMap, - ExpressionBuilderGraph workflow, - IReadOnlyList mashupArguments, - Editor.GraphView.WorkflowGraphView workflowGraphView = null) + ExpressionBuilderGraph workflow) { var inspectBuilder = ExpressionBuilder.GetVisualizerElement(source); if (inspectBuilder.ObservableType == null || !inspectBuilder.PublishNotifications || @@ -166,23 +96,23 @@ public static VisualizerDialogLauncher CreateVisualizerLauncher( return null; } - var layoutSettings = visualizerLayout.GetLayoutSettings(source); - var visualizerType = typeVisualizerMap.GetVisualizerType(layoutSettings?.VisualizerTypeName ?? string.Empty) - ?? typeVisualizerMap.GetTypeVisualizers(inspectBuilder).FirstOrDefault(); - if (visualizerType == null) + var visualizerType = typeVisualizerMap.GetVisualizerType(layoutSettings?.VisualizerTypeName ?? string.Empty); + visualizerType ??= typeVisualizerMap.GetTypeVisualizers(inspectBuilder).FirstOrDefault(); + if (visualizerType is null) { return null; } + var mashupArguments = GetMashupArguments(inspectBuilder, typeVisualizerMap); var visualizerFactory = new VisualizerFactory(inspectBuilder, visualizerType, mashupArguments); var visualizer = new Lazy(() => DeserializeVisualizerSettings( visualizerType, layoutSettings, - visualizerLayout, + workflow, visualizerFactory, typeVisualizerMap)); - var launcher = new VisualizerDialogLauncher(visualizer, visualizerFactory, workflow, source, workflowGraphView); + var launcher = new VisualizerDialogLauncher(visualizer, visualizerFactory, workflow, source); launcher.Text = source != null ? ExpressionBuilder.GetElementDisplayName(source) : null; return launcher; } @@ -203,48 +133,6 @@ static IReadOnlyList GetMashupArguments(InspectBuilder builde }).ToList(); } - public static Dictionary CreateVisualizerMapping( - ExpressionBuilderGraph workflow, - VisualizerLayout visualizerLayout, - TypeVisualizerMap typeVisualizerMap, - IServiceProvider provider = null, - IWin32Window owner = null, - Editor.GraphView.WorkflowGraphView graphView = null) - { - if (workflow == null) return null; - var visualizerMapping = (from node in workflow.TopologicalSort() - let source = (InspectBuilder)node.Value - let mashupArguments = GetMashupArguments(source, typeVisualizerMap) - let visualizerLauncher = CreateVisualizerLauncher( - source, - visualizerLayout, - typeVisualizerMap, - workflow, - mashupArguments, - graphView) - where visualizerLauncher != null - select new { source, visualizerLauncher }) - .ToDictionary(mapping => mapping.source, - mapping => mapping.visualizerLauncher); - foreach (var mapping in visualizerMapping) - { - var key = mapping.Key; - var visualizerLauncher = mapping.Value; - var layoutSettings = visualizerLayout.GetLayoutSettings(key); - if (layoutSettings != null) - { - visualizerLauncher.Bounds = layoutSettings.Bounds; - visualizerLauncher.WindowState = layoutSettings.WindowState; - if (layoutSettings.Visible) - { - visualizerLauncher.Show(owner, provider); - } - } - } - - return visualizerMapping; - } - public static XElement SerializeVisualizerSettings( DialogTypeVisualizer visualizer, IEnumerable> topologicalOrder) @@ -311,11 +199,11 @@ static XElement SerializeMashupSource( public static DialogTypeVisualizer DeserializeVisualizerSettings( Type visualizerType, VisualizerDialogSettings layoutSettings, - VisualizerLayout visualizerLayout, + ExpressionBuilderGraph workflow, VisualizerFactory visualizerFactory, TypeVisualizerMap typeVisualizerMap) { - if (layoutSettings?.VisualizerTypeName != visualizerType.FullName) + if (layoutSettings?.VisualizerTypeName != visualizerType?.FullName) { layoutSettings = default; } @@ -338,7 +226,7 @@ public static DialogTypeVisualizer DeserializeVisualizerSettings( layoutSettings.Mashups.Clear(); } - return visualizerFactory.CreateVisualizer(layoutSettings?.VisualizerSettings, visualizerLayout, typeVisualizerMap); + return visualizerFactory.CreateVisualizer(layoutSettings?.VisualizerSettings, workflow, typeVisualizerMap); } static int? GetMashupSourceIndex( @@ -353,7 +241,7 @@ public static DialogTypeVisualizer DeserializeVisualizerSettings( public static DialogTypeVisualizer CreateVisualizer( this VisualizerFactory visualizerFactory, XElement visualizerSettings, - VisualizerLayout visualizerLayout, + ExpressionBuilderGraph workflow, TypeVisualizerMap typeVisualizerMap) { DialogTypeVisualizer visualizer; @@ -386,19 +274,19 @@ public static DialogTypeVisualizer CreateVisualizer( if (mashupSourceElement == null) continue; var mashupSourceIndex = int.Parse(mashupSourceElement.Value); - var mashupSource = (InspectBuilder)visualizerLayout.DialogSettings[mashupSourceIndex]?.Tag; + var mashupSource = (InspectBuilder)workflow[mashupSourceIndex].Value; var mashupVisualizerTypeName = mashup.Element(nameof(VisualizerDialogSettings.VisualizerTypeName))?.Value; var mashupVisualizerType = typeVisualizerMap.GetVisualizerType(mashupVisualizerTypeName); mashupFactory = new VisualizerFactory(mashupSource, mashupVisualizerType); } - CreateMashupVisualizer(mashupVisualizer, visualizerFactory, mashupFactory, visualizerLayout, typeVisualizerMap, mashup); + CreateMashupVisualizer(mashupVisualizer, visualizerFactory, mashupFactory, workflow, typeVisualizerMap, mashup); } for (int i = index; i < visualizerFactory.MashupSources.Count; i++) { var mashupFactory = visualizerFactory.MashupSources[i]; - CreateMashupVisualizer(mashupVisualizer, visualizerFactory, mashupFactory, visualizerLayout, typeVisualizerMap); + CreateMashupVisualizer(mashupVisualizer, visualizerFactory, mashupFactory, workflow, typeVisualizerMap); } } @@ -409,7 +297,7 @@ static void CreateMashupVisualizer( MashupVisualizer mashupVisualizer, VisualizerFactory visualizerFactory, VisualizerFactory mashupFactory, - VisualizerLayout visualizerLayout, + ExpressionBuilderGraph workflow, TypeVisualizerMap typeVisualizerMap, XElement mashup = null) { @@ -434,7 +322,7 @@ static void CreateMashupVisualizer( } } - var nestedVisualizer = mashupFactory.CreateVisualizer(mashupVisualizerSettings, visualizerLayout, typeVisualizerMap); + var nestedVisualizer = mashupFactory.CreateVisualizer(mashupVisualizerSettings, workflow, typeVisualizerMap); mashupVisualizer.MashupSources.Add(mashupFactory.Source, nestedVisualizer); } } diff --git a/Bonsai.Editor/Layout/VisualizerDialogLauncher.cs b/Bonsai.Editor/Layout/VisualizerDialogLauncher.cs index 6cb186b21..32b6c4d14 100644 --- a/Bonsai.Editor/Layout/VisualizerDialogLauncher.cs +++ b/Bonsai.Editor/Layout/VisualizerDialogLauncher.cs @@ -3,39 +3,38 @@ using Bonsai.Expressions; using System.Reactive.Linq; using System.Windows.Forms; -using Bonsai.Editor.GraphView; using Bonsai.Editor.GraphModel; +using System.Linq; +using Bonsai.Editor; namespace Bonsai.Design { class VisualizerDialogLauncher : DialogLauncher, ITypeVisualizerContext { - readonly ExpressionBuilderGraph workflow; - readonly WorkflowGraphView graphView; ServiceContainer visualizerContext; IDisposable visualizerObserver; public VisualizerDialogLauncher( Lazy visualizer, VisualizerFactory visualizerFactory, - ExpressionBuilderGraph visualizerWorkflow, - InspectBuilder workflowSource, - WorkflowGraphView workflowGraphView) + ExpressionBuilderGraph workflow, + InspectBuilder workflowSource) { Visualizer = visualizer ?? throw new ArgumentNullException(nameof(visualizer)); VisualizerFactory = visualizerFactory ?? throw new ArgumentNullException(nameof(visualizerFactory)); Source = workflowSource ?? throw new ArgumentNullException(nameof(workflowSource)); - workflow = visualizerWorkflow; - graphView = workflowGraphView; + Workflow = workflow ?? throw new ArgumentNullException(nameof(workflow)); } public string Text { get; set; } public InspectBuilder Source { get; } + public ExpressionBuilderGraph Workflow { get; } + public Lazy Visualizer { get; } - public VisualizerFactory VisualizerFactory { get; } + private VisualizerFactory VisualizerFactory { get; } static IDisposable SubscribeDialog(IObservable source, TypeVisualizerDialog visualizerDialog) { @@ -55,7 +54,7 @@ protected override void InitializeComponents(TypeVisualizerDialog visualizerDial visualizerContext.AddService(typeof(ITypeVisualizerContext), this); visualizerContext.AddService(typeof(TypeVisualizerDialog), visualizerDialog); visualizerContext.AddService(typeof(IDialogTypeVisualizerService), visualizerDialog); - visualizerContext.AddService(typeof(ExpressionBuilderGraph), workflow); + visualizerContext.AddService(typeof(ExpressionBuilderGraph), Workflow); Visualizer.Value.Load(visualizerContext); var visualizerOutput = Visualizer.Value.Visualize(VisualizerFactory.Source.Output, visualizerContext); @@ -90,10 +89,10 @@ protected override void InitializeComponents(TypeVisualizerDialog visualizerDial void visualizerDialog_KeyDown(object sender, KeyEventArgs e) { - if (graphView != null && e.KeyCode == Keys.Back && e.Control) + if (e.KeyCode == Keys.Back && e.Control) { - graphView.SelectBuilderNode(Source); - graphView.EditorControl.ParentForm.Activate(); + var editorService = (IWorkflowEditorService)visualizerContext.GetService(typeof(IWorkflowEditorService)); + editorService.SelectBuilderNode(Source); } if (e.KeyCode == Keys.Delete && e.Control) @@ -148,36 +147,40 @@ MashupVisualizer GetMashupContainer(int x, int y, bool allowEmpty = true) return visualizer; } - public void CreateMashup(MashupVisualizer dialogMashup, VisualizerDialogLauncher visualizerDialog, TypeVisualizerMap typeVisualizerMap) + public void CreateMashup(MashupVisualizer dialogMashup, InspectBuilder source, Type visualizerType, TypeVisualizerMap typeVisualizerMap) { - if (visualizerDialog != null) + if (visualizerType == null) + throw new ArgumentNullException(nameof(visualizerType)); + + var dialogMashupType = dialogMashup.GetType(); + var mashupSourceType = LayoutHelper.GetMashupSourceType(dialogMashupType, visualizerType, typeVisualizerMap); + if (mashupSourceType != null) { - var dialogMashupType = dialogMashup.GetType(); - var visualizerFactory = visualizerDialog.VisualizerFactory; - var mashupSourceType = LayoutHelper.GetMashupSourceType(dialogMashupType, visualizerFactory.VisualizerType, typeVisualizerMap); - if (mashupSourceType != null) + UnloadMashups(); + if (mashupSourceType == typeof(DialogTypeVisualizer)) { - UnloadMashups(); - if (mashupSourceType == typeof(DialogTypeVisualizer)) - { - mashupSourceType = visualizerFactory.VisualizerType; - } - var visualizerMashup = (DialogTypeVisualizer)Activator.CreateInstance(mashupSourceType); - dialogMashup.MashupSources.Add(visualizerFactory.Source, visualizerMashup); - ReloadMashups(); + mashupSourceType = visualizerType; } + var visualizerMashup = (DialogTypeVisualizer)Activator.CreateInstance(mashupSourceType); + dialogMashup.MashupSources.Add(source, visualizerMashup); + ReloadMashups(); } } void visualizerDialog_DragDrop(object sender, DragEventArgs e) { - if (graphView != null && visualizerContext != null && e.Data.GetDataPresent(typeof(GraphNode))) + if (visualizerContext != null && e.Data.GetDataPresent(typeof(GraphNode))) { var graphNode = (GraphNode)e.Data.GetData(typeof(GraphNode)); + var inspectBuilder = (InspectBuilder)graphNode.Value; var typeVisualizerMap = (TypeVisualizerMap)visualizerContext.GetService(typeof(TypeVisualizerMap)); - var visualizerDialog = graphView.GetVisualizerDialogLauncher(graphNode); - var visualizer = GetMashupContainer(e.X, e.Y); - CreateMashup(visualizer, visualizerDialog, typeVisualizerMap); + var visualizerDialogMap = (VisualizerDialogMap)visualizerContext.GetService(typeof(VisualizerDialogMap)); + var visualizerType = GetVisualizerType(inspectBuilder, visualizerDialogMap, typeVisualizerMap); + if (visualizerType != null) + { + var visualizer = GetMashupContainer(e.X, e.Y); + CreateMashup(visualizer, inspectBuilder, visualizerType, typeVisualizerMap); + } } } @@ -187,17 +190,17 @@ void visualizerDialog_DragOver(object sender, DragEventArgs e) void visualizerDialog_DragEnter(object sender, DragEventArgs e) { - if (graphView != null && visualizerContext != null && e.Data.GetDataPresent(typeof(GraphNode))) + if (visualizerContext != null && e.Data.GetDataPresent(typeof(GraphNode))) { var graphNode = (GraphNode)e.Data.GetData(typeof(GraphNode)); - var visualizerDialog = graphView.GetVisualizerDialogLauncher(graphNode); - if (visualizerDialog != null && visualizerDialog != this) + var typeVisualizerMap = (TypeVisualizerMap)visualizerContext.GetService(typeof(TypeVisualizerMap)); + var visualizerDialogMap = (VisualizerDialogMap)visualizerContext.GetService(typeof(VisualizerDialogMap)); + var visualizerType = GetVisualizerType((InspectBuilder)graphNode.Value, visualizerDialogMap, typeVisualizerMap); + if (visualizerType != null) { var dialogMashupType = VisualizerFactory.VisualizerType; if (dialogMashupType.IsSubclassOf(typeof(MashupVisualizer))) { - var visualizerType = visualizerDialog.VisualizerFactory.VisualizerType; - var typeVisualizerMap = (TypeVisualizerMap)visualizerContext.GetService(typeof(TypeVisualizerMap)); var mashupVisualizerType = LayoutHelper.GetMashupSourceType(dialogMashupType, visualizerType, typeVisualizerMap); if (mashupVisualizerType != null) { @@ -207,5 +210,16 @@ void visualizerDialog_DragEnter(object sender, DragEventArgs e) } } } + + Type GetVisualizerType(InspectBuilder source, VisualizerDialogMap visualizerDialogMap, TypeVisualizerMap typeVisualizerMap) + { + if (visualizerDialogMap.TryGetValue(source, out VisualizerDialogLauncher dialogLauncher)) + { + if (dialogLauncher == this) + return null; + else return dialogLauncher.VisualizerFactory.VisualizerType; + } + else return typeVisualizerMap.GetTypeVisualizers(source).FirstOrDefault(); + } } } diff --git a/Bonsai.Editor/Layout/VisualizerDialogMap.cs b/Bonsai.Editor/Layout/VisualizerDialogMap.cs new file mode 100644 index 000000000..b4aa0c133 --- /dev/null +++ b/Bonsai.Editor/Layout/VisualizerDialogMap.cs @@ -0,0 +1,75 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Windows.Forms; +using Bonsai.Expressions; + +namespace Bonsai.Design +{ + internal class VisualizerDialogMap : IEnumerable + { + readonly TypeVisualizerMap typeVisualizerMap; + readonly Dictionary lookup; + + public VisualizerDialogMap(TypeVisualizerMap typeVisualizers) + { + typeVisualizerMap = typeVisualizers ?? throw new ArgumentNullException(nameof(typeVisualizers)); + lookup = new(); + } + + public VisualizerDialogLauncher this[InspectBuilder key] + { + get => lookup[key]; + } + + public bool TryGetValue(InspectBuilder key, out VisualizerDialogLauncher value) + { + return lookup.TryGetValue(key, out value); + } + + public void Show(VisualizerLayoutMap visualizerSettings, IServiceProvider provider = null, IWin32Window owner = null) + { + foreach (var dialogLauncher in lookup.Values) + { + var dialogSettings = visualizerSettings[dialogLauncher.Source]; + dialogLauncher.Bounds = dialogSettings.Bounds; + dialogLauncher.WindowState = dialogSettings.WindowState; + if (dialogSettings.Visible) + { + dialogLauncher.Show(owner, provider); + } + } + } + + public VisualizerDialogLauncher Add(InspectBuilder source, ExpressionBuilderGraph workflow, VisualizerDialogSettings dialogSettings) + { + var dialogLauncher = LayoutHelper.CreateVisualizerLauncher( + source, + dialogSettings, + typeVisualizerMap, + workflow); + Add(dialogLauncher); + return dialogLauncher; + } + + public void Add(VisualizerDialogLauncher item) + { + lookup.Add(item.Source, item); + } + + public bool Remove(VisualizerDialogLauncher item) + { + return lookup.Remove(item.Source); + } + + public IEnumerator GetEnumerator() + { + return lookup.Values.GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + } +} diff --git a/Bonsai.Editor/Layout/VisualizerDialogSettings.cs b/Bonsai.Editor/Layout/VisualizerDialogSettings.cs index 9b8f99795..af85354d7 100644 --- a/Bonsai.Editor/Layout/VisualizerDialogSettings.cs +++ b/Bonsai.Editor/Layout/VisualizerDialogSettings.cs @@ -6,9 +6,13 @@ namespace Bonsai.Design { +#pragma warning disable CS0612 // Type or member is obsolete [XmlInclude(typeof(WorkflowEditorSettings))] +#pragma warning restore CS0612 // Type or member is obsolete public class VisualizerDialogSettings { + public int? Index { get; set; } + [XmlIgnore] public object Tag { get; set; } @@ -35,6 +39,8 @@ public Rectangle Bounds public XElement VisualizerSettings { get; set; } + public VisualizerLayout NestedLayout { get; set; } + // [Obsolete] public Collection Mashups { get; } = new Collection(); diff --git a/Bonsai.Editor/Layout/VisualizerLayoutMap.cs b/Bonsai.Editor/Layout/VisualizerLayoutMap.cs new file mode 100644 index 000000000..172fa418c --- /dev/null +++ b/Bonsai.Editor/Layout/VisualizerLayoutMap.cs @@ -0,0 +1,201 @@ +using System.Collections; +using System.Collections.Generic; +using Bonsai.Expressions; + +namespace Bonsai.Design +{ + internal class VisualizerLayoutMap : IEnumerable + { + readonly TypeVisualizerMap typeVisualizerMap; + readonly Dictionary lookup; + + public VisualizerLayoutMap(TypeVisualizerMap typeVisualizers) + { + typeVisualizerMap = typeVisualizers; + lookup = new(); + } + + public VisualizerDialogSettings this[InspectBuilder key] + { + get => lookup[key]; + set => lookup[key] = value; + } + + public bool TryGetValue(InspectBuilder key, out VisualizerDialogSettings value) + { + return lookup.TryGetValue(key, out value); + } + + private void CreateVisualizerDialogs(ExpressionBuilderGraph workflow, VisualizerDialogMap visualizerDialogs) + { + for (int i = 0; i < workflow.Count; i++) + { + var builder = (InspectBuilder)workflow[i].Value; + if (lookup.TryGetValue(builder, out VisualizerDialogSettings dialogSettings)) + { + visualizerDialogs.Add(builder, workflow, dialogSettings); + } + + if (ExpressionBuilder.Unwrap(builder) is IWorkflowExpressionBuilder workflowBuilder) + { + CreateVisualizerDialogs(workflowBuilder.Workflow, visualizerDialogs); + } + } + } + + public VisualizerDialogMap CreateVisualizerDialogs(WorkflowBuilder workflowBuilder) + { + var visualizerDialogs = new VisualizerDialogMap(typeVisualizerMap); + CreateVisualizerDialogs(workflowBuilder.Workflow, visualizerDialogs); + return visualizerDialogs; + } + + public void Update(IEnumerable visualizerDialogs) + { + var unused = new HashSet(lookup.Keys); + foreach (var dialog in visualizerDialogs) + { + unused.Remove(dialog.Source); + if (!lookup.TryGetValue(dialog.Source, out VisualizerDialogSettings dialogSettings)) + { + dialogSettings = new VisualizerDialogSettings(); + dialogSettings.Tag = dialog.Source; + lookup.Add(dialog.Source, dialogSettings); + } + + var visible = dialog.Visible; + dialog.Hide(); + dialogSettings.Visible = visible; + dialogSettings.Bounds = dialog.Bounds; + dialogSettings.WindowState = dialog.WindowState; + + var visualizer = dialog.Visualizer.Value; + var visualizerType = visualizer.GetType(); + if (visualizerType.IsPublic) + { + dialogSettings.VisualizerTypeName = visualizerType.FullName; + dialogSettings.VisualizerSettings = LayoutHelper.SerializeVisualizerSettings( + visualizer, + dialog.Workflow); + } + } + + foreach (var builder in unused) + { + lookup.Remove(builder); + } + } + + public VisualizerLayout GetVisualizerLayout(WorkflowBuilder workflowBuilder) + { + return GetVisualizerLayout(workflowBuilder.Workflow); + } + + private VisualizerLayout GetVisualizerLayout(ExpressionBuilderGraph workflow) + { + var layout = new VisualizerLayout(); + for (int i = 0; i < workflow.Count; i++) + { + var builder = (InspectBuilder)workflow[i].Value; + var layoutSettings = new VisualizerDialogSettings { Index = i }; + + if (lookup.TryGetValue(builder, out VisualizerDialogSettings dialogSettings)) + { + layoutSettings.Visible = dialogSettings.Visible; + layoutSettings.Bounds = dialogSettings.Bounds; + layoutSettings.WindowState = dialogSettings.WindowState; + layoutSettings.VisualizerTypeName = dialogSettings.VisualizerTypeName; + layoutSettings.VisualizerSettings = dialogSettings.VisualizerSettings; + } + + if (ExpressionBuilder.Unwrap(builder) is IWorkflowExpressionBuilder workflowBuilder) + { + layoutSettings.NestedLayout = GetVisualizerLayout(workflowBuilder.Workflow); + } + + if (!layoutSettings.Bounds.IsEmpty || + layoutSettings.VisualizerTypeName != null || + layoutSettings.NestedLayout?.DialogSettings.Count > 0) + { + layout.DialogSettings.Add(layoutSettings); + } + } + + return layout; + } + + public static VisualizerLayoutMap FromVisualizerLayout( + WorkflowBuilder workflowBuilder, + VisualizerLayout layout, + TypeVisualizerMap typeVisualizers) + { + var visualizerSettings = new VisualizerLayoutMap(typeVisualizers); + visualizerSettings.SetVisualizerLayout(workflowBuilder.Workflow, layout); + return visualizerSettings; + } + + public void SetVisualizerLayout(WorkflowBuilder workflowBuilder, VisualizerLayout layout) + { + Clear(); + SetVisualizerLayout(workflowBuilder.Workflow, layout); + } + + private void SetVisualizerLayout(ExpressionBuilderGraph workflow, VisualizerLayout layout) + { + for (int i = 0; i < layout.DialogSettings.Count; i++) + { + var layoutSettings = layout.DialogSettings[i]; + var index = layoutSettings.Index.GetValueOrDefault(i); + if (index < workflow.Count) + { + var builder = (InspectBuilder)workflow[index].Value; + var dialogSettings = new VisualizerDialogSettings(); + dialogSettings.Tag = builder; + dialogSettings.Bounds = layoutSettings.Bounds; + dialogSettings.WindowState = layoutSettings.WindowState; + dialogSettings.Visible = layoutSettings.Visible; + dialogSettings.VisualizerTypeName = layoutSettings.VisualizerTypeName; + dialogSettings.VisualizerSettings = layoutSettings.VisualizerSettings; + Add(dialogSettings); + + if (layoutSettings.NestedLayout != null && + ExpressionBuilder.Unwrap(builder) is IWorkflowExpressionBuilder workflowBuilder) + { + SetVisualizerLayout(workflowBuilder.Workflow, layoutSettings.NestedLayout); + } + } + } + } + + public void Add(VisualizerDialogSettings item) + { + var builder = (InspectBuilder)item.Tag; + lookup.Add(builder, item); + } + + public bool ContainsKey(InspectBuilder builder) + { + return lookup.ContainsKey(builder); + } + + public bool Remove(InspectBuilder builder) + { + return lookup.Remove(builder); + } + + public void Clear() + { + lookup.Clear(); + } + + public IEnumerator GetEnumerator() + { + return lookup.Values.GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + } +} diff --git a/Bonsai.Editor/Layout/WorkflowEditorLauncher.cs b/Bonsai.Editor/Layout/WorkflowEditorLauncher.cs deleted file mode 100644 index 87bb1d83a..000000000 --- a/Bonsai.Editor/Layout/WorkflowEditorLauncher.cs +++ /dev/null @@ -1,188 +0,0 @@ -using System; -using System.Windows.Forms; -using Bonsai.Expressions; -using System.ComponentModel; -using Bonsai.Editor.GraphView; -using Bonsai.Editor; - -namespace Bonsai.Design -{ - class WorkflowEditorLauncher : DialogLauncher - { - bool userClosing; - readonly Func parentSelector; - readonly Func containerSelector; - - public WorkflowEditorLauncher(IWorkflowExpressionBuilder builder, Func parentSelector, Func containerSelector) - { - Builder = builder ?? throw new ArgumentNullException(nameof(builder)); - this.parentSelector = parentSelector ?? throw new ArgumentNullException(nameof(parentSelector)); - this.containerSelector = containerSelector ?? throw new ArgumentNullException(nameof(containerSelector)); - } - - internal IWorkflowExpressionBuilder Builder { get; } - - internal WorkflowGraphView ParentView - { - get { return parentSelector(); } - } - - internal WorkflowEditorControl Container - { - get { return containerSelector(); } - } - - internal IWin32Window Owner - { - get { return VisualizerDialog; } - } - - public VisualizerLayout VisualizerLayout { get; set; } - - public WorkflowGraphView WorkflowGraphView { get; private set; } - - public void UpdateEditorLayout() - { - if (WorkflowGraphView != null) - { - WorkflowGraphView.UpdateVisualizerLayout(); - VisualizerLayout = WorkflowGraphView.VisualizerLayout; - if (VisualizerDialog != null) - { - Bounds = VisualizerDialog.LayoutBounds; - } - } - } - - public void UpdateEditorText() - { - if (VisualizerDialog != null) - { - VisualizerDialog.Text = ExpressionBuilder.GetElementDisplayName(Builder); - if (VisualizerDialog.TopLevel == false) - { - Container.RefreshTab(Builder); - } - } - } - - public override void Show(IWin32Window owner, IServiceProvider provider) - { - if (VisualizerDialog != null && VisualizerDialog.TopLevel == false) - { - Container.SelectTab(Builder); - } - else base.Show(owner, provider); - } - - public override void Hide() - { - if (VisualizerDialog != null) - { - userClosing = false; - if (VisualizerDialog.TopLevel == false) - { - Container.CloseTab(Builder); - } - else base.Hide(); - } - } - - void EditorClosing(object sender, CancelEventArgs e) - { - if (userClosing) - { - e.Cancel = true; - ParentView.CloseWorkflowView(Builder); - ParentView.UpdateSelection(); - } - else - { - UpdateEditorLayout(); - WorkflowGraphView.HideEditorMapping(); - } - } - - protected override LauncherDialog CreateVisualizerDialog(IServiceProvider provider) - { - return new NestedEditorDialog(provider); - } - - protected override void InitializeComponents(TypeVisualizerDialog visualizerDialog, IServiceProvider provider) - { - var workflowEditor = Container; - if (workflowEditor == null) - { - workflowEditor = new WorkflowEditorControl(provider, ParentView.ReadOnly); - workflowEditor.SuspendLayout(); - workflowEditor.Dock = DockStyle.Fill; - workflowEditor.Font = ParentView.Font; - workflowEditor.Workflow = Builder.Workflow; - WorkflowGraphView = workflowEditor.WorkflowGraphView; - workflowEditor.ResumeLayout(false); - visualizerDialog.AddControl(workflowEditor); - visualizerDialog.Icon = Editor.Properties.Resources.Icon; - visualizerDialog.ShowIcon = true; - visualizerDialog.Activated += (sender, e) => workflowEditor.ActiveTab.UpdateSelection(); - visualizerDialog.FormClosing += (sender, e) => - { - if (e.CloseReason == CloseReason.UserClosing) - { - EditorClosing(sender, e); - } - }; - } - else - { - visualizerDialog.FormBorderStyle = FormBorderStyle.None; - visualizerDialog.Dock = DockStyle.Fill; - visualizerDialog.TopLevel = false; - visualizerDialog.Visible = true; - var tabState = workflowEditor.CreateTab(Builder, ParentView.ReadOnly, visualizerDialog); - WorkflowGraphView = tabState.WorkflowGraphView; - tabState.TabClosing += EditorClosing; - } - - userClosing = true; - visualizerDialog.BackColor = ParentView.ParentForm.BackColor; - WorkflowGraphView.BackColorChanged += (sender, e) => visualizerDialog.BackColor = ParentView.ParentForm.BackColor; - WorkflowGraphView.Launcher = this; - WorkflowGraphView.VisualizerLayout = VisualizerLayout; - WorkflowGraphView.SelectFirstGraphNode(); - WorkflowGraphView.Select(); - UpdateEditorText(); - } - - class NestedEditorDialog : LauncherDialog - { - IWorkflowEditorService editorService; - - public NestedEditorDialog(IServiceProvider provider) - { - editorService = (IWorkflowEditorService)provider.GetService(typeof(IWorkflowEditorService)); - } - - protected override void OnKeyDown(KeyEventArgs e) - { - if (e.KeyCode == Keys.Escape) - { - e.Handled = true; - } - base.OnKeyDown(e); - } - - protected override bool ProcessTabKey(bool forward) - { - var selected = SelectNextControl(ActiveControl, forward, true, true, false); - if (!selected) - { - var parent = Parent; - if (parent != null) return parent.SelectNextControl(this, forward, true, true, false); - else editorService.SelectNextControl(forward); - } - - return selected; - } - } - } -} diff --git a/Bonsai.Editor/Layout/WorkflowEditorSettings.cs b/Bonsai.Editor/Layout/WorkflowEditorSettings.cs index fa8337182..41dd89b07 100644 --- a/Bonsai.Editor/Layout/WorkflowEditorSettings.cs +++ b/Bonsai.Editor/Layout/WorkflowEditorSettings.cs @@ -1,5 +1,8 @@ -namespace Bonsai.Design +using System; + +namespace Bonsai.Design { + [Obsolete] public class WorkflowEditorSettings : VisualizerDialogSettings { public VisualizerDialogSettings EditorDialogSettings { get; set; } diff --git a/Bonsai.Editor/Properties/Resources.Designer.cs b/Bonsai.Editor/Properties/Resources.Designer.cs index 295eb181f..f9ef2ac92 100644 --- a/Bonsai.Editor/Properties/Resources.Designer.cs +++ b/Bonsai.Editor/Properties/Resources.Designer.cs @@ -286,6 +286,15 @@ internal static string InvalidReplaceGroupNode_Error { } } + /// + /// Looks up a localized string similar to The specified workflow path does not resolve to a workflow expression builder node.. + /// + internal static string InvalidWorkflowPath_Error { + get { + return ResourceManager.GetString("InvalidWorkflowPath_Error", resourceCulture); + } + } + /// /// Looks up a localized string similar to There was an error opening the workflow {0}: ///{1}. diff --git a/Bonsai.Editor/Properties/Resources.resx b/Bonsai.Editor/Properties/Resources.resx index d5b31a13b..86579078e 100644 --- a/Bonsai.Editor/Properties/Resources.resx +++ b/Bonsai.Editor/Properties/Resources.resx @@ -134,7 +134,7 @@ iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAALGPC/xhBQAAAAlwSFlzAAAO - vwAADr8BOAVTJAAAAK5JREFUOE9jQAffvn1LAOL9QPwfDYPEEqDKMAFQUgGIz3/at/P/q2jf/09FGFAw + vAAADrwBlbxySQAAAK5JREFUOE9jQAffvn1LAOL9QPwfDYPEEqDKMAFQUgGIz3/at/P/q2jf/09FGFAw SAwkB1IDUgvVBgFQze9fZ8ZgaETHb8pzQIa8RzEEyDkPksCmARuGGnIepjkB5DRsCvFhqHcSQAbsx+Zn QhikB6QXZABWBcRgkF4UA4gFtDOAVAwzgOJApCwaoWnhPDGpEIZREhIIADngpExMasSalEEAagh5mQkZ ACVJyM4MDAD69UvNH5WBiAAAAABJRU5ErkJggg== @@ -143,7 +143,7 @@ iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAALGPC/xhBQAAAAlwSFlzAAAO - vwAADr8BOAVTJAAAANdJREFUOE+dU7kNwzAM1CjZwQtpBA/gwqVX8CKpAggI4FojuHNrwIVahZeQhh7a + vAAADrwBlbxySQAAANdJREFUOE+dU7kNwzAM1CjZwQtpBA/gwqVX8CKpAggI4FojuHNrwIVahZeQhh7a cXLAFSJ5Rz2UKRFCsERHjAURs1xWg5I3op/mKbb3NjZjkxEx5FCDWpZ9wOK1e3SVsOTwHGCyZia08Eho Ao1s4kVssTWtUNi7PgLLtuwxPo6FgdPOnBJCAEYSgwZaGGTFJbXuQmi/GmjdhapBKjjrDqoGqeisOygG 1SWKEDjqnl5i9YyyC+Co+/6MPAv+yhQKs0ECaPEe5SvTqI4ywCb/faYUlPzhOxvzAkO1WA01cJaNAAAA @@ -153,7 +153,7 @@ iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAALGPC/xhBQAAAAlwSFlzAAAO - wAAADsABataJCQAAANRJREFUOE+1U8ENwjAMzBAMwAiMwAJMAI98kfgzGx86QqbgU5CCeOSHgi/YlZNY + vQAADr0BR/uQrQAAANRJREFUOE+1U8ENwjAMzBAMwAiMwAJMAI98kfgzGx86QqbgU5CCeOSHgi/YlZNY SFBx0qmNfXdtWse1SCl54kDMDVHzLOtBzSUxPM6nPG43+bJwFVFDDxpo2fYGm+N1v+uMLW/HA0JiFUKL gAYE43pV2Bp1nUOCmD1eTUTPeyzUIVadt+MRMMieRQiI2KoVLXngRcD0JCvEMgvh7QJAHQJYZvA/AdqM q75vQyRg9kec9xt5FoJMIQTaLNT1apAAWpRRlmn8RHOUAQ757TBpUPOL4+zcCzIffKHxkkn8AAAAAElF @@ -162,11 +162,11 @@ - iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8 - YQUAAAAJcEhZcwAADsQAAA7EAZUrDhsAAADBSURBVDhPY0AH3759SwDi/UD8Hw2DxBKgyjABUFIBiM8f - vX/0f8G2gv/GM41RMEgMJAdSA1IL1QYBUM3va/bUYGhExx2HOkCGvEcxBMg5D5LApgEbhhpyHqY5AeQ0 - dEU339z8H7kmEkMchqHeSQAZsB+bn2Fg5pmZGHIgDNID0gsyAKsCZIDLNSC9RBkAA+iuoZ8BhLxAcSBS - Fo3QtHCemFQIwygJCQSAHHBSJiY1Yk3KIAA1hLzMhAyAkiRkZwYGAEcIWvs/bCjHAAAAAElFTkSuQmCC + iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAALGPC/xhBQAAAAlwSFlzAAAO + wQAADsEBuJFr7QAAAMFJREFUOE9jQAffvn1LAOL9QPwfDYPEEqDKMAFQUgGIzx+9f/R/wbaC/8YzjVEw + SAwkB1IDUgvVBgFQze9r9tRgaETHHYc6QIa8RzEEyDkPksCmARuGGnIepjkB5DR0RTff3PwfuSYSQxyG + od5JABmwH5ufYWDmmZkYciAM0gPSCzIAqwJkgMs1IL1EGQAD6K6hnwGEvEBxIFIWjdC0cJ6YVAjDKAkJ + BIAccFImJjViTcogADWEvMyEDICSJGRnBgYARwha+z9sKMcAAAAASUVORK5CYII= @@ -219,7 +219,7 @@ Copyright (c) .NET Foundation and Contributors iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAALGPC/xhBQAAAAlwSFlzAAAO - wwAADsMBx2+oZAAAABh0RVh0U29mdHdhcmUAcGFpbnQubmV0IDQuMC41ZYUyZQAAAKdJREFUOE+lkMEN + wAAADsABataJCQAAABh0RVh0U29mdHdhcmUAcGFpbnQubmV0IDQuMC41ZYUyZQAAAKdJREFUOE+lkMEN wyAQBCktooV8U4KLyTuklnxSkJ0P300WaZ3jwDaSLY18HLeDTQDQkHPG7f5CuD4LrNnzc6Rp+PCRpFps hUVP8i9+Dzc1PD3eVVD1kEAn2ZAkjYANi8LECvyeKIKUEmKM5V1tOgHxs0ENiw1Y/Byz1S/0ZB6dLNbL UINDy/wpKGTXmlsv0QguowJlSFewx5Dg9BecFuxxKBhBGQDhC/DB5AQ227rCAAAAAElFTkSuQmCC @@ -341,4 +341,7 @@ NOTE: You will have to restart Bonsai for any changes to take effect. Help + + The specified workflow path does not resolve to a workflow expression builder node. + \ No newline at end of file diff --git a/Bonsai.Editor/WorkflowRunner.cs b/Bonsai.Editor/WorkflowRunner.cs index e3ed7e0a4..5d6f651e6 100644 --- a/Bonsai.Editor/WorkflowRunner.cs +++ b/Bonsai.Editor/WorkflowRunner.cs @@ -32,9 +32,11 @@ static void RunLayout( workflowBuilder = new WorkflowBuilder(workflowBuilder.Workflow.ToInspectableGraph()); BuildAssignProperties(workflowBuilder, propertyAssignments); + + var visualizerSettings = VisualizerLayoutMap.FromVisualizerLayout(workflowBuilder, layout, typeVisualizers); + var visualizerDialogs = visualizerSettings.CreateVisualizerDialogs(workflowBuilder); LayoutHelper.SetWorkflowNotifications(workflowBuilder.Workflow, publishNotifications: false); - LayoutHelper.SetLayoutTags(workflowBuilder.Workflow, layout); - LayoutHelper.SetLayoutNotifications(layout); + LayoutHelper.SetLayoutNotifications(workflowBuilder.Workflow, visualizerDialogs); var services = new System.ComponentModel.Design.ServiceContainer(); services.AddService(typeof(WorkflowBuilder), workflowBuilder); @@ -42,31 +44,14 @@ static void RunLayout( var cts = new CancellationTokenSource(); var contextMenu = new ContextMenuStrip(); - void CreateVisualizerMapping(ExpressionBuilderGraph workflow, VisualizerLayout layout) + foreach (var launcher in visualizerDialogs) { - var mapping = LayoutHelper.CreateVisualizerMapping(workflow, layout, typeVisualizers, services); - foreach (var launcher in mapping.Values.Where(launcher => launcher.Visualizer.IsValueCreated)) + var activeLauncher = launcher; + contextMenu.Items.Add(new ToolStripMenuItem(launcher.Text, null, (sender, e) => { - var activeLauncher = launcher; - contextMenu.Items.Add(new ToolStripMenuItem(launcher.Text, null, (sender, e) => - { - activeLauncher.Show(services); - })); - } - - foreach (var settings in layout.DialogSettings) - { - if (settings is WorkflowEditorSettings editorSettings && - editorSettings.Tag is ExpressionBuilder builder && - ExpressionBuilder.Unwrap(builder) is IWorkflowExpressionBuilder workflowBuilder && - editorSettings.EditorVisualizerLayout != null && - editorSettings.EditorDialogSettings.Visible) - { - CreateVisualizerMapping(workflowBuilder.Workflow, editorSettings.EditorVisualizerLayout); - } - } + activeLauncher.Show(services); + })); } - CreateVisualizerMapping(workflowBuilder.Workflow, layout); contextMenu.Items.Add(new ToolStripSeparator()); contextMenu.Items.Add(new ToolStripMenuItem("Stop", null, (sender, e) => cts.Cancel())); @@ -76,6 +61,7 @@ editorSettings.Tag is ExpressionBuilder builder && notifyIcon.ContextMenuStrip = contextMenu; notifyIcon.Visible = true; + visualizerDialogs.Show(visualizerSettings, services); using var synchronizationContext = new WindowsFormsSynchronizationContext(); runtimeWorkflow.Finally(() => { diff --git a/Bonsai/DependencyInspector.cs b/Bonsai/DependencyInspector.cs index 7076fb1a5..69be47211 100644 --- a/Bonsai/DependencyInspector.cs +++ b/Bonsai/DependencyInspector.cs @@ -34,10 +34,9 @@ static IEnumerable GetVisualizerSettings(VisualizerLay foreach (var settings in layout.DialogSettings) { yield return settings; - var editorSettings = settings as WorkflowEditorSettings; - if (editorSettings != null && editorSettings.EditorVisualizerLayout != null) + if (settings.NestedLayout != null) { - stack.Push(editorSettings.EditorVisualizerLayout); + stack.Push(settings.NestedLayout); } } } From db8ebff7f5d25c2bdae371dda1d9bc080128374c Mon Sep 17 00:00:00 2001 From: glopesdev Date: Tue, 2 Jul 2024 14:10:28 +0100 Subject: [PATCH 08/32] Add navigation commands to undo stack --- Bonsai.Editor/EditorForm.cs | 7 ++--- Bonsai.Editor/GraphModel/WorkflowEditor.cs | 26 ++++++++-------- .../GraphView/WorkflowEditorControl.cs | 30 +++++++++---------- Bonsai.Editor/GraphView/WorkflowGraphView.cs | 19 +++++++----- 4 files changed, 41 insertions(+), 41 deletions(-) diff --git a/Bonsai.Editor/EditorForm.cs b/Bonsai.Editor/EditorForm.cs index 48b1a34d5..cbaff432f 100644 --- a/Bonsai.Editor/EditorForm.cs +++ b/Bonsai.Editor/EditorForm.cs @@ -172,7 +172,6 @@ public EditorForm( definitionsPath = Path.Combine(Path.GetTempPath(), DefinitionsDirectory + "." + GuidHelper.GetProcessGuid().ToString()); editorControl = new WorkflowEditorControl(editorSite); editorControl.Enter += new EventHandler(editorControl_Enter); - editorControl.WorkflowPath = null; editorControl.Dock = DockStyle.Fill; workflowSplitContainer.Panel1.Controls.Add(editorControl); propertyGrid.BrowsableAttributes = browsableAttributes = DesignTimeAttributes; @@ -839,7 +838,7 @@ void ClearWorkflow() ClearWorkflowError(); saveWorkflowDialog.FileName = null; workflowBuilder.Workflow.Clear(); - editorControl.WorkflowPath = null; + editorControl.ResetNavigation(); visualizerSettings.Clear(); ResetProjectStatus(); UpdateTitle(); @@ -871,7 +870,7 @@ bool OpenWorkflow(string fileName, bool setWorkingDirectory) UpdateWorkflowDirectory(fileName, setWorkingDirectory); if (EditorResult == EditorResult.ReloadEditor) return false; - editorControl.WorkflowPath = null; + editorControl.ResetNavigation(); if (workflowBuilder.Workflow.Count > 0 && !editorControl.WorkflowGraphView.GraphView.Nodes.Any()) { try { workflowBuilder.Workflow.Build(); } @@ -885,7 +884,7 @@ bool OpenWorkflow(string fileName, bool setWorkingDirectory) } workflowBuilder = PrepareWorkflow(workflowBuilder, workflowVersion, out bool upgraded); - editorControl.WorkflowPath = null; + editorControl.ResetNavigation(); editorSite.ValidateWorkflow(); var layoutPath = LayoutHelper.GetLayoutPath(fileName); diff --git a/Bonsai.Editor/GraphModel/WorkflowEditor.cs b/Bonsai.Editor/GraphModel/WorkflowEditor.cs index c7fd40b15..dfa57bec8 100644 --- a/Bonsai.Editor/GraphModel/WorkflowEditor.cs +++ b/Bonsai.Editor/GraphModel/WorkflowEditor.cs @@ -21,8 +21,8 @@ class WorkflowEditor readonly Subject error; readonly Subject updateLayout; readonly Subject invalidateLayout; + readonly Subject workflowPathChanged; readonly Subject> updateSelection; - readonly Subject closeWorkflowEditor; WorkflowEditorPath workflowPath; public WorkflowEditor(IServiceProvider provider, IGraphView view) @@ -33,9 +33,9 @@ public WorkflowEditor(IServiceProvider provider, IGraphView view) error = new Subject(); updateLayout = new Subject(); invalidateLayout = new Subject(); + workflowPathChanged = new Subject(); updateSelection = new Subject>(); - closeWorkflowEditor = new Subject(); - WorkflowPath = null; + ResetNavigation(); } public ExpressionBuilderGraph Workflow { get; private set; } @@ -45,7 +45,7 @@ public WorkflowEditor(IServiceProvider provider, IGraphView view) public WorkflowEditorPath WorkflowPath { get { return workflowPath; } - set + private set { workflowPath = value; var workflowBuilder = (WorkflowBuilder)serviceProvider.GetService(typeof(WorkflowBuilder)); @@ -66,6 +66,7 @@ public WorkflowEditorPath WorkflowPath IsReadOnly = false; } updateLayout.OnNext(false); + workflowPathChanged.OnNext(workflowPath); } } @@ -75,9 +76,9 @@ public WorkflowEditorPath WorkflowPath public IObservable InvalidateLayout => invalidateLayout; - public IObservable> UpdateSelection => updateSelection; + public IObservable WorkflowPathChanged => workflowPathChanged; - public IObservable CloseWorkflowEditor => closeWorkflowEditor; + public IObservable> UpdateSelection => updateSelection; private static Node FindWorkflowValue(ExpressionBuilderGraph workflow, ExpressionBuilder value) { @@ -1462,14 +1463,6 @@ from successor in workflowNode.Successors addNode(); removeEdge(); }); - - var builder = ExpressionBuilder.Unwrap(workflowNode.Value); - var disableBuilder = builder as DisableBuilder; - var workflowExpressionBuilder = (disableBuilder != null ? disableBuilder.Builder : builder) as IWorkflowExpressionBuilder; - if (workflowExpressionBuilder != null) - { - closeWorkflowEditor.OnNext(workflowExpressionBuilder); - } } public void DeleteGraphNodes(IEnumerable nodes) @@ -2109,6 +2102,11 @@ public void NavigateTo(WorkflowEditorPath path) restoreSelectedNodes(); }); } + + public void ResetNavigation() + { + WorkflowPath = null; + } } enum CreateGraphNodeType diff --git a/Bonsai.Editor/GraphView/WorkflowEditorControl.cs b/Bonsai.Editor/GraphView/WorkflowEditorControl.cs index b3344be6a..4b9173e60 100644 --- a/Bonsai.Editor/GraphView/WorkflowEditorControl.cs +++ b/Bonsai.Editor/GraphView/WorkflowEditorControl.cs @@ -54,17 +54,6 @@ public int AnnotationPanelSize } } - public WorkflowEditorPath WorkflowPath - { - get { return WorkflowGraphView.WorkflowPath; } - set { WorkflowGraphView.WorkflowPath = value; } - } - - public ExpressionBuilderGraph Workflow - { - get { return WorkflowGraphView.Workflow; } - } - public void ExpandAnnotationPanel(ExpressionBuilder builder) { annotationPanel.Tag = builder; @@ -153,6 +142,20 @@ public void SelectTab(WorkflowGraphView workflowGraphView) } } + public void ResetNavigation() + { + CloseAll(); + WorkflowGraphView.Editor.ResetNavigation(); + } + + void CloseAll() + { + while (tabControl.TabCount > 1) + { + CloseTab(tabControl.TabPages[1]); + } + } + void CloseTab(TabPage tabPage) { var tabState = (TabPageController)tabPage.Tag; @@ -354,10 +357,7 @@ private void closeToolStripMenuItem_Click(object sender, EventArgs e) private void closeAllToolStripMenuItem_Click(object sender, EventArgs e) { - while (tabControl.TabCount > 1) - { - CloseTab(tabControl.TabPages[1]); - } + CloseAll(); } protected override void ScaleControl(SizeF factor, BoundsSpecified specified) diff --git a/Bonsai.Editor/GraphView/WorkflowGraphView.cs b/Bonsai.Editor/GraphView/WorkflowGraphView.cs index fb59d0468..9784d1595 100644 --- a/Bonsai.Editor/GraphView/WorkflowGraphView.cs +++ b/Bonsai.Editor/GraphView/WorkflowGraphView.cs @@ -95,12 +95,7 @@ public GraphViewControl GraphView public WorkflowEditorPath WorkflowPath { get { return Editor.WorkflowPath; } - set - { - Editor.WorkflowPath = value; - UpdateSelection(forceUpdate: true); - OnWorkflowPathChanged(EventArgs.Empty); - } + set { Editor.NavigateTo(value); } } public event EventHandler WorkflowPathChanged @@ -537,7 +532,8 @@ private void UpdateGraphLayout(bool validateWorkflow) EditorControl.SelectTab(this); if (EditorControl.AnnotationPanel.Tag is ExpressionBuilder builder) { - if (!EditorControl.Workflow.Descendants().Contains(builder)) + var workflowBuilder = (WorkflowBuilder)serviceProvider.GetService(typeof(WorkflowBuilder)); + if (!workflowBuilder.Workflow.Descendants().Contains(builder)) { EditorControl.AnnotationPanel.NavigateToString(string.Empty); EditorControl.AnnotationPanel.Tag = null; @@ -961,9 +957,15 @@ private void InitializeTheme() private void InitializeViewBindings() { - Editor.Error.Subscribe(ex => uiService.ShowError(ex)); + Editor.Error.Subscribe(uiService.ShowError); Editor.UpdateLayout.Subscribe(UpdateGraphLayout); Editor.InvalidateLayout.Subscribe(InvalidateGraphLayout); + Editor.WorkflowPathChanged.Subscribe(path => + { + UpdateSelection(forceUpdate: true); + OnWorkflowPathChanged(EventArgs.Empty); + }); + Editor.UpdateSelection.Subscribe(selection => { var activeView = graphView; @@ -974,6 +976,7 @@ private void InitializeViewBindings() return selection.Any(builder => ExpressionBuilder.Unwrap(builder) == nodeBuilder); }); }); + Editor.ResetNavigation(); } #endregion From e705635fbac4e06a52c5efaf42fd1e690558c6c9 Mon Sep 17 00:00:00 2001 From: glopesdev Date: Tue, 2 Jul 2024 14:14:09 +0100 Subject: [PATCH 09/32] Ensure explorer and status reset on workflow clear --- Bonsai.Editor/EditorForm.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/Bonsai.Editor/EditorForm.cs b/Bonsai.Editor/EditorForm.cs index cbaff432f..8e5a533de 100644 --- a/Bonsai.Editor/EditorForm.cs +++ b/Bonsai.Editor/EditorForm.cs @@ -839,6 +839,7 @@ void ClearWorkflow() saveWorkflowDialog.FileName = null; workflowBuilder.Workflow.Clear(); editorControl.ResetNavigation(); + editorSite.ValidateWorkflow(); visualizerSettings.Clear(); ResetProjectStatus(); UpdateTitle(); From 4a417e484c2f4ca430ba706b49b2838a4b8161af Mon Sep 17 00:00:00 2001 From: glopesdev Date: Sun, 14 Jul 2024 10:04:20 +0100 Subject: [PATCH 10/32] Add workflow editable status to explorer view --- Bonsai.Editor/ExplorerTreeView.cs | 59 +++++++++++++------ .../Properties/Resources.Designer.cs | 20 +++++++ Bonsai.Editor/Properties/Resources.resx | 23 +++++++- 3 files changed, 82 insertions(+), 20 deletions(-) diff --git a/Bonsai.Editor/ExplorerTreeView.cs b/Bonsai.Editor/ExplorerTreeView.cs index 9bdf637fc..55344c6f1 100644 --- a/Bonsai.Editor/ExplorerTreeView.cs +++ b/Bonsai.Editor/ExplorerTreeView.cs @@ -12,24 +12,25 @@ namespace Bonsai.Editor class ExplorerTreeView : ToolboxTreeView { bool activeDoubleClick; - readonly ImageList iconList; + readonly ImageList imageList; + readonly ImageList stateImageList; public ExplorerTreeView() { - iconList = new() - { - ColorDepth = ColorDepth.Depth8Bit, - ImageSize = new Size(16, 16), - TransparentColor = Color.Transparent - }; - StateImageList = iconList; + imageList = new(); + stateImageList = new(); + StateImageList = stateImageList; + ImageList = imageList; } protected override void ScaleControl(SizeF factor, BoundsSpecified specified) { - iconList.Images.Clear(); - iconList.Images.Add(Resources.StatusReadyImage); - iconList.Images.Add(Resources.StatusBlockedImage); + imageList.Images.Clear(); + stateImageList.Images.Clear(); + imageList.Images.Add(Resources.WorkflowEditableImage); + imageList.Images.Add(Resources.WorkflowReadOnlyImage); + stateImageList.Images.Add(Resources.StatusReadyImage); + stateImageList.Images.Add(Resources.StatusBlockedImage); base.ScaleControl(factor, specified); } @@ -61,9 +62,13 @@ public void UpdateWorkflow(string name, WorkflowBuilder workflowBuilder) Nodes.Clear(); var rootNode = Nodes.Add(name); - AddWorkflow(rootNode.Nodes, null, workflowBuilder.Workflow); + AddWorkflow(rootNode.Nodes, null, workflowBuilder.Workflow, ExplorerNodeType.Editable); - static void AddWorkflow(TreeNodeCollection nodes, WorkflowEditorPath basePath, ExpressionBuilderGraph workflow) + static void AddWorkflow( + TreeNodeCollection nodes, + WorkflowEditorPath basePath, + ExpressionBuilderGraph workflow, + ExplorerNodeType parentNodeType) { for (int i = 0; i < workflow.Count; i++) { @@ -71,11 +76,15 @@ static void AddWorkflow(TreeNodeCollection nodes, WorkflowEditorPath basePath, E if (ExpressionBuilder.Unwrap(builder) is IWorkflowExpressionBuilder workflowBuilder && workflowBuilder.Workflow != null) { + var nodeType = parentNodeType == ExplorerNodeType.ReadOnly || workflowBuilder is IncludeWorkflowBuilder + ? ExplorerNodeType.ReadOnly + : ExplorerNodeType.Editable; var displayName = ExpressionBuilder.GetElementDisplayName(builder); var builderPath = new WorkflowEditorPath(i, basePath); var node = nodes.Add(displayName); + node.ImageIndex = node.SelectedImageIndex = GetImageIndex(nodeType); node.Tag = builderPath; - AddWorkflow(node.Nodes, builderPath, workflowBuilder.Workflow); + AddWorkflow(node.Nodes, builderPath, workflowBuilder.Workflow, nodeType); } } } @@ -108,7 +117,17 @@ bool SelectNode(TreeNodeCollection nodes, WorkflowEditorPath path) return false; } - private static int GetImageIndex(ExplorerNodeStatus status) + private static int GetImageIndex(ExplorerNodeType status) + { + return status switch + { + ExplorerNodeType.Editable => 0, + ExplorerNodeType.ReadOnly => 1, + _ => throw new ArgumentException("Invalid node type.", nameof(status)) + }; + } + + private static int GetStateImageIndex(ExplorerNodeStatus status) { return status switch { @@ -120,7 +139,7 @@ private static int GetImageIndex(ExplorerNodeStatus status) public void SetNodeStatus(ExplorerNodeStatus status) { - var imageIndex = GetImageIndex(status); + var imageIndex = GetStateImageIndex(status); SetNodeImageIndex(Nodes, imageIndex); static void SetNodeImageIndex(TreeNodeCollection nodes, int index) @@ -139,7 +158,7 @@ static void SetNodeImageIndex(TreeNodeCollection nodes, int index) public void SetNodeStatus(IEnumerable pathElements, ExplorerNodeStatus status) { var nodes = Nodes; - var imageIndex = GetImageIndex(status); + var imageIndex = GetStateImageIndex(status); foreach (var path in pathElements.Prepend(null)) { var found = false; @@ -161,6 +180,12 @@ public void SetNodeStatus(IEnumerable pathElements, Explorer } } + enum ExplorerNodeType + { + Editable, + ReadOnly + } + enum ExplorerNodeStatus { Ready, diff --git a/Bonsai.Editor/Properties/Resources.Designer.cs b/Bonsai.Editor/Properties/Resources.Designer.cs index f9ef2ac92..998ec7559 100644 --- a/Bonsai.Editor/Properties/Resources.Designer.cs +++ b/Bonsai.Editor/Properties/Resources.Designer.cs @@ -611,6 +611,16 @@ internal static string VisualizerLayoutOnNullWorkflow_Error { } } + /// + /// Looks up a localized resource of type System.Drawing.Bitmap. + /// + internal static System.Drawing.Bitmap WorkflowEditableImage { + get { + object obj = ResourceManager.GetObject("WorkflowEditableImage", resourceCulture); + return ((System.Drawing.Bitmap)(obj)); + } + } + /// /// Looks up a localized string similar to Externalized properties of this workflow can be configured below.. /// @@ -619,5 +629,15 @@ internal static string WorkflowPropertiesDescription { return ResourceManager.GetString("WorkflowPropertiesDescription", resourceCulture); } } + + /// + /// Looks up a localized resource of type System.Drawing.Bitmap. + /// + internal static System.Drawing.Bitmap WorkflowReadOnlyImage { + get { + object obj = ResourceManager.GetObject("WorkflowReadOnlyImage", resourceCulture); + return ((System.Drawing.Bitmap)(obj)); + } + } } } diff --git a/Bonsai.Editor/Properties/Resources.resx b/Bonsai.Editor/Properties/Resources.resx index 86579078e..a0a0d3516 100644 --- a/Bonsai.Editor/Properties/Resources.resx +++ b/Bonsai.Editor/Properties/Resources.resx @@ -153,7 +153,7 @@ iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAALGPC/xhBQAAAAlwSFlzAAAO - vQAADr0BR/uQrQAAANRJREFUOE+1U8ENwjAMzBAMwAiMwAJMAI98kfgzGx86QqbgU5CCeOSHgi/YlZNY + vAAADrwBlbxySQAAANRJREFUOE+1U8ENwjAMzBAMwAiMwAJMAI98kfgzGx86QqbgU5CCeOSHgi/YlZNY SFBx0qmNfXdtWse1SCl54kDMDVHzLOtBzSUxPM6nPG43+bJwFVFDDxpo2fYGm+N1v+uMLW/HA0JiFUKL gAYE43pV2Bp1nUOCmD1eTUTPeyzUIVadt+MRMMieRQiI2KoVLXngRcD0JCvEMgvh7QJAHQJYZvA/AdqM q75vQyRg9kec9xt5FoJMIQTaLNT1apAAWpRRlmn8RHOUAQ757TBpUPOL4+zcCzIffKHxkkn8AAAAAElF @@ -163,7 +163,7 @@ iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAALGPC/xhBQAAAAlwSFlzAAAO - wQAADsEBuJFr7QAAAMFJREFUOE9jQAffvn1LAOL9QPwfDYPEEqDKMAFQUgGIzx+9f/R/wbaC/8YzjVEw + vgAADr4B6kKxwAAAAMFJREFUOE9jQAffvn1LAOL9QPwfDYPEEqDKMAFQUgGIzx+9f/R/wbaC/8YzjVEw SAwkB1IDUgvVBgFQze9r9tRgaETHHYc6QIa8RzEEyDkPksCmARuGGnIepjkB5DR0RTff3PwfuSYSQxyG od5JABmwH5ufYWDmmZkYciAM0gPSCzIAqwJkgMs1IL1EGQAD6K6hnwGEvEBxIFIWjdC0cJ6YVAjDKAkJ BIAccFImJjViTcogADWEvMyEDICSJGRnBgYARwha+z9sKMcAAAAASUVORK5CYII= @@ -219,7 +219,7 @@ Copyright (c) .NET Foundation and Contributors iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAALGPC/xhBQAAAAlwSFlzAAAO - wAAADsABataJCQAAABh0RVh0U29mdHdhcmUAcGFpbnQubmV0IDQuMC41ZYUyZQAAAKdJREFUOE+lkMEN + vQAADr0BR/uQrQAAABh0RVh0U29mdHdhcmUAcGFpbnQubmV0IDQuMC41ZYUyZQAAAKdJREFUOE+lkMEN wyAQBCktooV8U4KLyTuklnxSkJ0P300WaZ3jwDaSLY18HLeDTQDQkHPG7f5CuD4LrNnzc6Rp+PCRpFps hUVP8i9+Dzc1PD3eVVD1kEAn2ZAkjYANi8LECvyeKIKUEmKM5V1tOgHxs0ENiw1Y/Byz1S/0ZB6dLNbL UINDy/wpKGTXmlsv0QguowJlSFewx5Dg9BecFuxxKBhBGQDhC/DB5AQ227rCAAAAAElFTkSuQmCC @@ -344,4 +344,21 @@ NOTE: You will have to restart Bonsai for any changes to take effect. The specified workflow path does not resolve to a workflow expression builder node. + + + iVBORw0KGgoAAAANSUhEUgAAAA8AAAAPCAYAAAA71pVKAAAABGdBTUEAALGPC/xhBQAAAAlwSFlzAAAO + wAAADsABataJCQAAAH9JREFUOE/VjdEJwCAMRB2ls7ih/+7gHG7hBrY/+mk9iWKNVAqF0oMjJHmXiFcV + Y9xCCCY7dTaYEzIXBb1SKkkpm9FjfnsAH8bgcMAQypWXDdZaJ2ttqXWGPaFcfdg5V2DUfkYoF+CVCeXC + sn6Z+VF498el/0l49DK8MqGfSogTPS0oz8b0R/8AAAAASUVORK5CYII= + + + + + iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAALGPC/xhBQAAAAlwSFlzAAAO + wQAADsEBuJFr7QAAAJVJREFUOE+9kLENAyEQBCnFJbgEi1qoggKok4yAwAmCEHssTkL4/3UE9kob8Ghm + xZu/pJRyp+O4F8Ba65NuSwQOIXS6JZmWP7C1Vi9Zl3PO3Tmnk8wwgEAioaeCI3iVpJSuYe/9F0wvlwmX + 7/YjAfDpsgR4fusWTBAArD9MBRMRyGqMUQ8TBEv1sASgtfYYvY3Pv4wxL5igM/WVQzVaAAAAAElFTkSu + QmCC + + \ No newline at end of file From 63568fb53a76ffd9dac09ee69875078c9dc637d0 Mon Sep 17 00:00:00 2001 From: glopesdev Date: Sun, 14 Jul 2024 10:05:00 +0100 Subject: [PATCH 11/32] Hide ready state from explorer view --- Bonsai.Editor/ExplorerTreeView.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Bonsai.Editor/ExplorerTreeView.cs b/Bonsai.Editor/ExplorerTreeView.cs index 55344c6f1..8be943215 100644 --- a/Bonsai.Editor/ExplorerTreeView.cs +++ b/Bonsai.Editor/ExplorerTreeView.cs @@ -131,7 +131,7 @@ private static int GetStateImageIndex(ExplorerNodeStatus status) { return status switch { - ExplorerNodeStatus.Ready => 0, + ExplorerNodeStatus.Ready => -1, ExplorerNodeStatus.Blocked => 1, _ => throw new ArgumentException("Invalid node status.", nameof(status)) }; From 441a510c64f1b63e63c462b00858c47b0e75608c Mon Sep 17 00:00:00 2001 From: glopesdev Date: Sat, 3 Aug 2024 13:37:41 +0100 Subject: [PATCH 12/32] Ensure explorer view refreshes on build error --- Bonsai.Editor/EditorForm.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/Bonsai.Editor/EditorForm.cs b/Bonsai.Editor/EditorForm.cs index 8e5a533de..a7d41b884 100644 --- a/Bonsai.Editor/EditorForm.cs +++ b/Bonsai.Editor/EditorForm.cs @@ -2678,6 +2678,7 @@ public bool ValidateWorkflow() catch (WorkflowBuildException ex) { siteForm.HandleWorkflowError(ex); + siteForm.OnWorkflowValidated(EventArgs.Empty); return false; } } From f09ba1d79559283fd78d429413c768526eb29371 Mon Sep 17 00:00:00 2001 From: glopesdev Date: Sat, 3 Aug 2024 13:50:26 +0100 Subject: [PATCH 13/32] Ensure explorer view initializes on load --- Bonsai.Editor/EditorForm.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Bonsai.Editor/EditorForm.cs b/Bonsai.Editor/EditorForm.cs index a7d41b884..031aa2443 100644 --- a/Bonsai.Editor/EditorForm.cs +++ b/Bonsai.Editor/EditorForm.cs @@ -502,7 +502,8 @@ IObservable InitializeWorkflowExplorerWatcher() var workflowValidated = Observable.FromEventPattern( handler => Events.AddHandler(WorkflowValidated, handler), handler => Events.RemoveHandler(WorkflowValidated, handler)) - .Select(evt => selectionModel.SelectedView); + .Select(evt => selectionModel.SelectedView) + .Merge(Observable.Return(selectionModel.SelectedView)); return Observable.Merge(selectedViewChanged, workflowValidated.Do(view => { if (workflowBuilder.Workflow == null) From fdc9b4311b88aaea719e8e5a5c7646b5e80b4e2f Mon Sep 17 00:00:00 2001 From: glopesdev Date: Sat, 3 Aug 2024 16:21:00 +0100 Subject: [PATCH 14/32] Prescale state image list size to system DPI --- Bonsai.Editor/ExplorerTreeView.cs | 36 +++++++++++++++++++++++-------- 1 file changed, 27 insertions(+), 9 deletions(-) diff --git a/Bonsai.Editor/ExplorerTreeView.cs b/Bonsai.Editor/ExplorerTreeView.cs index 8be943215..d49b138d4 100644 --- a/Bonsai.Editor/ExplorerTreeView.cs +++ b/Bonsai.Editor/ExplorerTreeView.cs @@ -19,19 +19,37 @@ public ExplorerTreeView() { imageList = new(); stateImageList = new(); - StateImageList = stateImageList; - ImageList = imageList; - } - - protected override void ScaleControl(SizeF factor, BoundsSpecified specified) - { - imageList.Images.Clear(); - stateImageList.Images.Clear(); imageList.Images.Add(Resources.WorkflowEditableImage); imageList.Images.Add(Resources.WorkflowReadOnlyImage); +#if NETFRAMEWORK stateImageList.Images.Add(Resources.StatusReadyImage); stateImageList.Images.Add(Resources.StatusBlockedImage); - base.ScaleControl(factor, specified); +#else + // TreeView.StateImageList.ImageSize is internally scaled according to initial system DPI (not font). + // To avoid excessive scaling of images we must prepare correctly sized ImageList beforehand. + const float DefaultDpi = 96f; + using var graphics = CreateGraphics(); + var dpiScale = graphics.DpiY / DefaultDpi; + stateImageList.ImageSize = new Size( + (int)(16 * dpiScale), + (int)(16 * dpiScale)); + stateImageList.Images.Add(ResizeMakeBorder(Resources.StatusReadyImage, stateImageList.ImageSize)); + stateImageList.Images.Add(ResizeMakeBorder(Resources.StatusBlockedImage, stateImageList.ImageSize)); + + static Bitmap ResizeMakeBorder(Bitmap original, Size newSize) + { + //TODO: DrawImageUnscaledAndClipped gives best results but blending is not great + var image = new Bitmap(newSize.Width, newSize.Height, original.PixelFormat); + using var graphics = Graphics.FromImage(image); + var offsetX = (newSize.Width - original.Width) / 2; + var offsetY = (newSize.Height - original.Height) / 2; + graphics.DrawImageUnscaledAndClipped(original, new Rectangle(offsetX, offsetY, original.Width, original.Height)); + return image; + } +#endif + + StateImageList = stateImageList; + ImageList = imageList; } protected override void OnBeforeCollapse(TreeViewCancelEventArgs e) From cf66ea04fdc3cd702277005bb45673feda637f6a Mon Sep 17 00:00:00 2001 From: glopesdev Date: Sat, 3 Aug 2024 16:21:21 +0100 Subject: [PATCH 15/32] Save explorer splitter distance to editor settings --- Bonsai.Editor/EditorForm.cs | 4 ++++ Bonsai.Editor/EditorSettings.cs | 9 +++++++++ 2 files changed, 13 insertions(+) diff --git a/Bonsai.Editor/EditorForm.cs b/Bonsai.Editor/EditorForm.cs index 031aa2443..7a3f27683 100644 --- a/Bonsai.Editor/EditorForm.cs +++ b/Bonsai.Editor/EditorForm.cs @@ -262,6 +262,8 @@ void RestoreEditorSettings() themeRenderer.ActiveTheme = EditorSettings.Instance.EditorTheme; editorControl.AnnotationPanelSize = (int)Math.Round( EditorSettings.Instance.AnnotationPanelSize * scaleFactor.Width); + explorerSplitContainer.SplitterDistance = (int)Math.Round( + EditorSettings.Instance.ExplorerSplitterDistance * scaleFactor.Width); } void CloseEditorForm() @@ -269,6 +271,8 @@ void CloseEditorForm() Application.RemoveMessageFilter(hotKeys); EditorSettings.Instance.AnnotationPanelSize = (int)Math.Round( editorControl.AnnotationPanelSize * inverseScaleFactor.Width); + EditorSettings.Instance.ExplorerSplitterDistance = (int)Math.Round( + explorerSplitContainer.SplitterDistance * inverseScaleFactor.Width); var desktopBounds = WindowState != FormWindowState.Normal ? RestoreBounds : Bounds; EditorSettings.Instance.DesktopBounds = ScaleBounds(desktopBounds, inverseScaleFactor); if (WindowState == FormWindowState.Minimized) diff --git a/Bonsai.Editor/EditorSettings.cs b/Bonsai.Editor/EditorSettings.cs index 1759cf951..9c5edaaa3 100644 --- a/Bonsai.Editor/EditorSettings.cs +++ b/Bonsai.Editor/EditorSettings.cs @@ -21,6 +21,7 @@ sealed class EditorSettings internal EditorSettings(string path) { AnnotationPanelSize = 400; + ExplorerSplitterDistance = 300; settingsPath = path; } @@ -37,6 +38,8 @@ public static EditorSettings Instance public int AnnotationPanelSize { get; set; } + public int ExplorerSplitterDistance { get; set; } + public RecentlyUsedFileCollection RecentlyUsedFiles { get { return recentlyUsedFiles; } @@ -74,6 +77,11 @@ static EditorSettings Load() int.TryParse(reader.ReadElementContentAsString(), out int annotationPanelSize); settings.AnnotationPanelSize = annotationPanelSize; } + else if (reader.Name == nameof(ExplorerSplitterDistance)) + { + int.TryParse(reader.ReadElementContentAsString(), out int explorerSplitterDistance); + settings.ExplorerSplitterDistance = explorerSplitterDistance; + } else if (reader.Name == nameof(DesktopBounds)) { reader.ReadToFollowing(nameof(Rectangle.X)); @@ -120,6 +128,7 @@ public void Save() writer.WriteElementString(nameof(WindowState), WindowState.ToString()); writer.WriteElementString(nameof(EditorTheme), EditorTheme.ToString()); writer.WriteElementString(nameof(AnnotationPanelSize), AnnotationPanelSize.ToString(CultureInfo.InvariantCulture)); + writer.WriteElementString(nameof(ExplorerSplitterDistance), ExplorerSplitterDistance.ToString(CultureInfo.InvariantCulture)); writer.WriteStartElement(nameof(DesktopBounds)); writer.WriteElementString(nameof(Rectangle.X), DesktopBounds.X.ToString(CultureInfo.InvariantCulture)); From 2e5d18b255389ded9f9831653a97544d21743815 Mon Sep 17 00:00:00 2001 From: glopesdev Date: Sat, 3 Aug 2024 16:43:03 +0100 Subject: [PATCH 16/32] Size description boxes relative to each other --- Bonsai.Editor/EditorForm.cs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/Bonsai.Editor/EditorForm.cs b/Bonsai.Editor/EditorForm.cs index 7a3f27683..3109ea6cf 100644 --- a/Bonsai.Editor/EditorForm.cs +++ b/Bonsai.Editor/EditorForm.cs @@ -365,14 +365,10 @@ protected override void ScaleControl(SizeF factor, BoundsSpecified specified) inverseScaleFactor = new SizeF(1f / factor.Width, 1f / factor.Height); #if NETFRAMEWORK - const float DefaultToolboxSplitterDistance = 245f; var workflowSplitterScale = EditorSettings.IsRunningOnMono ? 0.5f / factor.Width : 1.0f; - var toolboxSplitterScale = EditorSettings.IsRunningOnMono ? 0.75f / factor.Height : 1.0f; - toolboxSplitterScale *= DefaultToolboxSplitterDistance / toolboxSplitContainer.SplitterDistance; panelSplitContainer.SplitterDistance = (int)(panelSplitContainer.SplitterDistance * factor.Height); workflowSplitContainer.SplitterDistance = (int)(workflowSplitContainer.SplitterDistance * workflowSplitterScale * factor.Height); propertiesSplitContainer.SplitterDistance = (int)(propertiesSplitContainer.SplitterDistance * factor.Height); - toolboxSplitContainer.SplitterDistance = (int)(toolboxSplitContainer.SplitterDistance * toolboxSplitterScale * factor.Height); workflowSplitContainer.Panel1.Padding = new Padding(0, 6, 0, 2); var imageSize = toolStrip.ImageScalingSize; @@ -386,6 +382,8 @@ protected override void ScaleControl(SizeF factor, BoundsSpecified specified) propertyGrid.LargeButtons = scalingFactor >= 2; } #endif + var toolboxBottomMargin = toolboxSplitContainer.Margin.Bottom; + toolboxSplitContainer.SplitterDistance = toolboxSplitContainer.Height - propertiesSplitContainer.SplitterDistance - toolboxBottomMargin; base.ScaleControl(factor, specified); } From 9d7bc66e3231d386e8c6e627c1dd4aec126aa371 Mon Sep 17 00:00:00 2001 From: glopesdev Date: Sat, 3 Aug 2024 16:44:53 +0100 Subject: [PATCH 17/32] Fix description box sizes in netcore --- Bonsai.Editor/EditorForm.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Bonsai.Editor/EditorForm.cs b/Bonsai.Editor/EditorForm.cs index 3109ea6cf..21f6e5d07 100644 --- a/Bonsai.Editor/EditorForm.cs +++ b/Bonsai.Editor/EditorForm.cs @@ -381,6 +381,9 @@ protected override void ScaleControl(SizeF factor, BoundsSpecified specified) statusStrip.ImageScalingSize = toolStrip.ImageScalingSize; propertyGrid.LargeButtons = scalingFactor >= 2; } +#else + const float PropertiesSplitterScale = 0.4f; // correct for overshoot rescaling in .NET core + propertiesSplitContainer.SplitterDistance = (int)(propertiesSplitContainer.SplitterDistance * PropertiesSplitterScale * factor.Height); #endif var toolboxBottomMargin = toolboxSplitContainer.Margin.Bottom; toolboxSplitContainer.SplitterDistance = toolboxSplitContainer.Height - propertiesSplitContainer.SplitterDistance - toolboxBottomMargin; From 9042dc70660f2a7a4a061b4dc4ddab353b241478 Mon Sep 17 00:00:00 2001 From: glopesdev Date: Sat, 3 Aug 2024 17:22:59 +0100 Subject: [PATCH 18/32] Avoid clipping breadcrumbs control --- Bonsai.Editor/GraphView/WorkflowEditorControl.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/Bonsai.Editor/GraphView/WorkflowEditorControl.cs b/Bonsai.Editor/GraphView/WorkflowEditorControl.cs index 4b9173e60..df96113bf 100644 --- a/Bonsai.Editor/GraphView/WorkflowEditorControl.cs +++ b/Bonsai.Editor/GraphView/WorkflowEditorControl.cs @@ -97,6 +97,7 @@ TabPageController InitializeTab(TabPage tabPage) tabPage.SuspendLayout(); var breadcrumbs = new WorkflowPathNavigationControl(serviceProvider); + breadcrumbs.Dock = DockStyle.Fill; breadcrumbs.WorkflowPath = null; breadcrumbs.WorkflowPathMouseClick += (sender, e) => workflowGraphView.WorkflowPath = e.Path; workflowGraphView.WorkflowPathChanged += (sender, e) => From ef178152512b940f174a1eec1d046b8cd3da6e15 Mon Sep 17 00:00:00 2001 From: glopesdev Date: Sun, 4 Aug 2024 02:22:57 +0100 Subject: [PATCH 19/32] Auto-compress path breadcrumbs to fit control size --- .../WorkflowEditorControl.Designer.cs | 1 - .../GraphView/WorkflowEditorControl.cs | 6 +- .../WorkflowPathNavigationControl.Designer.cs | 3 +- .../WorkflowPathNavigationControl.cs | 73 +++++++++++++++++-- 4 files changed, 72 insertions(+), 11 deletions(-) diff --git a/Bonsai.Editor/GraphView/WorkflowEditorControl.Designer.cs b/Bonsai.Editor/GraphView/WorkflowEditorControl.Designer.cs index 92b79d164..d34e5cc7d 100644 --- a/Bonsai.Editor/GraphView/WorkflowEditorControl.Designer.cs +++ b/Bonsai.Editor/GraphView/WorkflowEditorControl.Designer.cs @@ -191,7 +191,6 @@ private void InitializeComponent() this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F); this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; 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); diff --git a/Bonsai.Editor/GraphView/WorkflowEditorControl.cs b/Bonsai.Editor/GraphView/WorkflowEditorControl.cs index df96113bf..c3871a850 100644 --- a/Bonsai.Editor/GraphView/WorkflowEditorControl.cs +++ b/Bonsai.Editor/GraphView/WorkflowEditorControl.cs @@ -97,7 +97,6 @@ TabPageController InitializeTab(TabPage tabPage) tabPage.SuspendLayout(); var breadcrumbs = new WorkflowPathNavigationControl(serviceProvider); - breadcrumbs.Dock = DockStyle.Fill; breadcrumbs.WorkflowPath = null; breadcrumbs.WorkflowPathMouseClick += (sender, e) => workflowGraphView.WorkflowPath = e.Path; workflowGraphView.WorkflowPathChanged += (sender, e) => @@ -114,6 +113,11 @@ TabPageController InitializeTab(TabPage tabPage) navigationPanel.RowStyles.Add(new RowStyle(SizeType.AutoSize)); navigationPanel.Controls.Add(breadcrumbs); navigationPanel.Controls.Add(workflowGraphView); + + // TODO: This should be handled by docking, but some strange interaction prevents shrinking to min size + navigationPanel.Layout += (sender, e) => breadcrumbs.Width = navigationPanel.Width; + breadcrumbs.Width = navigationPanel.Width; + tabPage.Controls.Add(navigationPanel); tabPage.BackColor = workflowGraphView.BackColor; tabPage.ResumeLayout(false); diff --git a/Bonsai.Editor/GraphView/WorkflowPathNavigationControl.Designer.cs b/Bonsai.Editor/GraphView/WorkflowPathNavigationControl.Designer.cs index 01368109d..41edb79d9 100644 --- a/Bonsai.Editor/GraphView/WorkflowPathNavigationControl.Designer.cs +++ b/Bonsai.Editor/GraphView/WorkflowPathNavigationControl.Designer.cs @@ -33,18 +33,17 @@ private void InitializeComponent() // // flowLayoutPanel // - this.flowLayoutPanel.AutoSize = true; this.flowLayoutPanel.Dock = System.Windows.Forms.DockStyle.Fill; this.flowLayoutPanel.Location = new System.Drawing.Point(0, 0); this.flowLayoutPanel.Name = "flowLayoutPanel"; this.flowLayoutPanel.Size = new System.Drawing.Size(452, 29); this.flowLayoutPanel.TabIndex = 0; + this.flowLayoutPanel.WrapContents = false; // // EditorPathNavigationControl // this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F); this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; - this.AutoSize = true; this.Controls.Add(this.flowLayoutPanel); this.Name = "EditorPathNavigationControl"; this.Size = new System.Drawing.Size(452, 29); diff --git a/Bonsai.Editor/GraphView/WorkflowPathNavigationControl.cs b/Bonsai.Editor/GraphView/WorkflowPathNavigationControl.cs index bdc27c77a..bace621e1 100644 --- a/Bonsai.Editor/GraphView/WorkflowPathNavigationControl.cs +++ b/Bonsai.Editor/GraphView/WorkflowPathNavigationControl.cs @@ -15,6 +15,7 @@ partial class WorkflowPathNavigationControl : UserControl readonly IWorkflowEditorService editorService; readonly ThemeRenderer themeRenderer; WorkflowEditorPath workflowPath; + int totalPathWidth; public WorkflowPathNavigationControl(IServiceProvider provider) { @@ -78,35 +79,87 @@ static IEnumerable> GetPathElements(Wor private void SetPath(IEnumerable> pathElements) { SuspendLayout(); - var rootButton = CreateButton(editorService.GetProjectDisplayName(), null); + totalPathWidth = 0; flowLayoutPanel.Controls.Clear(); - flowLayoutPanel.Controls.Add(rootButton); + AddPathButton("...", null, createEvent: false, visible: false); + AddPathButton(editorService.GetProjectDisplayName(), null); foreach (var path in pathElements) { - var separator = CreateButton(">", null, createEvent: false); - var pathButton = CreateButton(path.Key, path.Value); - flowLayoutPanel.Controls.Add(separator); - flowLayoutPanel.Controls.Add(pathButton); + AddPathButton(">", null, createEvent: false); + AddPathButton(path.Key, path.Value); } + CompressPath(); ResumeLayout(true); } - private Button CreateButton(string text, WorkflowEditorPath path, bool createEvent = true) + private void CompressPath() + { + if (flowLayoutPanel.Controls.Count <= 4) + return; + + bool compressPath = false; + var totalWidth = totalPathWidth; + if (totalWidth > Width) + { + // adjust for inserting the ellipsis button + totalWidth -= flowLayoutPanel.Controls[1].Width; + totalWidth += flowLayoutPanel.Controls[0].Width; + compressPath = true; + } + + var excessWidth = totalWidth - Width; + for (int i = 2; i < flowLayoutPanel.Controls.Count - 4; i++) + { + // separator and breadcrumb buttons are hidden together + var visible = !compressPath || excessWidth <= 0; + if (i % 2 != 0) visible &= flowLayoutPanel.Controls[i - 1].Visible; + + // hide excess breadcrumb levels + flowLayoutPanel.Controls[i].Visible = visible; + if (excessWidth > 0) + { + excessWidth -= GetControlWidth(flowLayoutPanel.Controls[i]); + } + } + + // either the root or ellipsis button is shown + flowLayoutPanel.Controls[0].Visible = compressPath; + flowLayoutPanel.Controls[1].Visible = !compressPath; + } + + private int GetControlWidth(Control control) + { + return control.Width + control.Margin.Horizontal + flowLayoutPanel.Padding.Right; + } + + private BreadcrumbButtton AddPathButton(string text, WorkflowEditorPath path, bool createEvent = true, bool visible = true) { var breadcrumbButton = new BreadcrumbButtton { AutoSize = true, Locked = !createEvent, AutoSizeMode = AutoSizeMode.GrowAndShrink, + Visible = visible, Text = text, Tag = path }; if (createEvent) breadcrumbButton.MouseClick += BreadcrumbButton_MouseClick; + breadcrumbButton.ParentChanged += BreadcrumbButton_ParentChanged; SetBreadcrumbTheme(breadcrumbButton, themeRenderer); + flowLayoutPanel.Controls.Add(breadcrumbButton); + if (flowLayoutPanel.Controls.Count > 1) + totalPathWidth += GetControlWidth(breadcrumbButton); return breadcrumbButton; } + private void BreadcrumbButton_ParentChanged(object sender, EventArgs e) + { + var button = (Button)sender; + if (button.Parent == null) + button.Dispose(); + } + private void BreadcrumbButton_MouseClick(object sender, MouseEventArgs e) { var button = (Button)sender; @@ -114,6 +167,12 @@ private void BreadcrumbButton_MouseClick(object sender, MouseEventArgs e) OnWorkflowPathMouseClick(new WorkflowPathMouseEventArgs(path, e.Button, e.Clicks, e.X, e.Y, e.Delta)); } + protected override void OnLayout(LayoutEventArgs e) + { + CompressPath(); + base.OnLayout(e); + } + protected override void OnHandleDestroyed(EventArgs e) { themeRenderer.ThemeChanged -= ThemeRenderer_ThemeChanged; From 99a5a4efa38b4da97479bf9642a71154d7533109 Mon Sep 17 00:00:00 2001 From: glopesdev Date: Sun, 4 Aug 2024 02:24:40 +0100 Subject: [PATCH 20/32] Avoid renaming tab page on single tab mode --- Bonsai.Editor/GraphView/WorkflowEditorControl.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/Bonsai.Editor/GraphView/WorkflowEditorControl.cs b/Bonsai.Editor/GraphView/WorkflowEditorControl.cs index c3871a850..e11b7d90f 100644 --- a/Bonsai.Editor/GraphView/WorkflowEditorControl.cs +++ b/Bonsai.Editor/GraphView/WorkflowEditorControl.cs @@ -102,7 +102,6 @@ TabPageController InitializeTab(TabPage tabPage) workflowGraphView.WorkflowPathChanged += (sender, e) => { breadcrumbs.WorkflowPath = workflowGraphView.WorkflowPath; - tabState.Text = breadcrumbs.DisplayName; }; var navigationPanel = new TableLayoutPanel(); @@ -248,7 +247,6 @@ public string Text void UpdateDisplayText() { - //TabPage.Text = displayText + (WorkflowGraphView.ReadOnly ? ReadOnlySuffix : string.Empty) + CloseSuffix; TabPage.Text = displayText + (WorkflowGraphView.IsReadOnly ? ReadOnlySuffix : string.Empty); } From 1f97636b32d0b45c05d701dbe8b797672fc920b3 Mon Sep 17 00:00:00 2001 From: glopesdev Date: Sun, 4 Aug 2024 02:37:54 +0100 Subject: [PATCH 21/32] Size description box depending on editor settings --- Bonsai.Editor/EditorForm.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Bonsai.Editor/EditorForm.cs b/Bonsai.Editor/EditorForm.cs index 21f6e5d07..72fcb31a2 100644 --- a/Bonsai.Editor/EditorForm.cs +++ b/Bonsai.Editor/EditorForm.cs @@ -264,6 +264,8 @@ void RestoreEditorSettings() EditorSettings.Instance.AnnotationPanelSize * scaleFactor.Width); explorerSplitContainer.SplitterDistance = (int)Math.Round( EditorSettings.Instance.ExplorerSplitterDistance * scaleFactor.Width); + var toolboxBottomMargin = toolboxSplitContainer.Margin.Bottom; + toolboxSplitContainer.SplitterDistance = toolboxSplitContainer.Height - propertiesSplitContainer.SplitterDistance - toolboxBottomMargin; } void CloseEditorForm() @@ -385,8 +387,6 @@ protected override void ScaleControl(SizeF factor, BoundsSpecified specified) const float PropertiesSplitterScale = 0.4f; // correct for overshoot rescaling in .NET core propertiesSplitContainer.SplitterDistance = (int)(propertiesSplitContainer.SplitterDistance * PropertiesSplitterScale * factor.Height); #endif - var toolboxBottomMargin = toolboxSplitContainer.Margin.Bottom; - toolboxSplitContainer.SplitterDistance = toolboxSplitContainer.Height - propertiesSplitContainer.SplitterDistance - toolboxBottomMargin; base.ScaleControl(factor, specified); } From a5f7449c292f5332c4371faa86e32083e46c68f4 Mon Sep 17 00:00:00 2001 From: glopesdev Date: Mon, 5 Aug 2024 17:47:12 +0100 Subject: [PATCH 22/32] Compress layout XML representation --- Bonsai.Editor/Layout/VisualizerDialogSettings.cs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/Bonsai.Editor/Layout/VisualizerDialogSettings.cs b/Bonsai.Editor/Layout/VisualizerDialogSettings.cs index af85354d7..9074efff9 100644 --- a/Bonsai.Editor/Layout/VisualizerDialogSettings.cs +++ b/Bonsai.Editor/Layout/VisualizerDialogSettings.cs @@ -11,8 +11,16 @@ namespace Bonsai.Design #pragma warning restore CS0612 // Type or member is obsolete public class VisualizerDialogSettings { + [XmlIgnore] public int? Index { get; set; } + [XmlAttribute(nameof(Index))] + public string IndexXml + { + get => Index.HasValue ? Index.GetValueOrDefault().ToString() : null; + set => Index = !string.IsNullOrEmpty(value) ? int.Parse(value) : null; + } + [XmlIgnore] public object Tag { get; set; } @@ -44,12 +52,16 @@ public Rectangle Bounds // [Obsolete] public Collection Mashups { get; } = new Collection(); + public bool VisibleSpecified => Visible; + public bool LocationSpecified => !Location.IsEmpty; public bool SizeSpecified => !Size.IsEmpty; public bool WindowStateSpecified => WindowState != FormWindowState.Normal; + public bool NestedLayoutSpecified => NestedLayout?.DialogSettings.Count > 0; + public bool MashupsSpecified => false; } } From 96f9119feab4613376a313712dfc4037f8c76d75 Mon Sep 17 00:00:00 2001 From: glopesdev Date: Tue, 3 Sep 2024 17:26:29 +0100 Subject: [PATCH 23/32] Ensure project name is set before navigation reset --- Bonsai.Editor/EditorForm.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Bonsai.Editor/EditorForm.cs b/Bonsai.Editor/EditorForm.cs index 72fcb31a2..fe05d4547 100644 --- a/Bonsai.Editor/EditorForm.cs +++ b/Bonsai.Editor/EditorForm.cs @@ -891,6 +891,7 @@ bool OpenWorkflow(string fileName, bool setWorkingDirectory) } workflowBuilder = PrepareWorkflow(workflowBuilder, workflowVersion, out bool upgraded); + saveWorkflowDialog.FileName = fileName; editorControl.ResetNavigation(); editorSite.ValidateWorkflow(); @@ -908,7 +909,6 @@ bool OpenWorkflow(string fileName, bool setWorkingDirectory) } } - saveWorkflowDialog.FileName = fileName; ResetProjectStatus(); if (upgraded) { From 24dbff3e345bf74a779c5e3a41c36c5360ebfe23 Mon Sep 17 00:00:00 2001 From: glopesdev Date: Tue, 3 Sep 2024 18:45:52 +0100 Subject: [PATCH 24/32] Clear workflow error highlight --- Bonsai.Editor/EditorForm.cs | 19 ++++++++++++++----- Bonsai.Editor/GraphView/WorkflowGraphView.cs | 12 +++++++++++- 2 files changed, 25 insertions(+), 6 deletions(-) diff --git a/Bonsai.Editor/EditorForm.cs b/Bonsai.Editor/EditorForm.cs index fe05d4547..9c1a0c3dc 100644 --- a/Bonsai.Editor/EditorForm.cs +++ b/Bonsai.Editor/EditorForm.cs @@ -1355,10 +1355,7 @@ void ClearWorkflowError() { if (workflowError != null) { - statusStrip.ContextMenuStrip = null; - statusTextLabel.Text = Resources.ReadyStatus; - statusImageLabel.Image = Resources.StatusReadyImage; - explorerTreeView.SetNodeStatus(ExplorerNodeStatus.Ready); + ClearExceptionBuilderNode(workflowError); } exceptionCache.Clear(); @@ -1373,6 +1370,18 @@ void HighlightWorkflowError() } } + void ClearExceptionBuilderNode(WorkflowException ex) + { + var workflowPath = WorkflowEditorPath.GetExceptionPath(workflowBuilder, ex); + var selectedView = selectionModel.SelectedView; + selectedView.ClearGraphNode(workflowPath); + + statusStrip.ContextMenuStrip = null; + statusTextLabel.Text = Resources.ReadyStatus; + statusImageLabel.Image = Resources.StatusReadyImage; + explorerTreeView.SetNodeStatus(ExplorerNodeStatus.Ready); + } + void HighlightExceptionBuilderNode(WorkflowException ex, bool showMessageBox) { var workflowPath = WorkflowEditorPath.GetExceptionPath(workflowBuilder, ex); @@ -1381,13 +1390,13 @@ void HighlightExceptionBuilderNode(WorkflowException ex, bool showMessageBox) selectedView.HighlightGraphNode(workflowPath, showMessageBox); var buildException = ex is WorkflowBuildException; - var errorCaption = buildException ? Resources.BuildError_Caption : Resources.RuntimeError_Caption; statusTextLabel.Text = ex.Message; statusStrip.ContextMenuStrip = statusContextMenuStrip; statusImageLabel.Image = buildException ? Resources.StatusBlockedImage : Resources.StatusCriticalImage; explorerTreeView.SetNodeStatus(pathElements, ExplorerNodeStatus.Blocked); if (showMessageBox) { + var errorCaption = buildException ? Resources.BuildError_Caption : Resources.RuntimeError_Caption; editorSite.ShowError(ex.Message, errorCaption); } } diff --git a/Bonsai.Editor/GraphView/WorkflowGraphView.cs b/Bonsai.Editor/GraphView/WorkflowGraphView.cs index 9784d1595..75fe805ab 100644 --- a/Bonsai.Editor/GraphView/WorkflowGraphView.cs +++ b/Bonsai.Editor/GraphView/WorkflowGraphView.cs @@ -273,7 +273,17 @@ internal void SelectGraphNode(GraphNode node) UpdateSelection(); } + internal void ClearGraphNode(WorkflowEditorPath path) + { + SetGraphNodeHighlight(path, false, false); + } + internal void HighlightGraphNode(WorkflowEditorPath path, bool selectNode) + { + SetGraphNodeHighlight(path, selectNode, true); + } + + private void SetGraphNodeHighlight(WorkflowEditorPath path, bool selectNode, bool highlight) { if (selectNode) WorkflowPath = path?.Parent; @@ -291,7 +301,7 @@ internal void HighlightGraphNode(WorkflowEditorPath path, bool selectNode) GraphView.Invalidate(graphNode); if (selectNode) GraphView.SelectedNode = graphNode; - graphNode.Highlight = true; + graphNode.Highlight = highlight; break; } From 00f6c8c78a3e4db37ec22e9e428ad50cf9657053 Mon Sep 17 00:00:00 2001 From: glopesdev Date: Sat, 7 Sep 2024 07:45:36 +0100 Subject: [PATCH 25/32] Prevent clipping of breadcrumbs button --- Bonsai.Editor/GraphView/WorkflowEditorControl.cs | 2 ++ .../GraphView/WorkflowPathNavigationControl.Designer.cs | 5 ++--- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/Bonsai.Editor/GraphView/WorkflowEditorControl.cs b/Bonsai.Editor/GraphView/WorkflowEditorControl.cs index e11b7d90f..19e63876d 100644 --- a/Bonsai.Editor/GraphView/WorkflowEditorControl.cs +++ b/Bonsai.Editor/GraphView/WorkflowEditorControl.cs @@ -88,6 +88,7 @@ TabPageController InitializeTab(TabPage tabPage) tabPage.BackColor = workflowGraphView.BackColor; if (tabControl.SelectedTab == tabPage) InitializeTheme(tabPage); }; + workflowGraphView.Margin = new Padding(0); workflowGraphView.Dock = DockStyle.Fill; workflowGraphView.Font = Font; workflowGraphView.Tag = tabPage; @@ -97,6 +98,7 @@ TabPageController InitializeTab(TabPage tabPage) tabPage.SuspendLayout(); var breadcrumbs = new WorkflowPathNavigationControl(serviceProvider); + breadcrumbs.Margin = new Padding(0); breadcrumbs.WorkflowPath = null; breadcrumbs.WorkflowPathMouseClick += (sender, e) => workflowGraphView.WorkflowPath = e.Path; workflowGraphView.WorkflowPathChanged += (sender, e) => diff --git a/Bonsai.Editor/GraphView/WorkflowPathNavigationControl.Designer.cs b/Bonsai.Editor/GraphView/WorkflowPathNavigationControl.Designer.cs index 41edb79d9..e0b44b728 100644 --- a/Bonsai.Editor/GraphView/WorkflowPathNavigationControl.Designer.cs +++ b/Bonsai.Editor/GraphView/WorkflowPathNavigationControl.Designer.cs @@ -40,15 +40,14 @@ private void InitializeComponent() this.flowLayoutPanel.TabIndex = 0; this.flowLayoutPanel.WrapContents = false; // - // EditorPathNavigationControl + // WorkflowPathNavigationControl // this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F); this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; this.Controls.Add(this.flowLayoutPanel); - this.Name = "EditorPathNavigationControl"; + this.Name = "WorkflowPathNavigationControl"; this.Size = new System.Drawing.Size(452, 29); this.ResumeLayout(false); - this.PerformLayout(); } From acac41be288f9f21088aa54e5c3a346da62e4a68 Mon Sep 17 00:00:00 2001 From: glopesdev Date: Tue, 17 Sep 2024 09:30:56 +0100 Subject: [PATCH 26/32] Remove unnecessary unwrap call --- Bonsai.Editor/GraphModel/WorkflowQuery.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/Bonsai.Editor/GraphModel/WorkflowQuery.cs b/Bonsai.Editor/GraphModel/WorkflowQuery.cs index ce2211d6f..7bf32e2ad 100644 --- a/Bonsai.Editor/GraphModel/WorkflowQuery.cs +++ b/Bonsai.Editor/GraphModel/WorkflowQuery.cs @@ -37,7 +37,6 @@ public static ExpressionBuilder Find( var matches = TopologicalOrder(source.Workflow); if (current != null) { - current = ExpressionBuilder.Unwrap(current); if (findPrevious) matches = matches.TakeWhile(builder => builder != current); else matches = matches.SkipWhile(builder => builder != current).Skip(1); } From c8e4d3c50dfe52b7443ce7b4540e867dca940523 Mon Sep 17 00:00:00 2001 From: glopesdev Date: Tue, 17 Sep 2024 09:31:15 +0100 Subject: [PATCH 27/32] Clarify condition purpose --- Bonsai.Editor/GraphModel/WorkflowEditor.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Bonsai.Editor/GraphModel/WorkflowEditor.cs b/Bonsai.Editor/GraphModel/WorkflowEditor.cs index dfa57bec8..9a0ea653c 100644 --- a/Bonsai.Editor/GraphModel/WorkflowEditor.cs +++ b/Bonsai.Editor/GraphModel/WorkflowEditor.cs @@ -1923,8 +1923,9 @@ private void UngroupGraphNode(GraphNode node) } var workflowNode = GetGraphNodeTag(workflow, node); - if (!(ExpressionBuilder.Unwrap(workflowNode.Value) is WorkflowExpressionBuilder workflowBuilder)) + if (ExpressionBuilder.Unwrap(workflowNode.Value) is not WorkflowExpressionBuilder workflowBuilder) { + // Do not ungroup disabled groups return; } From 73f6407de6519ed9d5ac001284acc6c361ac908b Mon Sep 17 00:00:00 2001 From: glopesdev Date: Tue, 17 Sep 2024 10:11:58 +0100 Subject: [PATCH 28/32] Fix navigation to disabled group nodes This required also devising a reasonable strategy to represent that a workflow is disabled. Currently this is done by graying out all nodes inside a disabled nested workflow. --- Bonsai.Editor/ExplorerTreeView.cs | 2 +- Bonsai.Editor/GraphModel/WorkflowEditor.cs | 10 ++++----- .../GraphModel/WorkflowEditorPath.cs | 21 ++++++++++++------- Bonsai.Editor/GraphModel/WorkflowPathFlags.cs | 12 +++++++++++ Bonsai.Editor/GraphView/GraphViewControl.cs | 4 +++- Bonsai.Editor/GraphView/WorkflowGraphView.cs | 5 +++-- .../WorkflowPathNavigationControl.cs | 2 +- 7 files changed, 39 insertions(+), 17 deletions(-) create mode 100644 Bonsai.Editor/GraphModel/WorkflowPathFlags.cs diff --git a/Bonsai.Editor/ExplorerTreeView.cs b/Bonsai.Editor/ExplorerTreeView.cs index d49b138d4..7af53066b 100644 --- a/Bonsai.Editor/ExplorerTreeView.cs +++ b/Bonsai.Editor/ExplorerTreeView.cs @@ -91,7 +91,7 @@ static void AddWorkflow( for (int i = 0; i < workflow.Count; i++) { var builder = workflow[i].Value; - if (ExpressionBuilder.Unwrap(builder) is IWorkflowExpressionBuilder workflowBuilder && + if (ExpressionBuilder.GetWorkflowElement(builder) is IWorkflowExpressionBuilder workflowBuilder && workflowBuilder.Workflow != null) { var nodeType = parentNodeType == ExplorerNodeType.ReadOnly || workflowBuilder is IncludeWorkflowBuilder diff --git a/Bonsai.Editor/GraphModel/WorkflowEditor.cs b/Bonsai.Editor/GraphModel/WorkflowEditor.cs index 9a0ea653c..ba6a722c9 100644 --- a/Bonsai.Editor/GraphModel/WorkflowEditor.cs +++ b/Bonsai.Editor/GraphModel/WorkflowEditor.cs @@ -40,7 +40,7 @@ public WorkflowEditor(IServiceProvider provider, IGraphView view) public ExpressionBuilderGraph Workflow { get; private set; } - public bool IsReadOnly { get; private set; } + public WorkflowPathFlags WorkflowPathFlags { get; private set; } public WorkflowEditorPath WorkflowPath { @@ -51,19 +51,19 @@ private set var workflowBuilder = (WorkflowBuilder)serviceProvider.GetService(typeof(WorkflowBuilder)); if (workflowPath != null) { - var builder = ExpressionBuilder.Unwrap(workflowPath.Resolve(workflowBuilder, out bool isReadOnly)); - if (builder is not IWorkflowExpressionBuilder workflowExpressionBuilder) + var builder = workflowPath.Resolve(workflowBuilder, out WorkflowPathFlags pathFlags); + if (ExpressionBuilder.GetWorkflowElement(builder) is not IWorkflowExpressionBuilder workflowExpressionBuilder) { throw new ArgumentException(Resources.InvalidWorkflowPath_Error, nameof(value)); } Workflow = workflowExpressionBuilder.Workflow; - IsReadOnly = isReadOnly; + WorkflowPathFlags = pathFlags; } else { Workflow = workflowBuilder.Workflow; - IsReadOnly = false; + WorkflowPathFlags = WorkflowPathFlags.None; } updateLayout.OnNext(false); workflowPathChanged.OnNext(workflowPath); diff --git a/Bonsai.Editor/GraphModel/WorkflowEditorPath.cs b/Bonsai.Editor/GraphModel/WorkflowEditorPath.cs index 96c91e3a5..fb79428d8 100644 --- a/Bonsai.Editor/GraphModel/WorkflowEditorPath.cs +++ b/Bonsai.Editor/GraphModel/WorkflowEditorPath.cs @@ -41,9 +41,9 @@ public ExpressionBuilder Resolve(WorkflowBuilder workflowBuilder) return Resolve(workflowBuilder, out _); } - public ExpressionBuilder Resolve(WorkflowBuilder workflowBuilder, out bool isReadOnly) + public ExpressionBuilder Resolve(WorkflowBuilder workflowBuilder, out WorkflowPathFlags pathFlags) { - isReadOnly = false; + pathFlags = WorkflowPathFlags.None; var builder = default(ExpressionBuilder); var workflow = workflowBuilder.Workflow; foreach (var pathElement in GetPathElements()) @@ -53,11 +53,18 @@ public ExpressionBuilder Resolve(WorkflowBuilder workflowBuilder, out bool isRea throw new ArgumentException($"Unable to resolve workflow editor path.", nameof(workflowBuilder)); } - builder = workflow[pathElement.Index].Value; - if (ExpressionBuilder.Unwrap(builder) is IWorkflowExpressionBuilder nestedWorkflowBuilder) + builder = ExpressionBuilder.Unwrap(workflow[pathElement.Index].Value); + if (builder is DisableBuilder disableBuilder) + { + builder = disableBuilder.Builder; + pathFlags |= WorkflowPathFlags.Disabled; + } + + if (builder is IWorkflowExpressionBuilder nestedWorkflowBuilder) { workflow = nestedWorkflowBuilder.Workflow; - isReadOnly |= nestedWorkflowBuilder is IncludeWorkflowBuilder; + if (nestedWorkflowBuilder is IncludeWorkflowBuilder) + pathFlags |= WorkflowPathFlags.ReadOnly; } else workflow = null; } @@ -79,7 +86,7 @@ static WorkflowEditorPath GetExceptionPath(ExpressionBuilderGraph workflow, Work { var path = new WorkflowEditorPath(i, parent); if (ex.InnerException is WorkflowException nestedEx && - ExpressionBuilder.Unwrap(ex.Builder) is IWorkflowExpressionBuilder workflowBuilder) + ExpressionBuilder.GetWorkflowElement(ex.Builder) is IWorkflowExpressionBuilder workflowBuilder) { return GetExceptionPath(workflowBuilder.Workflow, nestedEx, path); } @@ -99,7 +106,7 @@ static WorkflowEditorPath GetBuilderPath(ExpressionBuilderGraph workflow, Expres { for (int i = 0; i < workflow.Count; i++) { - var builder = ExpressionBuilder.Unwrap(workflow[i].Value); + var builder = ExpressionBuilder.GetWorkflowElement(workflow[i].Value); if (builder == target) { pathElements.Add(i); diff --git a/Bonsai.Editor/GraphModel/WorkflowPathFlags.cs b/Bonsai.Editor/GraphModel/WorkflowPathFlags.cs new file mode 100644 index 000000000..c2d846296 --- /dev/null +++ b/Bonsai.Editor/GraphModel/WorkflowPathFlags.cs @@ -0,0 +1,12 @@ +using System; + +namespace Bonsai.Editor.GraphModel +{ + [Flags] + enum WorkflowPathFlags + { + None = 0x0, + ReadOnly = 0x1, + Disabled = 0x2 + } +} diff --git a/Bonsai.Editor/GraphView/GraphViewControl.cs b/Bonsai.Editor/GraphView/GraphViewControl.cs index 4e76f2069..6895c3b46 100644 --- a/Bonsai.Editor/GraphView/GraphViewControl.cs +++ b/Bonsai.Editor/GraphView/GraphViewControl.cs @@ -236,6 +236,8 @@ public event EventHandler SelectedNodeChanged public Color CursorColor { get; set; } + public WorkflowPathFlags PathFlags { get; set; } + public SvgRendererFactory IconRenderer { get; set; } public Image GraphicsProvider { get; set; } @@ -1069,7 +1071,7 @@ private void DrawNode( iconRendererState.Stroke = stroke; iconRendererState.CurrentColor = currentColor; iconRendererState.Translation = nodeRectangle.Location; - if (layout.Node.IsDisabled) + if (layout.Node.IsDisabled || (PathFlags & WorkflowPathFlags.Disabled) != 0) { graphics.FillEllipse(Brushes.DarkGray, nodeRectangle); } diff --git a/Bonsai.Editor/GraphView/WorkflowGraphView.cs b/Bonsai.Editor/GraphView/WorkflowGraphView.cs index 75fe805ab..9542ede7d 100644 --- a/Bonsai.Editor/GraphView/WorkflowGraphView.cs +++ b/Bonsai.Editor/GraphView/WorkflowGraphView.cs @@ -79,12 +79,12 @@ public WorkflowGraphView(IServiceProvider provider, WorkflowEditorControl owner) internal bool IsReadOnly { - get { return Editor.IsReadOnly; } + get { return (Editor.WorkflowPathFlags & WorkflowPathFlags.ReadOnly) != 0; } } internal bool CanEdit { - get { return !Editor.IsReadOnly && !editorState.WorkflowRunning; } + get { return !IsReadOnly && !editorState.WorkflowRunning; } } public GraphViewControl GraphView @@ -973,6 +973,7 @@ private void InitializeViewBindings() Editor.WorkflowPathChanged.Subscribe(path => { UpdateSelection(forceUpdate: true); + graphView.PathFlags = Editor.WorkflowPathFlags; OnWorkflowPathChanged(EventArgs.Empty); }); diff --git a/Bonsai.Editor/GraphView/WorkflowPathNavigationControl.cs b/Bonsai.Editor/GraphView/WorkflowPathNavigationControl.cs index bace621e1..e2918b8e5 100644 --- a/Bonsai.Editor/GraphView/WorkflowPathNavigationControl.cs +++ b/Bonsai.Editor/GraphView/WorkflowPathNavigationControl.cs @@ -65,7 +65,7 @@ static IEnumerable> GetPathElements(Wor foreach (var pathElement in workflowPath?.GetPathElements() ?? Enumerable.Empty()) { var builder = workflow[pathElement.Index].Value; - if (ExpressionBuilder.Unwrap(builder) is IWorkflowExpressionBuilder nestedWorkflowBuilder) + if (ExpressionBuilder.GetWorkflowElement(builder) is IWorkflowExpressionBuilder nestedWorkflowBuilder) { workflow = nestedWorkflowBuilder.Workflow; } From c8658a6f222e726aa6ea69f2272b4f61653e20c4 Mon Sep 17 00:00:00 2001 From: glopesdev Date: Tue, 17 Sep 2024 11:18:22 +0100 Subject: [PATCH 29/32] Avoid relaunching closed visualizer --- Bonsai.Editor/GraphView/WorkflowGraphView.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Bonsai.Editor/GraphView/WorkflowGraphView.cs b/Bonsai.Editor/GraphView/WorkflowGraphView.cs index 9542ede7d..d88206186 100644 --- a/Bonsai.Editor/GraphView/WorkflowGraphView.cs +++ b/Bonsai.Editor/GraphView/WorkflowGraphView.cs @@ -1410,7 +1410,7 @@ private ToolStripMenuItem CreateVisualizerMenuItem(string typeName, GraphNode se { var dialogLauncher = visualizerDialogs.Add(inspectBuilder, Workflow, dialogSettings); var ownerWindow = uiService.GetDialogOwnerWindow(); - visualizerDialog.Show(ownerWindow, serviceProvider); + dialogLauncher.Show(ownerWindow, serviceProvider); } } else From 245b3bee75d6136357612c2c78ab04273dd5d816 Mon Sep 17 00:00:00 2001 From: glopesdev Date: Tue, 17 Sep 2024 12:21:39 +0100 Subject: [PATCH 30/32] Allow navigating to selected node by keyboard --- Bonsai.Editor/EditorForm.Designer.cs | 2 +- Bonsai.Editor/EditorForm.cs | 2 +- Bonsai.Editor/ExplorerTreeView.cs | 35 ++++++++++++++++++++++++++++ 3 files changed, 37 insertions(+), 2 deletions(-) diff --git a/Bonsai.Editor/EditorForm.Designer.cs b/Bonsai.Editor/EditorForm.Designer.cs index 52cc5ec1b..bf670e572 100644 --- a/Bonsai.Editor/EditorForm.Designer.cs +++ b/Bonsai.Editor/EditorForm.Designer.cs @@ -1390,7 +1390,7 @@ private void InitializeComponent() this.explorerTreeView.Name = "explorerTreeView"; this.explorerTreeView.Size = new System.Drawing.Size(200, 137); this.explorerTreeView.TabIndex = 3; - this.explorerTreeView.NodeMouseDoubleClick += new System.Windows.Forms.TreeNodeMouseClickEventHandler(explorerTreeView_NodeMouseDoubleClick); + this.explorerTreeView.Navigate += new System.Windows.Forms.TreeViewEventHandler(explorerTreeView_Navigate); // // EditorForm // diff --git a/Bonsai.Editor/EditorForm.cs b/Bonsai.Editor/EditorForm.cs index 9c1a0c3dc..56019742b 100644 --- a/Bonsai.Editor/EditorForm.cs +++ b/Bonsai.Editor/EditorForm.cs @@ -1872,7 +1872,7 @@ void FindNextMatch(Func predicate, ExpressionBuilder cu } } - private void explorerTreeView_NodeMouseDoubleClick(object sender, TreeNodeMouseClickEventArgs e) + private void explorerTreeView_Navigate(object sender, TreeViewEventArgs e) { var workflowPath = (WorkflowEditorPath)e.Node?.Tag; editorControl.WorkflowGraphView.WorkflowPath = workflowPath; diff --git a/Bonsai.Editor/ExplorerTreeView.cs b/Bonsai.Editor/ExplorerTreeView.cs index 7af53066b..cfcb872e1 100644 --- a/Bonsai.Editor/ExplorerTreeView.cs +++ b/Bonsai.Editor/ExplorerTreeView.cs @@ -14,6 +14,7 @@ class ExplorerTreeView : ToolboxTreeView bool activeDoubleClick; readonly ImageList imageList; readonly ImageList stateImageList; + static readonly object EventNavigate = new(); public ExplorerTreeView() { @@ -52,6 +53,40 @@ static Bitmap ResizeMakeBorder(Bitmap original, Size newSize) ImageList = imageList; } + public event TreeViewEventHandler Navigate + { + add { Events.AddHandler(EventNavigate, value); } + remove { Events.RemoveHandler(EventNavigate, value); } + } + + protected virtual void OnNavigate(TreeViewEventArgs e) + { + if (Events[EventNavigate] is TreeViewEventHandler handler) + { + handler(this, e); + } + } + + protected override void OnNodeMouseDoubleClick(TreeNodeMouseClickEventArgs e) + { + if (e.Node is not null && HitTest(e.Location).Location != TreeViewHitTestLocations.PlusMinus) + { + OnNavigate(new TreeViewEventArgs(e.Node, TreeViewAction.ByMouse)); + } + + base.OnNodeMouseDoubleClick(e); + } + + protected override void OnKeyDown(KeyEventArgs e) + { + if (e.KeyCode == Keys.Return && SelectedNode != null) + { + OnNavigate(new TreeViewEventArgs(SelectedNode, TreeViewAction.ByKeyboard)); + } + + base.OnKeyDown(e); + } + protected override void OnBeforeCollapse(TreeViewCancelEventArgs e) { if (activeDoubleClick && e.Action == TreeViewAction.Collapse) From 7700ac22b8b0aaa66678546ad665df31c21b47e6 Mon Sep 17 00:00:00 2001 From: glopesdev Date: Tue, 17 Sep 2024 14:00:58 +0100 Subject: [PATCH 31/32] Refactor explorer tree view as a user control --- Bonsai.Editor/ExplorerTreeView.Designer.cs | 63 +++++++++++ Bonsai.Editor/ExplorerTreeView.cs | 53 ++++----- Bonsai.Editor/ExplorerTreeView.resx | 120 +++++++++++++++++++++ 3 files changed, 210 insertions(+), 26 deletions(-) create mode 100644 Bonsai.Editor/ExplorerTreeView.Designer.cs create mode 100644 Bonsai.Editor/ExplorerTreeView.resx diff --git a/Bonsai.Editor/ExplorerTreeView.Designer.cs b/Bonsai.Editor/ExplorerTreeView.Designer.cs new file mode 100644 index 000000000..6c591cbf6 --- /dev/null +++ b/Bonsai.Editor/ExplorerTreeView.Designer.cs @@ -0,0 +1,63 @@ +namespace Bonsai.Editor +{ + partial class ExplorerTreeView + { + /// + /// 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 Component Designer generated code + + /// + /// Required method for Designer support - do not modify + /// the contents of this method with the code editor. + /// + private void InitializeComponent() + { + this.treeView = new Bonsai.Editor.ToolboxTreeView(); + this.SuspendLayout(); + // + // treeView + // + this.treeView.BorderStyle = System.Windows.Forms.BorderStyle.None; + this.treeView.Dock = System.Windows.Forms.DockStyle.Fill; + this.treeView.Location = new System.Drawing.Point(0, 0); + this.treeView.Name = "treeView"; + this.treeView.Renderer = null; + this.treeView.Size = new System.Drawing.Size(150, 150); + this.treeView.TabIndex = 0; + this.treeView.BeforeCollapse += new System.Windows.Forms.TreeViewCancelEventHandler(this.treeView_BeforeCollapse); + this.treeView.BeforeExpand += new System.Windows.Forms.TreeViewCancelEventHandler(this.treeView_BeforeExpand); + this.treeView.NodeMouseDoubleClick += new System.Windows.Forms.TreeNodeMouseClickEventHandler(this.treeView_NodeMouseDoubleClick); + this.treeView.KeyDown += new System.Windows.Forms.KeyEventHandler(this.treeView_KeyDown); + this.treeView.MouseDown += new System.Windows.Forms.MouseEventHandler(this.treeView_MouseDown); + // + // ExplorerTreeView + // + this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F); + this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; + this.Controls.Add(this.treeView); + this.Name = "ExplorerTreeView"; + this.ResumeLayout(false); + + } + + #endregion + + private Bonsai.Editor.ToolboxTreeView treeView; + } +} diff --git a/Bonsai.Editor/ExplorerTreeView.cs b/Bonsai.Editor/ExplorerTreeView.cs index cfcb872e1..53bad674c 100644 --- a/Bonsai.Editor/ExplorerTreeView.cs +++ b/Bonsai.Editor/ExplorerTreeView.cs @@ -5,11 +5,12 @@ using System.Windows.Forms; using Bonsai.Editor.GraphModel; using Bonsai.Editor.Properties; +using Bonsai.Editor.Themes; using Bonsai.Expressions; namespace Bonsai.Editor { - class ExplorerTreeView : ToolboxTreeView + partial class ExplorerTreeView : UserControl { bool activeDoubleClick; readonly ImageList imageList; @@ -49,8 +50,15 @@ static Bitmap ResizeMakeBorder(Bitmap original, Size newSize) } #endif - StateImageList = stateImageList; - ImageList = imageList; + InitializeComponent(); + treeView.StateImageList = stateImageList; + treeView.ImageList = imageList; + } + + public ToolStripExtendedRenderer Renderer + { + get => treeView.Renderer; + set => treeView.Renderer = value; } public event TreeViewEventHandler Navigate @@ -67,54 +75,47 @@ protected virtual void OnNavigate(TreeViewEventArgs e) } } - protected override void OnNodeMouseDoubleClick(TreeNodeMouseClickEventArgs e) + private void treeView_NodeMouseDoubleClick(object sender, TreeNodeMouseClickEventArgs e) { - if (e.Node is not null && HitTest(e.Location).Location != TreeViewHitTestLocations.PlusMinus) + if (e.Node is not null && treeView.HitTest(e.Location).Location != TreeViewHitTestLocations.PlusMinus) { OnNavigate(new TreeViewEventArgs(e.Node, TreeViewAction.ByMouse)); } - - base.OnNodeMouseDoubleClick(e); } - protected override void OnKeyDown(KeyEventArgs e) + private void treeView_KeyDown(object sender, KeyEventArgs e) { - if (e.KeyCode == Keys.Return && SelectedNode != null) + if (e.KeyCode == Keys.Return && treeView.SelectedNode != null) { - OnNavigate(new TreeViewEventArgs(SelectedNode, TreeViewAction.ByKeyboard)); + OnNavigate(new TreeViewEventArgs(treeView.SelectedNode, TreeViewAction.ByKeyboard)); } - - base.OnKeyDown(e); } - protected override void OnBeforeCollapse(TreeViewCancelEventArgs e) + private void treeView_BeforeCollapse(object sender, TreeViewCancelEventArgs e) { if (activeDoubleClick && e.Action == TreeViewAction.Collapse) e.Cancel = true; activeDoubleClick = false; - base.OnBeforeCollapse(e); } - protected override void OnBeforeExpand(TreeViewCancelEventArgs e) + private void treeView_BeforeExpand(object sender, TreeViewCancelEventArgs e) { if (activeDoubleClick && e.Action == TreeViewAction.Expand) e.Cancel = true; activeDoubleClick = false; - base.OnBeforeExpand(e); } - protected override void OnMouseDown(MouseEventArgs e) + private void treeView_MouseDown(object sender, MouseEventArgs e) { activeDoubleClick = e.Clicks > 1; - base.OnMouseDown(e); } public void UpdateWorkflow(string name, WorkflowBuilder workflowBuilder) { - BeginUpdate(); - Nodes.Clear(); + treeView.BeginUpdate(); + treeView.Nodes.Clear(); - var rootNode = Nodes.Add(name); + var rootNode = treeView.Nodes.Add(name); AddWorkflow(rootNode.Nodes, null, workflowBuilder.Workflow, ExplorerNodeType.Editable); static void AddWorkflow( @@ -144,12 +145,12 @@ static void AddWorkflow( SetNodeStatus(ExplorerNodeStatus.Ready); rootNode.Expand(); - EndUpdate(); + treeView.EndUpdate(); } public void SelectNode(WorkflowEditorPath path) { - SelectNode(Nodes, path); + SelectNode(treeView.Nodes, path); } bool SelectNode(TreeNodeCollection nodes, WorkflowEditorPath path) @@ -159,7 +160,7 @@ bool SelectNode(TreeNodeCollection nodes, WorkflowEditorPath path) var nodePath = (WorkflowEditorPath)node.Tag; if (nodePath == path) { - SelectedNode = node; + treeView.SelectedNode = node; return true; } @@ -193,7 +194,7 @@ private static int GetStateImageIndex(ExplorerNodeStatus status) public void SetNodeStatus(ExplorerNodeStatus status) { var imageIndex = GetStateImageIndex(status); - SetNodeImageIndex(Nodes, imageIndex); + SetNodeImageIndex(treeView.Nodes, imageIndex); static void SetNodeImageIndex(TreeNodeCollection nodes, int index) { @@ -210,7 +211,7 @@ static void SetNodeImageIndex(TreeNodeCollection nodes, int index) public void SetNodeStatus(IEnumerable pathElements, ExplorerNodeStatus status) { - var nodes = Nodes; + var nodes = treeView.Nodes; var imageIndex = GetStateImageIndex(status); foreach (var path in pathElements.Prepend(null)) { diff --git a/Bonsai.Editor/ExplorerTreeView.resx b/Bonsai.Editor/ExplorerTreeView.resx new file mode 100644 index 000000000..1af7de150 --- /dev/null +++ b/Bonsai.Editor/ExplorerTreeView.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 From 6ac8553f9f741308626c71b2a038970da6c2465e Mon Sep 17 00:00:00 2001 From: glopesdev Date: Tue, 17 Sep 2024 14:56:33 +0100 Subject: [PATCH 32/32] Add single click navigation and context menu --- Bonsai.Editor/ExplorerTreeView.Designer.cs | 37 ++++++++++++-- Bonsai.Editor/ExplorerTreeView.cs | 59 +++++++++++++++++----- Bonsai.Editor/ExplorerTreeView.resx | 3 ++ 3 files changed, 81 insertions(+), 18 deletions(-) diff --git a/Bonsai.Editor/ExplorerTreeView.Designer.cs b/Bonsai.Editor/ExplorerTreeView.Designer.cs index 6c591cbf6..644bd0598 100644 --- a/Bonsai.Editor/ExplorerTreeView.Designer.cs +++ b/Bonsai.Editor/ExplorerTreeView.Designer.cs @@ -28,9 +28,36 @@ protected override void Dispose(bool disposing) /// private void InitializeComponent() { + this.components = new System.ComponentModel.Container(); + this.contextMenuStrip = new System.Windows.Forms.ContextMenuStrip(this.components); + this.expandToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); + this.collapseToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); this.treeView = new Bonsai.Editor.ToolboxTreeView(); + this.contextMenuStrip.SuspendLayout(); this.SuspendLayout(); // + // contextMenuStrip + // + this.contextMenuStrip.Items.AddRange(new System.Windows.Forms.ToolStripItem[] { + this.expandToolStripMenuItem, + this.collapseToolStripMenuItem}); + this.contextMenuStrip.Name = "contextMenuStrip"; + this.contextMenuStrip.Size = new System.Drawing.Size(120, 48); + // + // expandToolStripMenuItem + // + this.expandToolStripMenuItem.Name = "expandToolStripMenuItem"; + this.expandToolStripMenuItem.Size = new System.Drawing.Size(119, 22); + this.expandToolStripMenuItem.Text = "Expand"; + this.expandToolStripMenuItem.Click += new System.EventHandler(this.expandToolStripMenuItem_Click); + // + // collapseToolStripMenuItem + // + this.collapseToolStripMenuItem.Name = "collapseToolStripMenuItem"; + this.collapseToolStripMenuItem.Size = new System.Drawing.Size(119, 22); + this.collapseToolStripMenuItem.Text = "Collapse"; + this.collapseToolStripMenuItem.Click += new System.EventHandler(this.collapseToolStripMenuItem_Click); + // // treeView // this.treeView.BorderStyle = System.Windows.Forms.BorderStyle.None; @@ -40,11 +67,9 @@ private void InitializeComponent() this.treeView.Renderer = null; this.treeView.Size = new System.Drawing.Size(150, 150); this.treeView.TabIndex = 0; - this.treeView.BeforeCollapse += new System.Windows.Forms.TreeViewCancelEventHandler(this.treeView_BeforeCollapse); - this.treeView.BeforeExpand += new System.Windows.Forms.TreeViewCancelEventHandler(this.treeView_BeforeExpand); - this.treeView.NodeMouseDoubleClick += new System.Windows.Forms.TreeNodeMouseClickEventHandler(this.treeView_NodeMouseDoubleClick); + this.treeView.NodeMouseClick += new System.Windows.Forms.TreeNodeMouseClickEventHandler(this.treeView_NodeMouseClick); this.treeView.KeyDown += new System.Windows.Forms.KeyEventHandler(this.treeView_KeyDown); - this.treeView.MouseDown += new System.Windows.Forms.MouseEventHandler(this.treeView_MouseDown); + this.treeView.MouseUp += new System.Windows.Forms.MouseEventHandler(this.treeView_MouseUp); // // ExplorerTreeView // @@ -52,6 +77,7 @@ private void InitializeComponent() this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; this.Controls.Add(this.treeView); this.Name = "ExplorerTreeView"; + this.contextMenuStrip.ResumeLayout(false); this.ResumeLayout(false); } @@ -59,5 +85,8 @@ private void InitializeComponent() #endregion private Bonsai.Editor.ToolboxTreeView treeView; + private System.Windows.Forms.ContextMenuStrip contextMenuStrip; + private System.Windows.Forms.ToolStripMenuItem expandToolStripMenuItem; + private System.Windows.Forms.ToolStripMenuItem collapseToolStripMenuItem; } } diff --git a/Bonsai.Editor/ExplorerTreeView.cs b/Bonsai.Editor/ExplorerTreeView.cs index 53bad674c..5927b11d5 100644 --- a/Bonsai.Editor/ExplorerTreeView.cs +++ b/Bonsai.Editor/ExplorerTreeView.cs @@ -12,7 +12,6 @@ namespace Bonsai.Editor { partial class ExplorerTreeView : UserControl { - bool activeDoubleClick; readonly ImageList imageList; readonly ImageList stateImageList; static readonly object EventNavigate = new(); @@ -75,9 +74,11 @@ protected virtual void OnNavigate(TreeViewEventArgs e) } } - private void treeView_NodeMouseDoubleClick(object sender, TreeNodeMouseClickEventArgs e) + private void treeView_NodeMouseClick(object sender, TreeNodeMouseClickEventArgs e) { - if (e.Node is not null && treeView.HitTest(e.Location).Location != TreeViewHitTestLocations.PlusMinus) + if (e.Node is not null && + e.Button == MouseButtons.Left && + treeView.HitTest(e.Location).Location == TreeViewHitTestLocations.Label) { OnNavigate(new TreeViewEventArgs(e.Node, TreeViewAction.ByMouse)); } @@ -85,29 +86,59 @@ private void treeView_NodeMouseDoubleClick(object sender, TreeNodeMouseClickEven private void treeView_KeyDown(object sender, KeyEventArgs e) { - if (e.KeyCode == Keys.Return && treeView.SelectedNode != null) + if (treeView.SelectedNode is null) + return; + + if (e.KeyCode == Keys.Return) { OnNavigate(new TreeViewEventArgs(treeView.SelectedNode, TreeViewAction.ByKeyboard)); } + + if (e.Shift && e.KeyCode == Keys.F10) + { + var nodeBounds = treeView.SelectedNode.Bounds; + var middleX = nodeBounds.X + nodeBounds.Width / 2; + var middleY = nodeBounds.Y + nodeBounds.Height / 2; + ShowContextMenu(treeView.SelectedNode, middleX, middleY); + } } - private void treeView_BeforeCollapse(object sender, TreeViewCancelEventArgs e) + private void treeView_MouseUp(object sender, MouseEventArgs e) { - if (activeDoubleClick && e.Action == TreeViewAction.Collapse) - e.Cancel = true; - activeDoubleClick = false; + if (e.Button == MouseButtons.Right) + { + var selectedNode = treeView.GetNodeAt(e.X, e.Y); + if (selectedNode != null) + { + treeView.SelectedNode = selectedNode; + ShowContextMenu(selectedNode, e.X, e.Y); + } + } } - private void treeView_BeforeExpand(object sender, TreeViewCancelEventArgs e) + private void expandToolStripMenuItem_Click(object sender, EventArgs e) { - if (activeDoubleClick && e.Action == TreeViewAction.Expand) - e.Cancel = true; - activeDoubleClick = false; + treeView.SelectedNode?.Expand(); } - private void treeView_MouseDown(object sender, MouseEventArgs e) + private void collapseToolStripMenuItem_Click(object sender, EventArgs e) { - activeDoubleClick = e.Clicks > 1; + treeView.SelectedNode?.Collapse(); + } + + private void ShowContextMenu(TreeNode node, int x, int y) + { + if (node.Nodes.Count > 0) + { + expandToolStripMenuItem.Visible = !node.IsExpanded; + collapseToolStripMenuItem.Visible = node.IsExpanded; + } + else + { + expandToolStripMenuItem.Visible = false; + collapseToolStripMenuItem.Visible = false; + } + contextMenuStrip.Show(treeView, x, y); } public void UpdateWorkflow(string name, WorkflowBuilder workflowBuilder) diff --git a/Bonsai.Editor/ExplorerTreeView.resx b/Bonsai.Editor/ExplorerTreeView.resx index 1af7de150..2d8292bab 100644 --- a/Bonsai.Editor/ExplorerTreeView.resx +++ b/Bonsai.Editor/ExplorerTreeView.resx @@ -117,4 +117,7 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + 17, 17 + \ No newline at end of file