diff --git a/MultiTerm.Core/Model/MultiFormatString.cs b/MultiTerm.Core/Model/MultiFormatString.cs new file mode 100644 index 0000000..a7917dc --- /dev/null +++ b/MultiTerm.Core/Model/MultiFormatString.cs @@ -0,0 +1,35 @@ +using MultiTerm.Core.Types; + +namespace MultiTerm.Core.Model; + +public class MultiFormatString +{ + public List> FormatValuePairs { get; private set; } = new(); + + public void Add(FormatType format, string value) + { + // TODO check if value is valid + this.FormatValuePairs.Add(Tuple.Create(format, value)); + } + + public void Remove(int amount) + { + // guard amount <= 0 + if (amount <= 0) throw new ArgumentException($"{nameof(amount)} cannot be <= 0"); + + // limit amount to maximum + int listCount = this.FormatValuePairs.Count; + if (amount > listCount) + { + amount = listCount; + } + + // remove range + this.FormatValuePairs.RemoveRange( (listCount - amount) , amount); + } + + public override string ToString() + { + throw new NotImplementedException(); + } +} diff --git a/MultiTerm.Core/Types/FormatType.cs b/MultiTerm.Core/Types/FormatType.cs new file mode 100644 index 0000000..a0390c4 --- /dev/null +++ b/MultiTerm.Core/Types/FormatType.cs @@ -0,0 +1,19 @@ +namespace MultiTerm.Core.Types; + +public enum FormatType +{ + /// + /// UTF-16 encoded character + /// + Character, + + /// + /// Hex type + /// + Hexadecimal, + + /// + /// Binary type + /// + Binary +} diff --git a/MultiTerm.Core/ViewModel/SendReceiveViewModel.cs b/MultiTerm.Core/ViewModel/SendReceiveViewModel.cs index 73998e8..864373a 100644 --- a/MultiTerm.Core/ViewModel/SendReceiveViewModel.cs +++ b/MultiTerm.Core/ViewModel/SendReceiveViewModel.cs @@ -1,4 +1,6 @@ -using MultiTerm.Core.Types; +using CommunityToolkit.Mvvm.ComponentModel; +using MultiTerm.Core.Model; +using MultiTerm.Core.Types; namespace MultiTerm.Core.ViewModel; @@ -13,5 +15,9 @@ public partial class SendReceiveViewModel : TerminalViewModel } public override TerminalViewType ViewType => TerminalViewType.SendReceive; - + /// + /// Send data model. + /// + [ObservableProperty] + private MultiFormatString sendData = new(); } diff --git a/MultiTerm.Wpf.CustomControl/MultiFormatTextBox/Format.cs b/MultiTerm.Wpf.CustomControl/MultiFormatTextBox/Format.cs new file mode 100644 index 0000000..e42b3fe --- /dev/null +++ b/MultiTerm.Wpf.CustomControl/MultiFormatTextBox/Format.cs @@ -0,0 +1,55 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Windows.Input; +using System.Windows.Media; +using System.Windows; +using MultiTerm.Core.Types; + +namespace MultiTerm.Wpf.CustomControl; + +internal class Format +{ + private readonly string? backgroundColorResourceName; + + public string Name { get; set; } + public Brush BackgroundBrush { get; set; } + public FormatType AssociatedFormatType { get; set; } + public Predicate IsKeyValid { get; set; } + public Format(string name, Brush backgroundBrush, FormatType associatedFormatType, Predicate keyValidator) + { + this.Name = name; + this.BackgroundBrush = backgroundBrush; + this.AssociatedFormatType = associatedFormatType; + this.IsKeyValid = keyValidator; + } + public Format(string name, string backgroundColorResourceName, FormatType associatedFormatType, Predicate keyValidator) + { + this.Name = name; + this.backgroundColorResourceName = backgroundColorResourceName; + this.BackgroundBrush = Brushes.White; // set background brush to white + this.AssociatedFormatType = associatedFormatType; + this.IsKeyValid = keyValidator; + } + public static List GetListOfNames(IEnumerable formats) + { + return formats.Select(item => item.Name).ToList(); + } + public static void UpdateBackgroundBrushesFromResources(FrameworkElement fwElement, IEnumerable formats) + { + if (fwElement == null) throw new ArgumentNullException(nameof(fwElement)); + + foreach (var format in formats) + { + // if resource name not set => skip + if (format.backgroundColorResourceName == null) continue; + + // get background brush color from resources + try + { + format.BackgroundBrush = (SolidColorBrush)fwElement.FindResource(format.backgroundColorResourceName); + } + catch (Exception) { continue; } // ignore + } + } +} diff --git a/MultiTerm.Wpf.CustomControl/MultiFormatTextBox/MultiFormatTextBox.cs b/MultiTerm.Wpf.CustomControl/MultiFormatTextBox/MultiFormatTextBox.cs index 04de1bb..bba58c1 100644 --- a/MultiTerm.Wpf.CustomControl/MultiFormatTextBox/MultiFormatTextBox.cs +++ b/MultiTerm.Wpf.CustomControl/MultiFormatTextBox/MultiFormatTextBox.cs @@ -1,6 +1,9 @@ -using System; +using MultiTerm.Core.Model; +using MultiTerm.Core.Types; +using System; using System.Collections.Generic; -using System.Drawing; +using System.ComponentModel; +using System.Diagnostics; using System.Linq; using System.Runtime.CompilerServices; using System.Windows; @@ -40,69 +43,55 @@ namespace MultiTerm.Wpf.CustomControl; /// /// /// -internal class Format -{ - private readonly string? backgroundColorResourceName; - - public string Name { get; set; } - public Brush BackgroundBrush { get; set; } - public Predicate IsKeyValid { get; set; } - public Format(string name, Brush backgroundBrush, Predicate keyValidator) - { - this.Name = name; - this.BackgroundBrush = backgroundBrush; - this.IsKeyValid = keyValidator; - } - public Format(string name, string backgroundColorResourceName, Predicate keyValidator) - { - this.Name = name; - this.backgroundColorResourceName = backgroundColorResourceName; - this.BackgroundBrush = Brushes.White; // set background brush to white - this.IsKeyValid = keyValidator; - } - public static List GetListOfNames(IEnumerable formats) - { - return formats.Select(item => item.Name).ToList(); - } - public static void UpdateBackgroundBrushesFromResources(FrameworkElement fwElement, IEnumerable formats) - { - if (fwElement == null) throw new ArgumentNullException(nameof(fwElement)); - - foreach (var format in formats) - { - // if resource name not set => skip - if (format.backgroundColorResourceName == null) continue; - // get background brush color from resources - try - { - format.BackgroundBrush = (SolidColorBrush)fwElement.FindResource(format.backgroundColorResourceName); - } - catch (Exception) { continue; } // ignore - } - } -} 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 except space - //new Format("CHAR", Brushes.LightSkyBlue, delegate(Key k) { return (k != Key.Space); }), - new Format("CHAR", "MultiFormatTextBox.CHAR.Background", delegate(Key k) { return (k != Key.Space); }), + // character input, accepts all keys + new Format("CHAR", "MultiFormatTextBox.CHAR.Background", Core.Types.FormatType.Character, + delegate(Key k) { return true; }), // hex input, ignores all keys that are not inbetween 0 and F - //new Format("HEX", Brushes.LightGreen, delegate(Key k) { return (k >= Key.D0 && k <= Key.F); }), - new Format("HEX", "MultiFormatTextBox.HEX.Background", delegate(Key k) { return (k >= Key.D0 && k <= Key.F); }), + new Format("HEX", "MultiFormatTextBox.HEX.Background", Core.Types.FormatType.Hexadecimal, + delegate(Key k) { return (k >= Key.D0 && k <= Key.F); }), // binary input, ignores all keys except 0 and 1 - //new Format("BIN", Brushes.LightPink, delegate(Key k) { return (k == Key.D0 || k == Key.D1); }) - new Format("BIN", "MultiFormatTextBox.BIN.Background", delegate(Key k) { return (k == Key.D0 || k == Key.D1); }) + new Format("BIN", "MultiFormatTextBox.BIN.Background", Core.Types.FormatType.Binary, + delegate(Key k) { return (k == Key.D0 || k == Key.D1); }) }; - private ComboBox comboBox; - private RichTextBox richTextBox; - private Format currentlySelectedFormat; + #endregion + + #region private content + private const string comboBoxTemplateKey = "comboBox"; + private const string richTextBoxTemplateKey = "richTextBox"; + + private ComboBox? comboBox; + private RichTextBox? richTextBox; + private Format? currentlySelectedFormat; private int offsetContentStartToFormatStart; + private 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() { @@ -116,125 +105,210 @@ public class MultiFormatTextBox : Control // initialize format background brushes Format.UpdateBackgroundBrushesFromResources(this, formats); // set initially selected format - this.currentlySelectedFormat = formats!.First(); - + this.currentlySelectedFormat = formats.First(); + // get comboBox from template - var comboBox = GetTemplateChild("comboBox") as ComboBox; - if (comboBox != null) + 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 - var richTextBox = GetTemplateChild("richTextBox") as RichTextBox; - if (richTextBox != null) + if (GetTemplateChild(richTextBoxTemplateKey) is RichTextBox richTextBox) { this.richTextBox = richTextBox; + this.richTextBox.AcceptsReturn = false; this.richTextBox.KeyDown += RichTextBox_KeyDown; this.richTextBox.TextChanged += RichTextBox_TextChanged; this.offsetContentStartToFormatStart = 0; } + 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; } + // nothing to do } private void ComboBox_SelectionChanged(object sender, SelectionChangedEventArgs e) { - // 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) + lock (lockObj) // lock so no richtextbox textchanged event can happen { - 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(); + // match all formats with the name selected in the combobox + var matchingFormats = formats.Where(format => format.Name == (string)this.comboBox!.SelectedItem); - // insert space to separate formats - this.InsertSeparation(); + // 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(); - // set new start position - this.StoreCaretPosition(); + // insert space to separate formats + this.InsertSeparation(); - // focus textbox - this.richTextBox.Focus(); + // focus textbox + this.richTextBox!.Focus(); + } } private void InsertSeparation() { // disable event handler - this.richTextBox.TextChanged -= RichTextBox_TextChanged; + this.richTextBox!.TextChanged -= RichTextBox_TextChanged; - // store caret position before - this.StoreCaretPosition(); - - // insert + // 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 + // change background of last one character RtbChangeTextBackground(this.richTextBox, - start: this.richTextBox.Document.ContentStart.GetPositionAtOffset(-this.offsetContentStartToFormatStart), - end: this.richTextBox.Document.ContentEnd, + start: this.richTextBox!.Document.ContentStart.GetPositionAtOffset(caretIndexBefore), + end: this.richTextBox.CaretPosition, color: defaultBackgroundBrush); - // store new caret position after - this.StoreCaretPosition(); - // reenable this.richTextBox.TextChanged += RichTextBox_TextChanged; } - private void StoreCaretPosition() - { - this.offsetContentStartToFormatStart = this.richTextBox.CaretPosition.GetOffsetToPosition(this.richTextBox.Document.ContentStart); - } - 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) + 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) { - if (e.Changes.Count > 1 || e.Changes.Count == 0) { return; } + // no changes => exit + if (e.Changes.Count == 0) return; - // if something was added => change background color - if (e.Changes.First().AddedLength > 0) - { - RtbChangeTextBackground(this.richTextBox, - start: this.richTextBox.Document.ContentStart.GetPositionAtOffset(-this.offsetContentStartToFormatStart), - end: this.richTextBox.Document.ContentEnd, - color: this.currentlySelectedFormat.BackgroundBrush); - } - // if something was removed => update start of content to current location - else if(e.Changes.First().RemovedLength > 0) + lock (lockObj) // lock so no concurrent combobox change can happen { - this.StoreCaretPosition(); + 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 = this.ConvertRtbContentToMultiFormatString(this.richTextBox!); } } private static void RtbChangeTextBackground(RichTextBox rtb, TextPointer start, TextPointer end, Brush color) { - // Get text selection - TextSelection textRange = rtb.Selection; + // 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 - rtb.Selection.Select(rtb.Document.ContentEnd, rtb.Document.ContentEnd); + // deselect everything (set to end) + rtb.Selection.Select(end, end); } + private 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; + } } diff --git a/MultiTerm.Wpf.CustomControl/MultiTerm.Wpf.CustomControl.csproj b/MultiTerm.Wpf.CustomControl/MultiTerm.Wpf.CustomControl.csproj index c6ee76c..f432ab3 100644 --- a/MultiTerm.Wpf.CustomControl/MultiTerm.Wpf.CustomControl.csproj +++ b/MultiTerm.Wpf.CustomControl/MultiTerm.Wpf.CustomControl.csproj @@ -6,4 +6,8 @@ true + + + + diff --git a/MultiTerm.Wpf/View/SendReceiveView.xaml b/MultiTerm.Wpf/View/SendReceiveView.xaml index 9a70519..7d3a303 100644 --- a/MultiTerm.Wpf/View/SendReceiveView.xaml +++ b/MultiTerm.Wpf/View/SendReceiveView.xaml @@ -39,7 +39,7 @@