Merge pull request #983 from DingpingZhang/feature/highlight-text-block
feat(control): add the highlight-text-block control.
This commit is contained in:
5 files changed
+224
-2
No files matched your search
@@ -4,6 +4,9 @@ namespace HandyControlDemo.Data
|
|||||||
{
|
{
|
||||||
public class DemoItemModel : ObservableObject
|
public class DemoItemModel : ObservableObject
|
||||||
{
|
{
|
||||||
|
private bool _isVisible = true;
|
||||||
|
private string _queriesText = string.Empty;
|
||||||
|
|
||||||
public string Name { get; set; }
|
public string Name { get; set; }
|
||||||
|
|
||||||
public string GroupName { get; set; }
|
public string GroupName { get; set; }
|
||||||
@@ -14,7 +17,15 @@ namespace HandyControlDemo.Data
|
|||||||
|
|
||||||
public bool IsNew { get; set; }
|
public bool IsNew { get; set; }
|
||||||
|
|
||||||
private bool _isVisible = true;
|
public string QueriesText
|
||||||
|
{
|
||||||
|
get => _queriesText;
|
||||||
|
#if NET40
|
||||||
|
set => Set(nameof(QueriesText), ref _queriesText, value);
|
||||||
|
#else
|
||||||
|
set => Set(ref _queriesText, value);
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
public bool IsVisible
|
public bool IsVisible
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
xmlns:langs="clr-namespace:HandyControlDemo.Properties.Langs"
|
xmlns:langs="clr-namespace:HandyControlDemo.Properties.Langs"
|
||||||
xmlns:ex="clr-namespace:HandyControlDemo.Tools.Extension"
|
xmlns:ex="clr-namespace:HandyControlDemo.Tools.Extension"
|
||||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
|
xmlns:data="clr-namespace:HandyControlDemo.Data"
|
||||||
Margin="16,16,0,16"
|
Margin="16,16,0,16"
|
||||||
CornerRadius="10"
|
CornerRadius="10"
|
||||||
Effect="{StaticResource EffectShadow4}">
|
Effect="{StaticResource EffectShadow4}">
|
||||||
@@ -51,7 +52,7 @@
|
|||||||
<DataTemplate>
|
<DataTemplate>
|
||||||
<StackPanel Orientation="Horizontal">
|
<StackPanel Orientation="Horizontal">
|
||||||
<Image Width="16" Height="16" Source="{Binding ImageName}"/>
|
<Image Width="16" Height="16" Source="{Binding ImageName}"/>
|
||||||
<TextBlock Margin="6,0,0,0" Text="{ex:Lang Key={Binding Name}}"/>
|
<hc:HighlightTextBlock Margin="6,0,0,0" SourceText="{ex:Lang Key={Binding Name}}" QueriesText="{Binding QueriesText}" HighlightBrush="#f06632" />
|
||||||
<Path Visibility="{Binding IsNew,Converter={StaticResource Boolean2VisibilityConverter}}" VerticalAlignment="Center" Data="{StaticResource NewGeometry}" Stretch="Uniform" Margin="10,0,0,0" Width="30" Height="14" Fill="{DynamicResource DangerBrush}"/>
|
<Path Visibility="{Binding IsNew,Converter={StaticResource Boolean2VisibilityConverter}}" VerticalAlignment="Center" Data="{StaticResource NewGeometry}" Stretch="Uniform" Margin="10,0,0,0" Width="30" Height="14" Fill="{DynamicResource DangerBrush}"/>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</DataTemplate>
|
</DataTemplate>
|
||||||
|
|||||||
@@ -68,6 +68,7 @@ namespace HandyControlDemo.UserControl
|
|||||||
foreach (var item in ViewModelLocator.Instance.Main.DemoInfoCurrent.DemoItemList)
|
foreach (var item in ViewModelLocator.Instance.Main.DemoInfoCurrent.DemoItemList)
|
||||||
{
|
{
|
||||||
item.IsVisible = true;
|
item.IsVisible = true;
|
||||||
|
item.QueriesText = string.Empty;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
@@ -78,10 +79,12 @@ namespace HandyControlDemo.UserControl
|
|||||||
if (item.Name.ToLower().Contains(key))
|
if (item.Name.ToLower().Contains(key))
|
||||||
{
|
{
|
||||||
item.IsVisible = true;
|
item.IsVisible = true;
|
||||||
|
item.QueriesText = _searchKey;
|
||||||
}
|
}
|
||||||
else if (item.TargetCtlName.Replace("DemoCtl", "").ToLower().Contains(key))
|
else if (item.TargetCtlName.Replace("DemoCtl", "").ToLower().Contains(key))
|
||||||
{
|
{
|
||||||
item.IsVisible = true;
|
item.IsVisible = true;
|
||||||
|
item.QueriesText = _searchKey;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -89,10 +92,12 @@ namespace HandyControlDemo.UserControl
|
|||||||
if (!string.IsNullOrEmpty(name) && name.ToLower().Contains(key))
|
if (!string.IsNullOrEmpty(name) && name.ToLower().Contains(key))
|
||||||
{
|
{
|
||||||
item.IsVisible = true;
|
item.IsVisible = true;
|
||||||
|
item.QueriesText = _searchKey;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
item.IsVisible = false;
|
item.IsVisible = false;
|
||||||
|
item.QueriesText = string.Empty;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,204 @@
|
|||||||
|
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("SourceText", typeof(string), typeof(HighlightTextBlock),
|
||||||
|
new PropertyMetadata(null, OnSourceTextChanged));
|
||||||
|
|
||||||
|
public static readonly DependencyProperty QueriesTextProperty =
|
||||||
|
DependencyProperty.Register("QueriesText", typeof(string), typeof(HighlightTextBlock),
|
||||||
|
new PropertyMetadata(null, OnQueriesTextChanged));
|
||||||
|
|
||||||
|
public static readonly DependencyProperty HighlightBrushProperty =
|
||||||
|
DependencyProperty.Register("HighlightBrush", typeof(Brush), typeof(HighlightTextBlock),
|
||||||
|
new PropertyMetadata(Brushes.Yellow));
|
||||||
|
|
||||||
|
private static void OnSourceTextChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) =>
|
||||||
|
((HighlightTextBlock) d).RefreshInlines();
|
||||||
|
|
||||||
|
private static void OnQueriesTextChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) =>
|
||||||
|
((HighlightTextBlock) d).RefreshInlines();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Replace the original property Text for binding text.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Don't use the <see cref="TextBlock.Text"/> property!
|
||||||
|
/// Because the <see cref="TextBlock.Text"/> has some unique behaviors
|
||||||
|
/// that is not needed at <see cref="HighlightTextBlock"/> at <see cref="TextBlock"/>,
|
||||||
|
/// which will cause some unexpected issue.
|
||||||
|
/// </remarks>
|
||||||
|
public string SourceText
|
||||||
|
{
|
||||||
|
get => (string) GetValue(SourceTextProperty);
|
||||||
|
set => SetValue(SourceTextProperty, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the text that need to be highlighted.
|
||||||
|
/// It can be an array of text separated by spaces.
|
||||||
|
/// </summary>
|
||||||
|
public string QueriesText
|
||||||
|
{
|
||||||
|
get => (string) GetValue(QueriesTextProperty);
|
||||||
|
set => SetValue(QueriesTextProperty, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the <see cref="Brush"/> of the background of the highlight text.
|
||||||
|
/// </summary>
|
||||||
|
public Brush HighlightBrush
|
||||||
|
{
|
||||||
|
get => (Brush) GetValue(HighlightBrushProperty);
|
||||||
|
set => SetValue(HighlightBrushProperty, 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<Fragment> 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
|
||||||
|
{
|
||||||
|
Source = this,
|
||||||
|
Path = new PropertyPath(nameof(HighlightBrush)),
|
||||||
|
Mode = BindingMode.OneWay
|
||||||
|
});
|
||||||
|
return run;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IEnumerable<Fragment> SplitTextByOrderedDisjointIntervals(string sourceText, List<Range> 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<Range> MergeIntervals(List<Range> intervals)
|
||||||
|
{
|
||||||
|
if (!intervals?.Any() ?? true) return new List<Range>();
|
||||||
|
|
||||||
|
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<Range>();
|
||||||
|
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<Range> 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; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -83,6 +83,7 @@
|
|||||||
<Compile Include="$(MSBuildThisFileDirectory)Controls\Slider\RangeSlider\TwoWayRangeBase.cs" />
|
<Compile Include="$(MSBuildThisFileDirectory)Controls\Slider\RangeSlider\TwoWayRangeBase.cs" />
|
||||||
<Compile Include="$(MSBuildThisFileDirectory)Controls\Block\RunningBlock\RunningBlock.cs" />
|
<Compile Include="$(MSBuildThisFileDirectory)Controls\Block\RunningBlock\RunningBlock.cs" />
|
||||||
<Compile Include="$(MSBuildThisFileDirectory)Controls\Tag\TagContainer.cs" />
|
<Compile Include="$(MSBuildThisFileDirectory)Controls\Tag\TagContainer.cs" />
|
||||||
|
<Compile Include="$(MSBuildThisFileDirectory)Controls\Text\HighlightTextBlock.cs" />
|
||||||
<Compile Include="$(MSBuildThisFileDirectory)Controls\Text\SimpleText.cs" />
|
<Compile Include="$(MSBuildThisFileDirectory)Controls\Text\SimpleText.cs" />
|
||||||
<Compile Include="$(MSBuildThisFileDirectory)Controls\Window\GlowWindow.cs" />
|
<Compile Include="$(MSBuildThisFileDirectory)Controls\Window\GlowWindow.cs" />
|
||||||
<Compile Include="$(MSBuildThisFileDirectory)Data\EnumItem.cs" />
|
<Compile Include="$(MSBuildThisFileDirectory)Data\EnumItem.cs" />
|
||||||
|
|||||||
Reference in new issue
Block a user