watfordjc / csharp-stream-controller

My WIP stream controller for live streaming
MIT License
1 stars 0 forks source link

Make UI Accessible (a11y) #4

Closed watfordjc closed 4 years ago

watfordjc commented 4 years ago

Feature Branch

Feature branch has been merged into master.

Progress

Background

Accessibility. Don't need more background than that.

This needs fixing and continuous reassessment.

Accessibility in Windows 10

Tools

Accessibility Insights for Windows

Accessibility Insights for Windows helps developers find and fix accessibility issues in Windows apps

UIA (User Interface Automation)

Microsoft UI Automation is an accessibility framework that enables Windows applications to provide and consume programmatic information about user interfaces (UIs). It provides programmatic access to most UI elements on the desktop. It enables assistive technology products, such as screen readers, to provide information about the UI to end users and to manipulate the UI by means other than standard input. UI Automation also allows automated test scripts to interact with the UI.

WinAppDriver (Windows Application Driver)

Windows Application Driver (WinAppDriver) is a service to support Selenium-like UI Test Automation on Windows Applications. This service supports testing Universal Windows Platform (UWP), Windows Forms (WinForms), Windows Presentation Foundation (WPF), and Classic Windows (Win32) apps on Windows 10 PCs.

AccEvent (Accessible Event Watcher)

AccEvent is a legacy tool. We recommend using Accessibility Insights instead.

Inspect Tool (UI Inspect)

Inspect is a legacy tool. We recommend using Accessibility Insights instead.

UIA Verify (UI Automation Verify)

UI Automation Verify is a legacy tool. We recommend using Accessibility Insights instead.

watfordjc commented 4 years ago

Styles

The first control I have decided to style appears to also be the hardest to get a template for: the tray icon context menu.

The reasons are rather simple:

  1. It is one container control that has a rather small number of children,
  2. It is all alone in the window that doesn't visually exist, and,
  3. It is the context menu for a system tray icon.

That last reason is rather important, in terms of Handle OS settings for high contrast (or another TODO item that might mention dark/light mode). In Windows 10, the default user preference setting for Windows colours is Dark.

Dark (Mode)

Under Personalisation in Windows 10, under Colours, there is the Choose your colour dropdown menu.

At the time of this comment, the following Custom setting is the default (because default app mode is a recent customisation option):

So, all is good? Other than removing custom colours when High Contrast is enabled, dark mode is just something that can be added later. The windows use the Windows default colours of Light Mode, and if Microsoft were to change the default you would hope they would make it a lot easier to change the default colours in WPF apps. A system-wide dark mode theme would be the obvious option.

Not quite. Point 3 above. What exactly does Windows mean by "default Windows mode"?

Hitting F1 did nothing, let's try Get help and see what Microsoft's robot assistant says...

Personalize your PC by enabling the dark theme

You can change the appearance of your apps (like Mail and Calendar) and Windows interface (which includes the taskbar, Start menu, Action Center, touch keyboard, and more) instantly from light mode to dark mode – great for low-light conditions like working at night or for saving battery power. When you switch your apps or Windows interface to dark mode, they appear with a black background. When in light mode, the background appears white or gray.

To return Windows 10 to default settings:

  • Open Settings, then select Personalization. Select Colors, and under "Choose your color", select Custom from the dropdown options. Set "Choose your default Windows mode" to Dark, and "Choose your default app mode" to Light.

Note: The dark theme was added in Windows 10 version 1607. If you don't see these options in Settings, make sure you've installed the latest Windows Updates.

Default Windows mode includes the taskbar. If you look at the context menus for the clock, sound, battery, and network, they are all in dark mode when using the default settings. If I'm going to be looking at styles, it would make sense to start by looking at the one part of my app that doesn't follow the default Personalisation Colour settings.

ContextMenu

I opened Visual Studio Blend for the first time and... you can't easily create a custom ContextMenu control because, well, it disappears from the Live Visual Tree as soon as you try to click it.

This StackOverflow answer was really helpful here as it gave some code that would allow extracting any template:

var str = new StringBuilder();
using (var writer = new StringWriter(str))
    XamlWriter.Save(ContextMenu.Template, writer);
Debug.Write(str);

Or the version I ended up having due to me not (yet?) seeing the point of var, my tab-complete choices, and the fact I'm using C# 8 (simplified using statements):

StringBuilder stringBuilder = new StringBuilder();
using StringWriter stringWriter = new StringWriter(stringBuilder);
XamlWriter.Save(contextMenu1.Template, stringWriter);
Debug.Write(stringBuilder.ToString());

contextMenu1 contained the following, as I wasn't sure if I needed an instance of ContextMenu in order to generate a template that includes a ScrollViewer - something I'd seen mentioned elsewhere whilst searching for a template.

<Grid>
    <Button Content="Click Me!">
        <Button.ContextMenu>
            <ContextMenu x:Name="contextMenu1">
                <MenuItem Header="Test" />
                <MenuItem Header="Test2">
                    <MenuItem Header="Test4" />
                    <MenuItem Header="Test5" IsChecked="True" />
                </MenuItem>
            </ContextMenu>
        </Button.ContextMenu>
    </Button>
</Grid>

ContextMenu Standard Template

