using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Media;
namespace HandyControl.Controls;
public class HighlightTextBlock : TextBlock
{
public static readonly DependencyProperty SourceTextProperty =
DependencyProperty.Register(nameof(SourceText), typeof(string), typeof(HighlightTextBlock),
new PropertyMetadata(null, OnSourceTextChanged));
public static readonly DependencyProperty QueriesTextProperty =
DependencyProperty.Register(nameof(QueriesText), typeof(string), typeof(HighlightTextBlock),
new PropertyMetadata(null, OnQueriesTextChanged));
public static readonly DependencyProperty HighlightBrushProperty =
DependencyProperty.Register(nameof(HighlightBrush), typeof(Brush), typeof(HighlightTextBlock));
public static readonly DependencyProperty HighlightTextBrushProperty =
DependencyProperty.Register(nameof(HighlightTextBrush), typeof(Brush), typeof(HighlightTextBlock));
private static void OnSourceTextChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) =>
((HighlightTextBlock) d).RefreshInlines();
private static void OnQueriesTextChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) =>
((HighlightTextBlock) d).RefreshInlines();
///
/// Replace the original property Text for binding text.
///
///
/// Don't use the property!
/// Because the has some unique behaviors
/// that is not needed at at ,
/// which will cause some unexpected issue.
///
public string SourceText
{
get => (string) GetValue(SourceTextProperty);
set => SetValue(SourceTextProperty, value);
}
///
/// Gets or sets the text that need to be highlighted.
/// It can be an array of text separated by spaces.
///
public string QueriesText
{
get => (string) GetValue(QueriesTextProperty);
set => SetValue(QueriesTextProperty, value);
}
///
/// Gets or sets the of the background of the highlight text.
///
public Brush HighlightBrush
{
get => (Brush) GetValue(HighlightBrushProperty);
set => SetValue(HighlightBrushProperty, value);
}
public Brush HighlightTextBrush
{
get => (Brush) GetValue(HighlightTextBrushProperty);
set => SetValue(HighlightTextBrushProperty, value);
}
private void RefreshInlines()
{
Inlines.Clear();
if (string.IsNullOrEmpty(SourceText)) return;
if (string.IsNullOrEmpty(QueriesText))
{
Inlines.Add(SourceText);
return;
}
var sourceText = SourceText;
var queries = QueriesText.Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries);
var intervals = from query in queries.Distinct()
from interval in GetQueryIntervals(sourceText, query)
select interval;
var mergedIntervals = MergeIntervals(intervals.ToList());
var fragments = SplitTextByOrderedDisjointIntervals(sourceText, mergedIntervals);
Inlines.AddRange(GenerateRunElement(fragments));
}
private IEnumerable GenerateRunElement(IEnumerable fragments)
{
return from item in fragments
select item.IsQuery
? GetHighlightRun(item.Text)
: new Run(item.Text);
}
private Run GetHighlightRun(string highlightText)
{
var run = new Run(highlightText);
run.SetBinding(TextElement.BackgroundProperty, new Binding(nameof(HighlightBrush)) { Source = this });
run.SetBinding(TextElement.ForegroundProperty, new Binding(nameof(HighlightTextBrush)) { Source = this });
return run;
}
private static IEnumerable SplitTextByOrderedDisjointIntervals(string sourceText, List mergedIntervals)
{
if (string.IsNullOrEmpty(sourceText)) yield break;
if (!mergedIntervals?.Any() ?? true)
{
yield return new Fragment { Text = sourceText, IsQuery = false };
yield break;
}
var range0 = mergedIntervals.First();
int start0 = range0.Start;
int end0 = range0.End;
if (start0 > 0) yield return new Fragment { Text = sourceText.Substring(0, start0), IsQuery = false };
yield return new Fragment { Text = sourceText.Substring(start0, end0 - start0), IsQuery = true };
int previousEnd = end0;
foreach (var range in mergedIntervals.Skip(1))
{
int start = range.Start;
int end = range.End;
yield return new Fragment { Text = sourceText.Substring(previousEnd, start - previousEnd), IsQuery = false };
yield return new Fragment { Text = sourceText.Substring(start, end - start), IsQuery = true };
previousEnd = end;
}
if (previousEnd < sourceText.Length)
yield return new Fragment { Text = sourceText.Substring(previousEnd), IsQuery = false };
}
private static List MergeIntervals(List intervals)
{
if (!intervals?.Any() ?? true) return new List();
intervals.Sort((x, y) => x.Start != y.Start ? x.Start - y.Start : x.End - y.End);
var pointer = intervals[0];
int startPointer = pointer.Start;
int endPointer = pointer.End;
var result = new List();
foreach (var range in intervals.Skip(1))
{
int start = range.Start;
int end = range.End;
if (start <= endPointer)
{
if (endPointer < end)
{
endPointer = end;
}
}
else
{
result.Add(new Range { Start = startPointer, End = endPointer });
startPointer = start;
endPointer = end;
}
}
result.Add(new Range { Start = startPointer, End = endPointer });
return result;
}
private static IEnumerable GetQueryIntervals(string sourceText, string query)
{
if (string.IsNullOrEmpty(sourceText) || string.IsNullOrEmpty(query)) yield break;
int nextStartIndex = 0;
while (nextStartIndex < sourceText.Length)
{
int index = sourceText.IndexOf(query, nextStartIndex, StringComparison.CurrentCultureIgnoreCase);
if (index == -1) yield break;
nextStartIndex = index + query.Length;
yield return new Range { Start = index, End = nextStartIndex };
}
}
private struct Fragment
{
public string Text { get; set; }
public bool IsQuery { get; set; }
}
private struct Range
{
public int Start { get; set; }
public int End { get; set; }
}
}