Browse Source

Started adding keyboard accessibility to Menu.

Still quite a lot to do.
pull/58/head
Steven Kirk 11 years ago
parent
commit
43e80392df
  1. 62
      Perspex.Controls/Menu.cs
  2. 85
      Perspex.Controls/MenuItem.cs
  3. 4
      Perspex.Controls/Popup.cs
  4. 12
      Perspex.Controls/TopLevel.cs
  5. 3
      Perspex.Controls/Window.cs
  6. 24
      Perspex.Input/AccessKeyHandler.cs
  7. 23
      Perspex.Input/FocusManager.cs
  8. 4
      Perspex.Input/IMainMenu.cs
  9. 12
      Perspex.Themes.Default/MenuItemStyle.cs
  10. 24
      Tests/Perspex.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs

62
Perspex.Controls/Menu.cs

@ -13,11 +13,12 @@ namespace Perspex.Controls
using Perspex.LogicalTree;
using Perspex.Rendering;
using Perspex.Interactivity;
using Perspex.Controls.Primitives;
/// <summary>
/// A top-level menu control.
/// </summary>
public class Menu : ItemsControl, IFocusScope, IMainMenu
public class Menu : SelectingItemsControl, IFocusScope, IMainMenu
{
/// <summary>
/// Defines the default items panel used by a <see cref="Menu"/>.
@ -55,6 +56,29 @@ namespace Perspex.Controls
private set { this.SetValue(IsOpenProperty, value); }
}
/// <summary>
/// Closes the menu.
/// </summary>
public void CloseMenu()
{
foreach (MenuItem i in this.GetLogicalChildren())
{
i.IsSubMenuOpen = false;
}
this.IsOpen = false;
this.SelectedIndex = -1;
}
/// <summary>
/// Opens the menu in response to the Alt/F10 key.
/// </summary>
public void OpenMenu()
{
this.SelectedIndex = 0;
((IInputElement)this.SelectedItem)?.Focus();
}
/// <summary>
/// Called when the <see cref="MenuItem"/> is attached to the visual tree.
/// </summary>
@ -75,6 +99,13 @@ namespace Perspex.Controls
this.subscription = new CompositeDisposable(
pointerPress,
Disposable.Create(() => topLevel.Deactivated -= this.Deactivated));
var inputRoot = root as IInputRoot;
if (inputRoot != null && inputRoot.AccessKeyHandler != null)
{
inputRoot.AccessKeyHandler.MainMenu = this;
}
}
/// <summary>
@ -87,6 +118,20 @@ namespace Perspex.Controls
this.subscription.Dispose();
}
/// <summary>
/// Called when a key is pressed within the menu.
/// </summary>
/// <param name="e">The event args.</param>
protected override void OnKeyDown(KeyEventArgs e)
{
base.OnKeyDown(e);
if (this.IsOpen && e.Key == Key.Escape)
{
this.CloseMenu();
}
}
/// <summary>
/// Called when a submenu opens somewhere in the menu.
/// </summary>
@ -109,19 +154,6 @@ namespace Perspex.Controls
this.IsOpen = true;
}
/// <summary>
/// Closes the menu.
/// </summary>
private void CloseMenu()
{
foreach (MenuItem i in this.GetLogicalChildren())
{
i.IsSubMenuOpen = false;
}
this.IsOpen = false;
}
/// <summary>
/// Called when the top-level window is deactivated.
/// </summary>
@ -129,7 +161,7 @@ namespace Perspex.Controls
/// <param name="e">The event args.</param>
private void Deactivated(object sender, EventArgs e)
{
this.CloseMenu();
//this.CloseMenu();
}
/// <summary>

85
Perspex.Controls/MenuItem.cs

