VsixCommunity / Community.VisualStudio.Toolkit

Making it easier to write Visual Studio extensions
Other
249 stars 44 forks source link

Fonts and Colors #414

Open reduckted opened 1 year ago

reduckted commented 1 year ago

I wanted to define some custom colors for an extension I'm writing. It's a pretty convoluted process, so I created some wrapper classes and thought it belongs in the toolkit, so here it is. :grin:

Defining Custom Font and Color Settings

There are three parts to defining custom font and color settings.

  1. A provider defines the categories that the extension provides.
  2. A category is what appears in the "Show settings for" dropdown list in the Fonts and Colors options page. For example, "Text Editor", "Environment", and so on. A category defines one default font and zero or more colors.
  3. A color definition appears in the "Display items" list in the Fonts and Colors options page. A color definition defines the default foreground and background colors and font style. A user can customize this.

Font And Color Provider

You tell Visual Studio about the custom font and color definitions by using a class inheriting from BaseFontAndColorProvider:

[Guid("26442428-2cd7-xxxx-xxxx-f9b14087ca50")]
public class MyFontAndColorProvider : BaseFontAndColorProvider { }

ℹ️ Visual Studio uses a GUID to identify the provider, so make sure you add a GuidAttribute to the provider class.

The provider will find all categories defined in the same assembly as itself, and tell Visual Studio about them.

There are two things you need to do with the provider:

  1. You must declare that your package provides fonts and colors by using the ProvideFontsAndColorsAttribute:
[ProvideFontsAndColors(typeof(MyFontAndColorProvider))]
public class MyExtensionPackage : ToolkitPackage { }
  1. You must register the font and color providers when your package is initialized:
protected override async Task InitializeAsync(CancellationToken cancellationToken, IProgress<ServiceProgressData> progress)
{
    await this.RegisterFontAndColorProvidersAsync();
}

If you don't do both of these things, then Visual Studio won't know about your font and colors.

Font And Color Category

A category defines a default font and the colors that can be configured by the user. You define a category by inheriting from BaseFontAndColorCategory<T>. Just like commands and tool windows, the T type is the type of the inheriting class.

[Guid("e977c587-c06e-xxxx-xxxx-cbf9da1bdafa")]
public class MyFontAndColorCategory : BaseFontAndColorCategory<MyFontAndColorCategory>
{
    public override string Name => "My First Category";
}

ℹ️ Visual Studio uses a GUID to identify the category, so make sure you add a GuidAttribute to the category class.

Default Font

When you define a category, you can specify a default font or you can let it use an "Automatic" font. The "Automatic" font is the font that is used by Visual Studio for most of the UI elements. It can be configured by setting the font for the Environment category.

[Guid("e977c587-c06e-xxxx-xxxx-cbf9da1bdafa")]
public class MyFontAndColorCategory : BaseFontAndColorCategory<MyFontAndColorCategory>
{
    // Use Times New Roman as the default font for the category.
    public MyFontAndColorCategory() : base(new FontDefinition("Times New Roman", 14)) { }

    // ... snip ...
}

Color Definitions

The colors for a category are specified by defining properties of type ColorDefinition on the category class. The category will find these properties and tell Visual Studio about the colors.

[Guid("e977c587-c06e-xxxx-xxxx-cbf9da1bdafa")]
public class MyFontAndColorCategory : BaseFontAndColorCategory<MyFontAndColorCategory>
{
    // ... snip ...

    public ColorDefinition Primary { get; } = new(
        "Primary",
        defaultBackground: VisualStudioColor.Indexed(COLORINDEX.CI_RED),
        defaultForeground: VisualStudioColor.Indexed(COLORINDEX.CI_WHITE)
    );

    public ColorDefinition Secondary { get; } = new(
        "Secondary",
        defaultBackground: VisualStudioColor.Indexed(COLORINDEX.CI_YELLOW),
        defaultForeground: VisualStudioColor.Indexed(COLORINDEX.CI_BLACK),
    );
}

Color Definitions

A ColorDefinition defines the default values for an item that appears in the Fonts and Colors options page. You can also control what a user can customize about the item.

Every color needs a name. You can optionally specify a localized name and a description, though I have no idea where the description is shown.

You can specify the default foreground and background colors, and the default font style. You can also specify options that control what the user can customize. For example, you can prevent a bold font from being used, or prevent custom colors being selected, or prevent the background or foreground color from being customized at all.

There is also a line style and "marker visual style", though I'm not sure where they are used in Visual Studio. They're part of the underlying AllColorableItemInfo that Visual Studio uses, so I figured it was best to include them just in case someone needed them.

Colors

A specific color is specified using the VisualStudioColor class. There are five different ways that you can create a VisualStudioColor.

Indexed

"Indexed" colors are the predefined colors like Yellow, Red, Green, etc.

VisualStudioColor.Indexed(COLORINDEX.CI_RED);

VsColor

This allows you to use a color that is defined by the Visual Studio theme. The theme colors are defined in three separate enums: __VSSYSCOLOREX, __VSSYSCOLOREX2 and __VSSYSCOLOREX3. If you're trying to match other parts of Visual Studio, this is probably what you want to be using.

