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. 2
      MultiTerm.Core/ViewModel/SendReceiveViewModel.cs
  7. 313
      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 System.Collections.ObjectModel;
using System.Text;
namespace MultiTerm.Core.Model;
@ -6,46 +7,46 @@ namespace MultiTerm.Core.Model;
/// <summary>
/// A string that contains symbols of multiple formats such as a standard character as well as hexadecimal or binary parts.
/// </summary>
public class MultiFormatString
public class MultiFormatString: ObservableCollection<IFormattedCharacter>
{
/// <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>
public List<Tuple<FormatType, string>> FormatValuePairs { get; private set; } = new();
/// <summary>
/// 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)
/// <param name="index">index where to add item, usually items.Count when using Add method.
/// <param name="item">item to insert</param>
protected override void InsertItem(int index, IFormattedCharacter item)
{
// check if value is valid, if yes add to FormatValuePairs
if (ValidateValue(format, value))
if(item is FormattedCharacter formattedChar)
{
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>
/// 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)
protected override void RemoveItem(int index)
{
// guard amount <= 0
if (amount <= 0) throw new ArgumentException($"{nameof(amount)} cannot be <= 0");
// if formatted character => just remove
base.RemoveItem(index);
// limit amount to maximum
int listCount = this.FormatValuePairs.Count;
if (amount > listCount)
// if previous item is a spacing character => remove it too
if(this.ElementAt(index-1) is SpacingCharacter)
{
amount = listCount;
base.RemoveAt(index - 1);
}
// remove range
this.FormatValuePairs.RemoveRange( (listCount - amount) , amount);
}
/// <summary>
@ -59,15 +60,15 @@ public class MultiFormatString
// Internal function to check and finalize a Hexadecimal Conversion
// parameter == null will force ending
void finalizeHexConversion(Tuple<FormatType, string>? formatValuePair)
void finalizeHexConversion(FormattedCharacter? formattedChar)
{
// hex conversion ongoing?
if (String.IsNullOrEmpty(hexConversionCharacters) == false)
{
// ending conversion or reached limit of characters?
if (formatValuePair == null ||
formatValuePair.Item1 != FormatType.Hexadecimal ||
hexConversionCharacters.Count() == 4)
if (formattedChar == null ||
formattedChar.Item1 != FormatType.Hexadecimal ||
hexConversionCharacters.Length == 4)
{
// finalize conversion
stringBuilder.Append(FromHexString(hexConversionCharacters));
@ -79,15 +80,15 @@ public class MultiFormatString
// Internal function to check and finalize a Binary Conversion
// parameter == null will force ending
void finalizeBinaryConversion(Tuple<FormatType, string>? formatValuePair)
void finalizeBinaryConversion(FormattedCharacter? formattedChar)
{
// binary conversion ongoing?
if (String.IsNullOrEmpty(binaryConversionCharacters) == false)
{
// ending conversion or reached limit of characters?
if (formatValuePair == null ||
formatValuePair.Item1 != FormatType.Binary ||
binaryConversionCharacters.Count() == 16)
if (formattedChar == null ||
formattedChar.Item1 != FormatType.Binary ||
binaryConversionCharacters.Length == 16)
{
// finalize conversion
stringBuilder.Append(FromBinaryString(binaryConversionCharacters));
@ -98,28 +99,31 @@ public class MultiFormatString
}
// 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
finalizeHexConversion(formatValuePair);
finalizeBinaryConversion(formatValuePair);
finalizeHexConversion(formattedCharacter);
finalizeBinaryConversion(formattedCharacter);
switch (formatValuePair.Item1) // switch by format
switch (formattedCharacter.Item1) // switch by format
{
case FormatType.Character:
stringBuilder.Append(formatValuePair.Item2);
stringBuilder.Append(formattedCharacter.Item2);
break;
case FormatType.Hexadecimal:
hexConversionCharacters += formatValuePair.Item2;
hexConversionCharacters += formattedCharacter.Item2;
break;
case FormatType.Binary:
binaryConversionCharacters += formatValuePair.Item2;
binaryConversionCharacters += formattedCharacter.Item2;
break;
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)
{
// invalid if more than one character
if(value.Count() > 1)
if(value.Length > 1)
{ return false; }
// 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
// test with: returns: "Hello world" for "48656C6C6F20776F726C64"
// 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>
</PropertyGroup>
<ItemGroup>
<Folder Include="Model\" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.1.0" />
</ItemGroup>

@ -45,6 +45,6 @@ public partial class SendReceiveViewModel : TerminalViewModel
this.SendToCommunicationProtocol(this.SendableData.ToString());
// clear textbox
this.SendableData = new MultiFormatString();
this.SendableData.Clear();
}
}

@ -2,8 +2,8 @@
using MultiTerm.Core.Types;
using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.ComponentModel;
using System.Diagnostics;
using System.Linq;
using System.Windows;
using System.Windows.Controls;
@ -33,8 +33,8 @@ public class MultiFormatTextBox : Control
#endregion
#region private content
private const string comboBoxTemplateKey = "comboBox";
private const string richTextBoxTemplateKey = "richTextBox";
private const string comboBoxTemplateKey = "PART_ComboBox";
private const string richTextBoxTemplateKey = "PART_RichTextBox";
private ComboBox? comboBox;
private RichTextBox? richTextBox;
@ -93,8 +93,10 @@ public class MultiFormatTextBox : Control
{
this.richTextBox = richTextBox;
this.richTextBox.AcceptsReturn = false;
this.richTextBox.KeyDown += RichTextBox_KeyDown;
this.richTextBox.TextChanged += RichTextBox_TextChanged;
this.richTextBox.AcceptsTab = false;
this.richTextBox.PreviewKeyDown += RichTextBox_KeyDown;
this.richTextBox.PreviewTextInput += RichTextBox_PreviewTextInput;
this.richTextBox.SelectionChanged += RichTextBox_SelectionChanged;
}
else
{
@ -108,13 +110,134 @@ public class MultiFormatTextBox : Control
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)
// register to collection changed event
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)
{
@ -131,155 +254,101 @@ public class MultiFormatTextBox : Control
// 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)
if (e.Key == Key.Enter)
{
// TODO Raise EnterPressedEvent Here
e.Handled = true;
}
// ignore enter
if(e.Key == Key.Enter)
else if(e.Key == Key.Space)
{
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
// add space to MultiFormatString (will only work if currently character type is selected)
InsertCharacterAtCaretPositionIntoCurrentMultiFormatString(" ");
}
// back one
else if (e.Key == Key.Back)
{
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;
var cursorPos = GetRtbCaretPosition();
// if something was added => change background color
if (change.AddedLength > 0)
// remove previous element if it exists
try
{
if (this.CurrentMultiFormatString.ElementAt(cursorPos - 1) != null)
{
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;
this.CurrentMultiFormatString.RemoveAt(cursorPos - 1);
}
}
catch { }
// 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);
e.Handled = true;
}
// delete next (selection is not allowed)
else if (e.Key == Key.Delete)
{
var cursorPos = GetRtbCaretPosition();
// reenable event handler
this.richTextBox.TextChanged += RichTextBox_TextChanged;
// remove next element if it exists
try
{
if (this.CurrentMultiFormatString.ElementAt(cursorPos) != null)
{
this.CurrentMultiFormatString.RemoveAt(cursorPos);
}
}
catch { }
// update CurrentMultiFormatString
this.CurrentMultiFormatString = ConvertRtbContentToMultiFormatString(this.richTextBox!);
e.Handled = true;
}
}
private static void RtbChangeTextBackground(RichTextBox rtb, TextPointer start, TextPointer end, Brush color)
private int GetRtbCaretPosition()
{
// 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);
// magic dividor of 3.... nobody knows why...
return this.richTextBox!.Document.ContentStart.GetOffsetToPosition(this.richTextBox.CaretPosition) / 3;
}
// deselect everything (set to end)
rtb.Selection.Select(end, end);
private void SetRtbCaretPosition(int index)
{
// magic multiplicator of 3.... nobody knows why...
this.richTextBox!.CaretPosition = this.richTextBox.Document.ContentStart.GetPositionAtOffset(index*3);
}
private static MultiFormatString ConvertRtbContentToMultiFormatString(RichTextBox rtb)
private void RichTextBox_PreviewTextInput(object sender, TextCompositionEventArgs e)
{
MultiFormatString multiFormatString = new();
// store current caret position
int offsetToCaretPosition = rtb.Document.ContentStart.GetOffsetToPosition(rtb.CaretPosition);
// text is never directly inserted => therefore always handled
e.Handled = true;
// get number of symbols
var textLength = rtb.Document.ContentStart.GetOffsetToPosition(rtb.Document.ContentEnd);
// get full content as TextSelection object
var textRange = rtb.Selection;
// if more than one char inserted => illegal => cancel
if(e.Text.Length > 1) { return; }
// 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));
this.InsertCharacterAtCaretPositionIntoCurrentMultiFormatString(e.Text);
}
//skip empty textRange
if (textRange.IsEmpty)
{ continue; }
private void InsertCharacterAtCaretPositionIntoCurrentMultiFormatString(string text)
{
var caretPosition = this.GetRtbCaretPosition();
// extract background brush
var brush = (Brush)textRange.GetPropertyValue(TextElement.BackgroundProperty);
// add char to MultiFormatString
FormattedCharacter formattedCharacter = new(this.currentlySelectedFormat!.AssociatedFormatType, text);
// ignore this symbol, since its a separator
if (brush == defaultBackgroundBrush)
{ continue; }
this.CurrentMultiFormatString.Insert(caretPosition, formattedCharacter);
//this.CurrentMultiFormatString.Add(formattedCharacter);
}
// 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
}
}
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);
}
// deselect everything (set to end)
textRange.Select(
rtb.Document.ContentStart.GetPositionAtOffset(offsetToCaretPosition),
rtb.Document.ContentStart.GetPositionAtOffset(offsetToCaretPosition));
return multiFormatString;
}
}

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

Loading…
Cancel
Save