// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Text;
using System.Windows.Documents;
using System.Windows.Media;
namespace Microsoft.Management.UI.Internal
{
///
/// Builds a paragraph based on Text + Bold + Highlight information.
/// Bold are the segments of thexct that should be bold, and Highlight are
/// the segments of thext that should be highlighted (like search results).
///
internal class ParagraphBuilder : INotifyPropertyChanged
{
///
/// The text spans that should be bold.
///
private readonly List boldSpans;
///
/// The text spans that should be highlighted.
///
private readonly List highlightedSpans;
///
/// The text displayed.
///
private readonly StringBuilder textBuilder;
///
/// Paragraph built in BuildParagraph.
///
private readonly Paragraph paragraph;
///
/// Initializes a new instance of the ParagraphBuilder class.
///
/// Paragraph we will be adding lines to in BuildParagraph.
internal ParagraphBuilder(Paragraph paragraph)
{
if (paragraph == null)
{
throw new ArgumentNullException("paragraph");
}
this.paragraph = paragraph;
this.boldSpans = new List();
this.highlightedSpans = new List();
this.textBuilder = new StringBuilder();
}
#region INotifyPropertyChanged Members
///
/// Used to notify of property changes.
///
public event PropertyChangedEventHandler PropertyChanged;
#endregion
///
/// Gets the number of highlights.
///
internal int HighlightCount
{
get { return this.highlightedSpans.Count; }
}
///
/// Gets the paragraph built in BuildParagraph.
///
internal Paragraph Paragraph
{
get { return this.paragraph; }
}
///
/// Called after all the AddText calls have been made to build the paragraph
/// based on the current text.
/// This method goes over 3 collections simultaneouslly:
/// 1) characters in this.textBuilder
/// 2) spans in this.boldSpans
/// 3) spans in this.highlightedSpans
/// And adds the minimal number of Inlines to the paragraph so that all
/// characters that should be bold and/or highlighed are.
///
internal void BuildParagraph()
{
this.paragraph.Inlines.Clear();
int currentBoldIndex = 0;
TextSpan? currentBoldSpan = this.boldSpans.Count == 0 ? (TextSpan?)null : this.boldSpans[0];
int currentHighlightedIndex = 0;
TextSpan? currentHighlightedSpan = this.highlightedSpans.Count == 0 ? (TextSpan?)null : this.highlightedSpans[0];
bool currentBold = false;
bool currentHighlighted = false;
StringBuilder sequence = new StringBuilder();
int i = 0;
foreach (char c in this.textBuilder.ToString())
{
bool newBold = false;
bool newHighlighted = false;
ParagraphBuilder.MoveSpanToPosition(ref currentBoldIndex, ref currentBoldSpan, i, this.boldSpans);
newBold = currentBoldSpan == null ? false : currentBoldSpan.Value.Contains(i);
ParagraphBuilder.MoveSpanToPosition(ref currentHighlightedIndex, ref currentHighlightedSpan, i, this.highlightedSpans);
newHighlighted = currentHighlightedSpan == null ? false : currentHighlightedSpan.Value.Contains(i);
if (newBold != currentBold || newHighlighted != currentHighlighted)
{
ParagraphBuilder.AddInline(this.paragraph, currentBold, currentHighlighted, sequence);
}
sequence.Append(c);
currentHighlighted = newHighlighted;
currentBold = newBold;
i++;
}
ParagraphBuilder.AddInline(this.paragraph, currentBold, currentHighlighted, sequence);
}
///
/// Highlights all ocurrences of .
/// This is called after all calls to AddText have been made.
///
/// Search string.
/// True if search should be case sensitive.
/// True if we should search whole word only.
internal void HighlightAllInstancesOf(string search, bool caseSensitive, bool wholeWord)
{
this.highlightedSpans.Clear();
if (search == null || search.Trim().Length == 0)
{
this.BuildParagraph();
this.OnNotifyPropertyChanged("HighlightCount");
return;
}
string text = this.textBuilder.ToString();
StringComparison comparison = caseSensitive ? StringComparison.Ordinal : StringComparison.OrdinalIgnoreCase;
int start = 0;
int match;
while ((match = text.IndexOf(search, start, comparison)) != -1)
{
// false loop
do
{
if (wholeWord)
{
if (match > 0 && char.IsLetterOrDigit(text[match - 1]))
{
break;
}
if ((match + search.Length <= text.Length - 1) && char.IsLetterOrDigit(text[match + search.Length]))
{
break;
}
}
this.AddHighlight(match, search.Length);
}
while (false);
start = match + search.Length;
}
this.BuildParagraph();
this.OnNotifyPropertyChanged("HighlightCount");
}
///
/// Adds text to the paragraph later build with BuildParagraph.
///
/// Text to be added.
/// True if the text should be bold.
internal void AddText(string str, bool bold)
{
if (str == null)
{
throw new ArgumentNullException("str");
}
if (str.Length == 0)
{
return;
}
if (bold)
{
this.boldSpans.Add(new TextSpan(this.textBuilder.Length, str.Length));
}
this.textBuilder.Append(str);
}
///
/// Called before a derived class starts adding text
/// to reset the current content.
///
internal void ResetAllText()
{
this.boldSpans.Clear();
this.highlightedSpans.Clear();
this.textBuilder.Clear();
}
///
/// Adds an inline to based on the remaining parameters.
///
/// Paragraph to add Inline to.
/// True if text should be added in bold.
/// True if the text should be added with highlight.
/// The text to add and clear.
private static void AddInline(Paragraph currentParagraph, bool currentBold, bool currentHighlighted, StringBuilder sequence)
{
if (sequence.Length == 0)
{
return;
}
Run run = new Run(sequence.ToString());
if (currentHighlighted)
{
run.Background = ParagraphSearcher.HighlightBrush;
}
Inline inline = currentBold ? (Inline)new Bold(run) : run;
currentParagraph.Inlines.Add(inline);
sequence.Clear();
}
///
/// This is an auxiliar method in BuildParagraph to move the current bold or highlighed spans
/// according to the
/// The current bold and higlighed span should be ending ahead of the current position.
/// Moves and to the
/// propper span in according to the
/// This is an auxiliar method in BuildParagraph.
///
/// Current index within .
/// Current span within .
/// Caracter position. This comes from a position within this.textBuilder.
/// The collection of spans. This is either this.boldSpans or this.highlightedSpans.
private static void MoveSpanToPosition(ref int currentSpanIndex, ref TextSpan? currentSpan, int caracterPosition, List allSpans)
{
if (currentSpan == null || caracterPosition <= currentSpan.Value.End)
{
return;
}
for (int newBoldIndex = currentSpanIndex + 1; newBoldIndex < allSpans.Count; newBoldIndex++)
{
TextSpan newBoldSpan = allSpans[newBoldIndex];
if (caracterPosition <= newBoldSpan.End)
{
currentSpanIndex = newBoldIndex;
currentSpan = newBoldSpan;
return;
}
}
// there is no span ending ahead of current position, so
// we set the current span to null to prevent unecessary comparisons against the currentSpan
currentSpan = null;
}
///
/// Adds one individual text highlight
/// This is called after all calls to AddText have been made.
///
/// Highlight start.
/// Highlight length.
private void AddHighlight(int start, int length)
{
if (start < 0)
{
throw new ArgumentOutOfRangeException("start");
}
if (start + length > this.textBuilder.Length)
{
throw new ArgumentOutOfRangeException("length");
}
this.highlightedSpans.Add(new TextSpan(start, length));
}
///
/// Called internally to notify when a proiperty changed.
///
/// Property name.
private void OnNotifyPropertyChanged(string propertyName)
{
PropertyChangedEventHandler handler = this.PropertyChanged;
if (handler != null)
{
handler(this, new PropertyChangedEventArgs(propertyName));
}
}
///
/// A text span used to mark bold and highlighed segments.
///
internal struct TextSpan
{
///
/// Index of the first character in the span.
///
private readonly int start;
///
/// Index of the last character in the span.
///
private readonly int end;
///
/// Initializes a new instance of the TextSpan struct.
///
/// Index of the first character in the span.
/// Index of the last character in the span.
internal TextSpan(int start, int length)
{
if (start < 0)
{
throw new ArgumentOutOfRangeException("start");
}
if (length < 1)
{
throw new ArgumentOutOfRangeException("length");
}
this.start = start;
this.end = start + length - 1;
}
///
/// Gets the index of the first character in the span.
///
internal int Start
{
get { return this.start; }
}
///
/// Gets the index of the first character in the span.
///
internal int End
{
get
{
return this.end;
}
}
///
/// Returns true if the is between start and end (inclusive).
///
/// Position to verify if is in the span.
/// True if the is between start and end (inclusive).
internal bool Contains(int position)
{
return (position >= this.start) && (position <= this.end);
}
}
}
}