completely overhauled MultiFormatTextBox,

now proper spacing and combination of formats is supported,
the data is also cleanly displayed in the backend part
master
Jonas Arnold 3 years ago
parent 4524a975b8
commit a54b02150b
  1. 14
      MultiTerm.Core/Model/FormattedCharacter.cs
  2. 6
      MultiTerm.Core/Model/IFormattedCharacter.cs
  3. 168
      MultiTerm.Core/Model/MultiFormatString.cs
  4. 6
      MultiTerm.Core/Model/SpacingCharacter.cs
  5. 4
      MultiTerm.Core/MultiTerm.Core.csproj
  6. 4
      MultiTerm.Core/ViewModel/SendReceiveViewModel.cs
  7. 317
      MultiTerm.Wpf.CustomControl/MultiFormatTextBox/MultiFormatTextBox.cs
  8. 4
      MultiTerm.Wpf.CustomControl/MultiFormatTextBox/MultiFormatTextBox.xaml

@ -0,0 +1,14 @@
using MultiTerm.Core.Types;
namespace MultiTerm.Core.Model;
public class FormattedCharacter : Tuple<FormatType, string>, IFormattedCharacter
{
public FormattedCharacter(FormatType item1, string item2) : base(item1, item2)
{
}
public FormattedCharacter(Tuple<FormatType, string> tuple) : base(tuple.Item1, tuple.Item2)
{
}
}

@ -0,0 +1,6 @@
namespace MultiTerm.Core.Model;
public interface IFormattedCharacter
{
}

