diff --git a/Bonsai.Design/ObjectTextVisualizer.cs b/Bonsai.Design/ObjectTextVisualizer.cs
index 1c6de38e..9032dee5 100644
--- a/Bonsai.Design/ObjectTextVisualizer.cs
+++ b/Bonsai.Design/ObjectTextVisualizer.cs
@@ -3,9 +3,10 @@
using System.Windows.Forms;
using Bonsai;
using Bonsai.Design;
+using System.Diagnostics;
using System.Drawing;
using System.Reactive;
-using System.Text.RegularExpressions;
+using System.Text;
[assembly: TypeVisualizer(typeof(ObjectTextVisualizer), Target = typeof(object))]
@@ -30,25 +31,99 @@ public class ObjectTextVisualizer : BufferedVisualizer
///
protected override void ShowBuffer(IList> values)
{
- if (values.Count > 0)
+ if (values.Count == 0)
+ return;
+
+ var sb = new StringBuilder();
+
+ // Trim old values if bufferSize was reduced
+ while (buffer.Count >= bufferSize)
+ buffer.Dequeue();
+
+ // Add new values to the buffer (and only the ones which might appear)
+ for (int i = Math.Max(0, values.Count - bufferSize); i < values.Count; i++)
{
- base.ShowBuffer(values);
- textBox.Text = string.Join(Environment.NewLine, buffer);
- textPanel.Invalidate();
+ sb.Clear();
+ AppendDisplayText(sb, values[i].Value);
+ AppendString(sb.ToString());
}
+
+ // Update the visual representation of the buffer
+ RefreshVisualization(sb);
}
///
public override void Show(object value)
{
- value ??= string.Empty;
- var text = value.ToString();
- text = Regex.Replace(text, @"\r|\n", string.Empty);
- buffer.Enqueue(text);
- while (buffer.Count > bufferSize)
+ // Updates to this visualizer are expected to go through ShowBuffer
+ Debug.Fail($"Likely unintentional call to {nameof(ObjectTextVisualizer)}.{nameof(Show)}");
+
+ var stringBuilder = new StringBuilder();
+ AppendDisplayText(stringBuilder, value);
+ AppendString(stringBuilder.ToString());
+ RefreshVisualization(stringBuilder);
+ }
+
+ ///
+ /// Appends the display text for the specified object to the text buffer.
+ ///
+ /// The string builder which receives the display text.
+ /// The object for which to retrieve the display text.
+ protected virtual void AppendDisplayText(StringBuilder stringBuilder, object value)
+ {
+ string rawText = value?.ToString() ?? string.Empty;
+ stringBuilder.EnsureCapacity(stringBuilder.Length + rawText.Length);
+
+ foreach (var c in rawText)
{
+ switch (c)
+ {
+ // Carriage returns are presumed to be followed by line feeds, skip them entirely
+ case '\r':
+ continue;
+ // Newlines become space so that things like multi-line JSON or matrices are still visible
+ case '\n':
+ case '\x0085': // Next line character (NEL)
+ case '\x2028': // Unicode line separator
+ case '\x2029': // Unicode paragraph separator
+ stringBuilder.Append(' ');
+ break;
+ case '\t':
+ stringBuilder.Append(c);
+ break;
+ // Replace all other control characters with the "�" replacement character
+ case < ' ': // C0 control characters
+ case '\x007F': // Delete
+ case >= '\x0080' and <= '\x009F': // C1 control characters
+ stringBuilder.Append('\xFFFD');
+ break;
+ default:
+ stringBuilder.Append(c);
+ break;
+ }
+ }
+ }
+
+ private void AppendString(string value)
+ {
+ if (buffer.Count >= bufferSize)
buffer.Dequeue();
+
+ buffer.Enqueue(value);
+ }
+
+ private void RefreshVisualization(StringBuilder stringBuilder)
+ {
+ Debug.Assert(buffer.Count <= bufferSize);
+ stringBuilder.Clear();
+ foreach (var line in buffer)
+ {
+ if (stringBuilder.Length > 0)
+ stringBuilder.Append(Environment.NewLine);
+ stringBuilder.Append(line);
}
+ textBox.Text = stringBuilder.ToString();
+ textPanel.Invalidate();
}
///
@@ -106,7 +181,9 @@ public override void Unload()
{
bufferSize = 0;
textBox.Dispose();
+ textPanel.Dispose();
textBox = null;
+ textPanel = null;
buffer = null;
}
}