You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
314 lines
12 KiB
314 lines
12 KiB
using MultiTerm.Core.Model;
|
|
using MultiTerm.Core.Types;
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.ComponentModel;
|
|
using System.Diagnostics;
|
|
using System.Linq;
|
|
using System.Runtime.CompilerServices;
|
|
using System.Windows;
|
|
using System.Windows.Controls;
|
|
using System.Windows.Documents;
|
|
using System.Windows.Input;
|
|
using System.Windows.Media;
|
|
|
|
namespace MultiTerm.Wpf.CustomControl;
|
|
|
|
/// <summary>
|
|
/// Follow steps 1a or 1b and then 2 to use this custom control in a XAML file.
|
|
///
|
|
/// Step 1a) Using this custom control in a XAML file that exists in the current project.
|
|
/// Add this XmlNamespace attribute to the root element of the markup file where it is
|
|
/// to be used:
|
|
///
|
|
/// xmlns:MyNamespace="clr-namespace:MultiTerm.Wpf.CustomControl"
|
|
///
|
|
///
|
|
/// Step 1b) Using this custom control in a XAML file that exists in a different project.
|
|
/// Add this XmlNamespace attribute to the root element of the markup file where it is
|
|
/// to be used:
|
|
///
|
|
/// xmlns:MyNamespace="clr-namespace:MultiTerm.Wpf.CustomControl.MultiFormatTextBox;assembly=MultiTerm.Wpf.CustomControl.MultiFormatTextBox"
|
|
///
|
|
/// You will also need to add a project reference from the project where the XAML file lives
|
|
/// to this project and Rebuild to avoid compilation errors:
|
|
///
|
|
/// Right click on the target project in the Solution Explorer and
|
|
/// "Add Reference"->"Projects"->[Browse to and select this project]
|
|
///
|
|
///
|
|
/// Step 2)
|
|
/// Go ahead and use your control in the XAML file.
|
|
///
|
|
/// <MyNamespace:MultiFormatTextBox/>
|
|
///
|
|
/// </summary>
|
|
|
|
|
|
public class MultiFormatTextBox : Control
|
|
{
|
|
#region static content
|
|
private static readonly Brush defaultBackgroundBrush = Brushes.White;
|
|
private static readonly List<Format> formats = new()
|
|
{
|
|
// character input, accepts all keys
|
|
new Format("CHAR", "MultiFormatTextBox.CHAR.Background", Core.Types.FormatType.Character,
|
|
delegate(Key k) { return true; }),
|
|
// hex input, ignores all keys that are not inbetween 0 and F
|
|
new Format("HEX", "MultiFormatTextBox.HEX.Background", Core.Types.FormatType.Hexadecimal,
|
|
delegate(Key k) { return (k >= Key.D0 && k <= Key.F); }),
|
|
// binary input, ignores all keys except 0 and 1
|
|
new Format("BIN", "MultiFormatTextBox.BIN.Background", Core.Types.FormatType.Binary,
|
|
delegate(Key k) { return (k == Key.D0 || k == Key.D1); })
|
|
};
|
|
|
|
#endregion
|
|
|
|
#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 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()
|
|
{
|
|
DefaultStyleKeyProperty.OverrideMetadata(typeof(MultiFormatTextBox), new FrameworkPropertyMetadata(typeof(MultiFormatTextBox)));
|
|
}
|
|
|
|
public override void OnApplyTemplate()
|
|
{
|
|
base.OnApplyTemplate();
|
|
|
|
// initialize format background brushes
|
|
Format.UpdateBackgroundBrushesFromResources(this, formats);
|
|
// set initially selected format
|
|
this.currentlySelectedFormat = formats.First();
|
|
|
|
|
|
// get comboBox from template
|
|
if (GetTemplateChild(comboBoxTemplateKey) is ComboBox comboBox)
|
|
{
|
|
this.comboBox = comboBox;
|
|
this.comboBox.ItemsSource = Format.GetListOfNames(formats);
|
|
this.comboBox.SelectedItem = currentlySelectedFormat.Name;
|
|
this.comboBox.SelectionChanged += ComboBox_SelectionChanged;
|
|
}
|
|
else
|
|
{
|
|
throw new Exception($"Implementation fault, {comboBoxTemplateKey} not found in template.");
|
|
}
|
|
|
|
// get richTextBox from template
|
|
if (GetTemplateChild(richTextBoxTemplateKey) is RichTextBox richTextBox)
|
|
{
|
|
this.richTextBox = richTextBox;
|
|
this.richTextBox.AcceptsReturn = false;
|
|
this.richTextBox.KeyDown += RichTextBox_KeyDown;
|
|
this.richTextBox.TextChanged += RichTextBox_TextChanged;
|
|
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)
|
|
{
|
|
lock (lockObj) // lock so no richtextbox textchanged event can happen
|
|
{
|
|
// match all formats with the name selected in the combobox
|
|
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");
|
|
}
|
|
// 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)
|
|
{
|
|
e.Handled = true;
|
|
}
|
|
|
|
// ignore enter
|
|
if(e.Key == Key.Enter)
|
|
{
|
|
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
|
|
{
|
|
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)
|
|
{
|
|
// 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);
|
|
|
|
// deselect everything (set to end)
|
|
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;
|
|
}
|
|
}
|
|
|