@ -1,6 +1,9 @@
using System ;
using MultiTerm.Core.Model ;
using MultiTerm.Core.Types ;
using System ;
using System.Collections.Generic ;
using System.Collections.Generic ;
using System.Drawing ;
using System.ComponentModel ;
using System.Diagnostics ;
using System.Linq ;
using System.Linq ;
using System.Runtime.CompilerServices ;
using System.Runtime.CompilerServices ;
using System.Windows ;
using System.Windows ;
@ -40,69 +43,55 @@ namespace MultiTerm.Wpf.CustomControl;
/// <MyNamespace:MultiFormatTextBox/>
/// <MyNamespace:MultiFormatTextBox/>
///
///
/// </summary>
/// </summary>
internal class Format
{
private readonly string? backgroundColorResourceName ;
public string Name { get ; set ; }
public Brush BackgroundBrush { get ; set ; }
public Predicate < Key > IsKeyValid { get ; set ; }
public Format ( string name , Brush backgroundBrush , Predicate < Key > keyValidator )
{
this . Name = name ;
this . BackgroundBrush = backgroundBrush ;
this . IsKeyValid = keyValidator ;
}
public Format ( string name , string backgroundColorResourceName , Predicate < Key > keyValidator )
{
this . Name = name ;
this . backgroundColorResourceName = backgroundColorResourceName ;
this . BackgroundBrush = Brushes . White ; // set background brush to white
this . IsKeyValid = keyValidator ;
}
public static List < string > GetListOfNames ( IEnumerable < Format > formats )
{
return formats . Select ( item = > item . Name ) . ToList ( ) ;
}
public static void UpdateBackgroundBrushesFromResources ( FrameworkElement fwElement , IEnumerable < Format > 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
public class MultiFormatTextBox : Control
{
{
#region static content
private static readonly Brush defaultBackgroundBrush = Brushes . White ;
private static readonly Brush defaultBackgroundBrush = Brushes . White ;
private static readonly List < Format > formats = new ( )
private static readonly List < Format > formats = new ( )
{
{
// character input, accepts all keys except space
// character input, accepts all keys
//new Format("CHAR", Brushes.LightSkyBlue, delegate(Key k) { return (k != Key.Space); }) ,
new Format ( "CHAR" , "MultiFormatTextBox.CHAR.Background" , Core . Types . FormatType . Character ,
new Format ( "CHAR" , "MultiFormatTextBox.CHAR.Background" , delegate ( Key k ) { return ( k ! = Key . Space ) ; } ) ,
delegate ( Key k ) { return true ; } ) ,
// hex input, ignores all keys that are not inbetween 0 and F
// 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" , Core . Types . FormatType . Hexadecimal ,
new Format ( "HEX" , "MultiFormatTextBox.HEX.Background" , delegate ( Key k ) { return ( k > = Key . D0 & & k < = Key . F ) ; } ) ,
delegate ( Key k ) { return ( k > = Key . D0 & & k < = Key . F ) ; } ) ,
// binary input, ignores all keys except 0 and 1
// 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" , Core . Types . FormatType . Binary ,
new Format ( "BIN" , "MultiFormatTextBox.BIN.Background" , delegate ( Key k ) { return ( k = = Key . D0 | | k = = Key . D1 ) ; } )
delegate ( Key k ) { return ( k = = Key . D0 | | k = = Key . D1 ) ; } )
} ;
} ;
private ComboBox comboBox ;
# endregion
private RichTextBox richTextBox ;
private Format currentlySelectedFormat ;
#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 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 ) ) ;
/// <summary>
/// .NET Property for CurrentMultiFormatString.
/// </summary>
[Bindable(true)]
public MultiFormatString CurrentMultiFormatString
{
get { return ( MultiFormatString ) GetValue ( CurrentMultiFormatStringProperty ) ; }
set { SetValue ( CurrentMultiFormatStringProperty , value ) ; }
}
# endregion
static MultiFormatTextBox ( )
static MultiFormatTextBox ( )
{
{
@ -116,125 +105,210 @@ public class MultiFormatTextBox : Control
// initialize format background brushes
// initialize format background brushes
Format . UpdateBackgroundBrushesFromResources ( this , formats ) ;
Format . UpdateBackgroundBrushesFromResources ( this , formats ) ;
// set initially selected format
// set initially selected format
this . currentlySelectedFormat = formats ! . First ( ) ;
this . currentlySelectedFormat = formats . First ( ) ;
// get comboBox from template
// get comboBox from template
var comboBox = GetTemplateChild ( "comboBox" ) as ComboBox ;
if ( GetTemplateChild ( comboBoxTemplateKey ) is ComboBox comboBox )
if ( comboBox ! = null )
{
{
this . comboBox = comboBox ;
this . comboBox = comboBox ;
this . comboBox . ItemsSource = Format . GetListOfNames ( formats ) ;
this . comboBox . ItemsSource = Format . GetListOfNames ( formats ) ;
this . comboBox . SelectedItem = currentlySelectedFormat . Name ;
this . comboBox . SelectedItem = currentlySelectedFormat . Name ;
this . comboBox . SelectionChanged + = ComboBox_SelectionChanged ;
this . comboBox . SelectionChanged + = ComboBox_SelectionChanged ;
}
}
else
{
throw new Exception ( $"Implementation fault, {comboBoxTemplateKey} not found in template." ) ;
}
// get richTextBox from template
// get richTextBox from template
var richTextBox = GetTemplateChild ( "richTextBox" ) as RichTextBox ;
if ( GetTemplateChild ( richTextBoxTemplateKey ) is RichTextBox richTextBox )
if ( richTextBox ! = null )
{
{
this . richTextBox = richTextBox ;
this . richTextBox = richTextBox ;
this . richTextBox . AcceptsReturn = false ;
this . richTextBox . KeyDown + = RichTextBox_KeyDown ;
this . richTextBox . KeyDown + = RichTextBox_KeyDown ;
this . richTextBox . TextChanged + = RichTextBox_TextChanged ;
this . richTextBox . TextChanged + = RichTextBox_TextChanged ;
this . offsetContentStartToFormatStart = 0 ;
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 )
private void ComboBox_SelectionChanged ( object sender , SelectionChangedEventArgs e )
{
{
// match all formats with the name selected in the combobox
lock ( lockObj ) // lock so no richtextbox textchanged event can happen
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" ) ;
// match all formats with the name selected in the combobox
}
var matchingFormats = formats . Where ( format = > format . Name = = ( string ) this . comboBox ! . SelectedItem ) ;
// set currently selected format, reset offset counter
this . currentlySelectedFormat = matchingFormats . First ( ) ;
// insert space to separate formats
// check if exactly one format was matched
this . InsertSeparation ( ) ;
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
// insert space to separate formats
this . StoreCaretPosition ( ) ;
this . InsertSepara tion( ) ;
// focus textbox
// focus textbox
this . richTextBox . Focus ( ) ;
this . richTextBox ! . Focus ( ) ;
}
}
}
private void InsertSeparation ( )
private void InsertSeparation ( )
{
{
// disable event handler
// disable event handler
this . richTextBox . TextChanged - = RichTextBox_TextChanged ;
this . richTextBox ! . TextChanged - = RichTextBox_TextChanged ;
// store caret position before
// insert separator, manually updating caret position because wpf somehow does not do it....
this . StoreCaretPosition ( ) ;
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)}" ) ;
// insert
this . richTextBox . AppendText ( " " ) ;
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 ,
RtbChangeTextBackground ( this . richTextBox ,
start : this . richTextBox . Document . ContentStart . GetPositionAtOffset ( - this . offsetContentStartToFormatStart ) ,
start : this . richTextBox ! . Document . ContentStart . GetPositionAtOffset ( caretIndexBefore ) ,
end : this . richTextBox . Document . ContentEnd ,
end : this . richTextBox . CaretPosition ,
color : defaultBackgroundBrush ) ;
color : defaultBackgroundBrush ) ;
// store new caret position after
this . StoreCaretPosition ( ) ;
// reenable
// reenable
this . richTextBox . TextChanged + = RichTextBox_TextChanged ;
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 )
private void RichTextBox_KeyDown ( object sender , KeyEventArgs e )
{
{
// guard combobox null
// guard combobox null
if ( this . comboBox = = null ) throw new Exception ( $"{nameof(this.comboBox)} cannot be 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 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 ;
e . Handled = true ;
}
}
// ignore enter
if ( e . Key = = Key . Enter )
{
e . Handled = true ;
}
}
}
private void RichTextBox_TextChanged ( object sender , TextChangedEventArgs e )
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
lock ( lockObj ) // lock so no concurrent combobox change can happen
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 )
{
{
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 )
private static void RtbChangeTextBackground ( RichTextBox rtb , TextPointer start , TextPointer end , Brush color )
{
{
// Get text selection
// print offsets to start and end position
TextSelection textRange = rtb . Selection ;
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 ) ;
textRange . Select ( start , end ) ;
// Apply property to the selection:
// Apply property to the selection:
textRange . ApplyPropertyValue ( TextElement . BackgroundProperty , color ) ;
textRange . ApplyPropertyValue ( TextElement . BackgroundProperty , color ) ;
// deselect
// deselect everything (set to end)
rtb . Selection . Select ( rtb . Document . ContentEnd , rtb . Document . ContentEnd ) ;
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 ;
}
}
}