using MultiTerm.Core.Model; using MultiTerm.Core.Types; using System; using System.Collections.Generic; using System.ComponentModel; using System.Diagnostics; 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, accepts all keys new Format("CHAR", "MultiFormatTextBox.CHAR.Background", FormatType.Character, delegate(Key k) { return true; }), // hex input, ignores all keys that are not inbetween 0 and F new Format("HEX", "MultiFormatTextBox.HEX.Background", FormatType.Hexadecimal, delegate(Key k) { return (k >= Key.D0 && k <= Key.F); }), // binary input, ignores all keys except 0 and 1 new Format("BIN", "MultiFormatTextBox.BIN.Background", FormatType.Binary, delegate(Key k) { return (k == Key.D0 || k == Key.D1); }) }; #endregion #region private content private const string comboBoxTemplateKey = "comboBox"; private const string richTextBoxTemplateKey = "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("CurrentMultiFormatString", typeof(MultiFormatString), typeof(MultiFormatTextBox), new PropertyMetadata(null, OnCurrentMultiFormatStringChanged)); /// /// .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))); } 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.KeyDown += RichTextBox_KeyDown; this.richTextBox.TextChanged += RichTextBox_TextChanged; } 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; } if (e.NewValue is not MultiFormatString newString) { return; } // new value is an empty string => clear if(newString.FormatValuePairs.Count == 0) { mftb.richTextBox!.Document.Blocks.Clear(); } } 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(); // insert space to separate formats this.InsertSeparation(); // focus textbox this.richTextBox!.Focus(); } } private void InsertSeparation() { // disable event handler this.richTextBox!.TextChanged -= RichTextBox_TextChanged; // insert separator, manually updating caret position because wpf somehow does not do it.... int caretIndexBefore = this.richTextBox.Document.ContentStart.GetOffsetToPosition(this.richTextBox.CaretPosition); Debug.WriteLine($"caret position before inserting separator: offset={this.richTextBox.Document.ContentStart.GetOffsetToPosition(this.richTextBox.CaretPosition)}"); this.richTextBox.AppendText(" "); this.richTextBox.CaretPosition = this.richTextBox.Document.ContentStart.GetPositionAtOffset(caretIndexBefore + 1); Debug.WriteLine($"caret position after inserting separator: offset={this.richTextBox.Document.ContentStart.GetOffsetToPosition(this.richTextBox.CaretPosition)}"); // change background of last one character RtbChangeTextBackground(this.richTextBox, start: this.richTextBox!.Document.ContentStart.GetPositionAtOffset(caretIndexBefore), end: this.richTextBox.CaretPosition, color: defaultBackgroundBrush); // reenable this.richTextBox.TextChanged += RichTextBox_TextChanged; } private void RichTextBox_KeyDown(object sender, KeyEventArgs e) { // guard combobox null if (this.comboBox == null) throw new Exception($"{nameof(this.comboBox)} cannot be null"); // if key is invalid for this format => ignore it (handled = true) if (this.currentlySelectedFormat!.IsKeyValid(e.Key) == false) { e.Handled = true; } // ignore enter if(e.Key == Key.Enter) { e.Handled = true; } } private void RichTextBox_TextChanged(object sender, TextChangedEventArgs e) { // no changes => exit if (e.Changes.Count == 0) return; lock (lockObj) // lock so no concurrent combobox change can happen { foreach (var change in e.Changes) { // print new text Debug.WriteLine($"{nameof(RichTextBox_TextChanged)} changed to '{new TextRange(this.richTextBox!.Document.ContentStart, this.richTextBox.Document.ContentEnd).Text}'"); Debug.WriteLine($"{nameof(RichTextBox_TextChanged)} caret position '{this.richTextBox!.Document.ContentStart.GetOffsetToPosition(this.richTextBox.CaretPosition)}'"); // ignore changes that replace something if (change.RemovedLength == change.AddedLength) continue; // if something was added => change background color if (change.AddedLength > 0) { Debug.WriteLine($"{nameof(RichTextBox_TextChanged)} offset of change = {change.Offset}"); // disable event handler so the update does not trigger any TextChanged events (which it does, interestingly) this.richTextBox!.TextChanged -= RichTextBox_TextChanged; // update color RtbChangeTextBackground(this.richTextBox!, start: this.richTextBox!.Document.ContentStart.GetPositionAtOffset(change.Offset), end: this.richTextBox!.Document.ContentStart.GetPositionAtOffset(change.Offset + change.AddedLength), color: this.currentlySelectedFormat!.BackgroundBrush); // reenable event handler this.richTextBox.TextChanged += RichTextBox_TextChanged; } } // update CurrentMultiFormatString this.CurrentMultiFormatString = ConvertRtbContentToMultiFormatString(this.richTextBox!); } } private static void RtbChangeTextBackground(RichTextBox rtb, TextPointer start, TextPointer end, Brush color) { // print offsets to start and end position Debug.WriteLine($"Changing background of textbox offset {rtb.Document.ContentStart.GetOffsetToPosition(start)} until {rtb.Document.ContentStart.GetOffsetToPosition(end)} to {color}"); // get editable selection var textRange = rtb.Selection; textRange.Select(start, end); // Apply property to the selection: textRange.ApplyPropertyValue(TextElement.BackgroundProperty, color); // deselect everything (set to end) rtb.Selection.Select(end, end); } private static MultiFormatString ConvertRtbContentToMultiFormatString(RichTextBox rtb) { MultiFormatString multiFormatString = new(); // store current caret position int offsetToCaretPosition = rtb.Document.ContentStart.GetOffsetToPosition(rtb.CaretPosition); // get number of symbols var textLength = rtb.Document.ContentStart.GetOffsetToPosition(rtb.Document.ContentEnd); // get full content as TextSelection object var textRange = rtb.Selection; // loop through every character for (int offset = 0; offset < textLength; offset++) { // select one character textRange.Select(rtb.Document.ContentStart.GetPositionAtOffset(offset), rtb.Document.ContentStart.GetPositionAtOffset(offset + 1)); //skip empty textRange if (textRange.IsEmpty) { continue; } // extract background brush var brush = (Brush)textRange.GetPropertyValue(TextElement.BackgroundProperty); // ignore this symbol, since its a separator if (brush == defaultBackgroundBrush) { continue; } // search if it is a brush of a format foreach (var format in formats) { // format has this background brush => add to string with format and text if (brush == format.BackgroundBrush) { multiFormatString.Add(format.AssociatedFormatType, textRange.Text); break; // end loop } } } // deselect everything (set to end) textRange.Select( rtb.Document.ContentStart.GetPositionAtOffset(offsetToCaretPosition), rtb.Document.ContentStart.GetPositionAtOffset(offsetToCaretPosition)); return multiFormatString; } }