After a bit of copying and pasting in order to turn that single line from the Output window into something readable, and some newline insertions to reduce horizontal scroll and de-tabbing to remove superfluous indenting for a Github code block, I had this:

<ControlTemplate TargetType="ContextMenu"
                    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
                    xmlns:s="clr-namespace:System;assembly=System.Private.CoreLib"
                    xmlns:mwt="clr-namespace:Microsoft.Windows.Themes;assembly=PresentationFramework.Aero2"
                    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
    <mwt:SystemDropShadowChrome Color="#00FFFFFF" Name="Shdw" SnapsToDevicePixels="True">
        <Border BorderThickness="{TemplateBinding Border.BorderThickness}"
                BorderBrush="{TemplateBinding Border.BorderBrush}"
                Background="{TemplateBinding Panel.Background}"
                Name="ContextMenuBorder">
            <ScrollViewer Style="{DynamicResource {ComponentResourceKey TypeInTargetAssembly=FrameworkElement, ResourceId=MenuScrollViewer}}"
                            Name="ContextMenuScrollViewer"
                            Margin="1,0,1,0"
                            Grid.ColumnSpan="2">
                <Grid RenderOptions.ClearTypeHint="Enabled">
                    <Canvas Width="0" Height="0" HorizontalAlignment="Left" VerticalAlignment="Top">
                        <Rectangle Fill="{x:Null}" Name="OpaqueRect" Width="Auto" Height="Auto" />
                    </Canvas>
                    <Rectangle RadiusX="2" RadiusY="2"
                                Fill="#FFF1F1F1" Width="28" Margin="1,2,1,2" HorizontalAlignment="Left" />
                    <Rectangle Fill="#FFE2E3E3" Width="1" Margin="29,2,0,2" HorizontalAlignment="Left" />
                    <Rectangle Fill="#FFFFFFFF" Width="1" Margin="30,2,0,2" HorizontalAlignment="Left" />
                    <ItemsPresenter Name="ItemsPresenter" Margin="{TemplateBinding Control.Padding}"
                                    SnapsToDevicePixels="{TemplateBinding UIElement.SnapsToDevicePixels}"
                                    KeyboardNavigation.DirectionalNavigation="Cycle" />
                </Grid>
            </ScrollViewer>
        </Border>
    </mwt:SystemDropShadowChrome>
    <ControlTemplate.Triggers>
        <Trigger Property="ContextMenuService.HasDropShadow">
            <Setter Property="FrameworkElement.Margin" TargetName="Shdw">
                <Setter.Value>
                    <Thickness>0,0,5,5</Thickness>
                </Setter.Value>
            </Setter>
            <Setter Property="mwt:SystemDropShadowChrome.Color" TargetName="Shdw">
                <Setter.Value>
                    <Color>#71000000</Color>
                </Setter.Value>
            </Setter>
            <Trigger.Value>
                <s:Boolean>True</s:Boolean>
            </Trigger.Value>
        </Trigger>
        <Trigger Property="ScrollViewer.CanContentScroll" SourceName="ContextMenuScrollViewer">
            <Setter Property="Canvas.Top" TargetName="OpaqueRect">
                <Setter.Value>
                    <Binding Path="VerticalOffset" ElementName="ContextMenuScrollViewer" />
                </Setter.Value>
            </Setter>
            <Setter Property="Canvas.Left" TargetName="OpaqueRect">
                <Setter.Value>
                    <Binding Path="HorizontalOffset" ElementName="ContextMenuScrollViewer" />
                </Setter.Value>
            </Setter>
            <Trigger.Value>
                <s:Boolean>False</s:Boolean>
            </Trigger.Value>
        </Trigger>
    </ControlTemplate.Triggers>
</ControlTemplate>

That has a few errors when pasted into a ResourceDictionary in my program though:

  1. Each dictionary entry must have an associated key.

  2. Assembly System.Private.CoreLib was not found. Verify that you are not missing an assembly reference. Also, verify that your project and all referenced assemblies have been built.

    1. The name "Boolean" does not exist in the namespace "clr-namespace:System;assembly=System.Private.CoreLib".

    2. The type 's:Boolean' was not found. Verify that you are not missing an assembly reference and that all referenced assemblies have been built.

For the first error, <ControlTemplate TargetType="ContextMenu" becomes:

<ControlTemplate x:Key="ContextMenuTemplateLight" TargetType="ContextMenu"

For the second group of related errors, xmlns:s="clr-namespace:System;assembly=System.Private.CoreLib" becomes:

xmlns:s="clr-namespace:System;assembly=System.Runtime"

Finally, all the XML namespaces get merged into the XML document root:

<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
                    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
                    xmlns:local="clr-namespace:uk.JohnCook.dotnet.StreamController.Properties"
                    xmlns:s="clr-namespace:System;assembly=System.Runtime"
                    xmlns:mwt="clr-namespace:Microsoft.Windows.Themes;assembly=PresentationFramework.Aero2">

I believe the base ControlTemplate for ContextMenu is now complete, and just needs some customisation.

ContextMenu is just the outer layer of what needs templating. The ContextMenu in question contains MenuItem (with sub-menus) and Separator children, and the /Themes/Aero2.NormalColor.xaml theme suggests styling MenuItem could be complex.

watfordjc commented 4 years ago

Styles

As expected, MenuItem is going to be rather more difficult.

MenuItem