@ -15,11 +15,13 @@ namespace Perspex.Controls
using Perspex.Rendering;
using Perspex.Controls.Templates;
using Perspex.Controls.Presenters;
using Perspex.VisualTree;
/// <summary>
/// A menu item control.
/// </summary>
public class MenuItem : HeaderedItemsControl
public class MenuItem : SelectingItemsControl, ISelectable
{
/// <summary>
/// Defines the <see cref="Command"/> property.
@ -33,12 +35,24 @@ namespace Perspex.Controls
public static readonly PerspexProperty<object> CommandParameterProperty =
Button.CommandParameterProperty.AddOwner<MenuItem>();
/// <summary>
/// Defines the <see cref="Header"/> property.
/// </summary>
public static readonly PerspexProperty<object> HeaderProperty =
HeaderedItemsControl.HeaderProperty.AddOwner<MenuItem>();
/// <summary>
/// Defines the <see cref="Icon"/> property.
/// </summary>
public static readonly PerspexProperty<object> IconProperty =
PerspexProperty.Register<MenuItem, object>(nameof(Icon));
/// <summary>
/// Defines the <see cref="IsSelected"/> property.
/// </summary>
public static readonly PerspexProperty<bool> IsSelectedProperty =
ListBoxItem.IsSelectedProperty.AddOwner<MenuItem>();
/// <summary>
/// Defines the <see cref="IsSubMenuOpen"/> property.
/// </summary>
@ -109,6 +123,15 @@ namespace Perspex.Controls
set { this.SetValue(CommandParameterProperty, value); }
}
/// <summary>
/// Gets or sets the <see cref="MenuItem"/>'s header.
/// </summary>
public object Header
{
get { return this.GetValue(HeaderProperty); }
set { this.SetValue(HeaderProperty, value); }
}
/// <summary>
/// Gets or sets the icon that appears in a <see cref="MenuItem"/>.
/// </summary>
@ -118,6 +141,15 @@ namespace Perspex.Controls
set { this.SetValue(IconProperty, value); }
}
/// <summary>
/// Gets or sets a value indicating whether the <see cref="MenuItem"/> is currently selected.
/// </summary>
public bool IsSelected
{
get { return this.GetValue(IsSelectedProperty); }
set { this.SetValue(IsSelectedProperty, value); }
}
/// <summary>
/// Gets or sets a value that indicates whether the submenu of the <see cref="MenuItem"/> is
/// open.
@ -167,6 +199,20 @@ namespace Perspex.Controls
}
}
/// <summary>
/// Called when a key is pressed in the <see cref="MenuItem"/>.
/// </summary>
/// <param name="e">The event args.</param>
protected override void OnKeyDown(KeyEventArgs e)
{
base.OnKeyDown(e);
if (this.IsTopLevel && e.Key == Key.Down && this.HasSubMenu)
{
this.IsSubMenuOpen = true;
}
}
/// <summary>
/// Called when the pointer enters the <see cref="MenuItem"/>.
/// </summary>
@ -253,7 +299,8 @@ namespace Perspex.Controls
if (popup != null)
{
popup.Opened += this.PopupFirstOpened;
popup.PopupRootCreated += this.PopupRootCreated;
popup.Opened += this.PopupOpened;
}
}
@ -286,38 +333,48 @@ namespace Perspex.Controls
else
{
sender.CloseSubmenus();
sender.SelectedIndex = -1;
}
}
}
private void PopupOpened(object sender, EventArgs e)
{
this.SelectedIndex = 0;
}
/// <summary>
/// Called the first time the MenuItem's popup is opened.
/// Called when the MenuItem's popup root is opened.
/// </summary>
/// <param name="sender">The event sender.</param>
/// <param name="e">The event args.</param>
private void PopupFirstOpened(object sender, EventArgs e)
private void PopupRootCreated(object sender, EventArgs e)
{
var popup = (Popup)sender;
ItemsPresenter presenter = null;
// Our ItemsPresenter is in a Popup which means that it's only created when the
// Popup is opened, therefore it wasn't found by ItemsControl.OnTemplateApplied.
// Now the Popup has been opened for the first time it should exist, so make sure
// the PopupRoot's template is applied and look for the ItemsPresenter.
popup.PopupRoot.ApplyTemplate();
var presenter = popup.PopupRoot.FindControl<ItemsPresenter>("itemsPresenter");
foreach (var c in popup.PopupRoot.GetSelfAndVisualDescendents().OfType<Control>())
{
if (c.Name == "itemsPresenter" && c is ItemsPresenter)
{
presenter = c as ItemsPresenter;
break;
}
c.ApplyTemplate();
}
if (presenter != null)
{
// The presenter was found. First make its Panel's ChildLogicalParent point to
// this so that the child MenuItems will be logically parented by the parent
// MenuItem and then assign it to our Presenter property.
presenter.ApplyTemplate();
((IItemsPanel)presenter.Panel).ChildLogicalParent = this;
// The presenter was found. Set its TemplatedParent so it thinks that it had a
// normal birth; may it never know its own perveristy.
presenter.TemplatedParent = this;
this.Presenter = presenter;
}
// Don't call this event handler again.
popup.Opened -= this.PopupFirstOpened;
}
}
}

