//----------------------------------------------------------------------- // // Copyright (c) Microsoft Corporation. All rights reserved. // // // Implements ParagraphBuilder. // //----------------------------------------------------------------------- namespace Microsoft.Management.UI.Internal { using System; using System.Collections.Generic; using System.ComponentModel; using System.Text; using System.Windows.Documents; using System.Windows.Media; /// /// 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 List boldSpans; /// /// The text spans that should be highlighted /// private List highlightedSpans; /// /// The text displayed /// private StringBuilder textBuilder; /// /// Paragraph built in BuildParagraph /// private 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 simultaneously: /// 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 highlighted 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 occurrences 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 highlighted spans /// according to the /// The current bold and highlighted 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 unnecessary 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 property 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 highlighted segments /// internal struct TextSpan { /// /// Index of the first character in the span /// private int start; /// /// Index of the last character in the span /// private 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); } } } }