@ -1,4 +1,5 @@
using MultiTerm.Core.Types; using MultiTerm.Core.Types;
using System.Collections.ObjectModel;
using System.Text; using System.Text;
namespace MultiTerm.Core.Model; namespace MultiTerm.Core.Model;
@ -6,46 +7,46 @@ namespace MultiTerm.Core.Model;
/// <summary> /// <summary>
/// A string that contains symbols of multiple formats such as a standard character as well as hexadecimal or binary parts. /// A string that contains symbols of multiple formats such as a standard character as well as hexadecimal or binary parts.
/// </summary> /// </summary>
public class MultiFormatString public class MultiFormatString: ObservableCollection<IFormattedCharacter>
{ {
/// <summary> /// <summary>
/// List of Symbols that are stored in this <see cref="MultiFormatString"/> /// Inserts item at given index after checking if its valid using <see cref="ValidateValue(FormatType, string)"/>.
/// </summary> /// </summary>
public List<Tuple<FormatType, string>> FormatValuePairs { get; private set; } = new(); /// <param name="index">index where to add item, usually items.Count when using Add method.
/// <param name="item">item to insert</param>
/// <summary> protected override void InsertItem(int index, IFormattedCharacter item)
/// Adds a single character to <see cref="FormatValuePairs"/>.
/// </summary>
/// <param name="format">format of the <paramref name="value"/></param>
/// <param name="value">value of the symbol, must be convertible to an Unicode character</param>
public void Add(FormatType format, string value)
{ {
// check if value is valid, if yes add to FormatValuePairs if(item is FormattedCharacter formattedChar)
if (ValidateValue(format, value))
{ {
this.FormatValuePairs.Add(Tuple.Create(format, value)); //check if valid, only insert then
if(ValidateValue(formattedChar.Item1, formattedChar.Item2) == false)
{
return;
}
// check if spacing is required and insert
if(IsSpacingCharacterRequiredBeforeNewCharacter(this, index, formattedChar))
{
base.InsertItem(index, new SpacingCharacter());
base.InsertItem(index + 1, item);
return;
}
} }
base.InsertItem(index, item);
} }
/// <summary> protected override void RemoveItem(int index)
/// Removes a given amount of characters from the end of <see cref="FormatValuePairs"/>.
/// </summary>
/// <param name="amount">amount of characters to remove from the list, starting at the last item</param>
/// <exception cref="ArgumentException">if amount <= 0</exception>
public void Remove(int amount)
{ {
// guard amount <= 0 // if formatted character => just remove
if (amount <= 0) throw new ArgumentException($"{nameof(amount)} cannot be <= 0"); base.RemoveItem(index);
// limit amount to maximum // if previous item is a spacing character => remove it too
int listCount = this.FormatValuePairs.Count; if(this.ElementAt(index-1) is SpacingCharacter)
if (amount > listCount)
{ {
amount = listCount; base.RemoveAt(index - 1);
} }
// remove range
this.FormatValuePairs.RemoveRange( (listCount - amount) , amount);
} }
/// <summary> /// <summary>
@ -59,15 +60,15 @@ public class MultiFormatString
// Internal function to check and finalize a Hexadecimal Conversion // Internal function to check and finalize a Hexadecimal Conversion
// parameter == null will force ending // parameter == null will force ending
void finalizeHexConversion(Tuple<FormatType, string>? formatValuePair) void finalizeHexConversion(FormattedCharacter? formattedChar)
{ {
// hex conversion ongoing? // hex conversion ongoing?
if (String.IsNullOrEmpty(hexConversionCharacters) == false) if (String.IsNullOrEmpty(hexConversionCharacters) == false)
{ {
// ending conversion or reached limit of characters? // ending conversion or reached limit of characters?
if (formatValuePair == null || if (formattedChar == null ||
formatValuePair.Item1 != FormatType.Hexadecimal || formattedChar.Item1 != FormatType.Hexadecimal ||
hexConversionCharacters.Count() == 4) hexConversionCharacters.Length == 4)
{ {
// finalize conversion // finalize conversion
stringBuilder.Append(FromHexString(hexConversionCharacters)); stringBuilder.Append(FromHexString(hexConversionCharacters));
@ -79,15 +80,15 @@ public class MultiFormatString
// Internal function to check and finalize a Binary Conversion // Internal function to check and finalize a Binary Conversion
// parameter == null will force ending // parameter == null will force ending
void finalizeBinaryConversion(Tuple<FormatType, string>? formatValuePair) void finalizeBinaryConversion(FormattedCharacter? formattedChar)
{ {
// binary conversion ongoing? // binary conversion ongoing?
if (String.IsNullOrEmpty(binaryConversionCharacters) == false) if (String.IsNullOrEmpty(binaryConversionCharacters) == false)
{ {
// ending conversion or reached limit of characters? // ending conversion or reached limit of characters?
if (formatValuePair == null || if (formattedChar == null ||
formatValuePair.Item1 != FormatType.Binary || formattedChar.Item1 != FormatType.Binary ||
binaryConversionCharacters.Count() == 16) binaryConversionCharacters.Length == 16)
{ {
// finalize conversion // finalize conversion
stringBuilder.Append(FromBinaryString(binaryConversionCharacters)); stringBuilder.Append(FromBinaryString(binaryConversionCharacters));
@ -98,28 +99,31 @@ public class MultiFormatString
} }
// go through every character // go through every character
foreach (var formatValuePair in this.FormatValuePairs) foreach (var character in this.Items)
{ {
// can only handle FormattedCharacters
if(character is not FormattedCharacter formattedCharacter) { continue; }
// finalize ongoing conversions if there are any // finalize ongoing conversions if there are any
finalizeHexConversion(formatValuePair); finalizeHexConversion(formattedCharacter);
finalizeBinaryConversion(formatValuePair); finalizeBinaryConversion(formattedCharacter);
switch (formatValuePair.Item1) // switch by format switch (formattedCharacter.Item1) // switch by format
{ {
case FormatType.Character: case FormatType.Character:
stringBuilder.Append(formatValuePair.Item2); stringBuilder.Append(formattedCharacter.Item2);
break; break;
case FormatType.Hexadecimal: case FormatType.Hexadecimal:
hexConversionCharacters += formatValuePair.Item2; hexConversionCharacters += formattedCharacter.Item2;
break; break;
case FormatType.Binary: case FormatType.Binary:
binaryConversionCharacters += formatValuePair.Item2; binaryConversionCharacters += formattedCharacter.Item2;
break; break;
default: default:
throw new NotImplementedException($"'{nameof(ToString)}()' does not implement conversion for format {formatValuePair.Item1}"); throw new NotImplementedException($"'{nameof(ToString)}()' does not implement conversion for format {formattedCharacter.Item1}");
} }
} }
@ -137,7 +141,7 @@ public class MultiFormatString
public static bool ValidateValue(FormatType format, string value) public static bool ValidateValue(FormatType format, string value)
{ {
// invalid if more than one character // invalid if more than one character
if(value.Count() > 1) if(value.Length > 1)
{ return false; } { return false; }
// extract character // extract character
@ -167,6 +171,80 @@ public class MultiFormatString
} }
} }
public static bool IsSpacingCharacterRequiredBeforeNewCharacter(IEnumerable<IFormattedCharacter> collection, int indexOfInsertion, IFormattedCharacter newCharacter)
{
bool spacingCharRequired = false;
const int charsPerHexBundle = 2;
const int charsPerBinBundle = 8;
// no spacing required if new character is a spacing character
if(newCharacter is SpacingCharacter) { return false; }
// no handling implemented for items that are not a FormattedCharacter
if(newCharacter is not FormattedCharacter formattedCharacter)
{ throw new NotImplementedException($"'{nameof(IsSpacingCharacterRequiredBeforeNewCharacter)}()' does not implement handling for type {newCharacter.GetType()}"); }
// switch spacing required by type of format
spacingCharRequired = formattedCharacter.Item1 switch
{
FormatType.Character => CheckSpacingRequiredBecauseChangedFormat(collection, indexOfInsertion, formattedCharacter.Item1),
FormatType.Hexadecimal => CheckSpacingRequiredBecauseChangedFormat(collection, indexOfInsertion, formattedCharacter.Item1) ||
CheckSpacingRequiredBecauseBundleMaxCountReached(collection, indexOfInsertion, charsPerHexBundle, formattedCharacter.Item1),
FormatType.Binary => CheckSpacingRequiredBecauseChangedFormat(collection, indexOfInsertion, formattedCharacter.Item1) ||
CheckSpacingRequiredBecauseBundleMaxCountReached(collection, indexOfInsertion, charsPerBinBundle, formattedCharacter.Item1),
_ => throw new NotImplementedException($"'{nameof(IsSpacingCharacterRequiredBeforeNewCharacter)}()' does not implement handling for format {formattedCharacter.Item1}"),
};
return spacingCharRequired;
static bool CheckSpacingRequiredBecauseChangedFormat(IEnumerable<IFormattedCharacter> collection, int indexOfInsertion, FormatType newCharFormat)
{
// check if previous item is of other type
if (collection.Any())
{
int count = 1;
FormattedCharacter? prevChar = null;
while (prevChar is null)
{
try
{
prevChar = collection.ElementAt(indexOfInsertion - count++) as FormattedCharacter;
// format of previous is not equal to new => spacer required
if (prevChar != null && prevChar.Item1 != newCharFormat)
{
return true;
}
}
catch (Exception)
{
// end of collection => break while
break;
}
}
}
return false;
}
static bool CheckSpacingRequiredBecauseBundleMaxCountReached(IEnumerable<IFormattedCharacter> collection, int indexOfInsertion, int charsPerBundle, FormatType newCharFormat)
{
// spacing required if previous x characters are of same format
if (collection.Count() > charsPerBundle)
{
// get last x elements of collection
var lastXElements = collection.Take(new Range(new Index(indexOfInsertion - charsPerBundle), new Index(indexOfInsertion)));
// get elements of last x elements where the same format is used
var sameFormatElements = lastXElements.Where(x => x is FormattedCharacter formChar && formChar.Item1 == newCharFormat);
// elements of same Format are more (impossible) or equal to amount required for bundle
if (sameFormatElements.Count() >= charsPerBundle)
{
return true;
}
}
return false;
}
}
// from: https://stackoverflow.com/questions/724862/converting-from-hex-to-string // from: https://stackoverflow.com/questions/724862/converting-from-hex-to-string
// test with: returns: "Hello world" for "48656C6C6F20776F726C64" // test with: returns: "Hello world" for "48656C6C6F20776F726C64"
// see https://www.fileformat.info/info/charset/UTF-16/list.htm // see https://www.fileformat.info/info/charset/UTF-16/list.htm

@ -0,0 +1,6 @@
namespace MultiTerm.Core.Model;
public class SpacingCharacter : IFormattedCharacter
{
}

@ -6,10 +6,6 @@
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
</PropertyGroup> </PropertyGroup>
<ItemGroup>
<Folder Include="Model\" />
</ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.1.0" /> <PackageReference Include="CommunityToolkit.Mvvm" Version="8.1.0" />
</ItemGroup> </ItemGroup>

@ -43,8 +43,8 @@ public partial class SendReceiveViewModel : TerminalViewModel
this.TempSentDataString = this.SendableData.ToString(); this.TempSentDataString = this.SendableData.ToString();
// send data // send data
this.SendToCommunicationProtocol(this.SendableData.ToString()); this.SendToCommunicationProtocol(this.SendableData.ToString());
// clear textbox // clear textbox
this.SendableData = new MultiFormatString(); this.SendableData.Clear();
} }
} }

@ -2,8 +2,8 @@
using MultiTerm.Core.Types; using MultiTerm.Core.Types;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.Specialized;
using System.ComponentModel; using System.ComponentModel;
using System.Diagnostics;
using System.Linq; using System.Linq;
using System.Windows; using System.Windows;
using System.Windows.Controls; using System.Windows.Controls;
@ -33,8 +33,8 @@ public class MultiFormatTextBox : Control
#endregion #endregion
#region private content #region private content
private const string comboBoxTemplateKey = "comboBox"; private const string comboBoxTemplateKey = "PART_ComboBox";
private const string richTextBoxTemplateKey = "richTextBox"; private const string richTextBoxTemplateKey = "PART_RichTextBox";
private ComboBox? comboBox; private ComboBox? comboBox;
private RichTextBox? richTextBox; private RichTextBox? richTextBox;
@ -93,8 +93,10 @@ public class MultiFormatTextBox : Control
{ {
this.richTextBox = richTextBox; this.richTextBox = richTextBox;
this.richTextBox.AcceptsReturn = false; this.richTextBox.AcceptsReturn = false;
this.richTextBox.KeyDown += RichTextBox_KeyDown; this.richTextBox.AcceptsTab = false;
this.richTextBox.TextChanged += RichTextBox_TextChanged; this.richTextBox.PreviewKeyDown += RichTextBox_KeyDown;
this.richTextBox.PreviewTextInput += RichTextBox_PreviewTextInput;
this.richTextBox.SelectionChanged += RichTextBox_SelectionChanged;
} }
else else
{ {
@ -108,13 +110,134 @@ public class MultiFormatTextBox : Control
if (d is not MultiFormatTextBox mftb) { return; } if (d is not MultiFormatTextBox mftb) { return; }
if (e.NewValue is not MultiFormatString newString) { return; } if (e.NewValue is not MultiFormatString newString) { return; }
// new value is an empty string => clear // register to collection changed event
if(newString.FormatValuePairs.Count == 0) if (mftb.CurrentMultiFormatString is INotifyCollectionChanged incc)
{ {
mftb.richTextBox!.Document.Blocks.Clear(); incc.CollectionChanged += mftb.MultiFormatString_CollectionChanged;
} }
} }
/// <summary>
/// Reacts on Collection Changed Events.
/// </summary>
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.richTextBox!.Document.Blocks.Clear();
this.richTextBox!.Document.Blocks.Add(new Paragraph());
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 FormattedCharacter formattedCharacter)
{
toRemoveString = formattedCharacter.Item2;
}
else if(items[0] is SpacingCharacter spacingCharacter)
{
toRemoveString = " ";
}
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.Item2)
{
Background = this.currentlySelectedFormat!.BackgroundBrush
};
}
else if(item is SpacingCharacter)
{
// text as run with default background brush
run = new Run(" ")
{
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);
}
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);
}
private void ComboBox_SelectionChanged(object sender, SelectionChangedEventArgs e) private void ComboBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
{ {
@ -131,155 +254,101 @@ public class MultiFormatTextBox : Control
// set currently selected format, reset offset counter // set currently selected format, reset offset counter
this.currentlySelectedFormat = matchingFormats.First(); this.currentlySelectedFormat = matchingFormats.First();
// insert space to separate formats
this.InsertSeparation();
// focus textbox // focus textbox
this.richTextBox!.Focus(); 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) private void RichTextBox_KeyDown(object sender, KeyEventArgs e)
{ {
// guard combobox null if (e.Key == Key.Enter)
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)
{ {
// TODO Raise EnterPressedEvent Here
e.Handled = true; e.Handled = true;
} }
else if(e.Key == Key.Space)
{
e.Handled = true;
// ignore enter // add space to MultiFormatString (will only work if currently character type is selected)
if(e.Key == Key.Enter) InsertCharacterAtCaretPositionIntoCurrentMultiFormatString(" ");
{
e.Handled = true;
} }
} // back one
else if (e.Key == Key.Back)
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) var cursorPos = GetRtbCaretPosition();
{
// 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 // remove previous element if it exists
if (change.RemovedLength == change.AddedLength) continue; try
{
// if something was added => change background color if (this.CurrentMultiFormatString.ElementAt(cursorPos - 1) != null)
if (change.AddedLength > 0)
{ {
Debug.WriteLine($"{nameof(RichTextBox_TextChanged)} offset of change = {change.Offset}"); this.CurrentMultiFormatString.RemoveAt(cursorPos - 1);
}
// disable event handler so the update does not trigger any TextChanged events (which it does, interestingly) }
this.richTextBox!.TextChanged -= RichTextBox_TextChanged; catch { }
// update color e.Handled = true;
RtbChangeTextBackground(this.richTextBox!, }
start: this.richTextBox!.Document.ContentStart.GetPositionAtOffset(change.Offset), // delete next (selection is not allowed)
end: this.richTextBox!.Document.ContentStart.GetPositionAtOffset(change.Offset + change.AddedLength), else if (e.Key == Key.Delete)
color: this.currentlySelectedFormat!.BackgroundBrush); {
var cursorPos = GetRtbCaretPosition();
// reenable event handler // remove next element if it exists
this.richTextBox.TextChanged += RichTextBox_TextChanged; try
{
if (this.CurrentMultiFormatString.ElementAt(cursorPos) != null)
{
this.CurrentMultiFormatString.RemoveAt(cursorPos);
} }
} }
catch { }
// update CurrentMultiFormatString e.Handled = true;
this.CurrentMultiFormatString = ConvertRtbContentToMultiFormatString(this.richTextBox!);
} }
} }
private static void RtbChangeTextBackground(RichTextBox rtb, TextPointer start, TextPointer end, Brush color) private int GetRtbCaretPosition()
{ {
// print offsets to start and end position // magic dividor of 3.... nobody knows why...
Debug.WriteLine($"Changing background of textbox offset {rtb.Document.ContentStart.GetOffsetToPosition(start)} until {rtb.Document.ContentStart.GetOffsetToPosition(end)} to {color}"); return this.richTextBox!.Document.ContentStart.GetOffsetToPosition(this.richTextBox.CaretPosition) / 3;
// 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) private void SetRtbCaretPosition(int index)
{ {
MultiFormatString multiFormatString = new(); // magic multiplicator of 3.... nobody knows why...
// store current caret position this.richTextBox!.CaretPosition = this.richTextBox.Document.ContentStart.GetPositionAtOffset(index*3);
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 private void RichTextBox_PreviewTextInput(object sender, TextCompositionEventArgs e)
for (int offset = 0; offset < textLength; offset++) {
{ // text is never directly inserted => therefore always handled
// select one character e.Handled = true;
textRange.Select(rtb.Document.ContentStart.GetPositionAtOffset(offset), rtb.Document.ContentStart.GetPositionAtOffset(offset + 1));
//skip empty textRange // if more than one char inserted => illegal => cancel
if (textRange.IsEmpty) if(e.Text.Length > 1) { return; }
{ continue; }
// extract background brush this.InsertCharacterAtCaretPositionIntoCurrentMultiFormatString(e.Text);
var brush = (Brush)textRange.GetPropertyValue(TextElement.BackgroundProperty); }
// ignore this symbol, since its a separator private void InsertCharacterAtCaretPositionIntoCurrentMultiFormatString(string text)
if (brush == defaultBackgroundBrush) {
{ continue; } var caretPosition = this.GetRtbCaretPosition();
// search if it is a brush of a format // add char to MultiFormatString
foreach (var format in formats) FormattedCharacter formattedCharacter = new(this.currentlySelectedFormat!.AssociatedFormatType, text);
{
// 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) this.CurrentMultiFormatString.Insert(caretPosition, formattedCharacter);
textRange.Select( //this.CurrentMultiFormatString.Add(formattedCharacter);
rtb.Document.ContentStart.GetPositionAtOffset(offsetToCaretPosition), }
rtb.Document.ContentStart.GetPositionAtOffset(offsetToCaretPosition));
return multiFormatString; 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);
}
} }
} }

@ -22,8 +22,8 @@
<Setter.Value> <Setter.Value>
<ControlTemplate TargetType="{x:Type local:MultiFormatTextBox}"> <ControlTemplate TargetType="{x:Type local:MultiFormatTextBox}">
<DockPanel LastChildFill="True"> <DockPanel LastChildFill="True">
<ComboBox DockPanel.Dock="Left" x:Name="comboBox" Width="60"></ComboBox> <ComboBox DockPanel.Dock="Left" x:Name="PART_ComboBox" Width="60"></ComboBox>
<RichTextBox DockPanel.Dock="Right" x:Name="richTextBox"></RichTextBox> <RichTextBox DockPanel.Dock="Right" x:Name="PART_RichTextBox"></RichTextBox>
</DockPanel> </DockPanel>
<!--<ControlTemplate.Triggers> <!--<ControlTemplate.Triggers>
<Trigger Property="IsEnabled" Value="false"> <Trigger Property="IsEnabled" Value="false">

Loading…
Cancel
Save