fixed many bugs in MultiFormatString,

cleaned up IFormattedCharacter and subclasses,
master
Jonas Arnold 3 years ago
parent a54b02150b
commit eac55765c2
  1. 4
      MultiTerm.Core/Model/FormattedCharacter.cs
  2. 5
      MultiTerm.Core/Model/IFormattedCharacter.cs
  3. 299
      MultiTerm.Core/Model/MultiFormatString.cs
  4. 2
      MultiTerm.Core/Model/SpacingCharacter.cs
  5. 14
      MultiTerm.Wpf.CustomControl/MultiFormatTextBox/MultiFormatTextBox.cs

@ -4,6 +4,10 @@ namespace MultiTerm.Core.Model;
public class FormattedCharacter : Tuple<FormatType, string>, IFormattedCharacter public class FormattedCharacter : Tuple<FormatType, string>, IFormattedCharacter
{ {
public FormatType Format => this.Item1;
public string Character => this.Item2;
public FormattedCharacter(FormatType item1, string item2) : base(item1, item2) public FormattedCharacter(FormatType item1, string item2) : base(item1, item2)
{ {
} }

@ -2,5 +2,8 @@
public interface IFormattedCharacter public interface IFormattedCharacter
{ {
/// <summary>
/// The instance of <see cref="IFormattedCharacter"/> represents this character.
/// </summary>
string Character { get; }
} }

@ -9,6 +9,10 @@ namespace MultiTerm.Core.Model;
/// </summary> /// </summary>
public class MultiFormatString: ObservableCollection<IFormattedCharacter> public class MultiFormatString: ObservableCollection<IFormattedCharacter>
{ {
private const int charsPerHexBundle = 2;
private const int charsPerBinBundle = 8;
#region Insertion / Removal of Characters
/// <summary> /// <summary>
/// Inserts item at given index after checking if its valid using <see cref="ValidateValue(FormatType, string)"/>. /// Inserts item at given index after checking if its valid using <see cref="ValidateValue(FormatType, string)"/>.
/// </summary> /// </summary>
@ -16,25 +20,35 @@ public class MultiFormatString: ObservableCollection<IFormattedCharacter>
/// <param name="item">item to insert</param> /// <param name="item">item to insert</param>
protected override void InsertItem(int index, IFormattedCharacter item) protected override void InsertItem(int index, IFormattedCharacter item)
{ {
int offset = 0;
bool insertSpacingAfter = false;
if(item is FormattedCharacter formattedChar) if(item is FormattedCharacter formattedChar)
{ {
//check if valid, only insert then //check if valid, only insert then
if(ValidateValue(formattedChar.Item1, formattedChar.Item2) == false) if(ValidateValue(formattedChar.Format, formattedChar.Character) == false)
{ {
return; return;
} }
// check if spacing is required and insert // check if spacing is required and insert
if(IsSpacingCharacterRequiredBeforeNewCharacter(this, index, formattedChar)) if(IsSpacingCharacterRequiredBeforeNewCharacter(this, index + offset, formattedChar))
{ {
base.InsertItem(index, new SpacingCharacter()); base.InsertItem(index, new SpacingCharacter());
base.InsertItem(index + 1, item); offset++; // insert item with offset of 1
}
return; if(IsSpacingCharacterRequiredAfterNewCharacter(this, index + offset, formattedChar))
{
insertSpacingAfter = true;
} }
} }
base.InsertItem(index, item); base.InsertItem(index + offset, item);
if (insertSpacingAfter)
{
base.InsertItem(index + offset + 1, new SpacingCharacter());
}
} }
protected override void RemoveItem(int index) protected override void RemoveItem(int index)
@ -49,6 +63,141 @@ public class MultiFormatString: ObservableCollection<IFormattedCharacter>
} }
} }
private static bool IsSpacingCharacterRequiredBeforeNewCharacter(IEnumerable<IFormattedCharacter> collection, int indexOfInsertion, IFormattedCharacter newCharacter)
{
bool spacingCharRequired = false;
// 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 newFormattedCharacter)
{ throw new NotImplementedException($"'{nameof(IsSpacingCharacterRequiredBeforeNewCharacter)}()' does not implement handling for type {newCharacter.GetType()}"); }
// switch spacing required by type of format
spacingCharRequired = newFormattedCharacter.Format switch
{
FormatType.Character => CheckSpacingRequiredBecauseChangedFormat(collection, indexOfInsertion, newFormattedCharacter.Format),
FormatType.Hexadecimal => CheckSpacingRequiredBecauseChangedFormat(collection, indexOfInsertion, newFormattedCharacter.Format) ||
CheckSpacingRequiredBecauseBundleMaxCountReached(collection, indexOfInsertion, charsPerHexBundle, newFormattedCharacter.Format),
FormatType.Binary => CheckSpacingRequiredBecauseChangedFormat(collection, indexOfInsertion, newFormattedCharacter.Format) ||
CheckSpacingRequiredBecauseBundleMaxCountReached(collection, indexOfInsertion, charsPerBinBundle, newFormattedCharacter.Format),
_ => throw new NotImplementedException($"'{nameof(IsSpacingCharacterRequiredBeforeNewCharacter)}()' does not implement handling for format {newFormattedCharacter.Format}"),
};
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.Format != newCharFormat)
{
return true;
}
}
catch { break; } // end of collection => break while
}
}
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)
{
try
{
// 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.Format == newCharFormat);
// elements of same Format are more (impossible) or equal to amount required for bundle
if (sameFormatElements.Count() >= charsPerBundle)
{
return true;
}
}
catch { } // catch any exception, ignore it and return default
}
return false;
}
}
private static bool IsSpacingCharacterRequiredAfterNewCharacter(IEnumerable<IFormattedCharacter> collection, int indexOfInsertion, IFormattedCharacter newCharacter)
{
bool spacingCharRequired = false;
// 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 newFormattedCharacter)
{ throw new NotImplementedException($"'{nameof(IsSpacingCharacterRequiredAfterNewCharacter)}()' does not implement handling for type {newCharacter.GetType()}"); }
// if there is anything in the collection
if (collection.Any())
{
// check if next character in collection is a FormattedCharacter
try
{
var nextChar = collection.ElementAt(indexOfInsertion) as FormattedCharacter;
// format of next is not equal to new => spacer required
if (nextChar != null && nextChar.Format != newFormattedCharacter.Format)
{
return true;
}
}
catch { } // catch any exception and return default result
}
return spacingCharRequired;
}
#endregion
/// <summary>
/// Validates wether a certain <paramref name="value"/> is valid for the given <paramref name="format"/>.
/// </summary>
/// <returns>true if valid</returns>
public static bool ValidateValue(FormatType format, string value)
{
// invalid if more than one character
if(value.Length > 1)
{ return false; }
// extract character
var character = value.First();
switch (format)
{
case FormatType.Character:
// accept only ascii, not extended ascii or unicode
return char.IsAscii(character);
case FormatType.Hexadecimal:
return (character >= '0' && character <= '9' ||
character >= 'A' && character <= 'F' ||
character >= 'a' && character <= 'f');
case FormatType.Binary:
return (character == '0' || character == '1');
default:
throw new NotImplementedException($"'{nameof(ValidateValue)}()' does not implement validation for format {format}");
}
}
#region ToString
/// <summary> /// <summary>
/// Converts the <see cref="MultiFormatString"/> to a string that only contains Unicode (UTF-16) characters. /// Converts the <see cref="MultiFormatString"/> to a string that only contains Unicode (UTF-16) characters.
/// </summary> /// </summary>
@ -67,8 +216,8 @@ public class MultiFormatString: ObservableCollection<IFormattedCharacter>
{ {
// ending conversion or reached limit of characters? // ending conversion or reached limit of characters?
if (formattedChar == null || if (formattedChar == null ||
formattedChar.Item1 != FormatType.Hexadecimal || formattedChar.Format != FormatType.Hexadecimal ||
hexConversionCharacters.Length == 4) hexConversionCharacters.Length == charsPerHexBundle)
{ {
// finalize conversion // finalize conversion
stringBuilder.Append(FromHexString(hexConversionCharacters)); stringBuilder.Append(FromHexString(hexConversionCharacters));
@ -87,8 +236,8 @@ public class MultiFormatString: ObservableCollection<IFormattedCharacter>
{ {
// ending conversion or reached limit of characters? // ending conversion or reached limit of characters?
if (formattedChar == null || if (formattedChar == null ||
formattedChar.Item1 != FormatType.Binary || formattedChar.Format != FormatType.Binary ||
binaryConversionCharacters.Length == 16) binaryConversionCharacters.Length == charsPerBinBundle)
{ {
// finalize conversion // finalize conversion
stringBuilder.Append(FromBinaryString(binaryConversionCharacters)); stringBuilder.Append(FromBinaryString(binaryConversionCharacters));
@ -102,28 +251,28 @@ public class MultiFormatString: ObservableCollection<IFormattedCharacter>
foreach (var character in this.Items) foreach (var character in this.Items)
{ {
// can only handle FormattedCharacters // can only handle FormattedCharacters
if(character is not FormattedCharacter formattedCharacter) { continue; } if (character is not FormattedCharacter formattedCharacter) { continue; }
// finalize ongoing conversions if there are any // finalize ongoing conversions if there are any
finalizeHexConversion(formattedCharacter); finalizeHexConversion(formattedCharacter);
finalizeBinaryConversion(formattedCharacter); finalizeBinaryConversion(formattedCharacter);
switch (formattedCharacter.Item1) // switch by format switch (formattedCharacter.Format) // switch by format
{ {
case FormatType.Character: case FormatType.Character:
stringBuilder.Append(formattedCharacter.Item2); stringBuilder.Append(formattedCharacter.Character);
break; break;
case FormatType.Hexadecimal: case FormatType.Hexadecimal:
hexConversionCharacters += formattedCharacter.Item2; hexConversionCharacters += formattedCharacter.Character;
break; break;
case FormatType.Binary: case FormatType.Binary:
binaryConversionCharacters += formattedCharacter.Item2; binaryConversionCharacters += formattedCharacter.Character;
break; break;
default: default:
throw new NotImplementedException($"'{nameof(ToString)}()' does not implement conversion for format {formattedCharacter.Item1}"); throw new NotImplementedException($"'{nameof(ToString)}()' does not implement conversion for format {formattedCharacter.Format}");
} }
} }
@ -134,143 +283,33 @@ public class MultiFormatString: ObservableCollection<IFormattedCharacter>
return stringBuilder.ToString(); return stringBuilder.ToString();
} }
/// <summary>
/// Validates wether a certain <paramref name="value"/> is valid for the given <paramref name="format"/>.
/// </summary>
/// <returns>true if valid</returns>
public static bool ValidateValue(FormatType format, string value)
{
// invalid if more than one character
if(value.Length > 1)
{ return false; }
// extract character
var character = value.First();
switch (format)
{
case FormatType.Character:
return true;
case FormatType.Hexadecimal:
if (character >= '0' && character <= '9' ||
character >= 'A' && character <= 'F' ||
character >= 'a' && character <= 'f')
{ return true; }
else
{ return false; };
case FormatType.Binary:
if (character == '0' || character == '1')
{ return true; }
else
{ return false; };
default:
throw new NotImplementedException($"'{nameof(ValidateValue)}()' does not implement validation for format {format}");
}
}
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
private static string FromHexString(string hexString) private static string FromHexString(string hexString)
{ {
// Added PadLeft so strings with one character do not get ignored // Added PadLeft so strings with one character do not get ignored
string internalHexString = hexString.PadLeft(4, '0'); string internalHexString = hexString.PadLeft(charsPerHexBundle, '0');
var bytes = new byte[internalHexString.Length / 2]; var bytes = new byte[internalHexString.Length / 2];
for (var i = 0; i < bytes.Length; i++) for (var i = 0; i < bytes.Length; i++)
{ {
bytes[i] = Convert.ToByte(internalHexString.Substring(i * 2, 2), 16); bytes[i] = Convert.ToByte(internalHexString.Substring(i * 2, 2), 16);
} }
return Encoding.BigEndianUnicode.GetString(bytes); return Encoding.ASCII.GetString(bytes);
} }
private static string FromBinaryString(string binaryString) private static string FromBinaryString(string binaryString)
{ {
string internalBinaryString = binaryString.PadLeft(16, '0'); string internalBinaryString = binaryString.PadLeft(charsPerBinBundle, '0');
var bytes = new byte[internalBinaryString.Length / 8]; var bytes = new byte[internalBinaryString.Length / 8];
for (var i = 0; i < bytes.Length; i++) for (var i = 0; i < bytes.Length; i++)
{ {
bytes[i] = Convert.ToByte(internalBinaryString.Substring(i * 8, 8), 2); bytes[i] = Convert.ToByte(internalBinaryString.Substring(i * 8, 8), 2);
} }
return Encoding.BigEndianUnicode.GetString(bytes); return Encoding.ASCII.GetString(bytes);
} }
#endregion
} }

@ -2,5 +2,5 @@
public class SpacingCharacter : IFormattedCharacter public class SpacingCharacter : IFormattedCharacter
{ {
public string Character => " ";
} }

@ -165,13 +165,9 @@ public class MultiFormatTextBox : Control
} }
string toRemoveString = string.Empty; string toRemoveString = string.Empty;
if (items[0] is FormattedCharacter formattedCharacter) if (items[0] is IFormattedCharacter character)
{ {
toRemoveString = formattedCharacter.Item2; toRemoveString = character.Character;
}
else if(items[0] is SpacingCharacter spacingCharacter)
{
toRemoveString = " ";
} }
else else
{ {
@ -202,15 +198,15 @@ public class MultiFormatTextBox : Control
if (item is FormattedCharacter formattedCharacter) if (item is FormattedCharacter formattedCharacter)
{ {
// text as run with correct background brush // text as run with correct background brush
run = new Run(formattedCharacter.Item2) run = new Run(formattedCharacter.Character)
{ {
Background = this.currentlySelectedFormat!.BackgroundBrush Background = this.currentlySelectedFormat!.BackgroundBrush
}; };
} }
else if(item is SpacingCharacter) else if(item is SpacingCharacter spacingCharacter)
{ {
// text as run with default background brush // text as run with default background brush
run = new Run(" ") run = new Run(spacingCharacter.Character)
{ {
Background = defaultBackgroundBrush Background = defaultBackgroundBrush
}; };

Loading…
Cancel
Save