using MultiTerm.Core.Model; using MultiTerm.Core.Types; using System; using System.Collections.Generic; using System.Collections.Specialized; using System.ComponentModel; using System.Linq; using System.Windows; using System.Windows.Controls; using System.Windows.Documents; using System.Windows.Input; using System.Windows.Media; namespace MultiTerm.Wpf.CustomControl; public class MultiFormatTextBox : Control { #region static content private static readonly Brush defaultBackgroundBrush = Brushes.White; private static readonly List formats = new() { // character input new Format("CHAR", "MultiFormatTextBox.CHAR.Background", FormatType.Character), // hex input new Format("HEX", "MultiFormatTextBox.HEX.Background", FormatType.Hexadecimal), // binary input new Format("BIN", "MultiFormatTextBox.BIN.Background", FormatType.Binary) }; #endregion #region private content private const string comboBoxTemplateKey = "PART_ComboBox"; private const string richTextBoxTemplateKey = "PART_RichTextBox"; private ComboBox? comboBox; private RichTextBox? richTextBox; private Format? currentlySelectedFormat; private readonly object lockObj = new(); #endregion #region Dependency Properties public static readonly DependencyProperty CurrentMultiFormatStringProperty = DependencyProperty.Register( name: "CurrentMultiFormatString", propertyType: typeof(MultiFormatString), ownerType: typeof(MultiFormatTextBox), typeMetadata: new FrameworkPropertyMetadata(null, OnCurrentMultiFormatStringChanged)); public static readonly RoutedEvent EnterPressedEvent; public static readonly RoutedEvent ArrowUpPressedEvent; public static readonly RoutedEvent ArrowDownPressedEvent; /// /// .NET Property for /// public event RoutedEventHandler EnterPressed { add { this.AddHandler(EnterPressedEvent, value); } remove { this.RemoveHandler(EnterPressedEvent, value); } } /// /// .NET Property for /// public event RoutedEventHandler ArrowUpPressed { add { this.AddHandler(ArrowUpPressedEvent, value); } remove { this.RemoveHandler(ArrowUpPressedEvent, value); } } /// /// .NET Property for /// public event RoutedEventHandler ArrowDownPressed { add { this.AddHandler(ArrowDownPressedEvent, value); } remove { this.RemoveHandler(ArrowDownPressedEvent, value); } } /// /// .NET Property for CurrentMultiFormatString. /// [Bindable(true)] public MultiFormatString CurrentMultiFormatString { get { return (MultiFormatString)GetValue(CurrentMultiFormatStringProperty); } set { SetValue(CurrentMultiFormatStringProperty, value); } } #endregion static MultiFormatTextBox() { DefaultStyleKeyProperty.OverrideMetadata(typeof(MultiFormatTextBox), new FrameworkPropertyMetadata(typeof(MultiFormatTextBox))); EnterPressedEvent = EventManager.RegisterRoutedEvent("EnterPressed", RoutingStrategy.Bubble, typeof(RoutedEventArgs), typeof(MultiFormatTextBox)); ArrowUpPressedEvent = EventManager.RegisterRoutedEvent("ArrowUpPressed", RoutingStrategy.Bubble, typeof(RoutedEventArgs), typeof(MultiFormatTextBox)); ArrowDownPressedEvent = EventManager.RegisterRoutedEvent("ArrowDownPressed", RoutingStrategy.Bubble, typeof(RoutedEventArgs), typeof(MultiFormatTextBox)); } public override void OnApplyTemplate() { base.OnApplyTemplate(); // initialize format background brushes Format.UpdateBackgroundBrushesFromResources(this, formats); // set initially selected format this.currentlySelectedFormat = formats.First(); // get comboBox from template if (GetTemplateChild(comboBoxTemplateKey) is ComboBox comboBox) { this.comboBox = comboBox; this.comboBox.ItemsSource = Format.GetListOfNames(formats); this.comboBox.SelectedItem = currentlySelectedFormat.Name; this.comboBox.SelectionChanged += ComboBox_SelectionChanged; } else { throw new Exception($"Implementation fault, {comboBoxTemplateKey} not found in template."); } // get richTextBox from template if (GetTemplateChild(richTextBoxTemplateKey) is RichTextBox richTextBox) { this.richTextBox = richTextBox; this.richTextBox.AcceptsReturn = false; this.richTextBox.AcceptsTab = false; this.richTextBox.PreviewKeyDown += RichTextBox_KeyDown; this.richTextBox.PreviewTextInput += RichTextBox_PreviewTextInput; this.richTextBox.SelectionChanged += RichTextBox_SelectionChanged; DataObject.AddPastingHandler(this.richTextBox, OnRtbPaste); } else { throw new Exception($"Implementation fault, {richTextBoxTemplateKey} not found in template."); } } private static void OnCurrentMultiFormatStringChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { // extract instance and guard null if (d is not MultiFormatTextBox mftb) { return; } // register to collection changed event if (mftb.CurrentMultiFormatString is INotifyCollectionChanged incc) { // remove handler incc.CollectionChanged -= mftb.MultiFormatString_CollectionChanged; // add new handler if not null if(e.NewValue != null) { incc.CollectionChanged += mftb.MultiFormatString_CollectionChanged; } } // reset textbox and insert items if there are any mftb.ResetTextBox(); if(mftb.CurrentMultiFormatString.Count > 0) { mftb.AddItemsToTextBox(mftb.CurrentMultiFormatString, 0); } } /// /// Reacts on Collection Changed Events. /// private void MultiFormatString_CollectionChanged(object? sender, NotifyCollectionChangedEventArgs e) { switch (e.Action) { case NotifyCollectionChangedAction.Add: // guard null if(e.NewItems == null) { return; } // add list to textbox this.AddItemsToTextBox(e.NewItems, e.NewStartingIndex); break; case NotifyCollectionChangedAction.Remove: // guard null if (e.OldItems == null) { return; } // remove list to textbox this.RemoveItemsFromTextBox(e.OldItems, e.OldStartingIndex); break; case NotifyCollectionChangedAction.Reset: // clear richtextbox add new paragraph this.ResetTextBox(); break; case NotifyCollectionChangedAction.Replace: case NotifyCollectionChangedAction.Move: default: throw new NotImplementedException($"'{nameof(MultiFormatString_CollectionChanged)}()' does not support Action '{e.Action}'"); } } private void RemoveItemsFromTextBox(System.Collections.IList items, int startingIndex) { // extract paragraph from richtextbox (last block is paragraph per default) if (this.richTextBox!.Document.Blocks.LastBlock is not Paragraph paragraph) { throw new Exception($"'{nameof(RemoveItemsFromTextBox)}()' found that {nameof(richTextBox)} has no paragraph as last block."); } if (items.Count > 1) { throw new Exception("Not supported"); } string toRemoveString = string.Empty; if (items[0] is IFormattedCharacter character) { toRemoveString = character.Character; } else { throw new Exception("Not supported"); } var inlineToRemove = paragraph.Inlines.ElementAt(new Index(startingIndex)); // paragraph.Inlines = zero based counting var textInline = new TextRange(inlineToRemove.ContentStart, inlineToRemove.ContentEnd); if(textInline.Text != toRemoveString) { throw new Exception("Tried to remove other element..."); } paragraph.Inlines.Remove(inlineToRemove); } private void AddItemsToTextBox(System.Collections.IList items, int startingIndex) { int indexCounter = startingIndex; // extract paragraph from richtextbox (last block is paragraph per default) var paragraph = this.richTextBox!.Document.Blocks.LastBlock as Paragraph ?? throw new Exception($"'{nameof(AddItemsToTextBox)}()' found that {nameof(richTextBox)} has no paragraph as last block."); // iterate through formatted characters to add foreach (IFormattedCharacter item in items) { Run? run = null; if (item is FormattedCharacter formattedCharacter) { // text as run with correct background brush run = new Run(formattedCharacter.Character) { Background = this.GetBackgroundBrushForFormat(formattedCharacter.Format) }; } else if(item is SpacingCharacter spacingCharacter) { // text as run with default background brush run = new Run(spacingCharacter.Character) { Background = defaultBackgroundBrush }; } else { throw new Exception($"'{nameof(AddItemsToTextBox)}()' cannot handle items of type {item.GetType()}"); } // if this is the first element to enter in the paragraph => simply add it if (paragraph!.Inlines.Count == 0) { paragraph.Inlines.Add(run); } // if the previous item is outside of the range => insert at first position else if(indexCounter - 1 < 0) { paragraph.Inlines.InsertBefore(paragraph.Inlines.FirstInline, run); } else // add to paragraph of richtextbox after previous inline { var previousInline = paragraph.Inlines.ElementAt(new Index(indexCounter - 1)); // paragraph.Inlines = zero based counting paragraph.Inlines.InsertAfter(previousInline, run); } // increment counter indexCounter++; } // update caret position to index position this.SetRtbCaretPosition(indexCounter); } /// /// Clear richtextbox adds new paragraph. /// private void ResetTextBox() { this.richTextBox!.Document.Blocks.Clear(); this.richTextBox!.Document.Blocks.Add(new Paragraph()); } private void ComboBox_SelectionChanged(object sender, SelectionChangedEventArgs e) { lock (lockObj) // lock so no richtextbox textchanged event can happen { // match all formats with the name selected in the combobox var matchingFormats = formats.Where(format => format.Name == (string)this.comboBox!.SelectedItem); // check if exactly one format was matched if (matchingFormats.Count() != 1) { throw new Exception($"{nameof(ComboBox_SelectionChanged)} could not match a correct amount of formats"); } // set currently selected format, reset offset counter this.currentlySelectedFormat = matchingFormats.First(); // focus textbox this.richTextBox!.Focus(); } } private void RichTextBox_KeyDown(object sender, KeyEventArgs e) { if (e.Key == Key.Enter) { e.Handled = true; // raise event RoutedEventArgs args = new(EnterPressedEvent); RaiseEvent(args); } else if(e.Key == Key.Up) { e.Handled = true; // raise event RoutedEventArgs args = new(ArrowUpPressedEvent); RaiseEvent(args); } else if (e.Key == Key.Down) { e.Handled = true; // raise event RoutedEventArgs args = new(ArrowDownPressedEvent); RaiseEvent(args); } else if(e.Key == Key.Space) { e.Handled = true; // add space to MultiFormatString (will only work if currently character type is selected) InsertCharacterAtCaretPositionIntoCurrentMultiFormatString(" "); } // back one else if (e.Key == Key.Back) { var cursorPos = GetRtbCaretPosition(); // remove previous element if it exists try { if (this.CurrentMultiFormatString.ElementAt(cursorPos - 1) != null) { this.CurrentMultiFormatString.RemoveAt(cursorPos - 1); } } catch { } e.Handled = true; } // delete next (selection is not allowed) else if (e.Key == Key.Delete) { var cursorPos = GetRtbCaretPosition(); // remove next element if it exists try { if (this.CurrentMultiFormatString.ElementAt(cursorPos) != null) { this.CurrentMultiFormatString.RemoveAt(cursorPos); } } catch { } e.Handled = true; } } private int GetRtbCaretPosition() { // magic dividor of 3.... nobody knows why... return this.richTextBox!.Document.ContentStart.GetOffsetToPosition(this.richTextBox.CaretPosition) / 3; } private void SetRtbCaretPosition(int index) { // magic multiplicator of 3.... nobody knows why... this.richTextBox!.CaretPosition = this.richTextBox.Document.ContentStart.GetPositionAtOffset(index*3); } private void RichTextBox_PreviewTextInput(object sender, TextCompositionEventArgs e) { // text is never directly inserted => therefore always handled e.Handled = true; // if more than one char inserted => illegal => cancel if(e.Text.Length > 1) { return; } this.InsertCharacterAtCaretPositionIntoCurrentMultiFormatString(e.Text); } private void OnRtbPaste(object sender, DataObjectPastingEventArgs e) { // get pasted data if (e.DataObject.GetDataPresent(typeof(string))) { // get data as string var text = (string)e.DataObject.GetData(typeof(string)); // insert each character seperately foreach (var character in text) { this.InsertCharacterAtCaretPositionIntoCurrentMultiFormatString(character.ToString()); } } // cancel pasting e.CancelCommand(); } private void InsertCharacterAtCaretPositionIntoCurrentMultiFormatString(string text) { var caretPosition = this.GetRtbCaretPosition(); // add char to MultiFormatString FormattedCharacter formattedCharacter = new(this.currentlySelectedFormat!.AssociatedFormatType, text); this.CurrentMultiFormatString.Insert(caretPosition, formattedCharacter); //this.CurrentMultiFormatString.Add(formattedCharacter); } private void RichTextBox_SelectionChanged(object sender, RoutedEventArgs e) { // do not allow selection if (this.richTextBox!.Selection.Text.Length > 0) { this.richTextBox!.Selection.Select(this.richTextBox.Selection.End, this.richTextBox.Selection.End); } } private Brush GetBackgroundBrushForFormat(FormatType format) { var formatObj = formats.Where(f => f.AssociatedFormatType == format).First(); return formatObj.BackgroundBrush; } }