// Copyright (c) 2014 AlphaSierraPapa for the SharpDevelop Team // // Permission is hereby granted, free of charge, to any person obtaining a copy of this // software and associated documentation files (the "Software"), to deal in the Software // without restriction, including without limitation the rights to use, copy, modify, merge, // publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons // to whom the Software is furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all copies or // substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, // INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR // PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE // FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR // OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER // DEALINGS IN THE SOFTWARE. using System; using System.Windows; using System.Windows.Documents; using System.Windows.Media; using System.Windows.Media.TextFormatting; using ICSharpCode.AvalonEdit.Document; using ICSharpCode.AvalonEdit.Utils; namespace ICSharpCode.AvalonEdit.Rendering { // This class is internal because it does not need to be accessed by the user - it can be configured using TextEditorOptions. /// /// Element generator that displays · for spaces and » for tabs and a box for control characters. /// /// /// This element generator is present in every TextView by default; the enabled features can be configured using the /// . /// [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1702:CompoundWordsShouldBeCasedCorrectly", MessageId = "Whitespace")] sealed class SingleCharacterElementGenerator : VisualLineElementGenerator, IBuiltinElementGenerator { /// /// Gets/Sets whether to show · for spaces. /// public bool ShowSpaces { get; set; } /// /// Gets/Sets whether to show » for tabs. /// public bool ShowTabs { get; set; } /// /// Gets/Sets whether to show a box with the hex code for control characters. /// public bool ShowBoxForControlCharacters { get; set; } /// /// Creates a new SingleCharacterElementGenerator instance. /// public SingleCharacterElementGenerator() { this.ShowSpaces = true; this.ShowTabs = true; this.ShowBoxForControlCharacters = true; } void IBuiltinElementGenerator.FetchOptions(TextEditorOptions options) { this.ShowSpaces = options.ShowSpaces; this.ShowTabs = options.ShowTabs; this.ShowBoxForControlCharacters = options.ShowBoxForControlCharacters; } public override int GetFirstInterestedOffset(int startOffset) { DocumentLine endLine = CurrentContext.VisualLine.LastDocumentLine; StringSegment relevantText = CurrentContext.GetText(startOffset, endLine.EndOffset - startOffset); for (int i = 0; i < relevantText.Count; i++) { char c = relevantText.Text[relevantText.Offset + i]; switch (c) { case ' ': if (ShowSpaces) return startOffset + i; break; case '\t': if (ShowTabs) return startOffset + i; break; default: if (ShowBoxForControlCharacters && char.IsControl(c)) { return startOffset + i; } break; } } return -1; } public override VisualLineElement ConstructElement(int offset) { char c = CurrentContext.Document.GetCharAt(offset); if (ShowSpaces && c == ' ') { return new SpaceTextElement(CurrentContext.TextView.cachedElements.GetTextForNonPrintableCharacter("\u00B7", CurrentContext)); } else if (ShowTabs && c == '\t') { return new TabTextElement(CurrentContext.TextView.cachedElements.GetTextForNonPrintableCharacter("\u00BB", CurrentContext)); } else if (ShowBoxForControlCharacters && char.IsControl(c)) { var p = new VisualLineElementTextRunProperties(CurrentContext.GlobalTextRunProperties); p.SetForegroundBrush(Brushes.White); var textFormatter = TextFormatterFactory.Create(CurrentContext.TextView); var text = FormattedTextElement.PrepareText(textFormatter, TextUtilities.GetControlCharacterName(c), p); return new SpecialCharacterBoxElement(text); } else { return null; } } sealed class SpaceTextElement : FormattedTextElement { public SpaceTextElement(TextLine textLine) : base(textLine, 1) { BreakBefore = LineBreakCondition.BreakPossible; BreakAfter = LineBreakCondition.BreakDesired; } public override int GetNextCaretPosition(int visualColumn, LogicalDirection direction, CaretPositioningMode mode) { if (mode == CaretPositioningMode.Normal || mode == CaretPositioningMode.EveryCodepoint) return base.GetNextCaretPosition(visualColumn, direction, mode); else return -1; } public override bool IsWhitespace(int visualColumn) { return true; } } sealed class TabTextElement : VisualLineElement { internal readonly TextLine text; public TabTextElement(TextLine text) : base(2, 1) { this.text = text; } public override TextRun CreateTextRun(int startVisualColumn, ITextRunConstructionContext context) { // the TabTextElement consists of two TextRuns: // first a TabGlyphRun, then TextCharacters '\t' to let WPF handle the tab indentation if (startVisualColumn == this.VisualColumn) return new TabGlyphRun(this, this.TextRunProperties); else if (startVisualColumn == this.VisualColumn + 1) return new TextCharacters("\t", 0, 1, this.TextRunProperties); else throw new ArgumentOutOfRangeException("startVisualColumn"); } public override int GetNextCaretPosition(int visualColumn, LogicalDirection direction, CaretPositioningMode mode) { if (mode == CaretPositioningMode.Normal || mode == CaretPositioningMode.EveryCodepoint) return base.GetNextCaretPosition(visualColumn, direction, mode); else return -1; } public override bool IsWhitespace(int visualColumn) { return true; } } sealed class TabGlyphRun : TextEmbeddedObject { readonly TabTextElement element; TextRunProperties properties; public TabGlyphRun(TabTextElement element, TextRunProperties properties) { if (properties == null) throw new ArgumentNullException("properties"); this.properties = properties; this.element = element; } public override LineBreakCondition BreakBefore { get { return LineBreakCondition.BreakPossible; } } public override LineBreakCondition BreakAfter { get { return LineBreakCondition.BreakRestrained; } } public override bool HasFixedSize { get { return true; } } public override CharacterBufferReference CharacterBufferReference { get { return new CharacterBufferReference(); } } public override int Length { get { return 1; } } public override TextRunProperties Properties { get { return properties; } } public override TextEmbeddedObjectMetrics Format(double remainingParagraphWidth) { double width = Math.Min(0, element.text.WidthIncludingTrailingWhitespace - 1); return new TextEmbeddedObjectMetrics(width, element.text.Height, element.text.Baseline); } public override Rect ComputeBoundingBox(bool rightToLeft, bool sideways) { double width = Math.Min(0, element.text.WidthIncludingTrailingWhitespace - 1); return new Rect(0, 0, width, element.text.Height); } public override void Draw(DrawingContext drawingContext, Point origin, bool rightToLeft, bool sideways) { origin.Y -= element.text.Baseline; element.text.Draw(drawingContext, origin, InvertAxes.None); } } sealed class SpecialCharacterBoxElement : FormattedTextElement { public SpecialCharacterBoxElement(TextLine text) : base(text, 1) { } public override TextRun CreateTextRun(int startVisualColumn, ITextRunConstructionContext context) { return new SpecialCharacterTextRun(this, this.TextRunProperties); } } sealed class SpecialCharacterTextRun : FormattedTextRun { static readonly SolidColorBrush darkGrayBrush; static SpecialCharacterTextRun() { darkGrayBrush = new SolidColorBrush(Color.FromArgb(200, 128, 128, 128)); darkGrayBrush.Freeze(); } public SpecialCharacterTextRun(FormattedTextElement element, TextRunProperties properties) : base(element, properties) { } public override void Draw(DrawingContext drawingContext, Point origin, bool rightToLeft, bool sideways) { Point newOrigin = new Point(origin.X + 1.5, origin.Y); var metrics = base.Format(double.PositiveInfinity); Rect r = new Rect(newOrigin.X - 0.5, newOrigin.Y - metrics.Baseline, metrics.Width + 2, metrics.Height); drawingContext.DrawRoundedRectangle(darkGrayBrush, null, r, 2.5, 2.5); base.Draw(drawingContext, newOrigin, rightToLeft, sideways); } public override TextEmbeddedObjectMetrics Format(double remainingParagraphWidth) { TextEmbeddedObjectMetrics metrics = base.Format(remainingParagraphWidth); return new TextEmbeddedObjectMetrics(metrics.Width + 3, metrics.Height, metrics.Baseline); } public override Rect ComputeBoundingBox(bool rightToLeft, bool sideways) { Rect r = base.ComputeBoundingBox(rightToLeft, sideways); r.Width += 3; return r; } } } }