4
Perspex.Controls/Popup.cs

@ -49,6 +49,8 @@ namespace Perspex.Controls
public event EventHandler Opened;
public event EventHandler PopupRootCreated;
public Control Child
{
get { return this.GetValue(ChildProperty); }
@ -116,6 +118,8 @@ namespace Perspex.Controls
this.popupRoot.AddHandler(PopupRoot.PointerPressedEvent, this.MaybeClose, RoutingStrategies.Bubble, true);
this.topLevel.AddHandler(TopLevel.PointerPressedEvent, this.MaybeClose, RoutingStrategies.Tunnel);
this.PopupRootCreated?.Invoke(this, EventArgs.Empty);
this.popupRoot.Show();
this.IsOpen = true;
this.Opened?.Invoke(this, EventArgs.Empty);

12
Perspex.Controls/TopLevel.cs

@ -28,7 +28,7 @@ namespace Perspex.Controls
/// <see cref="PopupRoot"/>. It handles scheduling layout, styling and rendering as well as
/// tracking the window <see cref="ClientSize"/> and <see cref="IsActive"/> state.
/// </remarks>
public abstract class TopLevel : ContentControl, IInputRoot, ILayoutRoot, IRenderRoot, ICloseable, IFocusScope
public abstract class TopLevel : ContentControl, IInputRoot, ILayoutRoot, IRenderRoot, ICloseable
{
/// <summary>
/// Defines the <see cref="ClientSize"/> property.
@ -224,7 +224,7 @@ namespace Perspex.Controls
/// </summary>
IAccessKeyHandler IInputRoot.AccessKeyHandler
{
get;
get { return this.accessKeyHandler; }
}
/// <summary>
@ -294,7 +294,13 @@ namespace Perspex.Controls
this.Activated(this, EventArgs.Empty);
}
FocusManager.Instance.SetFocusScope(this);
var scope = this as IFocusScope;
if (scope != null)
{
FocusManager.Instance.SetFocusScope(scope);
}
this.IsActive = true;
}

3
Perspex.Controls/Window.cs

@ -9,12 +9,13 @@ namespace Perspex.Controls
using System;
using System.Reactive.Linq;
using System.Threading.Tasks;
using Perspex.Input;
using Perspex.Media;
using Perspex.Platform;
using Perspex.Styling;
using Splat;
public class Window : TopLevel, IStyleable
public class Window : TopLevel, IStyleable, IFocusScope
{
public static readonly PerspexProperty<string> TitleProperty =
PerspexProperty.Register<Window, string>("Title", "Window");

24
Perspex.Input/AccessKeyHandler.cs

@ -45,16 +45,17 @@ namespace Perspex.Input
this.owner = owner;
this.owner.AddHandler(InputElement.KeyDownEvent, this.OnKeyDown);
this.owner.AddHandler(InputElement.KeyDownEvent, this.OnPreviewKeyDown, RoutingStrategies.Tunnel);
this.owner.AddHandler(InputElement.KeyUpEvent, this.OnPreviewKeyUp, RoutingStrategies.Tunnel);
this.owner.AddHandler(InputElement.PointerPressedEvent, this.OnPreviewPointerPressed, RoutingStrategies.Tunnel);
}
/// <summary>
/// Handles Alt and F10 key presses in the window.
/// Handles the Alt/F10 keys being pressed in the window.
/// </summary>
/// <param name="sender">The event sender.</param>
/// <param name="e">The event args.</param>
protected virtual void OnKeyDown(object sender, KeyEventArgs e)
protected virtual void OnPreviewKeyDown(object sender, KeyEventArgs e)
{
if (e.Key == Key.LeftAlt || e.Key == Key.F10)
{
@ -63,6 +64,23 @@ namespace Perspex.Input
}
}
/// <summary>
/// Handles the Alt/F10 keys being released in the window.
/// </summary>
/// <param name="sender">The event sender.</param>
/// <param name="e">The event args.</param>
protected virtual void OnPreviewKeyUp(object sender, KeyEventArgs e)
{
if (e.Key == Key.LeftAlt || e.Key == Key.F10)
{
if (this.showingAccessKeys && this.MainMenu != null)
{
this.MainMenu.OpenMenu();
e.Handled = true;
}
}
}
/// <summary>
/// Handles pointer presses in the window.
/// </summary>

23
Perspex.Input/FocusManager.cs