First, there are four different types of MenuItem:

  1. A menu bar item that doesn't have any children. Think Help or Exit in some applications.
  2. A menu bar item that has a sub-menu. Think Edit.
  3. A menu item in a sub-menu that doesn't have any children. Think Close or Undo.
  4. A menu item in a sub-menu that has a sub-menu itself. Think Recent Files or Bookmarks.

All four of these, whilst being MenuItem, act differently.

  1. Using Regedit as an example, there aren't any of the first type.
  2. If you hover over one of the second type (File) there is a pale blue highlight. Open the File menu and File becomes highlighted with a darker shade of blue to indicate its sub-menu is open.
  3. Hover over the third type (Import..., Exit), and it is highlighted an even darker shade of blue.
  4. If you go into the Edit menu and open the fourth type (New), New becomes highlighted the same way it would if it didn't have a sub-menu and you were hovering over it (i.e. the third type). If it were using the Windows 10 WPF Aero2 theme, it would instead have had a blue border and no highlight.

So, there are four types of MenuItem. How difficult are they to style?

Work In Progress Styling

Default Style

A standard WPF context-menu in Aero2 theme

This is a ContextMenu. It contains several MenuItem.

Default Style with Background and Foreground colours changed

A standard WPF context-menu in Aero2 theme with a black background and white foreground. There is an ugly white box on the left side due to hard-coded colours.

With the changes already made to ContextMenu it no longer has hard-coded colours. That means it can be styled without those hard-coded colours making it look ugly:

A standard WPF context-menu in Aero2 theme with a replaced control template. With a black background and white foreground, there is no longer an ugly white box on the left side.

With ContextMenu being able to be styled, there is the rather more involved process of styling its MenuItems.

Modified Style

Right-click system tray context menu. First layer is a dark shade of grey with four options - the first two (default render, default capture) have sub-menus. The default render option has a pale yellow border around it with the sub-menu opened to the left. Sub-menu is a slightly lighter shade of grey and has four options, the third of which has a checkbox to the left of it that has a blue background and white check mark and border. The fourth option has a slightly lighter shade of grey background, pure white text, and a pale yellow border indicating the mouse or keyboard has highlighted it.

With a lot of guesswork and trial-and-error, I have managed to get the context menu and its sub-menus to look somewhat OK for a dark mode theme.

Am I done? Nowhere close.

Remember those four types of MenuItem I mentioned? Well, there is something I didn't say about them...

Modified Style Effect on Menu Bar

Application menu bar. Text is black on dark grey. Window menu is open. Audio Interfaces option is highlighted with a yellow border around it, but it still has black text on a dark grey background.

All thirteen colours used by Menu and MenuItem are hard-coded. Although some of them can be overridden for each individual menu item, they can't be overridden with dynamic styles/brushes/colours.

Hard-Coded Colours

These are some of the colours I am currently overriding (application-wide) between the loading of my menu template and the loading of my menu item template:

<SolidColorBrush x:Key="Menu.Static.Background" Color="#434343" />
<SolidColorBrush x:Key="Menu.Static.Border" Color="#FF999999" />
<SolidColorBrush x:Key="Menu.Static.Foreground" Color="LightGray" />

<SolidColorBrush x:Key="MenuItem.Highlight.Background" Color="#545454" />
<SolidColorBrush x:Key="MenuItem.Highlight.Border" Color="LightGoldenrodYellow" />

I can either have a dark theme for all menus, or I can have a light theme for all menus. The only way I can currently have both is if I duplicate everything and point every single MenuItem at the relevant ControlTemplate.

<MenuItem Template="LightSubMenuHeaderTemplate" Header="File">
  <MenuItem Template="LightSubMenuItemTemplate" Header="Exit"/>
</MenuItem>

I would, of course, also need two different sets of brushes/colours, and I will also need to update all four (now eight) templates to use the new brush names.

After doing all that I might need a third set for High Contrast mode, and I still won't be able to dynamically adjust the colours.

One of the things I came across a lot whilst modifying the context menu's item's colours was the word TemplateBinding. My menu item templates file does have the Style for x:Type MenuItem, and I'm assuming TemplateBinding refers to the properties defined in this Style or somewhere else (auto-complete doesn't offer the full list of MenuItem properties to choose from for TemplateBinding).

That means I can change some things from a StaticResource Menu.Static.Background brush to a TemplateBinding Background brush.

What I therefore think I need to work out is how to extend MenuItem so that additional properties can be added. If I'm on the right path, I would then extend the default Style, and then modify the four MenuItem ControlTemplates to refer to the new properties via TemplateBinding.

I think the thing I'm after is DependencyProperty:

The purpose of dependency properties is to provide a way to compute the value of a property based on the value of other inputs. These other inputs might include system properties such as themes and user preference, just-in-time property determination mechanisms such as data binding and animations/storyboards, multiple-use templates such as resources and styles, or values known through parent-child relationships with other elements in the element tree. —Dependency properties and CLR properties, Dependency properties overview

Styles and templates are two of the chief motivating scenarios for using dependency properties. Styles are particularly useful for setting properties that define application user interface (UI). Styles are typically defined as resources in XAML. Styles interact with the property system because they typically contain "setters" for particular properties, as well as "triggers" that change a property value based on the real-time value for another property. —Styles, Property functionality provided by a dependency property, Dependency properties overview

