// 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); } } } }