@ -9,25 +9,39 @@ namespace Perspex.Input
using System;
using System.Collections.Generic;
using System.Linq;
using Perspex.Interactivity;
using Perspex.VisualTree;
using Splat;
/// <summary>
/// Manages focus for the application.
/// </summary>
public class FocusManager : IFocusManager
{
private Dictionary<IFocusScope, IInputElement> focusScopes =
/// <summary>
/// The focus scopes in which the focus is currently defined.
/// </summary>
private Dictionary<IFocusScope, IInputElement> focusScopes =
new Dictionary<IFocusScope, IInputElement>();
/// <summary>
/// Gets the instance of the <see cref="IFocusManager"/>.
/// </summary>
public static IFocusManager Instance
{
get { return Locator.Current.GetService<IFocusManager>(); }
}
/// <summary>
/// Gets the currently focused <see cref="IInputElement"/>.
/// </summary>
public IInputElement Current
{
get { return KeyboardDevice.Instance.FocusedElement; }
}
/// <summary>
/// Gets the current focus scope.
/// </summary>
public IFocusScope Scope
{
get;
@ -51,6 +65,7 @@ namespace Perspex.Input
if (scope != null)
{
this.Scope = scope;
this.SetFocusedElement(scope, control, keyboardNavigated);
}
}
@ -73,8 +88,8 @@ namespace Perspex.Input
/// will change.
/// </remarks>
public void SetFocusedElement(
IFocusScope scope,
IInputElement element,
IFocusScope scope,
IInputElement element,
bool keyboardNavigated = false)
{
Contract.Requires<ArgumentNullException>(scope != null);

4
Perspex.Input/IMainMenu.cs

@ -11,5 +11,9 @@ namespace Perspex.Input
/// </summary>
public interface IMainMenu
{
/// <summary>
/// Opens the menu in response to the Alt/F10 key.
/// </summary>
void OpenMenu();
}
}

12
Perspex.Themes.Default/MenuItemStyle.cs

@ -41,7 +41,7 @@ namespace Perspex.Themes.Default
new Setter(MenuItem.TemplateProperty, ControlTemplate.Create<MenuItem>(this.TopLevelTemplate)),
},
},
new Style(x => x.OfType<MenuItem>().Class(":pointerover").Template().Name("root"))
new Style(x => x.OfType<MenuItem>().Class(":selected").Template().Name("root"))
{
Setters = new[]
{
@ -49,19 +49,19 @@ namespace Perspex.Themes.Default
new Setter(Border.BorderBrushProperty, new SolidColorBrush(0xff26a0da)),
},
},
new Style(x => x.OfType<MenuItem>().Class(":empty").Template().Name("rightArrow"))
new Style(x => x.OfType<MenuItem>().Class(":pointerover").Template().Name("root"))
{
Setters = new[]
{
new Setter(Path.IsVisibleProperty, false),
new Setter(Border.BackgroundProperty, new SolidColorBrush(0x3d26a0da)),
new Setter(Border.BorderBrushProperty, new SolidColorBrush(0xff26a0da)),
},
},
new Style(x => x.OfType<Menu>().Child().OfType<MenuItem>().PropertyEquals(MenuItem.IsSubMenuOpenProperty, true).Template().Name("root"))
new Style(x => x.OfType<MenuItem>().Class(":empty").Template().Name("rightArrow"))
{
Setters = new[]
{
new Setter(Border.BackgroundProperty, new SolidColorBrush(0x3d26a0da)),
new Setter(Border.BorderBrushProperty, new SolidColorBrush(0xff26a0da)),
new Setter(Path.IsVisibleProperty, false),
},
},
});

24
Tests/Perspex.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs

@ -97,6 +97,28 @@ namespace Perspex.Controls.Primitives.UnitTests
Assert.True(items[1].IsSelected);
}
[Fact]
public void Setting_SelectedIndex_Before_ApplyTemplate_Should_Set_Item_IsSelected_True()
{
var items = new[]
{
new Item(),
new Item(),
};
var target = new Target
{
Items = items,
Template = this.Template(),
};
target.SelectedIndex = 1;
target.ApplyTemplate();
Assert.False(items[0].IsSelected);
Assert.True(items[1].IsSelected);
}
[Fact]
public void Setting_SelectedItem_Should_Set_SelectedIndex()
{
@ -175,7 +197,7 @@ namespace Perspex.Controls.Primitives.UnitTests
new Item(),
new Item(),
};
var target = new Target
{
Items = items,

Loading…
Cancel
Save