DependencyProperty and Inheritance

There are three dependency properties that have so far appeared almost everywhere:

  1. Foreground
  2. Background
  3. BorderBrush

ContextMenu is based on MenuBase which is based on ItemsControl which is based on Control which has the dependency properties ForegroundProperty, BackgroundProperty, and BorderBrushProperty.

Menu is also based on MenuBase.

MenuItem is based on HeaderedItemsControl which is based on ItemsControl which is based on Control.

Ideally, I would add the new dependency properties to the relevant control so all controls inheriting from it can use the property. For example, Menu.Static.Separator could become SeparatorBrushProperty in MenuBase. I know I could replace the default ControlTemplate for something, but what about replacing a default Control?

After 12+ hours I have no idea if I'm anywhere close.

A style intended for type 'StyledMenuItem' cannot be applied to type 'MenuItem'

The problem, I think, is with the inheritance. StyledMenuItem appears to be working fine for the third type of menu item, but not the fourth type.

  1. A menu item in a sub-menu that doesn't have any children. Think Close or Undo.
  2. A menu item in a sub-menu that has a sub-menu itself. Think Recent Files or Bookmarks.

I created a new solution when I had trouble working out how to bind a sub-menu list to collection, and with the rest of my code out of the way it was easier to get to the code I needed to use. I need to break this problem down...

What is a Sub-Menu?

A sub-menu is a Popup that contains an ItemsPresenter. A Popup can only have one Child, so in the case of a MenuItem, ItemsPresenter is "insert ItemsPanel here", and as far as I can tell ItemsPanel in this case is a vertical StackPanel.

After trying to think of a simpler control I could derive from, I have decided to go with a CheckBox. It isn't a HeaderedContentControl, but it is a ContentControl and in terms of part of what I'm trying to do (adding DependencyProperty to existing control type), is it a suitable place to start:

<SolidColorBrush x:Key="OptionMark.Static.Glyph" Color="#FF212121" />

A hard-coded Aero2.NormalColor colour for the tick/check/glyph. I think I'll standardise on glyph so my British English doesn't get in the way. The first Google result for wpf change checkbox color is StackOverflow post How to change the color of the CheckMark in WPF CheckBox control? Not the answer, but at least the question is Googleable.

watfordjc commented 4 years ago

Custom CheckBox

Following on from the previous comment, I need to break the problem down. A CheckBox is probably the simplest place to start.

ColorCheckBox Setup

Let's start from scratch. Note that I am only focusing on Windows 10 here, so I'm using Aero2.NormalColor styles. I will probably move them from Generic.xaml once I am done, into the correct theme file.

  1. Create a new Visual Studio 2019 WPF App (.NET Core) solution, calling it… CustomControls.
  2. Right-click the project, Add, New Item, Custom Control (WPF). Call it… ColorCheckBox.
  3. If it isn't already open, open ColorCheckBox.cs.
  4. Rename CustomControl1 to ColorCheckBox.
  5. Change ColorCheckBox's inheritance from Control to Checkbox
  6. Open MainWindow.xaml.
  7. Stick a ColorCheckBox inside <Grid></Grid>, making the text big and coloured to highlight a couple of UI problems we can try to fix:
    <StackPanel Orientation="Vertical" Grid.Row="0" Grid.Column="0"
            HorizontalAlignment="Center"
            VerticalAlignment="Center">
    <local:ColorCheckBox Content="Test ColorCheckBox"  VerticalContentAlignment="Center"
                            Foreground="DarkGreen" 
                            FontFamily="Open Sans"
                            FontSize="48" />
    </StackPanel>
  8. Open Themes\Generic.xaml and replace <Style></Style> with the Aero2 style for CheckBox from CheckBox.xaml.
  9. Replace x:Type CheckBox with x:Type local:ColorCheckBox.
  10. Above the ColorCheckBox style, add the Aero2 styles for FocusVisual and OptionMarkFocusVisual from FocusVisual.xaml
  11. Click on Run.
  12. Click the checkbox to see a black glyph in a tiny checkbox:

A black glyph in a tiny square to the left of big green text.

Adding Glyph Colour Customisation

Including all the code changes in this comment is going to get tiring. CustomControls is now its own GitHub repository so I can refer to code changes by linking to diffs instead. It might eventually end up as a library StreamController depends on.

There are a total of 12 hard-coded colours for CheckBox, one for each of four states (Static, MouseOver, Pressed, Disabled) for each of three things (Background, Border, Glyph).

Following the naming convention used in SytemColors, I will likely call the brushes StateItemBrush, omitting State when it is Static. OptionMark.Static.Glyph will therefore become GlyphBrush, and OptionMark.MouseOver.Glyph will become MouseOverGlyphBrush.

I am also going to define the default brushes in C# using the Aero2.NormalColor colours and use those brushes as the default metadata for the dependency properties. That will both remove the hard-coded colours from the templates and allow templates to define their own defaults through setters.

As all the hard coded colours are in hex, I am just going to manually convert them to ARGB [diff] by copying and pasting and then typing the0xs. For example,

<SolidColorBrush x:Key="OptionMark.Static.Glyph" Color="#FF212121" />

will become:

private static readonly Brush GlyphBrush = new SolidColorBrush(Color.FromArgb(0xFF, 0x21, 0x21, 0x21));

