#region License Information // 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. /* HeuristicLab * Copyright (C) Heuristic and Evolutionary Algorithms Laboratory (HEAL) * * This file is part of HeuristicLab. * * HeuristicLab is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * HeuristicLab is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with HeuristicLab. If not, see . */ #endregion using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Windows; using System.Windows.Media; using System.Windows.Threading; using ICSharpCode.AvalonEdit.Document; using ICSharpCode.AvalonEdit.Rendering; using ICSharpCode.NRefactory.Editor; using ICSharpCode.SharpDevelop.Editor; namespace ICSharpCode.AvalonEdit.AddIn { /// /// Handles the text markers for a code editor. /// public sealed class TextMarkerService : DocumentColorizingTransformer, IBackgroundRenderer, ITextMarkerService, ITextViewConnect { TextSegmentCollection markers; TextDocument document; public TextMarkerService(TextDocument document) { if (document == null) throw new ArgumentNullException("document"); this.document = document; this.markers = new TextSegmentCollection(document); } #region ITextMarkerService public ITextMarker Create(int startOffset, int length) { if (markers == null) throw new InvalidOperationException("Cannot create a marker when not attached to a document"); int textLength = document.TextLength; if (startOffset < 0 || startOffset > textLength) throw new ArgumentOutOfRangeException("startOffset", startOffset, "Value must be between 0 and " + textLength); if (length < 0 || startOffset + length > textLength) throw new ArgumentOutOfRangeException("length", length, "length must not be negative and startOffset+length must not be after the end of the document"); TextMarker m = new TextMarker(this, startOffset, length); markers.Add(m); // no need to mark segment for redraw: the text marker is invisible until a property is set return m; } public IEnumerable GetMarkersAtOffset(int offset) { if (markers == null) return Enumerable.Empty(); else return markers.FindSegmentsContaining(offset); } public IEnumerable TextMarkers { get { return markers ?? Enumerable.Empty(); } } public void RemoveAll(Predicate predicate) { if (predicate == null) throw new ArgumentNullException("predicate"); if (markers != null) { foreach (TextMarker m in markers.ToArray()) { if (predicate(m)) Remove(m); } } } public void Remove(ITextMarker marker) { if (marker == null) throw new ArgumentNullException("marker"); TextMarker m = marker as TextMarker; if (markers != null && markers.Remove(m)) { Redraw(m); m.OnDeleted(); } } /// /// Redraws the specified text segment. /// internal void Redraw(ISegment segment) { foreach (var view in textViews) { view.Redraw(segment, DispatcherPriority.Normal); } if (RedrawRequested != null) RedrawRequested(this, EventArgs.Empty); } public event EventHandler RedrawRequested; #endregion #region DocumentColorizingTransformer protected override void ColorizeLine(DocumentLine line) { if (markers == null) return; int lineStart = line.Offset; int lineEnd = lineStart + line.Length; foreach (TextMarker marker in markers.FindOverlappingSegments(lineStart, line.Length)) { Brush foregroundBrush = null; if (marker.ForegroundColor != null) { foregroundBrush = new SolidColorBrush(marker.ForegroundColor.Value); foregroundBrush.Freeze(); } ChangeLinePart( Math.Max(marker.StartOffset, lineStart), Math.Min(marker.EndOffset, lineEnd), element => { if (foregroundBrush != null) { element.TextRunProperties.SetForegroundBrush(foregroundBrush); } Typeface tf = element.TextRunProperties.Typeface; element.TextRunProperties.SetTypeface(new Typeface( tf.FontFamily, marker.FontStyle ?? tf.Style, marker.FontWeight ?? tf.Weight, tf.Stretch )); } ); } } #endregion #region IBackgroundRenderer public KnownLayer Layer { get { // draw behind selection return KnownLayer.Selection; } } public void Draw(TextView textView, DrawingContext drawingContext) { if (textView == null) throw new ArgumentNullException("textView"); if (drawingContext == null) throw new ArgumentNullException("drawingContext"); if (markers == null || !textView.VisualLinesValid) return; var visualLines = textView.VisualLines; if (visualLines.Count == 0) return; int viewStart = visualLines.First().FirstDocumentLine.Offset; int viewEnd = visualLines.Last().LastDocumentLine.EndOffset; foreach (TextMarker marker in markers.FindOverlappingSegments(viewStart, viewEnd - viewStart)) { if (marker.BackgroundColor != null) { BackgroundGeometryBuilder geoBuilder = new BackgroundGeometryBuilder(); geoBuilder.AlignToWholePixels = true; geoBuilder.CornerRadius = 3; geoBuilder.AddSegment(textView, marker); Geometry geometry = geoBuilder.CreateGeometry(); if (geometry != null) { Color color = marker.BackgroundColor.Value; SolidColorBrush brush = new SolidColorBrush(color); brush.Freeze(); drawingContext.DrawGeometry(brush, null, geometry); } } var underlineMarkerTypes = TextMarkerTypes.SquigglyUnderline | TextMarkerTypes.NormalUnderline | TextMarkerTypes.DottedUnderline; if ((marker.MarkerTypes & underlineMarkerTypes) != 0) { foreach (Rect r in BackgroundGeometryBuilder.GetRectsForSegment(textView, marker)) { Point startPoint = r.BottomLeft; Point endPoint = r.BottomRight; Brush usedBrush = new SolidColorBrush(marker.MarkerColor); usedBrush.Freeze(); if ((marker.MarkerTypes & TextMarkerTypes.SquigglyUnderline) != 0) { double offset = 2.5; int count = Math.Max((int)((endPoint.X - startPoint.X) / offset) + 1, 4); StreamGeometry geometry = new StreamGeometry(); using (StreamGeometryContext ctx = geometry.Open()) { ctx.BeginFigure(startPoint, false, false); ctx.PolyLineTo(CreatePoints(startPoint, endPoint, offset, count).ToArray(), true, false); } geometry.Freeze(); Pen usedPen = new Pen(usedBrush, 1); usedPen.Freeze(); drawingContext.DrawGeometry(Brushes.Transparent, usedPen, geometry); } if ((marker.MarkerTypes & TextMarkerTypes.NormalUnderline) != 0) { Pen usedPen = new Pen(usedBrush, 1); usedPen.Freeze(); drawingContext.DrawLine(usedPen, startPoint, endPoint); } if ((marker.MarkerTypes & TextMarkerTypes.DottedUnderline) != 0) { Pen usedPen = new Pen(usedBrush, 1); usedPen.DashStyle = DashStyles.Dot; usedPen.Freeze(); drawingContext.DrawLine(usedPen, startPoint, endPoint); } } } } } IEnumerable CreatePoints(Point start, Point end, double offset, int count) { for (int i = 0; i < count; i++) yield return new Point(start.X + i * offset, start.Y - ((i + 1) % 2 == 0 ? offset : 0)); } #endregion #region ITextViewConnect readonly List textViews = new List(); void ITextViewConnect.AddToTextView(TextView textView) { if (textView != null && !textViews.Contains(textView)) { Debug.Assert(textView.Document == document); textViews.Add(textView); } } void ITextViewConnect.RemoveFromTextView(TextView textView) { if (textView != null) { Debug.Assert(textView.Document == document); textViews.Remove(textView); } } #endregion } public sealed class TextMarker : TextSegment, ITextMarker { readonly TextMarkerService service; public TextMarker(TextMarkerService service, int startOffset, int length) { if (service == null) throw new ArgumentNullException("service"); this.service = service; this.StartOffset = startOffset; this.Length = length; this.markerTypes = TextMarkerTypes.None; } public event EventHandler Deleted; public bool IsDeleted { get { return !this.IsConnectedToCollection; } } public void Delete() { service.Remove(this); } internal void OnDeleted() { if (Deleted != null) Deleted(this, EventArgs.Empty); } void Redraw() { service.Redraw(this); } Color? backgroundColor; public Color? BackgroundColor { get { return backgroundColor; } set { if (backgroundColor != value) { backgroundColor = value; Redraw(); } } } Color? foregroundColor; public Color? ForegroundColor { get { return foregroundColor; } set { if (foregroundColor != value) { foregroundColor = value; Redraw(); } } } FontWeight? fontWeight; public FontWeight? FontWeight { get { return fontWeight; } set { if (fontWeight != value) { fontWeight = value; Redraw(); } } } FontStyle? fontStyle; public FontStyle? FontStyle { get { return fontStyle; } set { if (fontStyle != value) { fontStyle = value; Redraw(); } } } public object Tag { get; set; } TextMarkerTypes markerTypes; public TextMarkerTypes MarkerTypes { get { return markerTypes; } set { if (markerTypes != value) { markerTypes = value; Redraw(); } } } Color markerColor; public Color MarkerColor { get { return markerColor; } set { if (markerColor != value) { markerColor = value; Redraw(); } } } public object ToolTip { get; set; } } }