diff --git a/PettingZoo.Core/Macros/BasePayloadMacro.cs b/PettingZoo.Core/Macros/BasePayloadMacro.cs new file mode 100644 index 0000000..13f40fe --- /dev/null +++ b/PettingZoo.Core/Macros/BasePayloadMacro.cs @@ -0,0 +1,22 @@ +namespace PettingZoo.Core.Macros +{ + public abstract class BasePayloadMacro : IPayloadMacro + { + public string DisplayName { get; } + public string MacroText { get; } + + public string MacroCommand { get; } + + + protected BasePayloadMacro(string macroCommand, string displayName) + { + MacroCommand = macroCommand; + + DisplayName = displayName; + MacroText = "{{" + macroCommand + "}}"; + } + + + public abstract string GetValue(); + } +} diff --git a/PettingZoo.Core/Macros/DateTimePayloadMacro.cs b/PettingZoo.Core/Macros/DateTimePayloadMacro.cs new file mode 100644 index 0000000..2e6dec3 --- /dev/null +++ b/PettingZoo.Core/Macros/DateTimePayloadMacro.cs @@ -0,0 +1,18 @@ +using System; + +namespace PettingZoo.Core.Macros +{ + public class JsonDateTimePayloadMacro : BasePayloadMacro + { + public JsonDateTimePayloadMacro() + : base("JsonUtcNow", "Current date/time (yyyy-mm-ddThh:mm:ss.mmmZ)") + { + } + + + public override string GetValue() + { + return DateTime.UtcNow.ToString("yyyy-MM-dd'T'HH:mm:ss.fff'Z'"); + } + } +} diff --git a/PettingZoo.Core/Macros/IPayloadMacroProcessor.cs b/PettingZoo.Core/Macros/IPayloadMacroProcessor.cs new file mode 100644 index 0000000..4cb7b34 --- /dev/null +++ b/PettingZoo.Core/Macros/IPayloadMacroProcessor.cs @@ -0,0 +1,18 @@ +using System.Collections.Generic; + +namespace PettingZoo.Core.Macros +{ + public interface IPayloadMacroProcessor + { + string Apply(string payload); + + IEnumerable Macros { get; } + } + + + public interface IPayloadMacro + { + public string DisplayName { get; } + public string MacroText { get; } + } +} diff --git a/PettingZoo.Core/Macros/NewGuidPayloadMacro.cs b/PettingZoo.Core/Macros/NewGuidPayloadMacro.cs new file mode 100644 index 0000000..325eb4d --- /dev/null +++ b/PettingZoo.Core/Macros/NewGuidPayloadMacro.cs @@ -0,0 +1,18 @@ +using System; + +namespace PettingZoo.Core.Macros +{ + public class NewGuidPayloadMacro : BasePayloadMacro + { + public NewGuidPayloadMacro() + : base("NewGuid", "Generate GUID") + { + } + + + public override string GetValue() + { + return Guid.NewGuid().ToString(); + } + } +} diff --git a/PettingZoo.Core/Macros/PayloadMacroProcessor.cs b/PettingZoo.Core/Macros/PayloadMacroProcessor.cs new file mode 100644 index 0000000..10cc9a8 --- /dev/null +++ b/PettingZoo.Core/Macros/PayloadMacroProcessor.cs @@ -0,0 +1,41 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text.RegularExpressions; + +namespace PettingZoo.Core.Macros +{ + public class PayloadMacroProcessor : IPayloadMacroProcessor + { + private readonly BasePayloadMacro[] macros; + public IEnumerable Macros => macros; + + + public PayloadMacroProcessor() + { + macros = new BasePayloadMacro[] + { + new NewGuidPayloadMacro(), + new JsonDateTimePayloadMacro() + }; + } + + + // For now we only support simple one-keyboard macros, but this could be extended with parameters if required + private static readonly Regex MacroRegex = new("{{(.+?)}}", RegexOptions.Compiled); + + + public string Apply(string payload) + { + return MacroRegex.Replace(payload, match => + { + var macroCommand = match.Groups[1].Value.Trim(); + var macro = macros.FirstOrDefault(m => string.Equals(m.MacroCommand, macroCommand, StringComparison.CurrentCultureIgnoreCase)); + + return macro != null + ? macro.GetValue() + : match.Groups[0].Value; + }); + } + } +} diff --git a/PettingZoo/PettingZoo.csproj b/PettingZoo/PettingZoo.csproj index 47877d3..cc761d5 100644 --- a/PettingZoo/PettingZoo.csproj +++ b/PettingZoo/PettingZoo.csproj @@ -148,7 +148,7 @@ PublicResXFileCodeGenerator - ResXFileCodeGenerator + PublicResXFileCodeGenerator PayloadEditorStrings.Designer.cs diff --git a/PettingZoo/Program.cs b/PettingZoo/Program.cs index 223ea45..f0f717a 100644 --- a/PettingZoo/Program.cs +++ b/PettingZoo/Program.cs @@ -6,6 +6,7 @@ using System.Windows.Markup; using PettingZoo.Core.Connection; using PettingZoo.Core.ExportImport; using PettingZoo.Core.Generator; +using PettingZoo.Core.Macros; using PettingZoo.Core.Settings; using PettingZoo.RabbitMQ; using PettingZoo.Settings.LiteDB; @@ -84,6 +85,7 @@ namespace PettingZoo container.Register(); container.RegisterSingleton(); container.Register(); + container.RegisterSingleton(); container.RegisterInstance(new ExportImportFormatProvider( new TapetiCmdExportFormat(), diff --git a/PettingZoo/UI/Tab/Publisher/PayloadEditorControl.xaml b/PettingZoo/UI/Tab/Publisher/PayloadEditorControl.xaml index e548248..097460a 100644 --- a/PettingZoo/UI/Tab/Publisher/PayloadEditorControl.xaml +++ b/PettingZoo/UI/Tab/Publisher/PayloadEditorControl.xaml @@ -21,18 +21,36 @@ - - - + + + + + + + + + Style="{StaticResource Payload}"> + + + + + + + + + + + + + diff --git a/PettingZoo/UI/Tab/Publisher/PayloadEditorControl.xaml.cs b/PettingZoo/UI/Tab/Publisher/PayloadEditorControl.xaml.cs index 6ebdd89..45925fc 100644 --- a/PettingZoo/UI/Tab/Publisher/PayloadEditorControl.xaml.cs +++ b/PettingZoo/UI/Tab/Publisher/PayloadEditorControl.xaml.cs @@ -2,7 +2,9 @@ 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 @@ -88,6 +90,32 @@ namespace PettingZoo.UI.Tab.Publisher } + 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; @@ -95,6 +123,21 @@ namespace PettingZoo.UI.Tab.Publisher } + private IPayloadMacroProcessor? macroProcessor; + public IPayloadMacroProcessor? MacroProcessor + { + get => macroProcessor; + set + { + if (value == macroProcessor) + return; + + macroProcessor = value; + UpdateMacroContextMenu(); + } + } + + private readonly ErrorHighlightingTransformer errorHighlightingTransformer = new(); public PayloadEditorControl() @@ -123,6 +166,12 @@ namespace PettingZoo.UI.Tab.Publisher viewModel.Payload = value; }); + this.OnPropertyChanges(EnableMacrosProperty) + .ObserveOn(SynchronizationContext.Current!) + .Subscribe(value => + { + viewModel.EnableMacros = value; + }); viewModel.PropertyChanged += (_, args) => { @@ -139,6 +188,10 @@ namespace PettingZoo.UI.Tab.Publisher case nameof(viewModel.Payload): SetValue(PayloadProperty, viewModel.Payload); break; + + case nameof(viewModel.EnableMacros): + SetValue(EnableMacrosProperty, viewModel.EnableMacros); + break; } }; @@ -207,5 +260,70 @@ namespace PettingZoo.UI.Tab.Publisher // 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(); + } } } diff --git a/PettingZoo/UI/Tab/Publisher/PayloadEditorStrings.Designer.cs b/PettingZoo/UI/Tab/Publisher/PayloadEditorStrings.Designer.cs index 298ec51..bb328bc 100644 --- a/PettingZoo/UI/Tab/Publisher/PayloadEditorStrings.Designer.cs +++ b/PettingZoo/UI/Tab/Publisher/PayloadEditorStrings.Designer.cs @@ -22,7 +22,7 @@ namespace PettingZoo.UI.Tab.Publisher { [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")] [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] - internal class PayloadEditorStrings { + public class PayloadEditorStrings { private static global::System.Resources.ResourceManager resourceMan; @@ -36,7 +36,7 @@ namespace PettingZoo.UI.Tab.Publisher { /// Returns the cached ResourceManager instance used by this class. /// [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - internal static global::System.Resources.ResourceManager ResourceManager { + public static global::System.Resources.ResourceManager ResourceManager { get { if (object.ReferenceEquals(resourceMan, null)) { global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("PettingZoo.UI.Tab.Publisher.PayloadEditorStrings", typeof(PayloadEditorStrings).Assembly); @@ -51,7 +51,7 @@ namespace PettingZoo.UI.Tab.Publisher { /// resource lookups using this strongly typed resource class. /// [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - internal static global::System.Globalization.CultureInfo Culture { + public static global::System.Globalization.CultureInfo Culture { get { return resourceCulture; } @@ -60,10 +60,19 @@ namespace PettingZoo.UI.Tab.Publisher { } } + /// + /// Looks up a localized string similar to Enable macros (right-click editor to insert). + /// + public static string CheckEnableMacros { + get { + return ResourceManager.GetString("CheckEnableMacros", resourceCulture); + } + } + /// /// Looks up a localized string similar to JSON. /// - internal static string ContentTypeJson { + public static string ContentTypeJson { get { return ResourceManager.GetString("ContentTypeJson", resourceCulture); } @@ -72,7 +81,7 @@ namespace PettingZoo.UI.Tab.Publisher { /// /// Looks up a localized string similar to Other. /// - internal static string ContentTypeOther { + public static string ContentTypeOther { get { return ResourceManager.GetString("ContentTypeOther", resourceCulture); } @@ -81,16 +90,79 @@ namespace PettingZoo.UI.Tab.Publisher { /// /// Looks up a localized string similar to Plain text. /// - internal static string ContentTypePlain { + public static string ContentTypePlain { get { return ResourceManager.GetString("ContentTypePlain", resourceCulture); } } + /// + /// Looks up a localized string similar to Copy. + /// + public static string ContextMenuCopy { + get { + return ResourceManager.GetString("ContextMenuCopy", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Cut. + /// + public static string ContextMenuCut { + get { + return ResourceManager.GetString("ContextMenuCut", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Insert macro. + /// + public static string ContextMenuInsertMacro { + get { + return ResourceManager.GetString("ContextMenuInsertMacro", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Paste. + /// + public static string ContextMenuPaste { + get { + return ResourceManager.GetString("ContextMenuPaste", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Redo. + /// + public static string ContextMenuRedo { + get { + return ResourceManager.GetString("ContextMenuRedo", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Undo. + /// + public static string ContextMenuUndo { + get { + return ResourceManager.GetString("ContextMenuUndo", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Show available macros. + /// + public static string ShowMacrosHint { + get { + return ResourceManager.GetString("ShowMacrosHint", resourceCulture); + } + } + /// /// Looks up a localized string similar to Invalid: {0}. /// - internal static string ValidationError { + public static string ValidationError { get { return ResourceManager.GetString("ValidationError", resourceCulture); } @@ -99,7 +171,7 @@ namespace PettingZoo.UI.Tab.Publisher { /// /// Looks up a localized string similar to Valid. /// - internal static string ValidationOk { + public static string ValidationOk { get { return ResourceManager.GetString("ValidationOk", resourceCulture); } @@ -108,7 +180,7 @@ namespace PettingZoo.UI.Tab.Publisher { /// /// Looks up a localized string similar to Valid syntax. /// - internal static string ValidationOkSyntax { + public static string ValidationOkSyntax { get { return ResourceManager.GetString("ValidationOkSyntax", resourceCulture); } @@ -117,7 +189,7 @@ namespace PettingZoo.UI.Tab.Publisher { /// /// Looks up a localized string similar to Validating.... /// - internal static string ValidationValidating { + public static string ValidationValidating { get { return ResourceManager.GetString("ValidationValidating", resourceCulture); } diff --git a/PettingZoo/UI/Tab/Publisher/PayloadEditorStrings.resx b/PettingZoo/UI/Tab/Publisher/PayloadEditorStrings.resx index 0784f9f..990fa89 100644 --- a/PettingZoo/UI/Tab/Publisher/PayloadEditorStrings.resx +++ b/PettingZoo/UI/Tab/Publisher/PayloadEditorStrings.resx @@ -117,6 +117,9 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=6.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + Enable macros (right-click editor to insert) + JSON @@ -126,6 +129,27 @@ Plain text + + Copy + + + Cut + + + Insert macro + + + Paste + + + Redo + + + Undo + + + Show available macros + Invalid: {0} diff --git a/PettingZoo/UI/Tab/Publisher/PayloadEditorViewModel.cs b/PettingZoo/UI/Tab/Publisher/PayloadEditorViewModel.cs index 98aa140..d531539 100644 --- a/PettingZoo/UI/Tab/Publisher/PayloadEditorViewModel.cs +++ b/PettingZoo/UI/Tab/Publisher/PayloadEditorViewModel.cs @@ -2,6 +2,7 @@ using System.ComponentModel; using System.Reactive.Linq; using System.Windows; +using System.Windows.Input; using ICSharpCode.AvalonEdit.Highlighting; using Newtonsoft.Json; using Newtonsoft.Json.Linq; @@ -60,10 +61,11 @@ namespace PettingZoo.UI.Tab.Publisher private string contentType = ContentTypeJson; private PayloadEditorContentType contentTypeSelection = PayloadEditorContentType.Json; private bool fixedJson; - + private ValidationInfo validationInfo = new(ValidationStatus.OkSyntax); private string payload = ""; + private bool enableMacros; public string ContentType @@ -136,6 +138,7 @@ namespace PettingZoo.UI.Tab.Publisher public Visibility ContentTypeVisibility => FixedJson ? Visibility.Collapsed : Visibility.Visible; + public string Payload { get => payload; @@ -143,6 +146,13 @@ namespace PettingZoo.UI.Tab.Publisher } + public bool EnableMacros + { + get => enableMacros; + set => SetField(ref enableMacros, value); + } + + public IHighlightingDefinition? SyntaxHighlighting => ContentTypeSelection == PayloadEditorContentType.Json ? HighlightingManager.Instance.GetDefinition(@"Json") : null; diff --git a/PettingZoo/UI/Tab/Publisher/PublisherViewModel.cs b/PettingZoo/UI/Tab/Publisher/PublisherViewModel.cs index 7e6b5f6..a8dbc2f 100644 --- a/PettingZoo/UI/Tab/Publisher/PublisherViewModel.cs +++ b/PettingZoo/UI/Tab/Publisher/PublisherViewModel.cs @@ -5,6 +5,7 @@ using System.Windows.Controls; using System.Windows.Input; using PettingZoo.Core.Connection; using PettingZoo.Core.Generator; +using PettingZoo.Core.Macros; using PettingZoo.WPF.ViewModel; namespace PettingZoo.UI.Tab.Publisher @@ -20,6 +21,7 @@ namespace PettingZoo.UI.Tab.Publisher { private readonly IConnection connection; private readonly IExampleGenerator exampleGenerator; + private readonly IPayloadMacroProcessor payloadMacroProcessor; private readonly ITabFactory tabFactory; private bool sendToExchange = true; @@ -155,10 +157,11 @@ namespace PettingZoo.UI.Tab.Publisher string IPublishDestination.RoutingKey => SendToExchange ? RoutingKey : Queue; - public PublisherViewModel(ITabFactory tabFactory, IConnection connection, IExampleGenerator exampleGenerator, ReceivedMessageInfo? fromReceivedMessage = null) + public PublisherViewModel(ITabFactory tabFactory, IConnection connection, IExampleGenerator exampleGenerator, IPayloadMacroProcessor payloadMacroProcessor, ReceivedMessageInfo? fromReceivedMessage = null) { this.connection = connection; this.exampleGenerator = exampleGenerator; + this.payloadMacroProcessor = payloadMacroProcessor; this.tabFactory = tabFactory; publishCommand = new DelegateCommand(PublishExecute, PublishCanExecute); @@ -207,7 +210,7 @@ namespace PettingZoo.UI.Tab.Publisher if (rawPublisherView == null) { - rawPublisherViewModel = new RawPublisherViewModel(connection, this); + rawPublisherViewModel = new RawPublisherViewModel(connection, this, payloadMacroProcessor); rawPublisherViewModel.PublishCommand.CanExecuteChanged += (_, _) => { publishCommand.RaiseCanExecuteChanged(); @@ -228,7 +231,7 @@ namespace PettingZoo.UI.Tab.Publisher if (tapetiPublisherView == null) { - tapetiPublisherViewModel = new TapetiPublisherViewModel(connection, this, exampleGenerator); + tapetiPublisherViewModel = new TapetiPublisherViewModel(connection, this, exampleGenerator, payloadMacroProcessor); tapetiPublisherViewModel.PublishCommand.CanExecuteChanged += (_, _) => { publishCommand.RaiseCanExecuteChanged(); @@ -263,14 +266,14 @@ namespace PettingZoo.UI.Tab.Publisher if (TapetiPublisherViewModel.IsTapetiMessage(fromReceivedMessage)) { - var tapetiPublisherViewModel = new TapetiPublisherViewModel(connection, this, exampleGenerator, fromReceivedMessage); + var tapetiPublisherViewModel = new TapetiPublisherViewModel(connection, this, exampleGenerator, payloadMacroProcessor, fromReceivedMessage); tapetiPublisherView = new TapetiPublisherView(tapetiPublisherViewModel); MessageType = MessageType.Tapeti; } else { - var rawPublisherViewModel = new RawPublisherViewModel(connection, this, fromReceivedMessage); + var rawPublisherViewModel = new RawPublisherViewModel(connection, this, payloadMacroProcessor, fromReceivedMessage); rawPublisherView = new RawPublisherView(rawPublisherViewModel); MessageType = MessageType.Raw; @@ -306,7 +309,7 @@ namespace PettingZoo.UI.Tab.Publisher public class DesignTimePublisherViewModel : PublisherViewModel { - public DesignTimePublisherViewModel() : base(null!, null!, null!) + public DesignTimePublisherViewModel() : base(null!, null!, null!, null!) { } diff --git a/PettingZoo/UI/Tab/Publisher/RawPublisherView.xaml b/PettingZoo/UI/Tab/Publisher/RawPublisherView.xaml index c788f02..bc36848 100644 --- a/PettingZoo/UI/Tab/Publisher/RawPublisherView.xaml +++ b/PettingZoo/UI/Tab/Publisher/RawPublisherView.xaml @@ -131,6 +131,7 @@