Skip to content

Commit

Permalink
Merge pull request #1359 from glopesdev/issue-1208
Browse files Browse the repository at this point in the history
Add support for in-editor docs browser
  • Loading branch information
glopesdev authored May 17, 2023
2 parents 35d6789 + 30f3c01 commit a5225c4
Show file tree
Hide file tree
Showing 9 changed files with 346 additions and 129 deletions.
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

0 comments on commit a5225c4

Please sign in to comment.