PettingZoo/PettingZoo/UI/Tab/Publisher/PayloadEditorControl.xaml.cs

330 lines
10 KiB
C#

using System;
using System.Reactive.Linq;
using System.Threading;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using PettingZoo.Core.Macros;
using PettingZoo.Core.Validation;
namespace PettingZoo.UI.Tab.Publisher
{
/// <summary>
/// Interaction logic for PayloadEditorControl.xaml
/// </summary>
public partial class PayloadEditorControl
{
private readonly PayloadEditorViewModel viewModel = new();
public static readonly DependencyProperty ContentTypeProperty
= DependencyProperty.Register(
"ContentType",
typeof(string),
typeof(PayloadEditorControl),
new FrameworkPropertyMetadata("")
{
BindsTwoWayByDefault = true,
DefaultUpdateSourceTrigger = UpdateSourceTrigger.PropertyChanged
}
);
public string ContentType
{
get => viewModel.ContentType;
set
{
if (value == viewModel.ContentType)
return;
SetValue(ContentTypeProperty, value);
viewModel.ContentType = value;
}
}
public static readonly DependencyProperty FixedJsonProperty
= DependencyProperty.Register(
"FixedJson",
typeof(bool),
typeof(PayloadEditorControl),
new PropertyMetadata(false)
);
public bool FixedJson
{
get => viewModel.FixedJson;
set
{
if (value == viewModel.FixedJson)
return;
SetValue(FixedJsonProperty, value);
viewModel.FixedJson = value;
}
}
public static readonly DependencyProperty PayloadProperty
= DependencyProperty.Register(
"Payload",
typeof(string),
typeof(PayloadEditorControl),
new FrameworkPropertyMetadata("")
{
BindsTwoWayByDefault = true,
DefaultUpdateSourceTrigger = UpdateSourceTrigger.PropertyChanged
}
);
public string Payload
{
get => viewModel.Payload;
set
{
if (value == viewModel.Payload)
return;
SetValue(PayloadProperty, value);
viewModel.Payload = value;
}
}
public static readonly DependencyProperty EnableMacrosProperty
= DependencyProperty.Register(
"EnableMacros",
typeof(bool),
typeof(PayloadEditorControl),
new FrameworkPropertyMetadata(false)
{
BindsTwoWayByDefault = true,
DefaultUpdateSourceTrigger = UpdateSourceTrigger.PropertyChanged
}
);
public bool EnableMacros
{
get => viewModel.EnableMacros;
set
{
if (value == viewModel.EnableMacros)
return;
SetValue(EnableMacrosProperty, value);
viewModel.EnableMacros = value;
}
}
public IPayloadValidator? Validator
{
get => viewModel.Validator;
set => viewModel.Validator = value;
}
private IPayloadMacroProcessor? macroProcessor;
public IPayloadMacroProcessor? MacroProcessor
{
get => macroProcessor;
set
{
if (value == macroProcessor)
return;
macroProcessor = value;
UpdateMacroContextMenu();
}
}
private readonly ErrorHighlightingTransformer errorHighlightingTransformer = new();
public PayloadEditorControl()
{
// Keep the exposed properties in sync with the ViewModel
this.OnPropertyChanges<string>(ContentTypeProperty)
.ObserveOn(SynchronizationContext.Current!)
.Subscribe(value =>
{
viewModel.ContentType = value;
});
this.OnPropertyChanges<bool>(FixedJsonProperty)
.ObserveOn(SynchronizationContext.Current!)
.Subscribe(value =>
{
viewModel.FixedJson = value;
});
this.OnPropertyChanges<string>(PayloadProperty)
.ObserveOn(SynchronizationContext.Current!)
.Subscribe(value =>
{
viewModel.Payload = value;
});
this.OnPropertyChanges<bool>(EnableMacrosProperty)
.ObserveOn(SynchronizationContext.Current!)
.Subscribe(value =>
{
viewModel.EnableMacros = value;
});
viewModel.PropertyChanged += (_, args) =>
{
switch (args.PropertyName)
{
case nameof(viewModel.ContentType):
SetValue(ContentTypeProperty, viewModel.ContentType);
break;
case nameof(viewModel.FixedJson):
SetValue(FixedJsonProperty, viewModel.FixedJson);
break;
case nameof(viewModel.Payload):
SetValue(PayloadProperty, viewModel.Payload);
break;
case nameof(viewModel.EnableMacros):
SetValue(EnableMacrosProperty, viewModel.EnableMacros);
break;
}
};
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
// ...this is intended though, and well explained here:
// https://github.com/icsharpcode/AvalonEdit/issues/84
Editor.Document.Text = Payload;
var editorTriggered = false;
Editor.TextChanged += (_, _) =>
{
editorTriggered = true;
try
{
Payload = Editor.Document.Text;
}
finally
{
editorTriggered = false;
}
};
viewModel.PropertyChanged += (_, args) =>
{
if (args.PropertyName != nameof(viewModel.ValidationInfo) &&
(args.PropertyName != nameof(viewModel.Payload) || editorTriggered))
return;
Dispatcher.Invoke(() =>
{
switch (args.PropertyName)
{
case nameof(viewModel.ValidationInfo):
if (errorHighlightingTransformer.ErrorPosition == viewModel.ValidationInfo.ErrorPosition)
return;
errorHighlightingTransformer.ErrorPosition = viewModel.ValidationInfo.ErrorPosition;
// This can probably be optimized to only redraw the affected line, but the message is typically so small it's not worth the effort at the moment
Editor.TextArea.TextView.Redraw();
break;
case nameof(viewModel.Payload):
Editor.Document.Text = viewModel.Payload;
break;
}
});
};
// 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...
DataContextContainer.DataContext = viewModel;
}
private void UpdateMacroContextMenu()
{
ContextMenuInsertMacro.Items.Clear();
if (macroProcessor == null)
return;
foreach (var macro in macroProcessor.Macros)
{
var macroMenuItem = new MenuItem
{
Header = macro.DisplayName
};
macroMenuItem.Click += (_, _) =>
{
Editor.SelectedText = macro.MacroText;
var length = Editor.SelectionLength;
Editor.SelectionLength = 0;
Editor.SelectionStart += length;
viewModel.EnableMacros = true;
};
ContextMenuInsertMacro.Items.Add(macroMenuItem);
}
}
private void Undo_Click(object sender, RoutedEventArgs e)
{
Editor.Undo();
}
private void Redo_Click(object sender, RoutedEventArgs e)
{
Editor.Redo();
}
private void Cut_Click(object sender, RoutedEventArgs e)
{
Editor.Cut();
}
private void Copy_Click(object sender, RoutedEventArgs e)
{
Editor.Copy();
}
private void Paste_Click(object sender, RoutedEventArgs e)
{
Editor.Paste();
}
private void ContextMenu_OnOpened(object sender, RoutedEventArgs e)
{
ContextMenuUndo.IsEnabled = Editor.CanUndo;
ContextMenuRedo.IsEnabled = Editor.CanRedo;
ContextMenuCut.IsEnabled = Editor.SelectionLength > 0;
ContextMenuCopy.IsEnabled = Editor.SelectionLength > 0;
ContextMenuPaste.IsEnabled = Clipboard.ContainsText();
}
}
}