Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for in-editor docs browser #1359

Merged
merged 16 commits into from
May 17, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
125 changes: 59 additions & 66 deletions Bonsai.Editor/EditorForm.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ namespace Bonsai.Editor
public partial class EditorForm : Form
{
const float DefaultEditorScale = 1.0f;
const string EditorUid = "editor";
const string BonsaiExtension = ".bonsai";
const string BonsaiPackageName = "Bonsai";
const string ExtensionsDirectory = "Extensions";
Expand Down Expand Up @@ -253,11 +254,15 @@ void RestoreEditorSettings()

WindowState = EditorSettings.Instance.WindowState;
themeRenderer.ActiveTheme = EditorSettings.Instance.EditorTheme;
editorControl.WebViewSize = (int)Math.Round(
EditorSettings.Instance.WebViewSize * scaleFactor.Width);
}

void CloseEditorForm()
{
Application.RemoveMessageFilter(hotKeys);
EditorSettings.Instance.WebViewSize = (int)Math.Round(
editorControl.WebViewSize * inverseScaleFactor.Width);
var desktopBounds = WindowState != FormWindowState.Normal ? RestoreBounds : Bounds;
EditorSettings.Instance.DesktopBounds = ScaleBounds(desktopBounds, inverseScaleFactor);
if (WindowState == FormWindowState.Minimized)
Expand Down Expand Up @@ -1531,55 +1536,6 @@ private void toolboxTreeView_ItemDrag(object sender, ItemDragEventArgs e)
}
}

static string GetElementName(object component)
{
var name = ExpressionBuilder.GetElementDisplayName(component);
if (component is ExternalizedProperty workflowProperty &&
!string.IsNullOrWhiteSpace(workflowProperty.Name) &&
workflowProperty.Name != workflowProperty.MemberName)
{
return name + " (" + workflowProperty.MemberName + ")";
}

var componentType = component.GetType();
if (component is BinaryOperatorBuilder binaryOperator && binaryOperator.Operand != null)
{
var operandType = binaryOperator.Operand.GetType();
if (operandType.IsGenericType) operandType = operandType.GetGenericArguments()[0];
return name + " (" + ExpressionBuilder.GetElementDisplayName(operandType) + ")";
}
else if (component is SubscribeSubject subscribeSubject && componentType.IsGenericType)
{
componentType = componentType.GetGenericArguments()[0];
if (string.IsNullOrWhiteSpace(subscribeSubject.Name))
{
name = name.Substring(0, name.IndexOf("`"));
}
return name + " (" + ExpressionBuilder.GetElementDisplayName(componentType) + ")";
}
else
{
if (component is INamedElement namedExpressionBuilder && !string.IsNullOrWhiteSpace(namedExpressionBuilder.Name))
{
name += " (" + ExpressionBuilder.GetElementDisplayName(componentType) + ")";
}

return name;
}
}

static string GetElementDescription(object component)
{
if (component is WorkflowExpressionBuilder workflowExpressionBuilder)
{
var description = workflowExpressionBuilder.Description;
if (!string.IsNullOrEmpty(description)) return description;
}

var descriptionAttribute = (DescriptionAttribute)TypeDescriptor.GetAttributes(component)[typeof(DescriptionAttribute)];
return descriptionAttribute.Description;
}

