MegaBites-AI/Windows-powershell
0372
1// Copyright (c) Microsoft Corporation.2// Licensed under the MIT License.3 4using System;5using System.Collections.Generic;6using System.ComponentModel;7using System.Text;8using System.Windows.Documents;9using System.Windows.Media;10 11namespace Microsoft.Management.UI.Internal12{13 /// <summary>14 /// Builds a paragraph based on Text + Bold + Highlight information.15 /// Bold are the segments of the text that should be bold, and Highlight are16 /// the segments of the text that should be highlighted (like search results).17 /// </summary>18 internal class ParagraphBuilder : INotifyPropertyChanged19 {20 /// <summary>21 /// The text spans that should be bold.22 /// </summary>23 private readonly List<TextSpan> boldSpans;24 25 /// <summary>26 /// The text spans that should be highlighted.27 /// </summary>28 private readonly List<TextSpan> highlightedSpans;29 30 /// <summary>31 /// The text displayed.32 /// </summary>33 private readonly StringBuilder textBuilder;34 35 /// <summary>36 /// Paragraph built in BuildParagraph.37 /// </summary>38 private readonly Paragraph paragraph;39 40 /// <summary>41 /// Initializes a new instance of the ParagraphBuilder class.42 /// </summary>43 /// <param name="paragraph">Paragraph we will be adding lines to in BuildParagraph.</param>44 internal ParagraphBuilder(Paragraph paragraph)45 {46 ArgumentNullException.ThrowIfNull(paragraph);47 48 this.paragraph = paragraph;49 this.boldSpans = new List<TextSpan>();50 this.highlightedSpans = new List<TextSpan>();51 this.textBuilder = new StringBuilder();52 }53 54 #region INotifyPropertyChanged Members55 /// <summary>56 /// Used to notify of property changes.57 /// </summary>58 public event PropertyChangedEventHandler PropertyChanged;59 #endregion60 61 /// <summary>62 /// Gets the number of highlights.63 /// </summary>64 internal int HighlightCount65 {66 get { return this.highlightedSpans.Count; }67 }68 69 /// <summary>70 /// Gets the paragraph built in BuildParagraph.71 /// </summary>72 internal Paragraph Paragraph73 {74 get { return this.paragraph; }75 }76 77 /// <summary>78 /// Called after all the AddText calls have been made to build the paragraph79 /// based on the current text.80 /// This method goes over 3 collections simultaneously:81 /// 1) characters in this.textBuilder82 /// 2) spans in this.boldSpans83 /// 3) spans in this.highlightedSpans84 /// And adds the minimal number of Inlines to the paragraph so that all85 /// characters that should be bold and/or highlighted are.86 /// </summary>87 internal void BuildParagraph()88 {89 this.paragraph.Inlines.Clear();90 91 int currentBoldIndex = 0;92 TextSpan? currentBoldSpan = this.boldSpans.Count == 0 ? (TextSpan?)null : this.boldSpans[0];93 int currentHighlightedIndex = 0;94 TextSpan? currentHighlightedSpan = this.highlightedSpans.Count == 0 ? (TextSpan?)null : this.highlightedSpans[0];95 96 bool currentBold = false;97 bool currentHighlighted = false;98 99 StringBuilder sequence = new StringBuilder();100 int i = 0;101 foreach (char c in this.textBuilder.ToString())102 {103 bool newBold = false;104 bool newHighlighted = false;105 106 ParagraphBuilder.MoveSpanToPosition(ref currentBoldIndex, ref currentBoldSpan, i, this.boldSpans);107 newBold = currentBoldSpan == null ? false : currentBoldSpan.Value.Contains(i);108 109 ParagraphBuilder.MoveSpanToPosition(ref currentHighlightedIndex, ref currentHighlightedSpan, i, this.highlightedSpans);110 newHighlighted = currentHighlightedSpan == null ? false : currentHighlightedSpan.Value.Contains(i);111 112 if (newBold != currentBold || newHighlighted != currentHighlighted)113 {114 ParagraphBuilder.AddInline(this.paragraph, currentBold, currentHighlighted, sequence);115 }116 117 sequence.Append(c);118 119 currentHighlighted = newHighlighted;120 currentBold = newBold;121 i++;122 }123 124 ParagraphBuilder.AddInline(this.paragraph, currentBold, currentHighlighted, sequence);125 }126 127 /// <summary>128 /// Highlights all occurrences of <paramref name="search"/>.129 /// This is called after all calls to AddText have been made.130 /// </summary>131 /// <param name="search">Search string.</param>132 /// <param name="caseSensitive">True if search should be case sensitive.</param>133 /// <param name="wholeWord">True if we should search whole word only.</param>134 internal void HighlightAllInstancesOf(string search, bool caseSensitive, bool wholeWord)135 {136 this.highlightedSpans.Clear();137 138 if (search == null || search.Trim().Length == 0)139 {140 this.BuildParagraph();141 this.OnNotifyPropertyChanged("HighlightCount");142 return;143 }144 145 string text = this.textBuilder.ToString();146 StringComparison comparison = caseSensitive ? StringComparison.Ordinal : StringComparison.OrdinalIgnoreCase;147 int start = 0;148 int match;149 while ((match = text.IndexOf(search, start, comparison)) != -1)150 {151 // false loop152 do153 {154 if (wholeWord)155 {156 if (match > 0 && char.IsLetterOrDigit(text[match - 1]))157 {158 break;159 }160 161 if ((match + search.Length <= text.Length - 1) && char.IsLetterOrDigit(text[match + search.Length]))162 {163 break;164 }165 }166 167 this.AddHighlight(match, search.Length);168 }169 while (false);170 171 start = match + search.Length;172 }173 174 this.BuildParagraph();175 this.OnNotifyPropertyChanged("HighlightCount");176 }177 178 /// <summary>179 /// Adds text to the paragraph later build with BuildParagraph.180 /// </summary>181 /// <param name="str">Text to be added.</param>182 /// <param name="bold">True if the text should be bold.</param>183 internal void AddText(string str, bool bold)184 {185 ArgumentNullException.ThrowIfNull(str);186 187 if (str.Length == 0)188 {189 return;190 }191 192 if (bold)193 {194 this.boldSpans.Add(new TextSpan(this.textBuilder.Length, str.Length));195 }196 197 this.textBuilder.Append(str);198 }199 200 /// <summary>201 /// Called before a derived class starts adding text202 /// to reset the current content.203 /// </summary>204 internal void ResetAllText()205 {206 this.boldSpans.Clear();207 this.highlightedSpans.Clear();208 this.textBuilder.Clear();209 }210 211 /// <summary>212 /// Adds an inline to <paramref name="currentParagraph"/> based on the remaining parameters.213 /// </summary>214 /// <param name="currentParagraph">Paragraph to add Inline to.</param>215 /// <param name="currentBold">True if text should be added in bold.</param>216 /// <param name="currentHighlighted">True if the text should be added with highlight.</param>217 /// <param name="sequence">The text to add and clear.</param>218 private static void AddInline(Paragraph currentParagraph, bool currentBold, bool currentHighlighted, StringBuilder sequence)219 {220 if (sequence.Length == 0)221 {222 return;223 }224 225 Run run = new Run(sequence.ToString());226 if (currentHighlighted)227 {228 run.Background = ParagraphSearcher.HighlightBrush;229 }230 231 Inline inline = currentBold ? (Inline)new Bold(run) : run;232 currentParagraph.Inlines.Add(inline);233 sequence.Clear();234 }235 236 /// <summary>237 /// This is an auxiliar method in BuildParagraph to move the current bold or highlighted spans238 /// according to the <paramref name="caracterPosition"/>239 /// The current bold and highlighted span should be ending ahead of the current position.240 /// Moves <paramref name="currentSpanIndex"/> and <paramref name="currentSpan"/> to the241 /// proper span in <paramref name="allSpans"/> according to the <paramref name="caracterPosition"/>242 /// This is an auxiliar method in BuildParagraph.243 /// </summary>244 /// <param name="currentSpanIndex">Current index within <paramref name="allSpans"/>.</param>245 /// <param name="currentSpan">Current span within <paramref name="allSpans"/>.</param>246 /// <param name="caracterPosition">Character position. This comes from a position within this.textBuilder.</param>247 /// <param name="allSpans">The collection of spans. This is either this.boldSpans or this.highlightedSpans.</param>248 private static void MoveSpanToPosition(ref int currentSpanIndex, ref TextSpan? currentSpan, int caracterPosition, List<TextSpan> allSpans)249 {250 if (currentSpan == null || caracterPosition <= currentSpan.Value.End)251 {252 return;253 }254 255 for (int newBoldIndex = currentSpanIndex + 1; newBoldIndex < allSpans.Count; newBoldIndex++)256 {257 TextSpan newBoldSpan = allSpans[newBoldIndex];258 if (caracterPosition <= newBoldSpan.End)259 {260 currentSpanIndex = newBoldIndex;261 currentSpan = newBoldSpan;262 return;263 }264 }265 266 // there is no span ending ahead of current position, so267 // we set the current span to null to prevent unnecessary comparisons against the currentSpan268 currentSpan = null;269 }270 271 /// <summary>272 /// Adds one individual text highlight273 /// This is called after all calls to AddText have been made.274 /// </summary>275 /// <param name="start">Highlight start.</param>276 /// <param name="length">Highlight length.</param>277 private void AddHighlight(int start, int length)278 {279 ArgumentOutOfRangeException.ThrowIfNegative(start);280 ArgumentOutOfRangeException.ThrowIfGreaterThan(start + length, this.textBuilder.Length, nameof(length));281 282 this.highlightedSpans.Add(new TextSpan(start, length));283 }284 285 /// <summary>286 /// Called internally to notify when a property changed.287 /// </summary>288 /// <param name="propertyName">Property name.</param>289 private void OnNotifyPropertyChanged(string propertyName)290 {291 PropertyChangedEventHandler handler = this.PropertyChanged;292 if (handler != null)293 {294 handler(this, new PropertyChangedEventArgs(propertyName));295 }296 }297 298 /// <summary>299 /// A text span used to mark bold and highlighted segments.300 /// </summary>301 internal struct TextSpan302 {303 /// <summary>304 /// Index of the first character in the span.305 /// </summary>306 private readonly int start;307 308 /// <summary>309 /// Index of the last character in the span.310 /// </summary>311 private readonly int end;312 313 /// <summary>314 /// Initializes a new instance of the TextSpan struct.315 /// </summary>316 /// <param name="start">Index of the first character in the span.</param>317 /// <param name="length">Index of the last character in the span.</param>318 internal TextSpan(int start, int length)319 {320 ArgumentOutOfRangeException.ThrowIfNegative(start);321 ArgumentOutOfRangeException.ThrowIfLessThan(length, 1);322 323 this.start = start;324 this.end = start + length - 1;325 }326 327 /// <summary>328 /// Gets the index of the first character in the span.329 /// </summary>330 internal int Start331 {332 get { return this.start; }333 }334 335 /// <summary>336 /// Gets the index of the first character in the span.337 /// </summary>338 internal int End339 {340 get341 {342 return this.end;343 }344 }345 346 /// <summary>347 /// Returns true if the <paramref name="position"/> is between start and end (inclusive).348 /// </summary>349 /// <param name="position">Position to verify if is in the span.</param>350 /// <returns>True if the <paramref name="position"/> is between start and end (inclusive).</returns>351 internal bool Contains(int position)352 {353 return (position >= this.start) && (position <= this.end);354 }355 }356 }357}358 