That leaves the issue of BorderBrush, which MenuItem inherits from Control. I'm going to call that DefaultBorderBrush.

After defining four Brush dependency properties for glyph state colours, switching the template to use the dependency properties, and adding the colours to the window's XAML [diff], I have the following:

A green glyph in a tiny square to the left of big green text.

As with MenuItem, it works. The problem isn't in this area, it is in the area of the wrong type of child object being created by a container.

Well, I do now have a ColorCheckBox, surely there must be a ListItem of some sort that uses CheckBox?

The only thing I can find is RibbonCheckBox. Back to MenuItem then…

watfordjc commented 4 years ago

Analysing MenuItem

Given how complex MenuItem is, it looks like I am going to have to duplicate the control just to add some dependent properties and modify the control templates.

Outside of MenuItem Class

MenuItem.cs starts with a enum MenuItemRole. This enum defines the four different types of MenuItem.

Attributes and Class Definition

Then comes the MenuItem class. It has some attributes, inherits from HeaderedItemsControl, and uses interface ICommandSource:

[DefaultEvent("Click")]
[Localizability(LocalizationCategory.Menu)]
[TemplatePart(Name = "PART_Popup", Type = typeof(Popup))]
[StyleTypedProperty(Property = "ItemContainerStyle", StyleTargetType = typeof(MenuItem))]
public class MenuItem : HeaderedItemsControl, ICommandSource

Attributes

DefaultEventAttribute

Specifies the default event for a component.

Click is the default event for a MenuItem.

LocalizabilityAttribute

Specifies the localization attributes for a binary XAML (BAML) class or class member.

There are three types of attributes:

  • Category. This specifies whether a value should be modifiable from a localizer tool. See Category.
  • Readability. This specifies whether a localizer tool should read (and display) a value. See Readability.
  • Modifiability. This specifies whether a localizer tool allows a value to be modified. See Modifiability.

There is a LocalizationCategory Enum that defines LocalizationCategory.Menu as:

A Menu or related control such as MenuItem.

TemplatePartAttribute

Represents an attribute that is applied to the class definition to identify the types of the named parts that are used for templating.

Remarks

Control authors apply this attribute to the class definition to inform template authors the types of the parts to use for styling the class. These parts are usually required in the template and have a specific predefined name. There can only be one element with a given name in any template.

Templates for MenuItem should contain a Popup with Name="PART_Popup".

StyleTypedPropertyAttribute

Represents an attribute that is applied to the class definition and determines the TargetTypes of the properties that are of type Style.

Remarks

Control authors apply this attribute to the class definition to specify the TargetTypes of the properties that are of type Style. For example, if you look at the declaration of the ListBox class, this attribute is used to specify that the TargetType of the ItemContainerStyle property is ListBoxItem.

Subclasses inherit this definition but can redefine the TargetType of the property by using this attribute on its own class definition.

Properties

Property: Gets or sets the name of the property that is of type Style. StyleTargetType: Gets or sets the TargetType of the Property this attribute is specifying. TypeId: When implemented in a derived class, gets a unique identifier for this Attribute. (Inherited from Attribute)

<MenuItem.ItemContainerStyle>
    <Style TargetType="MenuItem">
    </Style>
</MenuItem.ItemContainerStyle>

I'd need to change that attribute to StyledMenuItem.

Class Definition/Declaration

public class MenuItem : HeaderedItemsControl, ICommandSource

We're in the right place as we're wanting to know about MenuItem.

Inherits From HeaderedItemsControl

Represents a control that contains multiple items and has a header.

Implements the ICommandSource Interface

Defines an object that knows how to invoke a command.


The Start of StyledMenuItem

There is no need to redefine MenuItemRole as we can reuse it.

Other than that, the only thing that needs to change are:

  1. The StyleTypedProperty attribute.
  2. The class name.
  3. Inheritance (unless we need to ditch everything inside MenuItem).
  4. Removing ICommandSource as we can probably inherit MenuItem's implementation.
[DefaultEvent("Click")]
[Localizability(LocalizationCategory.Menu)]
[TemplatePart(Name = "PART_Popup", Type = typeof(Popup))]
[StyleTypedProperty(Property = "ItemContainerStyle", StyleTargetType = typeof(StyledMenuItem))]
public class StyledMenuItem : MenuItem
{
}

MenuItem Types and their Style Keys

// ----------------------------------------------------------------------------
//  Defines the names of the resources to be consumed by the MenuItem style.
//  Used to restyle several roles of MenuItem without having to restyle
//  all of the control.
// ----------------------------------------------------------------------------

There is a public static ResourceKey getter for each type/style of MenuItem, each of which is named in PascalCase with a name ending in TemplateKey.

There is then a private static ComponentResourceKey for each type/style, each of which is named in _camelCase with a name ending in TemplateKey.

The public getter sets the private variable by instantiating a new ComponentResourceKey if it is not null, otherwise returns the value of the private variable.

_topLevelItemTemplateKey = new ComponentResourceKey(typeof(MenuItem), "TopLevelItemTemplateKey");

ComponentResourceKey(Type, Object)

Initializes a new instance of a ComponentResourceKey, specifying the Type that defines the key, and an object to use as an additional resource identifier.

ComponentResourceKey

