Uppercasing in AvalonEdit

2019-05-25 06:54发布

问题:

I am writing a movie script editor using AvalonEdit.

I extended the DocumentLine class to have a "Type" property, with the value representing either a "Character", "Dialog Line", etc.

I would like document lines of a certain type within the script to be written in upper case (character names for example).

Is there an extension point within the rendering pipeline that would allow me to fetch a document line and change it's casing ?

I tried creating a class that extends DocumentColorizingTransformer, but changing the casing within the "protected override void ColorizeLine(DocumentLine line)" method didn't work.

回答1:

This is difficult, because upper-casing can change the mapping between displayed characters and the document (visual columns vs. document offsets).

For example, the single character 'ß' (German sharp s) exists only as a lower-case letter, and gets converted to the two-character string "SS" when calling string.ToUpper(). Editing this text is tricky: we can't allow the user to replace only one of the 'S', as the underlying document only contains the 'ß'.

A simple solution is to use the char.ToUpper() method instead, enforcing a one-to-one mapping between original and uppercase characters. This will keep letters like 'ß' unchanged.

In AvalonEdit 4.2, only two transformations are allowed on the already generated VisualLineElements:

  • Change the text run properties, e.g. font size, text color, etc.
  • Split a VisualLineElement in two - this is used internally by ChangeLinePart() so that properties can be changed for a text portion.

This means it is not possible to do a text replacement in a colorizer, you need to implement this using a VisualLineElementGenerator.

/// <summary>
/// Makes all text after a colon (until the end of line) upper-case.
/// </summary>
public class UppercaseGenerator : VisualLineElementGenerator
{
    public override int GetFirstInterestedOffset(int startOffset)
    {
        TextDocument document = CurrentContext.Document;
        int endOffset = CurrentContext.VisualLine.LastDocumentLine.EndOffset;
        for (int i = startOffset; i < endOffset; i++) {
            char c = document.GetCharAt(i);
            if (c == ':')
                return i + 1;
        }
        return -1;
    }

    public override VisualLineElement ConstructElement(int offset)
    {
        DocumentLine line = CurrentContext.Document.GetLineByOffset(offset);
        return new UppercaseText(CurrentContext.VisualLine, line.EndOffset - offset);
    }

    /// <summary>
    /// Displays a portion of the document text, but upper-cased.
    /// </summary>
    class UppercaseText : VisualLineText
    {
        public UppercaseText(VisualLine parentVisualLine, int length) : base(parentVisualLine, length)
        {
        }

        protected override VisualLineText CreateInstance(int length)
        {
            return new UppercaseText(ParentVisualLine, length);
        }

        public override TextRun CreateTextRun(int startVisualColumn, ITextRunConstructionContext context)
        {
            if (context == null)
                throw new ArgumentNullException("context");

            int relativeOffset = startVisualColumn - VisualColumn;
            StringSegment text = context.GetText(context.VisualLine.FirstDocumentLine.Offset + RelativeTextOffset + relativeOffset, DocumentLength - relativeOffset);
            char[] uppercase = new char[text.Count];
            for (int i = 0; i < text.Count; i++) {
                uppercase[i] = char.ToUpper(text.Text[text.Offset + i]);
            }
            return new TextCharacters(uppercase, 0, uppercase.Length, this.TextRunProperties);
        }
    }
}

In AvalonEdit 4.3.0.8868, I added the method VisualLine.ReplaceElement(). This can be used to replace the default VisualText elements with UppercaseText elements within a line transformer (colorizer).

Note that it is also possible to implement support for 'ß' being displayed as 'SS'. For that, you would have to implement your own copy of VisualLineText instead of just overriding the existing one. Then you can use a visual length that differs from the document length. The GetRelativeOffset and GetVisualColumns methods would be used to provide the mapping between the document and visual coordinates.


There is another option you could use: small caps.

// in the colorizer:
ChangeLinePart(start, end, e => e.TextRunProperties.SetTypographyProperties(new CapsTypography()));

// helper class
class CapsTypography : DefaultTextRunTypographyProperties
{
    public override FontCapitals Capitals {
        get { return FontCapitals.SmallCaps; }
    }
}

However, WPF will render small caps only when using an OpenType font that supports them. In my testing, Cambria worked with small caps, most other fonts don't. Also, the SetTypographyProperties method and DefaultTextRunTypographyProperties class require AvalonEdit 4.3.