void editorControl_Enter(object sender, EventArgs e)
{
var selectedView = selectionModel.SelectedView;
Expand All @@ -1602,9 +1558,9 @@ private void selectionModel_SelectionChanged(object sender, EventArgs e)

private void GetSelectionDescription(object[] selectedObjects, out string displayName, out string description)
{
var displayNames = selectedObjects.Select(GetElementName).Distinct().Reverse().ToArray();
var displayNames = selectedObjects.Select(ElementHelper.GetElementName).Distinct().Reverse().ToArray();
displayName = string.Join(CultureInfo.CurrentCulture.TextInfo.ListSeparator + " ", displayNames);
var objectDescriptions = selectedObjects.Select(GetElementDescription).Distinct().Reverse().ToArray();
var objectDescriptions = selectedObjects.Select(ElementHelper.GetElementDescription).Distinct().Reverse().ToArray();
description = objectDescriptions.Length == 1 ? objectDescriptions[0] : string.Empty;
}

Expand Down Expand Up @@ -1645,8 +1601,8 @@ private void UpdatePropertyGrid()
var launcher = selectedView.Launcher;
if (launcher != null)
{
displayName = GetElementName(launcher.Builder);
description = GetElementDescription(launcher.Builder);
displayName = ElementHelper.GetElementName(launcher.Builder);
description = ElementHelper.GetElementDescription(launcher.Builder);
}
else
{
Expand Down Expand Up @@ -2045,6 +2001,7 @@ private void toolboxTreeView_MouseUp(object sender, MouseEventArgs e)
renameSubjectToolStripMenuItem.Visible = false;
goToDefinitionToolStripMenuItem.Visible = false;
insertBeforeToolStripMenuItem.Visible = true;
toolboxDocsToolStripMenuItem.Visible = true;
}
toolboxContextMenuStrip.Show(toolboxTreeView, e.X, e.Y);
}
Expand Down Expand Up @@ -2303,13 +2260,10 @@ private void reloadExtensionsDebugToolStripMenuItem_Click(object sender, EventAr

#region Help Menu

private async Task OpenDocumentationAsync(ExpressionBuilder builder)
static bool TryGetAssemblyResource(string path, out string assemblyName, out string resourceName)
{
var selectedElement = ExpressionBuilder.GetWorkflowElement(builder);
if (selectedElement is IncludeWorkflowBuilder include &&
!string.IsNullOrEmpty(include.Path))
if (!string.IsNullOrEmpty(path))
{
var path = include.Path;
const char AssemblySeparator = ':';
var separatorIndex = path.IndexOf(AssemblySeparator);
if (separatorIndex >= 0 && !Path.IsPathRooted(path) && path.EndsWith(BonsaiExtension))
Expand All @@ -2318,19 +2272,36 @@ private async Task OpenDocumentationAsync(ExpressionBuilder builder)
var nameElements = path.Split(new[] { AssemblySeparator }, 2);
if (!string.IsNullOrEmpty(nameElements[0]))
{
var assemblyName = nameElements[0];
var resourceName = string.Join(ExpressionHelper.MemberSeparator, nameElements);
await OpenDocumentationAsync(assemblyName, resourceName);
return;
assemblyName = nameElements[0];
resourceName = string.Join(ExpressionHelper.MemberSeparator, nameElements);
return true;
}
}
}

await OpenDocumentationAsync(selectedElement.GetType());
assemblyName = default;
resourceName = default;
return false;
}

private async Task OpenDocumentationAsync(ExpressionBuilder builder)
{
var selectedElement = ExpressionBuilder.GetWorkflowElement(builder);
if (selectedElement is IncludeWorkflowBuilder include &&
TryGetAssemblyResource(include.Path, out string assemblyName, out string resourceName))
{
await OpenDocumentationAsync(assemblyName, resourceName);
}
else await OpenDocumentationAsync(selectedElement.GetType());
}

