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. 297
      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 FormatType Format => this.Item1;
public string Character => this.Item2;
public FormattedCharacter(FormatType item1, string item2) : base(item1, item2)
{
}

@ -2,5 +2,8 @@
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>
public class MultiFormatString: ObservableCollection<IFormattedCharacter>
{
private const int charsPerHexBundle = 2;
private const int charsPerBinBundle = 8;
#region Insertion / Removal of Characters
/// <summary>
/// Inserts item at given index after checking if its valid using <see cref="ValidateValue(FormatType, string)"/>.
/// </summary>
@ -16,25 +20,35 @@ public class MultiFormatString: ObservableCollection<IFormattedCharacter>
/// <param name="item">item to insert</param>
protected override void InsertItem(int index, IFormattedCharacter item)
{
int offset = 0;
bool insertSpacingAfter = false;
if(item is FormattedCharacter formattedChar)
{
//check if valid, only insert then
if(ValidateValue(formattedChar.Item1, formattedChar.Item2) == false)
if(ValidateValue(formattedChar.Format, formattedChar.Character) == false)
{
return;
}
// 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 + 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)
@ -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>
/// Converts the <see cref="MultiFormatString"/> to a string that only contains Unicode (UTF-16) characters.
/// </summary>
@ -67,8 +216,8 @@ public class MultiFormatString: ObservableCollection<IFormattedCharacter>
{
// ending conversion or reached limit of characters?
if (formattedChar == null ||
formattedChar.Item1 != FormatType.Hexadecimal ||
hexConversionCharacters.Length == 4)
formattedChar.Format != FormatType.Hexadecimal ||
hexConversionCharacters.Length == charsPerHexBundle)
{
// finalize conversion
stringBuilder.Append(FromHexString(hexConversionCharacters));
@ -87,8 +236,8 @@ public class MultiFormatString: ObservableCollection<IFormattedCharacter>
{
// ending conversion or reached limit of characters?
if (formattedChar == null ||
formattedChar.Item1 != FormatType.Binary ||
binaryConversionCharacters.Length == 16)
formattedChar.Format != FormatType.Binary ||
binaryConversionCharacters.Length == charsPerBinBundle)
{
// finalize conversion
stringBuilder.Append(FromBinaryString(binaryConversionCharacters));
@ -108,22 +257,22 @@ public class MultiFormatString: ObservableCollection<IFormattedCharacter>
finalizeHexConversion(formattedCharacter);
finalizeBinaryConversion(formattedCharacter);
switch (formattedCharacter.Item1) // switch by format
switch (formattedCharacter.Format) // switch by format
{
case FormatType.Character:
stringBuilder.Append(formattedCharacter.Item2);
stringBuilder.Append(formattedCharacter.Character);
break;
case FormatType.Hexadecimal:
hexConversionCharacters += formattedCharacter.Item2;
hexConversionCharacters += formattedCharacter.Character;
break;
case FormatType.Binary:
binaryConversionCharacters += formattedCharacter.Item2;
binaryConversionCharacters += formattedCharacter.Character;
break;
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();
}
/// <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
// test with: returns: "Hello world" for "48656C6C6F20776F726C64"
// see https://www.fileformat.info/info/charset/UTF-16/list.htm
private static string FromHexString(string hexString)
{
// 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];
for (var i = 0; i < bytes.Length; i++)
{
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)
{
string internalBinaryString = binaryString.PadLeft(16, '0');
string internalBinaryString = binaryString.PadLeft(charsPerBinBundle, '0');
var bytes = new byte[internalBinaryString.Length / 8];
for (var i = 0; i < bytes.Length; i++)
{
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 string Character => " ";
}

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

Loading…
Cancel
Save