Defines or references resource keys based on class names in external assemblies, as well as an additional identifier.

Remarks

This class implements an object type that is useful for creating keys that are based on types in assemblies plus an identifier. Because you define or reference the type, you do not need to request a specific ResourceDictionary, and more than one set of resources can exist in the assembly, each differentiated by the type of their ComponentResourceKey.

If you want an easily accessible key, you can define a static property on your control class code that returns a ComponentResourceKey, constructed with a TypeInTargetAssembly that exists in the external resource assembly, and a ResourceId. The key can be used for defining alternate default styles for controls in an assembly, by swapping the original external resource assembly for a custom one. You can also define a named resource part within a larger control style or template to expose a customization entry point. This is particularly useful if you are defining a theme resource dictionary for your control.

These won't need renaming, can just change their types to StyledMenuItem.


MenuItem Constructors

Next up are the constructors. The default public constructor just calls base().

static MenuItem() calls OverrideMetaData on some inherited dependency properties, makes two calls to EventManager.RegisterClassHandler(), and there is also this:

_dType = DependencyObjectType.FromSystemTypeInternal(typeof(MenuItem));

At the end of MenuItem.cs _dType is defined.

// Returns the DependencyObjectType for the registered ThemeStyleKey's default
// value. Controls will override this method to return approriate types.
internal override DependencyObjectType DTypeThemeStyleKey
{
    get { return _dType; }
}

private static DependencyObjectType _dType;

DependencyObjectType represents a specific underlying system (CLR) Type of a DependencyObject. DependencyObjectType is essentially a wrapper for the (CLR) Type so that it can extend some of its capabilities.

This class has no public constructor. Instances of this class can only be obtained through properties on other objects (such as DependencyObject.DependencyObjectType), or through the static method FromSystemType. —DependencyObjectType Class

_dType and DTypeThemeStyleKey aren't used anywhere else in the file, so I'll ignore them for now.

OverrideMetadata

MenuItem calls OverrideMetadata() on the following dependency properties:

If my understanding is correct, I only need to override dependency properties that I am going to change the default values for.

DefaultStyleKey is used for the Separatorss. FocusVisualStyle is for keyboard focus. Foreground is set to SystemColors.MenuTextBrush and Font* are set to SystemFonts.Message*.

I'll come back to OverrideMetadata.


MenuItem Methods

The primary thing wrong with my StyledMenuItem is with MenuItemRole.SubmenuHeader. I think what I'll do is make some notes on every method in MenuItem, breaking it down by #region.

Public Events

For these, they are all registered with typeof(MenuItem) and use RoutingStrategy.Bubble.

I don't know if I can just let the base MenuItem class deal with them.

Public Properties

This section of the file is way too long. Instead of going through it all, I am going to instead copy and paste MenuItem.cs into CustomControls and see what breaks.

watfordjc commented 4 years ago

Attached Properties

After trying to find some way of creating a class that inherits from MenuItem without copying huge chunks of code from MS.Internal and other methods/properties that are internal only, I have decided to backtrack.

After playing around with attached properties for a bit, I think I have started working out how I'm going to do things.

First, I created a class in CustomControls to contain an attached property. I decided to go with a Brush type as I'm trying to do styling. For a default SolidColorBrush, I went with Brushes.Blue. I set the attached property on the ColorCheckBox going with Brushes.Gold, and then in the template I set the value of Background to the value of the attached property. Here is a diff of the changes that produced the following:

Unchecked tiny checkbox with a gold background to the left of big green text.

As I'd want to change a lot of colours in a lot of places, I then added a second checkbox and attempted to change them simultaneously using a timer. That didn't work, as I'd need to iterate over every object I want to change the attached property's value of.

After mulling it over for a bit, I realised they were both inside a StackPanel. I changed the timer so it changed the attached property on the StackPanel to Brushes.DarkGoldenRod, and I changed the template for ColorCheckBox to add a DataTrigger so that if it has an ancestor of x:Type StackPanel whose BackgroundColor (attached property) value of the becomes Brushes.DarkGoldenRod, the checkbox background colour becomes dark golden rod. A diff for that can be found here, and this is the result:

Two rows of checkboxes. Unchecked tiny checkbox with a dark golden rod background to the left of big green text on the first row, Unchecked tiny checkbox with a dark golden rod background to the left of big purple text on the second row.

Dark Mode

Basing a trigger on a fixed colour is a rather limited exercise, but this is where everything comes together. The BackgroundColor attached property was set on a StackPanel but the colour was only applied to its descendant ColorCheckBoxes.

I already have two of the things I need to implement dark mode: an enum and some untested methods for the current mode (as well as separation of Windows mode from App mode), and I have a way of getting windows of a certain type.

As the system tray ContextMenu is a descendant of a Window, I should be able to set an attached property of type WindowsTheme to a window, and then have the ContextMenu and its MenuItems switch colour palette.

WindowsTheme