private async Task OpenDocumentationAsync(Type type)
{
if (type.IsGenericType && !type.IsGenericTypeDefinition)
{
type = type.GetGenericTypeDefinition();
}

var uid = type.FullName;
var assemblyName = type.Assembly.GetName().Name;
await OpenDocumentationAsync(assemblyName, uid);
Expand All @@ -2346,8 +2317,21 @@ private async Task OpenDocumentationAsync(string assemblyName, string uid)

try
{
var editorControl = selectionModel.SelectedView.EditorControl;
var url = await documentationProvider.GetDocumentationAsync(assemblyName, uid);
EditorDialog.OpenUrl(url);
if (!ModifierKeys.HasFlag(Keys.Control) && editorControl.WebViewInitialized)
{
editorControl.WebView.CoreWebView2.Navigate(url.AbsoluteUri);
var nameSeparator = uid.LastIndexOf(ExpressionHelper.MemberSeparator);
if (nameSeparator >= 0)
{
var name = uid.Substring(nameSeparator + 1);
var categoryName = GetPackageDisplayName(uid.Substring(0, nameSeparator));
editorControl.ExpandWebView(label: $"{name} ({categoryName})");
}
else editorControl.ExpandWebView(label: uid == EditorUid ? Resources.Editor_HelpLabel : uid);
}
else EditorDialog.OpenUrl(url);
}
catch (ArgumentException ex) when (ex.ParamName == nameof(assemblyName))
{
Expand All @@ -2372,7 +2356,7 @@ private async Task OpenDocumentationAsync(string assemblyName, string uid)

private async void docsToolStripMenuItem_Click(object sender, EventArgs e)
{
if (ModifierKeys != Keys.None)
if (ModifierKeys != Keys.None && ModifierKeys != Keys.Control)
{
return;
}
Expand All @@ -2382,6 +2366,14 @@ private async void docsToolStripMenuItem_Click(object sender, EventArgs e)
var typeNode = toolboxTreeView.SelectedNode;
if (typeNode != null && typeNode.Tag != null)
{
var elementCategory = WorkflowGraphView.GetToolboxElementCategory(typeNode);
if (elementCategory == ~ElementCategory.Workflow &&
TryGetAssemblyResource(typeNode.Name, out string assemblyName, out string resourceName))
{
await OpenDocumentationAsync(assemblyName, resourceName);
return;
}

var type = Type.GetType(typeNode.Name);
if (type != null)
{
Expand All @@ -2402,7 +2394,8 @@ private async void docsToolStripMenuItem_Click(object sender, EventArgs e)
}
}

EditorDialog.ShowDocs();
var editorAssemblyName = GetType().Assembly.GetName().Name;
await OpenDocumentationAsync(editorAssemblyName, EditorUid);
}

private void forumToolStripMenuItem_Click(object sender, EventArgs e)
Expand Down
66 changes: 32 additions & 34 deletions Bonsai.Editor/EditorSettings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,17 +12,6 @@ namespace Bonsai.Editor
sealed class EditorSettings
{
const int MaxRecentFiles = 25;
const string RecentlyUsedFilesElement = "RecentlyUsedFiles";
const string DesktopBoundsElement = "DesktopBounds";
const string WindowStateElement = "WindowState";
const string RecentlyUsedFileElement = "RecentlyUsedFile";
const string FileTimestampElement = "Timestamp";
const string FileNameElement = "Name";
const string RectangleXElement = "X";
const string RectangleYElement = "Y";
const string RectangleWidthElement = "Width";
const string RectangleHeightElement = "Height";
const string EditorThemeElement = "EditorTheme";
const string SettingsFileName = "Bonsai.exe.settings";
static readonly string SettingsPath = Path.Combine(Path.GetDirectoryName(Assembly.GetEntryAssembly().Location), SettingsFileName);
static readonly Lazy<EditorSettings> instance = new Lazy<EditorSettings>(Load);
Expand All @@ -31,6 +20,7 @@ sealed class EditorSettings

internal EditorSettings()
{
WebViewSize = 400;
}

public static EditorSettings Instance
Expand All @@ -44,6 +34,8 @@ public static EditorSettings Instance

public ColorTheme EditorTheme { get; set; }

public int WebViewSize { get; set; }

public RecentlyUsedFileCollection RecentlyUsedFiles
{
get { return recentlyUsedFiles; }
Expand All @@ -62,39 +54,44 @@ static EditorSettings Load()
while (reader.Read())
{
if (reader.NodeType != XmlNodeType.Element) continue;
if (reader.Name == WindowStateElement)
if (reader.Name == nameof(WindowState))
{
Enum.TryParse(reader.ReadElementContentAsString(), out FormWindowState windowState);
settings.WindowState = windowState;
}
else if (reader.Name == EditorThemeElement)
else if (reader.Name == nameof(EditorTheme))
{
Enum.TryParse(reader.ReadElementContentAsString(), out ColorTheme editorTheme);
settings.EditorTheme = editorTheme;
}
else if (reader.Name == DesktopBoundsElement)
else if (reader.Name == nameof(WebViewSize))
{
int.TryParse(reader.ReadElementContentAsString(), out int webViewSize);
settings.WebViewSize = webViewSize;
}
else if (reader.Name == nameof(DesktopBounds))
{
reader.ReadToFollowing(RectangleXElement);
reader.ReadToFollowing(nameof(Rectangle.X));
int.TryParse(reader.ReadElementContentAsString(), out int x);
reader.ReadToFollowing(RectangleYElement);
reader.ReadToFollowing(nameof(Rectangle.Y));
int.TryParse(reader.ReadElementContentAsString(), out int y);
reader.ReadToFollowing(RectangleWidthElement);
reader.ReadToFollowing(nameof(Rectangle.Width));
int.TryParse(reader.ReadElementContentAsString(), out int width);
reader.ReadToFollowing(RectangleHeightElement);
reader.ReadToFollowing(nameof(Rectangle.Height));
int.TryParse(reader.ReadElementContentAsString(), out int height);
settings.DesktopBounds = new Rectangle(x, y, width, height);
}
else if (reader.Name == RecentlyUsedFilesElement)
else if (reader.Name == nameof(RecentlyUsedFiles))
{
var fileReader = reader.ReadSubtree();
while (fileReader.ReadToFollowing(RecentlyUsedFileElement))
while (fileReader.ReadToFollowing(nameof(RecentlyUsedFile)))
{
if (fileReader.Name == RecentlyUsedFileElement)
if (fileReader.Name == nameof(RecentlyUsedFile))
{
string fileName;
fileReader.ReadToFollowing(FileTimestampElement);
fileReader.ReadToFollowing(nameof(RecentlyUsedFile.Timestamp));
DateTimeOffset.TryParse(fileReader.ReadElementContentAsString(), out DateTimeOffset timestamp);
fileReader.ReadToFollowing(FileNameElement);
fileReader.ReadToFollowing(nameof(RecentlyUsedFile.Name));
fileName = fileReader.ReadElementContentAsString();
settings.recentlyUsedFiles.Add(timestamp, fileName);
}
Expand All @@ -114,24 +111,25 @@ public void Save()
using (var writer = XmlWriter.Create(SettingsPath, new XmlWriterSettings { Indent = true }))
{
writer.WriteStartElement(typeof(EditorSettings).Name);
writer.WriteElementString(WindowStateElement, WindowState.ToString());
writer.WriteElementString(EditorThemeElement, EditorTheme.ToString());
writer.WriteElementString(nameof(WindowState), WindowState.ToString());
writer.WriteElementString(nameof(EditorTheme), EditorTheme.ToString());
writer.WriteElementString(nameof(WebViewSize), WebViewSize.ToString(CultureInfo.InvariantCulture));

writer.WriteStartElement(DesktopBoundsElement);
writer.WriteElementString(RectangleXElement, DesktopBounds.X.ToString(CultureInfo.InvariantCulture));
writer.WriteElementString(RectangleYElement, DesktopBounds.Y.ToString(CultureInfo.InvariantCulture));
writer.WriteElementString(RectangleWidthElement, DesktopBounds.Width.ToString(CultureInfo.InvariantCulture));
writer.WriteElementString(RectangleHeightElement, DesktopBounds.Height.ToString(CultureInfo.InvariantCulture));
writer.WriteStartElement(nameof(DesktopBounds));
writer.WriteElementString(nameof(Rectangle.X), DesktopBounds.X.ToString(CultureInfo.InvariantCulture));
writer.WriteElementString(nameof(Rectangle.Y), DesktopBounds.Y.ToString(CultureInfo.InvariantCulture));
writer.WriteElementString(nameof(Rectangle.Width), DesktopBounds.Width.ToString(CultureInfo.InvariantCulture));
writer.WriteElementString(nameof(Rectangle.Height), DesktopBounds.Height.ToString(CultureInfo.InvariantCulture));
writer.WriteEndElement();

if (recentlyUsedFiles.Count > 0)
{
writer.WriteStartElement(RecentlyUsedFilesElement);
writer.WriteStartElement(nameof(RecentlyUsedFiles));
foreach (var file in recentlyUsedFiles)
{
writer.WriteStartElement(RecentlyUsedFileElement);
writer.WriteElementString(FileTimestampElement, file.Timestamp.ToString("o"));
writer.WriteElementString(FileNameElement, file.FileName);
writer.WriteStartElement(nameof(RecentlyUsedFile));
writer.WriteElementString(nameof(RecentlyUsedFile.Timestamp), file.Timestamp.ToString("o"));
writer.WriteElementString(nameof(RecentlyUsedFile.Name), file.FileName);
writer.WriteEndElement();
}
writer.WriteEndElement();
Expand Down
Loading