1
0
mirror of synced 2024-11-14 17:33:49 +00:00

Changed payload editor to Avalon

- Syntax highlighting
- Highlight for JSON validation errors
- Much improved tab handling and all the other advantages of using a proper editor
This commit is contained in:
Mark van Renswoude 2021-12-25 10:46:59 +01:00
parent 78de8e5196
commit b549729bf5
13 changed files with 253 additions and 52 deletions

View File

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@ -19,7 +19,7 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<None Remove="Images\Connecting.svg" /> <None Remove="Images\Busy.svg" />
<None Remove="Images\Dock.svg" /> <None Remove="Images\Dock.svg" />
<None Remove="Images\Error.svg" /> <None Remove="Images\Error.svg" />
<None Remove="Images\Ok.svg" /> <None Remove="Images\Ok.svg" />
@ -40,6 +40,7 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="AvalonEdit" Version="6.1.2.30" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" /> <PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
<PackageReference Include="SharpVectors" Version="1.7.7" /> <PackageReference Include="SharpVectors" Version="1.7.7" />
<PackageReference Include="SimpleInjector" Version="5.3.2" /> <PackageReference Include="SimpleInjector" Version="5.3.2" />
@ -54,7 +55,7 @@
<ItemGroup> <ItemGroup>
<Resource Include="Images\Undock.svg" /> <Resource Include="Images\Undock.svg" />
<Resource Include="Images\Connecting.svg" /> <Resource Include="Images\Busy.svg" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View File

@ -1,6 +1,7 @@
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" <ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:ui="clr-namespace:PettingZoo.UI"> xmlns:ui="clr-namespace:PettingZoo.UI"
xmlns:avalonedit="http://icsharpcode.net/sharpdevelop/avalonedit">
<!-- Global styling --> <!-- Global styling -->
<Style x:Key="WindowStyle" TargetType="{x:Type Window}"> <Style x:Key="WindowStyle" TargetType="{x:Type Window}">
<Setter Property="Background" Value="{DynamicResource {x:Static SystemColors.ControlBrushKey}}"/> <Setter Property="Background" Value="{DynamicResource {x:Static SystemColors.ControlBrushKey}}"/>
@ -83,10 +84,11 @@
</Style> </Style>
<Style x:Key="Payload" TargetType="{x:Type TextBox}"> <Style x:Key="Payload" TargetType="{x:Type avalonedit:TextEditor}">
<Setter Property="AcceptsReturn" Value="True" />
<Setter Property="AcceptsTab" Value="True" />
<Setter Property="VerticalScrollBarVisibility" Value="Visible" />
<Setter Property="FontFamily" Value="Consolas,Courier New" /> <Setter Property="FontFamily" Value="Consolas,Courier New" />
</Style> </Style>
<Style x:Key="ControlBorder" TargetType="{x:Type Border}">
<Setter Property="BorderThickness" Value="1" />
</Style>
</ResourceDictionary> </ResourceDictionary>

View File

@ -0,0 +1,37 @@
using System;
using System.Windows.Media;
using ICSharpCode.AvalonEdit.Document;
using ICSharpCode.AvalonEdit.Rendering;
namespace PettingZoo.UI
{
public class ErrorHighlightingTransformer : DocumentColorizingTransformer
{
public Brush BackgroundBrush { get; set; }
public TextPosition? ErrorPosition { get; set; }
public ErrorHighlightingTransformer()
{
BackgroundBrush = new SolidColorBrush(Color.FromRgb(255, 230, 230));
}
protected override void ColorizeLine(DocumentLine line)
{
if (ErrorPosition == null)
return;
if (line.LineNumber != Math.Max(ErrorPosition.Value.Row, 1))
return;
var lineStartOffset = line.Offset;
ChangeLinePart(lineStartOffset, lineStartOffset + line.Length,
element =>
{
element.BackgroundBrush = BackgroundBrush;
});
}
}
}

View File

@ -69,6 +69,8 @@ namespace PettingZoo.UI
public void Dispose() public void Dispose()
{ {
BindingOperations.ClearBinding(this, ItemsSourceProperty); BindingOperations.ClearBinding(this, ItemsSourceProperty);
GC.SuppressFinalize(this);
} }

View File