I have the following options in my enum (Github doesn't like 0-based numbered lists):

In order to determine the theme to use, a window can call CurrentWindowsTheme(bool applicationTheme) where applicationTheme is true if wanting the "app mode" preference, or false if wanting the "Windows mode" preference.

If the answer is HighContrast, that is all that is needed to know. Use the colours from the Windows theme. If the answer is Default, calling DefaultTheme(bool applicationTheme) will give an answer if the the default for app/Windows mode is Light or Dark.

As I'm only focusing on the system tray context menu for now, I need that list of hard-coded colours I'm going to be triplicating:

<SolidColorBrush x:Key="Menu.Static.Background" Color="#FFF0F0F0" />
<SolidColorBrush x:Key="Menu.Static.Border" Color="#FF999999" />
<SolidColorBrush x:Key="Menu.Static.Foreground" Color="#FF212121" />

<SolidColorBrush x:Key="Menu.Static.Separator" Color="#FFD7D7D7" />

<SolidColorBrush x:Key="Menu.Disabled.Background" Color="#3DDADADA" />
<SolidColorBrush x:Key="Menu.Disabled.Border" Color="#FFDADADA" />
<SolidColorBrush x:Key="Menu.Disabled.Foreground" Color="#FF707070" />

<SolidColorBrush x:Key="MenuItem.Selected.Background" Color="#3D26A0DA" />
<SolidColorBrush x:Key="MenuItem.Selected.Border" Color="#FF26A0DA" />

<SolidColorBrush x:Key="MenuItem.Highlight.Background" Color="#3D26A0DA" />
<SolidColorBrush x:Key="MenuItem.Highlight.Border" Color="#FF26A0DA" />

<SolidColorBrush x:Key="MenuItem.Highlight.Disabled.Background" Color="#0A000000" />
<SolidColorBrush x:Key="MenuItem.Highlight.Disabled.Border" Color="#21000000" />

I might also want to change the FocusVisualStyle, so here's the Aero2.NormalColor defaults extracted from FocusVisual.xaml:

<!-- [[AeroLite.NormalColor, Aero2.NormalColor]] -->

<!-- Focus Visual -->
<LinearGradientBrush x:Key="MenuItemSelectionFill"
                        StartPoint="0,0"
                        EndPoint="0,1">
    <LinearGradientBrush.GradientStops>
        <GradientStop Color="#34C5EBFF"
                        Offset="0"/>
        <GradientStop Color="#3481D8FF"
                        Offset="1"/>
    </LinearGradientBrush.GradientStops>
</LinearGradientBrush>
<LinearGradientBrush x:Key="MenuItemPressedFill"
                        StartPoint="0,0"
                        EndPoint="0,1">
    <LinearGradientBrush.GradientStops>
        <GradientStop Color="#28717070"
                        Offset="0"/>
        <GradientStop Color="#50717070"
                        Offset="0.75"/>
        <GradientStop Color="#90717070"
                        Offset="1"/>
    </LinearGradientBrush.GradientStops>
</LinearGradientBrush>

Does Github markdown do coloured tables? No. Does Github allow HTML? Some, but styles are stripped out.

Not to worry, I still have Visual Studio Blend open from when I was trying to work out the vertical line in menu items. The vertical line doesn't translate well, but I'm just after a way of visually representing colour schemes:

WindowsTheme.Light

A pseudo menu and sub-menu with 4 items each. The first and third items in both menus are in regular style. The second in both menus is in disabled style. The fourth in the menu is in sub-menu open style, and the fourth in the sub-menu is in focused/selected style.

The "menu" and "sub-menu" use the following colour for their borders:

The first and third Menu options (in both menus) use the following colours:

The second Menu.Disabled option (in both menus) uses the following colours:

The fourth MenuItem.Selected option in the first menu uses the following colours:

The fourth MenuItem.Highlight option in the first menu uses the following colours:

WindowsTheme.Dark

I already have my colour palette from my system tray work, so after a bit of tweaking I have this palette:

<SolidColorBrush x:Key="Menu.Static.Background" Color="#FF434343" />
<SolidColorBrush x:Key="Menu.Static.Border" Color="#FF999999" />
<SolidColorBrush x:Key="Menu.Static.Foreground" Color="LightGray" />

<SolidColorBrush x:Key="Menu.Static.Separator" Color="#FFD7D7D7" />

<SolidColorBrush x:Key="Menu.Disabled.Background" Color="#3DDADADA" />
<SolidColorBrush x:Key="Menu.Disabled.Border" Color="#FFDADADA" />
<SolidColorBrush x:Key="Menu.Disabled.Foreground" Color="#FF707070" />

<SolidColorBrush x:Key="MenuItem.Selected.Background" Color="#D0434343" />
<SolidColorBrush x:Key="MenuItem.Selected.Border" Color="LightGoldenrodYellow" />

<SolidColorBrush x:Key="MenuItem.Highlight.Background" Color="#D0434343" />
<SolidColorBrush x:Key="MenuItem.Highlight.Border" Color="LightGoldenrodYellow" />

<SolidColorBrush x:Key="MenuItem.Highlight.Disabled.Background" Color="#0A000000" />
<SolidColorBrush x:Key="MenuItem.Highlight.Disabled.Border" Color="#21000000"

Which creates this colour scheme:

Same as previous menu, but with a dark mode scheme. The selected item now has a light yellow goldenrod border, and the highlighted item now has an identical border with a lighter shade of grey background.

There is a problem here: menu items use the same foreground colour as menu bar items. To improve contrast ratio, I will need two additional brushes: MenuItem.Selected.Foreground and MenuItem.Highlight.Foreground.

The lighter grey (via transparency) used for the highlighted item needs a white text foreground for contrast reasons. Although the selected item that is the parent of the sub-menu doesn't use a different background colour, using white text there might also improve contrast.

<SolidColorBrush x:Key="MenuItem.Selected.Foreground" Color="White" />
<SolidColorBrush x:Key="MenuItem.Highlight.Foreground" Color="White" />

That change has the following effect:

Same as previous menu and sub-menu image, but now the selected and highlighted items have white text.

As I'm only looking at the menu items in the context menu for now, I think I have the colour scheme and now just need to write the Style, ControlTemplate and the theme switching stuff.

Custom Theme

Out of curiosity, I fed in my Web site's usual colour scheme to have MenuItem.Selected use its background colour.

Same as the previous menu, but the menu and sub-menu now have black text on a pastel pink background. The selected item in the parent menu has a yellow background and gold border, and the focused/highlighted item in the sub-menu has white text on a purple background with a gold border.

I don't think that is making it into the light theme, even though that pastel pink background colour is used for my (non-Christmas) branding on Twitter, YouTube, and Twitch. It is already in the app, as with my secondary pastel purple colour, in the system tray and window status bar icons for obs-websocket heartbeats.

watfordjc commented 4 years ago

Visual Tree

The problem with using attached properties is that a context menu is outside of the visual tree of a window.

After a bit of thinking I think I have settled on the following…

GetStyledResourceDictionary

GetStyledResourceDictionary(WindowsTheme windowsTheme) is a new method in WindowUtilityLibrary that converts a WindowsTheme to a resource dictionary for that theme. It adds two dictionaries, one for colours, and one for theme. It then returns a Collection<ResourceDictionary> equal to the merged dictionaries.

Using WindowsTheme.Dark as an example, the two dictionaries added are DarkColours.xaml and DarkTheme.xaml. The colours dictionary just contains a load of brushes.

The theme contains the following in its resource dictionary:

<ResourceDictionary.MergedDictionaries>
    <ResourceDictionary Source="DarkColours.xaml" />
    <ResourceDictionary Source="..\Templates\ContextMenuTemplate.xaml" />
    <ResourceDictionary Source="..\Templates\MenuItemTemplate.xaml" />
</ResourceDictionary.MergedDictionaries>

<Style x:Key="ContextMenuStyle" TargetType="{x:Type ContextMenu}" BasedOn="{StaticResource ResourceKey=StyledContextMenu}">
    <Style.Resources>
        <ResourceDictionary Source="DarkColours.xaml" />
    </Style.Resources>
</Style>

<Style x:Key="MenuItemStyle" TargetType="{x:Type MenuItem}" BasedOn="{StaticResource ResourceKey=StyledMenuItem}">
    <Style.Resources>
        <ResourceDictionary Source="DarkColours.xaml" />
    </Style.Resources>
</Style>

The merged dictionary includes everything needed for the styles, and the styles are based on named styles (e.g. StyledMenuItem from MenuItemTemplate.xaml) with the relevant colour scheme in the style's resource dictionary. There is probably some unnecessary XAML loading, but I'm not going to try changing it until I finally make some commits (it has been a while).

Light Theme

There is very little change to the light theme. The most noticeable one from the Aero2.NormalColor default is that of checkboxes: their background colour is usually the highlighted colour. The border of a highlighted menu item also covers the left side border of a checkbox, which just looked bad.

So, minimally changed light theme for context menu and menu items:

Light theme context menu with sub-menu open. Black text on grey background. Heading of opened sub-menu has a pale blue background, and selected item in sub-menu has a pale blue highlight. Checked item in sub-menu has a blue background checkbox with a white check mark.

Dark Theme

The dark theme is, essentially, what I was going for in the last post. There is something missing which is part of material design: each sub-menu should have a slightly lighter background than its parent menu/sub-menu to simulate "depth". That isn't an accessibility issue, so I'll come back to it later.

Dark theme context menu with sub-menu open. Light gray text on dark gray background. Heading of opened sub-menu has a pale yellow border and white text, and selected item in sub-menu has a pale yellow border, slightly lighter shade of grey background, and white text. Checked item in sub-menu has a blue background checkbox with a white check mark and border.

High Contrast

I haven't dealt with high contrast yet, but the only thing that should need doing for that is fixing all of the brushes to the limited set of system colours for high contrast mode.

Other Notes

The vertical bar could probably do with some further work: I just had to sleep on the issue of it being off a pixel or two on mouse over. The sub-menu popup doesn't have an equivalent of Rectangle 3 which is why, in light mode, there is still a slight visual discrepancy between the two menu types.

Other than that, the two menu types are rather close in style/look now. The popup sub-menu now has a drop shadow if its parent ContextMenu has a drop shadow, which is itself determined by Windows preferences. I don't think sub-menus from menu bars have drop shadows.

In order to reference the styles in XAML, the window's constructor creates the merged dictionary before InitializeComponent() is called. I'm not sure if that is a problem.

Hot swapping of themes in response to a user changing their Windows preferences is something that still needs working out. I am hoping that swapping one theme with another by clearing the merged dictionary doesn't cause a crash due to the styles temporarily not existing.

There is still an issue with an inconsistent <Separator /> height, but I'll come back to that later. I think I have previously worked out how to fix it, but it looks like I've misplaced that change.