@ -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 = "c omboBox" ;
private const string richTextBoxTemplateKey = "r ichTextBox" ;
private const string comboBoxTemplateKey = "PART_C omboBox" ;
private const string richTextBoxTemplateKey = "PART_R ichTextBox" ;
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 ;
}
}