@ -79,7 +79,7 @@
<StatusBar DockPanel.Dock="Bottom"> <StatusBar DockPanel.Dock="Bottom">
<StatusBarItem> <StatusBarItem>
<StackPanel Orientation="Horizontal"> <StackPanel Orientation="Horizontal">
<Image Source="{svgc:SvgImage Source=/Images/Connecting.svg, AppName=PettingZoo}" Width="16" Height="16" Margin="4" Visibility="{Binding ConnectionStatusConnecting}" /> <Image Source="{svgc:SvgImage Source=/Images/Busy.svg, AppName=PettingZoo}" Width="16" Height="16" Margin="4" Visibility="{Binding ConnectionStatusConnecting}" />
<Image Source="{svgc:SvgImage Source=/Images/Ok.svg, AppName=PettingZoo}" Width="16" Height="16" Margin="4" Visibility="{Binding ConnectionStatusOk}" /> <Image Source="{svgc:SvgImage Source=/Images/Ok.svg, AppName=PettingZoo}" Width="16" Height="16" Margin="4" Visibility="{Binding ConnectionStatusOk}" />
<Image Source="{svgc:SvgImage Source=/Images/Error.svg, AppName=PettingZoo}" Width="16" Height="16" Margin="4" Visibility="{Binding ConnectionStatusError}" /> <Image Source="{svgc:SvgImage Source=/Images/Error.svg, AppName=PettingZoo}" Width="16" Height="16" Margin="4" Visibility="{Binding ConnectionStatusError}" />
<TextBlock Text="{Binding ConnectionStatus}" VerticalAlignment="Center"/> <TextBlock Text="{Binding ConnectionStatus}" VerticalAlignment="Center"/>

View File