VisualStudioColor.VsColor(__VSSYSCOLOREX.VSCOLOR_ENVIRONMENT_BACKGROUND);

SysColor

This creates a color using a Windows system color. There's no enum for the system colors, but you can find their values here. These types of colors probably aren't that useful because they don't take into account the Visual Studio theme. You're better off using the VsColor method.

VisualStudioColor.SysColor(13); // System highlight color.

Rgb

This lets you create any color that you want. You can use red, green and blue components, or use a System.Windows.Media.Color.

VisualStudioColor.SysColor(200, 130, 40);
VisualStudioColor.SysColor(Colors.AliceBlue);

Automatic

This defines an "automatic" color. I think is the best way to use them:

  1. In the ColorDefinition, set the AutomaticBackground color to a VsColor (for example, VisualStudioColor.VsColor(__VSSYSCOLOREX.VSCOLOR_ENVIRONMENT_BACKGROUND)).
  2. Set the DefaultBackground color to Automatic.

When you select Automatic for the color in the Fonts and Colors options page, the VSCOLOR_ENVIRONMENT_BACKGROUND will be used.

Getting Fonts and Colors

Once you've defined the font and color categories, you'll probably want to get the actual font and color values that the user has customized. This can be done via the VS.FontsAndColors object.

To get the configured font and colors for a category, use the GetConfiguredFontAndColorsAsync method. Specify the type of the category as a generic parameter:

ConfiguredFontAndColorSet<MyFontAndColorCategory> set;
set = VS.FontsAndColors.GetConfiguredFontAndColorsAsync<MyFontAndColorCategory>();
// ... use it ...
set.Dispose();

The ConfiguredFontAndColorSet<T> provides access to the font and each of the color definitions. This is a "live" object - if the user customizes the font or color, the changes will be reflected in this object - so you don't need to get a new set each time you want to get the configured font or colors. Just make sure you dispose of it when you no longer need it.

Font

The font is available in the Font property as a ConfiguredFont. If the user customizes the font for the category, this object will be updated with the new font family and size.

This type is geared towards being used in WPF. It implements INotifyPropertyChanged, so you can bind directly to it in XAML.

class MyViewModel {
    public ConfiguredFont Font => set.Font;
}
<TextBlock FontFamily="{Binding Font.Family}" FontSize="{Binding Font.Size}"></TextBlock>

Family is a FontFamily object. If you're not using the font in WPF, then the font family is also available as a string via the FamilyName property.

ℹ️ The font can be "automatic". In that case, the Family property will be null and the FamilyName property will be the same as FontDefinition.Automatic.FamilyName.

Colors

The configured colors can be accessed via the GetColor method. You pass in the ColorDefinition of the color that you need. The ConfiguredFontAndColorSet<T> conveniently has a Category property that provides access to the category, so you can use that to access the ColorDefinition objects.

set.GetColor(set.Category.Primary);

The color is a ConfiguredColor. Just like the ConfiguredFont, this object will update if the user customizes the color settings. It also implements INotifyPropertyChanged, so you can bind directly to it in XAML as well.

class MyViewModel {
    public ConfiguredColor Primary { get; } = set.GetColor(set.Category.Primary);
}
<TextBlock Foreground="{Binding Primary.ForegroundBrush}"></TextBlock>

Brushes are available from the ForegroundBrush and BackgroundBrush properties. If you're not using it in WPF, there's also ForegroundColor and BackgroundColor properties that provide the color as a Color object.

The font style is also available. This is a FontStyle enum, but it essentially defines whether or not the font should be bold, as that's the only thing the user can customize. If you're using it in WPF, there's also a FontWeight property that you can bind to.

Events

As mentioned above, the ConfiguredFont and ConfiguredColor classes implement INotifyPropertyChanged, so you could add event handlers to them to detect when fonts and colors are customized, but there is an easier way. The ConfiguredFontAndColorSet<T> has two events that are raised whenever the font or any of the colors are changed:

Code Analyzers

Since there are a few things that you need to do to get Visual Studio to be aware of the font and color definitions, I've also added some code analyzers to detect mistakes.

Code Description
CVST006 The provider does not have a GuidAttribute. Visual Studio identifies the providers by GUID, so it's important that the provider has an explicitly defined GUID.
CVST007 The category does not have a GuidAttribute. Just like providers, the categories are also identified by GUID.
CVST008 You've defined a category but not a provider. Without a provider, Visual Studio has no way of knowing about the category.
CSVT009 You've defined a provider, but haven't added the ProvideFontsAndColorsAttribute to your package.
CVST010 You've added the ProvideFontsAndColorsAttribute to your package, but you haven't called RegisterFontAndColorProvidersAsync when initializing the package.

Demo

And to finish things off, the demo extension has an example of using font color definitions. Use View -> Other Windows -> Fonts and Colors Window to open the tool window. This window has four boxes. Each box uses a custom color definition.

In the Fonts and Colors options page, select the "Fonts and Colors Demo" item in the dropdown (it's towards the bottom). From there you can customize the four colors that are used on the tool window. When you save, the tool window will update with the new colors, and shows the events that were emitted.

fonts-and-colors-demo

RobertvanderHulst commented 1 year ago

Dave, I have not tested this yet, but it looks like you have put an awfull lot of work in this. Thanks for sharing!