// 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.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Windows;
using System.Windows.Documents;
using ICSharpCode.AvalonEdit.Document;
using ICSharpCode.NRefactory.Editor;
namespace ICSharpCode.AvalonEdit.Highlighting
{
///
/// Stores rich-text formatting.
///
public sealed class RichTextModel
{
List stateChangeOffsets = new List();
List stateChanges = new List();
int GetIndexForOffset(int offset)
{
if (offset < 0)
throw new ArgumentOutOfRangeException("offset");
int index = stateChangeOffsets.BinarySearch(offset);
if (index < 0) {
// If no color change exists directly at offset,
// create a new one.
index = ~index;
stateChanges.Insert(index, stateChanges[index - 1].Clone());
stateChangeOffsets.Insert(index, offset);
}
return index;
}
int GetIndexForOffsetUseExistingSegment(int offset)
{
if (offset < 0)
throw new ArgumentOutOfRangeException("offset");
int index = stateChangeOffsets.BinarySearch(offset);
if (index < 0) {
// If no color change exists directly at offset,
// return the index of the color segment that contains offset.
index = ~index - 1;
}
return index;
}
int GetEnd(int index)
{
// Gets the end of the color segment no. index.
if (index + 1 < stateChangeOffsets.Count)
return stateChangeOffsets[index + 1];
else
return int.MaxValue;
}
///
/// Creates a new RichTextModel.
///
public RichTextModel()
{
stateChangeOffsets.Add(0);
stateChanges.Add(new HighlightingColor());
}
///
/// Creates a RichTextModel from a CONTIGUOUS list of HighlightedSections.
///
internal RichTextModel(int[] stateChangeOffsets, HighlightingColor[] stateChanges)
{
Debug.Assert(stateChangeOffsets[0] == 0);
this.stateChangeOffsets.AddRange(stateChangeOffsets);
this.stateChanges.AddRange(stateChanges);
}
#region UpdateOffsets
///
/// Updates the start and end offsets of all segments stored in this collection.
///
/// TextChangeEventArgs instance describing the change to the document.
public void UpdateOffsets(TextChangeEventArgs e)
{
if (e == null)
throw new ArgumentNullException("e");
UpdateOffsets(e.GetNewOffset);
}
///
/// Updates the start and end offsets of all segments stored in this collection.
///
/// OffsetChangeMap instance describing the change to the document.
public void UpdateOffsets(OffsetChangeMap change)
{
if (change == null)
throw new ArgumentNullException("change");
UpdateOffsets(change.GetNewOffset);
}
///
/// Updates the start and end offsets of all segments stored in this collection.
///
/// OffsetChangeMapEntry instance describing the change to the document.
public void UpdateOffsets(OffsetChangeMapEntry change)
{
UpdateOffsets(change.GetNewOffset);
}
void UpdateOffsets(Func updateOffset)
{
int readPos = 1;
int writePos = 1;
while (readPos < stateChangeOffsets.Count) {
Debug.Assert(writePos <= readPos);
int newOffset = updateOffset(stateChangeOffsets[readPos], AnchorMovementType.Default);
if (newOffset == stateChangeOffsets[writePos - 1]) {
// offset moved to same position as previous offset
// -> previous segment has length 0 and gets overwritten with this segment
stateChanges[writePos - 1] = stateChanges[readPos];
} else {
stateChangeOffsets[writePos] = newOffset;
stateChanges[writePos] = stateChanges[readPos];
writePos++;
}
readPos++;
}
// Delete all entries that were not written to
stateChangeOffsets.RemoveRange(writePos, stateChangeOffsets.Count - writePos);
stateChanges.RemoveRange(writePos, stateChanges.Count - writePos);
}
#endregion
///
/// Appends another RichTextModel after this one.
///
internal void Append(int offset, int[] newOffsets, HighlightingColor[] newColors)
{
Debug.Assert(newOffsets.Length == newColors.Length);
Debug.Assert(newOffsets[0] == 0);
// remove everything before offset:
while (stateChangeOffsets.Count > 0 && stateChangeOffsets.Last() <= offset) {
stateChangeOffsets.RemoveAt(stateChangeOffsets.Count - 1);
stateChanges.RemoveAt(stateChanges.Count - 1);
}
// Append the new segments
for (int i = 0; i < newOffsets.Length; i++) {
stateChangeOffsets.Add(offset + newOffsets[i]);
stateChanges.Add(newColors[i]);
}
}
///
/// Gets a copy of the HighlightingColor for the specified offset.
///
public HighlightingColor GetHighlightingAt(int offset)
{
return stateChanges[GetIndexForOffsetUseExistingSegment(offset)].Clone();
}
///
/// Applies the HighlightingColor to the specified range of text.
/// If the color specifies null for some properties, existing highlighting is preserved.
///
public void ApplyHighlighting(int offset, int length, HighlightingColor color)
{
if (color == null || color.IsEmptyForMerge) {
// Optimization: don't split the HighlightingState when we're not changing
// any property. For example, the "Punctuation" color in C# is
// empty by default.
return;
}
int startIndex = GetIndexForOffset(offset);
int endIndex = GetIndexForOffset(offset + length);
for (int i = startIndex; i < endIndex; i++) {
stateChanges[i].MergeWith(color);
}
}
///
/// Sets the HighlightingColor for the specified range of text,
/// completely replacing the existing highlighting in that area.
///
public void SetHighlighting(int offset, int length, HighlightingColor color)
{
if (length <= 0)
return;
int startIndex = GetIndexForOffset(offset);
int endIndex = GetIndexForOffset(offset + length);
stateChanges[startIndex] = color != null ? color.Clone() : new HighlightingColor();
stateChanges.RemoveRange(startIndex + 1, endIndex - (startIndex + 1));
stateChangeOffsets.RemoveRange(startIndex + 1, endIndex - (startIndex + 1));
}
///
/// Sets the foreground brush on the specified text segment.
///
public void SetForeground(int offset, int length, HighlightingBrush brush)
{
int startIndex = GetIndexForOffset(offset);
int endIndex = GetIndexForOffset(offset + length);
for (int i = startIndex; i < endIndex; i++) {
stateChanges[i].Foreground = brush;
}
}
///
/// Sets the background brush on the specified text segment.
///
public void SetBackground(int offset, int length, HighlightingBrush brush)
{
int startIndex = GetIndexForOffset(offset);
int endIndex = GetIndexForOffset(offset + length);
for (int i = startIndex; i < endIndex; i++) {
stateChanges[i].Background = brush;
}
}
///
/// Sets the font weight on the specified text segment.
///
public void SetFontWeight(int offset, int length, FontWeight weight)
{
int startIndex = GetIndexForOffset(offset);
int endIndex = GetIndexForOffset(offset + length);
for (int i = startIndex; i < endIndex; i++) {
stateChanges[i].FontWeight = weight;
}
}
///
/// Sets the font style on the specified text segment.
///
public void SetFontStyle(int offset, int length, FontStyle style)
{
int startIndex = GetIndexForOffset(offset);
int endIndex = GetIndexForOffset(offset + length);
for (int i = startIndex; i < endIndex; i++) {
stateChanges[i].FontStyle = style;
}
}
///
/// Retrieves the highlighted sections in the specified range.
/// The highlighted sections will be sorted by offset, and there will not be any nested or overlapping sections.
///
public IEnumerable GetHighlightedSections(int offset, int length)
{
int index = GetIndexForOffsetUseExistingSegment(offset);
int pos = offset;
int endOffset = offset + length;
while (pos < endOffset) {
int endPos = Math.Min(endOffset, GetEnd(index));
yield return new HighlightedSection {
Offset = pos,
Length = endPos - pos,
Color = stateChanges[index].Clone()
};
pos = endPos;
index++;
}
}
///
/// Creates WPF Run instances that can be used for TextBlock.Inlines.
///
/// The text source that holds the text for this RichTextModel.
public Run[] CreateRuns(ITextSource textSource)
{
Run[] runs = new Run[stateChanges.Count];
for (int i = 0; i < runs.Length; i++) {
int startOffset = stateChangeOffsets[i];
int endOffset = i + 1 < stateChangeOffsets.Count ? stateChangeOffsets[i + 1] : textSource.TextLength;
Run r = new Run(textSource.GetText(startOffset, endOffset - startOffset));
HighlightingColor state = stateChanges[i];
RichText.ApplyColorToTextElement(r, state);
runs[i] = r;
}
return runs;
}
}
}