[VSX] How to add a custom tooltip to the Visual Studio’s editor ?

July 7th, 2014 | Posted by Tom in .NET | Article | Extensibility | VSX | Read: 841

Some parts of the latest versions of Visual Studio are developed using MEF (Managed Extensibility Framework), allowing developers to easily create addins  to extend the features of the IDE. On this article, we’ll cover the basics to add custom tooltips to the Visual Studio’s editor.

A tooltip is a small amount of information which appears just under the mouse, when this one is no longer moving:

image

To create a custom tooltip, we first need to create a class that implement IMouseProcessorProvider:

[Export(typeof(IMouseProcessorProvider))]
[TextViewRole(PredefinedTextViewRoles.Document)]
[ContentType("xaml")]
[Name("MyCustomTooltip")]
internal sealed class CustomTooltipHandlerProvider : IMouseProcessorProvider
{
}

Using MEF, we export the type IMouseProcessorProvider so Visual Studio will know that it can loads the content of this file as a plugin.

The ContentType attribute is used to indicate for which type of document the tooltip will be available. There are a lot of default content types but you can easily creates your own if needed. In our case, the plugin will be loaded for the XAML files.

Once this is done, we have to implement the content of the interface so this is the method GetAssociatedProcessor, returning the MouseProcessorBase that will be used to display our tooltip:

[Import]
public IToolTipProviderFactory ToolTipProviderFactory { get; set; }

[Import]
public ITextStructureNavigatorSelectorService NavigatorService { get; set; }

[Import]
internal IClassifierAggregatorService AggregatorService { get; set; }

public IMouseProcessor GetAssociatedProcessor(IWpfTextView view)
{
    return new CustomTooltipMouseProcessor(view, this.ToolTipProviderFactory, this.NavigatorService.GetTextStructureNavigator(view.TextBuffer), this.AggregatorService.GetClassifier(view.TextBuffer));
}

Now, let’s take a look at the CustomTooltipMouseProcessor class that we have to create. It’s a simple class that derives from MouseProcessorBase:

internal class CustomTooltipMouseProcessor : MouseProcessorBase
{
    private readonly IWpfTextView _view;

    private readonly IToolTipProvider _toolTipProvider;
    private readonly ITextStructureNavigator _navigatorService;
    private readonly IClassifier _classifier;

    private readonly ICollection<FontFamily> _systemFonts; 

    private bool IsToolTipShown { get; set; }

    internal CustomTooltipMouseProcessor(IWpfTextView view, IToolTipProviderFactory toolTipProviderFactory, ITextStructureNavigator navigatorService, IClassifier classifier)
    {
        this._view = view;
        this._toolTipProvider = toolTipProviderFactory.GetToolTipProvider(this._view);
        this._navigatorService = navigatorService;
        this._classifier = classifier;

        this._systemFonts = Fonts.SystemFontFamilies;

        this.IsToolTipShown = false;
    }
}

The most important part of this class is the PreProcessMouseMove method, that will handle any mouse move in the editor:

public override void PreprocessMouseMove(MouseEventArgs e)
{
    var colorConverter = new ColorConverter();

    foreach (ITextViewLine viewLine in this._view.TextViewLines)
    {
        // Get the span for each line
        var lineSpan = new SnapshotSpan(viewLine.Start, viewLine.End);

        // Get the classification for each span
        var spans = this._classifier.GetClassificationSpans(lineSpan);
        foreach (var span in spans)
        {
            // If we are on a XAML attribute
            if (span.ClassificationType.Classification == "XAML Attribute Value")
            {
                // Get the attribute value
                var xamlAttributeValue = span.Span.GetText().Replace("\"", "");

                // Check if the attribute's value is a valid color
                if (colorConverter.IsValid(xamlAttributeValue))
                {
                    #region Tooltip management for FontFamily

                    var convertedColor = colorConverter.ConvertFromInvariantString(xamlAttributeValue);
                    if (convertedColor != null)
                    {
                        var xamlColor = (Color)convertedColor;

                        var spanAtMousePosition = Helpers.SpanHelpers.GetSpanAtMousePosition(this._view, this._navigatorService);
                        if (spanAtMousePosition.HasValue)
                        {
                            var textAtMousePosition = spanAtMousePosition.Value.GetText();

                            if (!string.IsNullOrWhiteSpace(textAtMousePosition))
                            {
                                if (xamlAttributeValue.Contains(textAtMousePosition))
                                {
                                    e.Handled = true;

                                    if (!this.IsToolTipShown)
                                    {
                                        this.IsToolTipShown = true;

                                        var copyHexColorHyperlink = new Hyperlink(new Run("Copy"));
                                        copyHexColorHyperlink.Click += (sender, args) =>
                                        {
                                            Clipboard.SetText(xamlColor.ColorToHexString());

                                            this._toolTipProvider.ClearToolTip();
                                        };

                                        this._toolTipProvider.ClearToolTip();
                                        this._toolTipProvider.ShowToolTip(spanAtMousePosition.Value.Snapshot.CreateTrackingSpan(spanAtMousePosition.Value.Span, SpanTrackingMode.EdgeExclusive),
                                            new Border
                                            {
                                                Background = new SolidColorBrush(Colors.LightGray),
                                                Padding = new Thickness(10),
                                                Child = new StackPanel
                                                {
                                                    Orientation = Orientation.Horizontal,
                                                    Children =
                                                    {
                                                        new Rectangle
                                                        {
                                                            Height = 30,
                                                            Width = 30,
                                                            Fill = new SolidColorBrush(xamlColor)
                                                        },
                                                        new TextBlock
                                                        {
                                                            Margin = new Thickness(10, 0, 0, 0),
                                                            Inlines =
                                                            {
                                                                //new Run(xamlAttributeValue),
                                                                //new LineBreak(),
                                                                new Run(xamlColor.ColorToHexString()),
                                                                new Run(" ("),
                                                                copyHexColorHyperlink,
                                                                new Run(")"),
                                                                new LineBreak(),
                                                                new Run(string.Format("{0}", xamlColor.ColorToRgbString())),
                                                            }
                                                        }
                                                    }
                                                }
                                            }, PopupStyles.PositionClosest);
                                    }

                                    return;
                                }
                            }
                        }
                    }

                    #endregion
                }
                // Check if the attribute's value is a valid color
                else if (this._systemFonts.Any(ff => ff.Source == xamlAttributeValue))
                {
                    #region Tooltip management for FontFamily

                    var xamlFontFamily = this._systemFonts.First(ff => ff.Source == xamlAttributeValue);

                    var spanAtMousePosition = Helpers.SpanHelpers.GetSpanAtMousePosition(this._view, this._navigatorService);
                    if (spanAtMousePosition.HasValue)
                    {
                        var textAtMousePosition = spanAtMousePosition.Value.GetText();
                        if (!string.IsNullOrWhiteSpace(textAtMousePosition))
                        {
                            // FontFamily can have whitespace in their name.
                            if (xamlAttributeValue.Contains(textAtMousePosition))
                            {
                                e.Handled = true;

                                if (!this.IsToolTipShown)
                                {
                                    this.IsToolTipShown = true;

                                    this._toolTipProvider.ClearToolTip();
                                    this._toolTipProvider.ShowToolTip(spanAtMousePosition.Value.Snapshot.CreateTrackingSpan(spanAtMousePosition.Value.Span, SpanTrackingMode.EdgeExclusive),
                                        new Border
                                        {
                                            Background = new SolidColorBrush(Colors.LightGray),
                                            Padding = new Thickness(10),
                                            Child = new StackPanel
                                            {
                                                Orientation = Orientation.Vertical,
                                                Children =
                                                {
                                                    new TextBlock
                                                    {
                                                        Inlines =
                                                        {
                                                            new Run{ Text = string.Format("{0} ({1})", xamlFontFamily.FamilyNames[this._view.VisualElement.Language], 10), FontSize = 10, FontFamily = xamlFontFamily },
                                                            new LineBreak(),
                                                            new Run{ Text = string.Format("{0} ({1})", xamlFontFamily.FamilyNames[this._view.VisualElement.Language], 12), FontSize = 12, FontFamily = xamlFontFamily },
                                                            new LineBreak(),
                                                            new Run{ Text = string.Format("{0} ({1})", xamlFontFamily.FamilyNames[this._view.VisualElement.Language], 15), FontSize = 15, FontFamily = xamlFontFamily },
                                                            new LineBreak(),
                                                            new Run{ Text = string.Format("{0} ({1})", xamlFontFamily.FamilyNames[this._view.VisualElement.Language], 18), FontSize = 18, FontFamily = xamlFontFamily },
                                                            new LineBreak(),
                                                            new Run{ Text = string.Format("{0} ({1})", xamlFontFamily.FamilyNames[this._view.VisualElement.Language], 20), FontSize = 20, FontFamily = xamlFontFamily },
                                                            new LineBreak(),
                                                            new Run{ Text = string.Format("{0} ({1})", xamlFontFamily.FamilyNames[this._view.VisualElement.Language], 25), FontSize = 25, FontFamily = xamlFontFamily },
                                                        }
                                                    }
                                                }
                                            }
                                        }, PopupStyles.PositionClosest);
                                }

                                return;
                            }
                        }
                    }

                    #endregion
                }
            }
        }
    }

    this._toolTipProvider.ClearToolTip();
    IsToolTipShown = false;
}

This method has a lot of lines of code but, basically, the process is simple:

  1. For each line in the editor, we get the line
  2. We then get the classifications of items on the line
  3. For each classification, we check if we are on a XAML attribute
  4. If yes, we check if this attribute’s value is a color or font family
  5. If yes, we get the text under the mouse
  6. If the text under the mouse is contained by the value of the XAML attribute, we proceed the next part of the code, which displays a custom tooltip using the method ShowToolTip of the ToolTipProvider

Thanks to the use of WPF in the IDE, we can display cool new tooltips (in my case, i’ve coded them but you can easily use UserControls).

When running this code, we can see that Visual Studio will launch and when we are in a XAML document, hovering a color or font display the new tooltip:

image

image

image

Those of you who are using Resharper or Web Essentials may have recognize some of the same features ;)

As you can see, it’s really easy to develop addins for Visual Studio and this can help you to increase your productivity.

 

Happy coding!

You can follow any responses to this entry through the RSS 2.0 You can leave a response, or trackback.

One Response

Add Comment Register



Leave a Reply