@ -115,6 +115,8 @@ namespace PettingZoo.UI.Main
private static T? GetParent<T>(object originalSource) where T : DependencyObject private static T? GetParent<T>(object originalSource) where T : DependencyObject
{ {
var current = originalSource as DependencyObject; var current = originalSource as DependencyObject;
if (current is not Visual)
return null;
while (current != null) while (current != null)
{ {

View File

@ -17,15 +17,23 @@
<RadioButton Content="JSON" Style="{StaticResource TypeSelection}" IsChecked="{Binding ContentTypeSelection, Converter={StaticResource EnumBooleanConverter}, ConverterParameter={x:Static publisher:PayloadEditorContentType.Json}}" /> <RadioButton Content="JSON" Style="{StaticResource TypeSelection}" IsChecked="{Binding ContentTypeSelection, Converter={StaticResource EnumBooleanConverter}, ConverterParameter={x:Static publisher:PayloadEditorContentType.Json}}" />
<RadioButton Content="Plain text" Style="{StaticResource TypeSelection}" IsChecked="{Binding ContentTypeSelection, Converter={StaticResource EnumBooleanConverter}, ConverterParameter={x:Static publisher:PayloadEditorContentType.Plain}}" /> <RadioButton Content="Plain text" Style="{StaticResource TypeSelection}" IsChecked="{Binding ContentTypeSelection, Converter={StaticResource EnumBooleanConverter}, ConverterParameter={x:Static publisher:PayloadEditorContentType.Plain}}" />
<RadioButton Content="Other" Style="{StaticResource TypeSelection}" IsChecked="{Binding ContentTypeSelection, Converter={StaticResource EnumBooleanConverter}, ConverterParameter={x:Static publisher:PayloadEditorContentType.Other}}" /> <RadioButton Content="Other" Style="{StaticResource TypeSelection}" IsChecked="{Binding ContentTypeSelection, Converter={StaticResource EnumBooleanConverter}, ConverterParameter={x:Static publisher:PayloadEditorContentType.Other}}" />
<TextBox Width="200" Text="{Binding ContentType, UpdateSourceTrigger=PropertyChanged}" /> <TextBox Name="TextBoxForBorder" Width="200" Text="{Binding ContentType, UpdateSourceTrigger=PropertyChanged}" />
</StackPanel> </StackPanel>
<StackPanel Orientation="Horizontal" DockPanel.Dock="Bottom" Visibility="{Binding JsonValidationVisibility}" Margin="0,8,0,0"> <StackPanel Orientation="Horizontal" DockPanel.Dock="Bottom" Visibility="{Binding ValidationVisibility}" Margin="0,8,0,0">
<Image Source="{svgc:SvgImage Source=/Images/Ok.svg, AppName=PettingZoo}" Width="16" Height="16" Margin="4" Visibility="{Binding JsonValidationOk}" /> <Image Source="{svgc:SvgImage Source=/Images/Ok.svg, AppName=PettingZoo}" Width="16" Height="16" Margin="4" Visibility="{Binding ValidationOk}" />
<Image Source="{svgc:SvgImage Source=/Images/Error.svg, AppName=PettingZoo}" Width="16" Height="16" Margin="4" Visibility="{Binding JsonValidationError}" /> <Image Source="{svgc:SvgImage Source=/Images/Error.svg, AppName=PettingZoo}" Width="16" Height="16" Margin="4" Visibility="{Binding ValidationError}" />
<TextBlock Text="{Binding JsonValidationMessage}" Margin="4" /> <Image Source="{svgc:SvgImage Source=/Images/Busy.svg, AppName=PettingZoo}" Width="16" Height="16" Margin="4" Visibility="{Binding ValidationValidating}" />
<TextBlock Text="{Binding ValidationMessage}" Margin="4" />
</StackPanel> </StackPanel>
<TextBox Text="{Binding Payload, UpdateSourceTrigger=PropertyChanged}" Style="{StaticResource Payload}" /> <Border Style="{StaticResource ControlBorder}" Name="EditorBorder">
<avalonedit:TextEditor
xmlns:avalonedit="http://icsharpcode.net/sharpdevelop/avalonedit"
Name="Editor"
SyntaxHighlighting="{Binding SyntaxHighlighting}"
Style="{StaticResource Payload}"
/>
</Border>
</DockPanel> </DockPanel>
</UserControl> </UserControl>

View File

@ -86,6 +86,9 @@ namespace PettingZoo.UI.Tab.Publisher
} }
} }
private readonly ErrorHighlightingTransformer errorHighlightingTransformer = new();
public PayloadEditorControl() public PayloadEditorControl()
{ {
// Keep the exposed properties in sync with the ViewModel // Keep the exposed properties in sync with the ViewModel
@ -133,6 +136,42 @@ namespace PettingZoo.UI.Tab.Publisher
InitializeComponent(); InitializeComponent();
// I'm not sure how to get a standard control border, all I could find were workaround:
// https://social.msdn.microsoft.com/Forums/en-US/5e007497-8d5a-401d-ac5b-9e1356fe9b64/default-borderbrush-for-textbox-listbox-etc
// So I'll just copy it from another TextBox. I truly hate WPF some times for making standard things so complicated. </rant>
EditorBorder.BorderBrush = TextBoxForBorder.BorderBrush;
Editor.Options.IndentationSize = 2;
Editor.TextArea.TextView.LineTransformers.Add(errorHighlightingTransformer);
// Avalon doesn't play nice with bindings it seems:
// https://stackoverflow.com/questions/18964176/two-way-binding-to-avalonedit-document-text-using-mvvm
Editor.Document.Text = Payload;
Editor.TextChanged += (_, _) =>
{
Payload = Editor.Document.Text;
};
viewModel.PropertyChanged += (_, args) =>
{
if (args.PropertyName != nameof(viewModel.ValidationInfo))
return;
Dispatcher.Invoke(() =>
{
if (errorHighlightingTransformer.ErrorPosition == viewModel.ValidationInfo.ErrorPosition)
return;
errorHighlightingTransformer.ErrorPosition = viewModel.ValidationInfo.ErrorPosition;
// TODO this can probably be optimized to only redraw the affected line
Editor.TextArea.TextView.Redraw();
});
};
// Setting the DataContext for the UserControl is a major PITA when binding the control's properties, // Setting the DataContext for the UserControl is a major PITA when binding the control's properties,
// so I've moved the ViewModel one level down to get the best of both worlds... // so I've moved the ViewModel one level down to get the best of both worlds...
DataContextContainer.DataContext = viewModel; DataContextContainer.DataContext = viewModel;

View File

@ -88,20 +88,29 @@ namespace PettingZoo.UI.Tab.Publisher {
} }
/// <summary> /// <summary>
/// Looks up a localized string similar to Invalid JSON: {0}. /// Looks up a localized string similar to Invalid: {0}.
/// </summary> /// </summary>
internal static string JsonValidationError { internal static string ValidationError {
get { get {
return ResourceManager.GetString("JsonValidationError", resourceCulture); return ResourceManager.GetString("ValidationError", resourceCulture);
} }
} }
/// <summary> /// <summary>
/// Looks up a localized string similar to Valid JSON. /// Looks up a localized string similar to Valid.
/// </summary> /// </summary>
internal static string JsonValidationOk { internal static string ValidationOk {
get { get {
return ResourceManager.GetString("JsonValidationOk", resourceCulture); return ResourceManager.GetString("ValidationOk", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Validating....
/// </summary>
internal static string ValidationValidating {
get {
return ResourceManager.GetString("ValidationValidating", resourceCulture);
} }
} }
} }

View File

@ -112,10 +112,10 @@
<value>2.0</value> <value>2.0</value>
</resheader> </resheader>
<resheader name="reader"> <resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value> <value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=6.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader> </resheader>
<resheader name="writer"> <resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value> <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=6.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader> </resheader>
<data name="ContentTypeJson" xml:space="preserve"> <data name="ContentTypeJson" xml:space="preserve">
<value>JSON</value> <value>JSON</value>
@ -126,10 +126,13 @@
<data name="ContentTypePlain" xml:space="preserve"> <data name="ContentTypePlain" xml:space="preserve">
<value>Plain text</value> <value>Plain text</value>
</data> </data>
<data name="JsonValidationError" xml:space="preserve"> <data name="ValidationError" xml:space="preserve">
<value>Invalid JSON: {0}</value> <value>Invalid: {0}</value>
</data> </data>
<data name="JsonValidationOk" xml:space="preserve"> <data name="ValidationOk" xml:space="preserve">
<value>Valid JSON</value> <value>Valid</value>
</data>
<data name="ValidationValidating" xml:space="preserve">
<value>Validating...</value>
</data> </data>
</root> </root>

View File

@ -1,8 +1,9 @@
using System; using System;
using System.ComponentModel; using System.ComponentModel;
using System.Reactive.Linq; using System.Reactive.Linq;
using System.Reactive.Subjects;
using System.Windows; using System.Windows;
using ICSharpCode.AvalonEdit.Highlighting;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq; using Newtonsoft.Json.Linq;
namespace PettingZoo.UI.Tab.Publisher namespace PettingZoo.UI.Tab.Publisher
@ -15,6 +16,38 @@ namespace PettingZoo.UI.Tab.Publisher
}; };
public enum ValidationStatus
{
NotSupported,
Validating,
Ok,
Error
}
public readonly struct ValidationInfo
{
public ValidationStatus Status { get; }
public string Message { get; }
public TextPosition? ErrorPosition { get; }
public ValidationInfo(ValidationStatus status, string? message = null, TextPosition? errorPosition = null)
{
Status = status;
Message = message ?? status switch
{
ValidationStatus.NotSupported => "",
ValidationStatus.Validating => PayloadEditorStrings.ValidationValidating,
ValidationStatus.Ok => PayloadEditorStrings.ValidationOk,
ValidationStatus.Error => throw new InvalidOperationException(@"Message required for Error validation status"),
_ => throw new ArgumentException(@"Unsupported validation status", nameof(status))
};
ErrorPosition = errorPosition;
}
}
public class PayloadEditorViewModel : BaseViewModel public class PayloadEditorViewModel : BaseViewModel
{ {
private const string ContentTypeJson = "application/json"; private const string ContentTypeJson = "application/json";
@ -24,8 +57,7 @@ namespace PettingZoo.UI.Tab.Publisher
private PayloadEditorContentType contentTypeSelection = PayloadEditorContentType.Json; private PayloadEditorContentType contentTypeSelection = PayloadEditorContentType.Json;
private bool fixedJson; private bool fixedJson;
private bool jsonValid = true; private ValidationInfo validationInfo = new(ValidationStatus.Ok);
private string jsonValidationMessage;
private string payload = ""; private string payload = "";
@ -59,7 +91,7 @@ namespace PettingZoo.UI.Tab.Publisher
get => contentTypeSelection; get => contentTypeSelection;
set set
{ {
if (!SetField(ref contentTypeSelection, value, otherPropertiesChanged: new [] { nameof(JsonValidationVisibility) })) if (!SetField(ref contentTypeSelection, value, otherPropertiesChanged: new [] { nameof(ValidationVisibility), nameof(SyntaxHighlighting) }))
return; return;
ContentType = ContentTypeSelection switch ContentType = ContentTypeSelection switch
@ -80,23 +112,22 @@ namespace PettingZoo.UI.Tab.Publisher
set => SetField(ref fixedJson, value); set => SetField(ref fixedJson, value);
} }
public Visibility JsonValidationVisibility => ContentTypeSelection == PayloadEditorContentType.Json ? Visibility.Visible : Visibility.Collapsed;
public Visibility JsonValidationOk => JsonValid ? Visibility.Visible : Visibility.Collapsed;
public Visibility JsonValidationError => !JsonValid ? Visibility.Visible : Visibility.Collapsed;
public ValidationInfo ValidationInfo
public string JsonValidationMessage
{ {
get => jsonValidationMessage; get => validationInfo;
private set => SetField(ref jsonValidationMessage, value); private set => SetField(ref validationInfo, value, otherPropertiesChanged: new[] { nameof(ValidationOk), nameof(ValidationError), nameof(ValidationValidating), nameof(ValidationMessage) });
} }
public bool JsonValid public Visibility ValidationVisibility => ContentTypeSelection == PayloadEditorContentType.Json ? Visibility.Visible : Visibility.Collapsed;
{
get => jsonValid; public string ValidationMessage => ValidationInfo.Message;
private set => SetField(ref jsonValid, value, otherPropertiesChanged: new[] { nameof(JsonValidationOk), nameof(JsonValidationError) });
} public Visibility ValidationOk => ValidationInfo.Status == ValidationStatus.Ok ? Visibility.Visible : Visibility.Collapsed;
public Visibility ValidationError => ValidationInfo.Status == ValidationStatus.Error ? Visibility.Visible : Visibility.Collapsed;
public Visibility ValidationValidating => ValidationInfo.Status == ValidationStatus.Validating ? Visibility.Visible : Visibility.Collapsed;
public Visibility ContentTypeVisibility => FixedJson ? Visibility.Collapsed : Visibility.Visible; public Visibility ContentTypeVisibility => FixedJson ? Visibility.Collapsed : Visibility.Visible;
@ -108,26 +139,48 @@ namespace PettingZoo.UI.Tab.Publisher
} }
public IHighlightingDefinition? SyntaxHighlighting => ContentTypeSelection == PayloadEditorContentType.Json
? HighlightingManager.Instance.GetDefinition(@"Json")
: null;
public PayloadEditorViewModel() public PayloadEditorViewModel()
{ {
jsonValidationMessage = PayloadEditorStrings.JsonValidationOk; var observable = Observable.FromEventPattern<PropertyChangedEventHandler, PropertyChangedEventArgs>(
Observable.FromEventPattern<PropertyChangedEventHandler, PropertyChangedEventArgs>(
h => PropertyChanged += h, h => PropertyChanged += h,
h => PropertyChanged -= h) h => PropertyChanged -= h)
.Where(e => e.EventArgs.PropertyName == nameof(Payload)) .Where(e => e.EventArgs.PropertyName == nameof(Payload));
observable
.Subscribe(_ => ValidatingPayload());
observable
.Throttle(TimeSpan.FromMilliseconds(500)) .Throttle(TimeSpan.FromMilliseconds(500))
.Subscribe(_ => ValidatePayload()); .Subscribe(_ => ValidatePayload());
} }
private void ValidatingPayload()
{
if (ValidationInfo.Status == ValidationStatus.Validating)
return;
if (ContentTypeSelection != PayloadEditorContentType.Json)
{
ValidationInfo = new ValidationInfo(ValidationStatus.NotSupported);
return;
}
ValidationInfo = new ValidationInfo(ValidationStatus.Validating);
}
private void ValidatePayload() private void ValidatePayload()
{ {
if (ContentTypeSelection != PayloadEditorContentType.Json) if (ContentTypeSelection != PayloadEditorContentType.Json)
{ {
JsonValid = true; ValidationInfo = new ValidationInfo(ValidationStatus.NotSupported);
JsonValidationMessage = PayloadEditorStrings.JsonValidationOk;
return; return;
} }
@ -136,13 +189,15 @@ namespace PettingZoo.UI.Tab.Publisher
if (!string.IsNullOrEmpty(Payload)) if (!string.IsNullOrEmpty(Payload))
JToken.Parse(Payload); JToken.Parse(Payload);
JsonValid = true; ValidationInfo = new ValidationInfo(ValidationStatus.Ok);
JsonValidationMessage = PayloadEditorStrings.JsonValidationOk; }
catch (JsonReaderException e)
{
ValidationInfo = new ValidationInfo(ValidationStatus.Error, e.Message, new TextPosition(e.LineNumber, e.LinePosition));
} }
catch (Exception e) catch (Exception e)
{ {
JsonValid = false; ValidationInfo = new ValidationInfo(ValidationStatus.Error, e.Message);
JsonValidationMessage = string.Format(PayloadEditorStrings.JsonValidationError, e.Message);
} }
} }
} }

View File

@ -0,0 +1,43 @@
using System;
namespace PettingZoo.UI
{
public readonly struct TextPosition : IEquatable<TextPosition>
{
public int Row { get; }
public int Column { get; }
public TextPosition(int row, int column)
{
Row = row;
Column = column;
}
public bool Equals(TextPosition other)
{
return Row == other.Row && Column == other.Column;
}
public override bool Equals(object? obj)
{
return obj is TextPosition other && Equals(other);
}
public override int GetHashCode()
{
return HashCode.Combine(Row, Column);
}
public static bool operator ==(TextPosition left, TextPosition right)
{
return left.Equals(right);
}
public static bool operator !=(TextPosition left, TextPosition right)
{
return !(left == right);
}
}
}