diff --git a/Microsoft.Toolkit.Uwp.SampleApp/Microsoft.Toolkit.Uwp.SampleApp.csproj b/Microsoft.Toolkit.Uwp.SampleApp/Microsoft.Toolkit.Uwp.SampleApp.csproj index 3c4e453acdc..0ca17158fd1 100644 --- a/Microsoft.Toolkit.Uwp.SampleApp/Microsoft.Toolkit.Uwp.SampleApp.csproj +++ b/Microsoft.Toolkit.Uwp.SampleApp/Microsoft.Toolkit.Uwp.SampleApp.csproj @@ -269,6 +269,7 @@ + @@ -396,6 +397,7 @@ + @@ -416,6 +418,10 @@ ClipboardHelperPage.xaml + + + MenuPage.xaml + NetworkHelperPage.xaml @@ -669,6 +675,10 @@ MSBuild:Compile Designer + + MSBuild:Compile + Designer + Designer MSBuild:Compile diff --git a/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/Menu/Commands/VsCommands.cs b/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/Menu/Commands/VsCommands.cs new file mode 100644 index 00000000000..0b1677de09f --- /dev/null +++ b/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/Menu/Commands/VsCommands.cs @@ -0,0 +1,66 @@ +// ****************************************************************** +// Copyright (c) Microsoft. All rights reserved. +// This code is licensed under the MIT License (MIT). +// THE CODE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, +// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +// IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +// TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH +// THE CODE OR THE USE OR OTHER DEALINGS IN THE CODE. +// ****************************************************************** + +using System; +using System.Windows.Input; +using Windows.UI.Popups; + +namespace Microsoft.Toolkit.Uwp.SampleApp.Menu.Commands +{ + internal class NewProjectCommand : ICommand + { + public bool CanExecute(object parameter) + { + return true; + } + + public async void Execute(object parameter) + { + var dialog = new MessageDialog("Create New Project"); + await dialog.ShowAsync(); + } + + public event EventHandler CanExecuteChanged; + } + + internal class NewFileCommand : ICommand + { + public bool CanExecute(object parameter) + { + return true; + } + + public async void Execute(object parameter) + { + var dialog = new MessageDialog("Create New File"); + await dialog.ShowAsync(); + } + + public event EventHandler CanExecuteChanged; + } + + internal class GenericCommand : ICommand + { + public bool CanExecute(object parameter) + { + return true; + } + + public async void Execute(object parameter) + { + var dialog = new MessageDialog(parameter.ToString()); + await dialog.ShowAsync(); + } + + public event EventHandler CanExecuteChanged; + } +} diff --git a/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/Menu/Menu.bind b/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/Menu/Menu.bind new file mode 100644 index 00000000000..f927cc0ba6a --- /dev/null +++ b/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/Menu/Menu.bind @@ -0,0 +1,136 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/Menu/Menu.png b/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/Menu/Menu.png new file mode 100644 index 00000000000..61179a3e5c8 Binary files /dev/null and b/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/Menu/Menu.png differ diff --git a/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/Menu/MenuPage.xaml b/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/Menu/MenuPage.xaml new file mode 100644 index 00000000000..d93bf2694a7 --- /dev/null +++ b/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/Menu/MenuPage.xaml @@ -0,0 +1,139 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/Menu/MenuPage.xaml.cs b/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/Menu/MenuPage.xaml.cs new file mode 100644 index 00000000000..44ae500a78d --- /dev/null +++ b/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/Menu/MenuPage.xaml.cs @@ -0,0 +1,45 @@ +// ****************************************************************** +// Copyright (c) Microsoft. All rights reserved. +// This code is licensed under the MIT License (MIT). +// THE CODE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, +// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +// IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +// TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH +// THE CODE OR THE USE OR OTHER DEALINGS IN THE CODE. +// ****************************************************************** + +using Windows.UI.Xaml.Controls; +using Windows.UI.Xaml.Navigation; + +namespace Microsoft.Toolkit.Uwp.SampleApp.SamplePages +{ + public sealed partial class MenuPage + { + public MenuPage() + { + InitializeComponent(); + } + + protected override void OnNavigatedTo(NavigationEventArgs e) + { + base.OnNavigatedTo(e); + + Shell.Current.RegisterNewCommand("Add Item to file menu", (sender, args) => + { + var flyoutItem = new MenuFlyoutItem + { + Text = "Click to remove" + }; + + flyoutItem.Click += (a, b) => + { + FileMenu.Items.Remove(flyoutItem); + }; + + FileMenu.Items.Add(flyoutItem); + }); + } + } +} diff --git a/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/samples.json b/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/samples.json index 29435224e5c..349ec80165b 100644 --- a/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/samples.json +++ b/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/samples.json @@ -228,6 +228,15 @@ "XamlCodeFile": "OrbitViewXaml.bind", "Icon": "/SamplePages/OrbitView/OrbitView.png", "DocumentationUrl": "https://raw.githubusercontent.com/Microsoft/UWPCommunityToolkit/dev/docs/controls/OrbitView.md" + }, + { + "Name": "Menu", + "Type": "MenuPage", + "About": "Represents a Windows menu control that enables you to hierarchically organize elements associated with commands and event handlers.", + "CodeUrl": "https://github.com/Microsoft/UWPCommunityToolkit/tree/master/Microsoft.Toolkit.Uwp.UI.Controls/Menu", + "XamlCodeFile": "Menu.bind", + "Icon": "/SamplePages/Menu/Menu.png", + "DocumentationUrl": "https://raw.githubusercontent.com/Microsoft/UWPCommunityToolkit/dev/docs/controls/Menu.md" } ] }, diff --git a/Microsoft.Toolkit.Uwp.SampleApp/Shell.xaml b/Microsoft.Toolkit.Uwp.SampleApp/Shell.xaml index 6b760d60228..bdb21dfc6dc 100644 --- a/Microsoft.Toolkit.Uwp.SampleApp/Shell.xaml +++ b/Microsoft.Toolkit.Uwp.SampleApp/Shell.xaml @@ -4,15 +4,15 @@ xmlns:controls="using:Microsoft.Toolkit.Uwp.UI.Controls" xmlns:controlsLocal="using:Microsoft.Toolkit.Uwp.SampleApp.Controls" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" - xmlns:local="using:Microsoft.Toolkit.Uwp.SampleApp" - xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:developerTools="using:Microsoft.Toolkit.Uwp.DeveloperTools" xmlns:extensions="using:Microsoft.Toolkit.Uwp.UI.Extensions" + xmlns:local="using:Microsoft.Toolkit.Uwp.SampleApp" + xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" + extensions:StatusBar.IsVisible="False" extensions:TitleBar.BackgroundColor="{StaticResource Grey-01}" extensions:TitleBar.ButtonBackgroundColor="{StaticResource Grey-03}" extensions:TitleBar.ButtonForegroundColor="{StaticResource Grey-04}" extensions:TitleBar.ForegroundColor="{StaticResource Grey-04}" - extensions:StatusBar.IsVisible="False" Loaded="Shell_OnLoaded" SizeChanged="Page_SizeChanged" mc:Ignorable="d"> @@ -66,9 +66,11 @@ Text="Sample App" /> - - + diff --git a/Microsoft.Toolkit.Uwp.UI.Controls/Menu/Menu.Events.cs b/Microsoft.Toolkit.Uwp.UI.Controls/Menu/Menu.Events.cs new file mode 100644 index 00000000000..0556421bd84 --- /dev/null +++ b/Microsoft.Toolkit.Uwp.UI.Controls/Menu/Menu.Events.cs @@ -0,0 +1,342 @@ +// ****************************************************************** +// Copyright (c) Microsoft. All rights reserved. +// This code is licensed under the MIT License (MIT). +// THE CODE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, +// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +// IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +// TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH +// THE CODE OR THE USE OR OTHER DEALINGS IN THE CODE. +// ****************************************************************** + +using System.Linq; +using System.Text; +using Windows.System; +using Windows.UI.Core; +using Windows.UI.Xaml; +using Windows.UI.Xaml.Controls; +using Windows.UI.Xaml.Controls.Primitives; +using Windows.UI.Xaml.Input; + +namespace Microsoft.Toolkit.Uwp.UI.Controls +{ + /// + /// Menu Control defines a menu of choices for users to invoke. + /// + public partial class Menu + { + private const string CtrlValue = "CTRL"; + private const string ShiftValue = "SHIFT"; + private const string AltValue = "ALT"; + private Control _lastFocusElement; + private bool _altHandled; + private bool _isLostFocus = true; + private Control _lastFocusElementBeforeMenu; + + private bool AllowTooltip => (bool)GetValue(AllowTooltipProperty); + + private static bool NavigateUsingKeyboard(object element, KeyEventArgs args, Menu menu, Orientation orientation) + { + if (!menu.IsOpened && element is MenuItem) + { + if (((args.VirtualKey == VirtualKey.Down || args.VirtualKey == VirtualKey.Enter) && orientation == Orientation.Horizontal) || + ((args.VirtualKey == VirtualKey.Right || args.VirtualKey == VirtualKey.Enter) && orientation == Orientation.Vertical)) + { + menu.SelectedMenuItem.ShowMenu(); + return true; + } + + if ((args.VirtualKey == VirtualKey.Left && orientation == Orientation.Horizontal) || + (args.VirtualKey == VirtualKey.Up && orientation == Orientation.Vertical)) + { + GetNextMenuItem(menu, -1); + return true; + } + + if ((args.VirtualKey == VirtualKey.Right && orientation == Orientation.Horizontal) || + (args.VirtualKey == VirtualKey.Down && orientation == Orientation.Vertical)) + { + GetNextMenuItem(menu, +1); + return true; + } + } + + if (args.VirtualKey == VirtualKey.Left) + { + if (element is MenuFlyoutItem) + { + menu.IsInTransitionState = true; + menu.SelectedMenuItem.HideMenu(); + GetNextMenuItem(menu, -1).ShowMenu(); + return true; + } + + if (element is MenuFlyoutSubItem) + { + var menuFlyoutSubItem = (MenuFlyoutSubItem)element; + if (menuFlyoutSubItem.Parent is MenuItem && element == menu._lastFocusElement) + { + menu.IsInTransitionState = true; + menu.SelectedMenuItem.HideMenu(); + GetNextMenuItem(menu, -1).ShowMenu(); + return true; + } + } + } + + if (args.VirtualKey == VirtualKey.Right) + { + if (element is MenuFlyoutItem) + { + menu.IsInTransitionState = true; + menu.SelectedMenuItem.HideMenu(); + GetNextMenuItem(menu, +1).ShowMenu(); + return true; + } + } + + return false; + } + + private static MenuItem GetNextMenuItem(Menu menu, int moveCount) + { + var currentMenuItemIndex = menu.Items.IndexOf(menu.SelectedMenuItem); + var nextIndex = (currentMenuItemIndex + moveCount + menu.Items.Count) % menu.Items.Count; + var nextItem = menu.Items.ElementAt(nextIndex) as MenuItem; + nextItem?.Focus(FocusState.Keyboard); + return nextItem; + } + + private static string MapInputToGestureKey(VirtualKey key) + { + var isCtrlDown = Window.Current.CoreWindow.GetKeyState(VirtualKey.Control).HasFlag(CoreVirtualKeyStates.Down); + var isShiftDown = Window.Current.CoreWindow.GetKeyState(VirtualKey.Shift).HasFlag(CoreVirtualKeyStates.Down); + var isAltDown = Window.Current.CoreWindow.GetKeyState(VirtualKey.Menu).HasFlag(CoreVirtualKeyStates.Down); + + if (!isCtrlDown && !isShiftDown && !isAltDown) + { + return null; + } + + StringBuilder gestureKeyBuilder = new StringBuilder(); + + if (isCtrlDown) + { + gestureKeyBuilder.Append(CtrlValue); + gestureKeyBuilder.Append("+"); + } + + if (isShiftDown) + { + gestureKeyBuilder.Append(ShiftValue); + gestureKeyBuilder.Append("+"); + } + + if (isAltDown) + { + gestureKeyBuilder.Append(AltValue); + gestureKeyBuilder.Append("+"); + } + + if (key == VirtualKey.None) + { + gestureKeyBuilder.Remove(gestureKeyBuilder.Length - 1, 1); + } + else + { + gestureKeyBuilder.Append(key); + } + + return gestureKeyBuilder.ToString(); + } + + private static void OrientationPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + var menu = (Menu)d; + if (menu._wrapPanel != null) + { + menu._wrapPanel.Orientation = menu.Orientation; + foreach (MenuItem menuItem in menu.Items) + { + if (menuItem.FlyoutButton?.Flyout != null) + { + menuItem.FlyoutButton.Flyout.Placement = menu.Orientation == Orientation.Horizontal + ? FlyoutPlacementMode.Bottom + : FlyoutPlacementMode.Right; + } + } + } + } + + private static void RemoveElementFromCache(FrameworkElement descendant) + { + var value = descendant.GetValue(InputGestureTextProperty); + if (value == null) + { + return; + } + + var inputGestureText = value.ToString().ToUpper(); + if (!MenuItemInputGestureCache.ContainsKey(inputGestureText)) + { + return; + } + + var cachedMenuItem = MenuItemInputGestureCache[inputGestureText]; + if (cachedMenuItem == descendant) + { + MenuItemInputGestureCache.Remove(inputGestureText); + } + } + + private void Menu_Loaded(object sender, RoutedEventArgs e) + { + _wrapPanel = ItemsPanelRoot as WrapPanel.WrapPanel; + if (_wrapPanel != null) + { + _wrapPanel.Orientation = Orientation; + } + + LostFocus -= Menu_LostFocus; + LostFocus += Menu_LostFocus; + Dispatcher.AcceleratorKeyActivated -= Dispatcher_AcceleratorKeyActivated; + Window.Current.CoreWindow.KeyDown -= CoreWindow_KeyDown; + Dispatcher.AcceleratorKeyActivated += Dispatcher_AcceleratorKeyActivated; + Window.Current.CoreWindow.KeyDown += CoreWindow_KeyDown; + } + + private void Menu_Unloaded(object sender, RoutedEventArgs e) + { + Dispatcher.AcceleratorKeyActivated -= Dispatcher_AcceleratorKeyActivated; + Window.Current.CoreWindow.KeyDown -= CoreWindow_KeyDown; + + // Clear Cache + foreach (MenuItem menuItem in Items) + { + var menuFlyoutItems = menuItem.GetMenuFlyoutItems(); + foreach (var flyoutItem in menuFlyoutItems) + { + RemoveElementFromCache(flyoutItem); + } + + RemoveElementFromCache(menuItem); + } + } + + private void CoreWindow_KeyDown(CoreWindow sender, KeyEventArgs args) + { + if (IsInTransitionState) + { + return; + } + + var element = FocusManager.GetFocusedElement(); + + if (NavigateUsingKeyboard(element, args, this, Orientation)) + { + return; + } + + string gestureKey = MapInputToGestureKey(args.VirtualKey); + + if (gestureKey == null) + { + return; + } + + if (MenuItemInputGestureCache.ContainsKey(gestureKey)) + { + var cachedMenuItem = MenuItemInputGestureCache[gestureKey]; + if (cachedMenuItem is MenuFlyoutItem) + { + var menuFlyoutItem = (MenuFlyoutItem)cachedMenuItem; + menuFlyoutItem.Command?.Execute(menuFlyoutItem.CommandParameter); + } + } + } + + private void Menu_LostFocus(object sender, RoutedEventArgs e) + { + _isLostFocus = true; + + if (AllowTooltip) + { + HideSubItemTooltips(); + } + } + + private void Dispatcher_AcceleratorKeyActivated(CoreDispatcher sender, AcceleratorKeyEventArgs args) + { + _lastFocusElement = FocusManager.GetFocusedElement() as Control; + if (args.VirtualKey == VirtualKey.Menu && !args.KeyStatus.WasKeyDown) + { + _altHandled = false; + } + else if (args.KeyStatus.IsMenuKeyDown && args.KeyStatus.IsKeyReleased && !_altHandled) + { + _altHandled = true; + string gestureKey = MapInputToGestureKey(args.VirtualKey); + + if (gestureKey == null) + { + return; + } + + if (MenuItemInputGestureCache.ContainsKey(gestureKey)) + { + var cachedMenuItem = MenuItemInputGestureCache[gestureKey]; + if (cachedMenuItem is MenuItem) + { + var menuItem = (MenuItem)cachedMenuItem; + menuItem.ShowMenu(); + menuItem.Focus(FocusState.Keyboard); + } + } + } + else if (args.VirtualKey == VirtualKey.Menu && args.KeyStatus.IsKeyReleased && !_altHandled) + { + _altHandled = true; + if (!IsOpened) + { + if (_isLostFocus) + { + LostFocus -= Menu_LostFocus; + Focus(FocusState.Programmatic); + _lastFocusElementBeforeMenu = _lastFocusElement; + _isLostFocus = false; + + if (AllowTooltip) + { + ShowSubItemToolTips(); + } + + LostFocus += Menu_LostFocus; + } + else + { + _lastFocusElementBeforeMenu?.Focus(FocusState.Keyboard); + } + } + } + } + + private void ShowSubItemToolTips() + { + foreach (var item in Items) + { + var i = item as MenuItem; + i?.ShowTooltip(); + } + } + + private void HideSubItemTooltips() + { + foreach (var item in Items) + { + var i = item as MenuItem; + i?.HideTooltip(); + } + } + } +} diff --git a/Microsoft.Toolkit.Uwp.UI.Controls/Menu/Menu.Extensions.cs b/Microsoft.Toolkit.Uwp.UI.Controls/Menu/Menu.Extensions.cs new file mode 100644 index 00000000000..12d4989c478 --- /dev/null +++ b/Microsoft.Toolkit.Uwp.UI.Controls/Menu/Menu.Extensions.cs @@ -0,0 +1,95 @@ +// ****************************************************************** +// Copyright (c) Microsoft. All rights reserved. +// This code is licensed under the MIT License (MIT). +// THE CODE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, +// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +// IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +// TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH +// THE CODE OR THE USE OR OTHER DEALINGS IN THE CODE. +// ****************************************************************** + +using Windows.UI.Xaml; + +namespace Microsoft.Toolkit.Uwp.UI.Controls +{ + /// + /// Menu Control defines a menu of choices for users to invoke. + /// + public partial class Menu + { + private const string InputGestureTextName = "InputGestureText"; + private const string AllowTooltipName = "AllowTooltip"; + + /// + /// Sets the text describing an input gesture that will call the command tied to the specified item. + /// + public static readonly DependencyProperty InputGestureTextProperty = DependencyProperty.RegisterAttached(InputGestureTextName, typeof(string), typeof(FrameworkElement), new PropertyMetadata(null, InputGestureTextChanged)); + + private static void InputGestureTextChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e) + { + var element = sender as FrameworkElement; + + var inputGestureValue = element?.GetValue(InputGestureTextProperty).ToString(); + if (string.IsNullOrEmpty(inputGestureValue)) + { + return; + } + + inputGestureValue = inputGestureValue.ToUpper(); + if (MenuItemInputGestureCache.ContainsKey(inputGestureValue)) + { + MenuItemInputGestureCache[inputGestureValue] = element; + return; + } + + MenuItemInputGestureCache.Add(inputGestureValue.ToUpper(), element); + } + + /// + /// Gets InputGestureText attached property + /// + /// Target MenuFlyoutItem + /// Input gesture text + public static string GetInputGestureText(FrameworkElement obj) + { + return (string)obj.GetValue(InputGestureTextProperty); + } + + /// + /// Sets InputGestureText attached property + /// + /// Target MenuFlyoutItem + /// Input gesture text + public static void SetInputGestureText(FrameworkElement obj, string value) + { + obj.SetValue(InputGestureTextProperty, value); + } + + /// + /// Gets or sets a value indicating whether to allow tooltip on alt or not + /// + public static readonly DependencyProperty AllowTooltipProperty = DependencyProperty.RegisterAttached(AllowTooltipName, typeof(bool), typeof(Menu), new PropertyMetadata(false)); + + /// + /// Gets AllowTooltip attached property + /// + /// Target Menu + /// AllowTooltip + public static bool GetAllowTooltip(Menu obj) + { + return (bool)obj.GetValue(AllowTooltipProperty); + } + + /// + /// Sets AllowTooltip attached property + /// + /// Target Menu + /// AllowTooltip + public static void SetAllowTooltip(Menu obj, bool value) + { + obj.SetValue(AllowTooltipProperty, value); + } + } +} diff --git a/Microsoft.Toolkit.Uwp.UI.Controls/Menu/Menu.cs b/Microsoft.Toolkit.Uwp.UI.Controls/Menu/Menu.cs new file mode 100644 index 00000000000..d732a766fc8 --- /dev/null +++ b/Microsoft.Toolkit.Uwp.UI.Controls/Menu/Menu.cs @@ -0,0 +1,140 @@ +// ****************************************************************** +// Copyright (c) Microsoft. All rights reserved. +// This code is licensed under the MIT License (MIT). +// THE CODE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, +// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +// IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +// TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH +// THE CODE OR THE USE OR OTHER DEALINGS IN THE CODE. +// ****************************************************************** + +using System.Collections.Generic; +using Windows.UI.Xaml; +using Windows.UI.Xaml.Controls; +using Windows.UI.Xaml.Controls.Primitives; + +namespace Microsoft.Toolkit.Uwp.UI.Controls +{ + /// + /// Menu Control defines a menu of choices for users to invoke. + /// + public partial class Menu : ItemsControl + { + private WrapPanel.WrapPanel _wrapPanel; + + /// + /// Initializes a new instance of the class. + /// + public Menu() + { + DefaultStyleKey = typeof(Menu); + } + + // even if we have multiple menus in the same page we need only one cache because only one menu item will have certain short cut. + private static readonly Dictionary MenuItemInputGestureCache = new Dictionary(); + + /// + /// Gets or sets the orientation of the Menu, Horizontal or vertical means that child controls will be added horizontally + /// until the width of the panel can't fit more control then a new row is added to fit new horizontal added child controls, + /// vertical means that child will be added vertically until the height of the panel is received then a new column is added + /// + public Orientation Orientation + { + get { return (Orientation)GetValue(OrientationProperty); } + set { SetValue(OrientationProperty, value); } + } + + /// + /// Identifies the dependency property. + /// + public static readonly DependencyProperty OrientationProperty = + DependencyProperty.Register( + nameof(Orientation), + typeof(Orientation), + typeof(Menu), + new PropertyMetadata(Orientation.Horizontal, OrientationPropertyChanged)); + + /// + /// Identifies the dependency property. + /// + public static readonly DependencyProperty MenuFlyoutStyleProperty = DependencyProperty.Register(nameof(MenuFlyoutStyle), typeof(Style), typeof(MenuItem), new PropertyMetadata(default(Style))); + + /// + /// Gets or sets the menu style for MenuItem + /// + public Style MenuFlyoutStyle + { + get { return (Style)GetValue(MenuFlyoutStyleProperty); } + set { SetValue(MenuFlyoutStyleProperty, value); } + } + + /// + /// Identifies the dependency property. + /// + public static readonly DependencyProperty TooltipStyleProperty = DependencyProperty.Register(nameof(TooltipStyle), typeof(Style), typeof(Menu), new PropertyMetadata(default(Style))); + + /// + /// Gets or sets the tooltip styles for menu + /// + public Style TooltipStyle + { + get { return (Style)GetValue(TooltipStyleProperty); } + set { SetValue(TooltipStyleProperty, value); } + } + + /// + /// Identifies the dependency property. + /// + public static readonly DependencyProperty TooltipPlacementProperty = DependencyProperty.Register(nameof(TooltipPlacement), typeof(PlacementMode), typeof(Menu), new PropertyMetadata(default(PlacementMode))); + + /// + /// Gets or sets the tooltip placement on menu + /// + public PlacementMode TooltipPlacement + { + get { return (PlacementMode)GetValue(TooltipPlacementProperty); } + set { SetValue(TooltipPlacementProperty, value); } + } + + /// + /// Gets the current selected menu header item + /// + public MenuItem SelectedMenuItem { get; internal set; } + + /// + /// Gets a value indicating whether the menu is opened or not + /// + public bool IsOpened { get; internal set; } + + /// + /// Gets or sets a value indicating whether the menu is in transition state between menu closing state and menu opened state. + /// + internal bool IsInTransitionState { get; set; } + + /// + protected override void OnApplyTemplate() + { + Loaded -= Menu_Loaded; + Unloaded -= Menu_Unloaded; + + Loaded += Menu_Loaded; + Unloaded += Menu_Unloaded; + + base.OnApplyTemplate(); + } + + /// + protected override bool IsItemItsOwnContainerOverride(object item) + { + return item is MenuItem; + } + + /// + protected override DependencyObject GetContainerForItemOverride() + { + return new MenuItem(); + } + } +} diff --git a/Microsoft.Toolkit.Uwp.UI.Controls/Menu/Menu.xaml b/Microsoft.Toolkit.Uwp.UI.Controls/Menu/Menu.xaml new file mode 100644 index 00000000000..2e839fa10f1 --- /dev/null +++ b/Microsoft.Toolkit.Uwp.UI.Controls/Menu/Menu.xaml @@ -0,0 +1,72 @@ + + + + + + + diff --git a/Microsoft.Toolkit.Uwp.UI.Controls/Menu/MenuItem.cs b/Microsoft.Toolkit.Uwp.UI.Controls/Menu/MenuItem.cs new file mode 100644 index 00000000000..0b4ccac10f4 --- /dev/null +++ b/Microsoft.Toolkit.Uwp.UI.Controls/Menu/MenuItem.cs @@ -0,0 +1,305 @@ +// ****************************************************************** +// Copyright (c) Microsoft. All rights reserved. +// This code is licensed under the MIT License (MIT). +// THE CODE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, +// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +// IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +// TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH +// THE CODE OR THE USE OR OTHER DEALINGS IN THE CODE. +// ****************************************************************** + +using System.Collections.Generic; +using System.Linq; +using Windows.Foundation.Collections; +using Windows.Foundation.Metadata; +using Windows.UI.Xaml; +using Windows.UI.Xaml.Controls; +using Windows.UI.Xaml.Controls.Primitives; +using Windows.UI.Xaml.Input; + +namespace Microsoft.Toolkit.Uwp.UI.Controls +{ + /// + /// Menu Item is the items main container for Class Menu control + /// + public class MenuItem : ItemsControl + { + private const string FlyoutButtonName = "FlyoutButton"; + private readonly bool _isAccessKeySupported = ApiInformation.IsApiContractPresent("Windows.Foundation.UniversalApiContract", 3); + private Menu _parentMenu; + private bool _isOpened; + private MenuFlyout _menuFlyout; + + internal Button FlyoutButton { get; private set; } + + /// + /// Identifies the dependency property. + /// + public static readonly DependencyProperty HeaderProperty = DependencyProperty.Register(nameof(Header), typeof(string), typeof(MenuItem), new PropertyMetadata(default(string))); + + /// + /// Gets or sets the title to appear in the title bar + /// + public string Header + { + get { return (string)GetValue(HeaderProperty); } + set { SetValue(HeaderProperty, value); } + } + + /// + /// Gets a value indicating whether the menu is opened or not + /// + public bool IsOpened + { + get + { + return _isOpened; + } + + private set + { + _parentMenu.IsOpened = _isOpened = value; + } + } + + /// + /// Initializes a new instance of the class. + /// + public MenuItem() + { + DefaultStyleKey = typeof(MenuItem); + IsFocusEngagementEnabled = true; + } + + /// + /// This method is used to show the menu for current item + /// + public void ShowMenu() + { + FlyoutButton?.Flyout?.ShowAt(FlyoutButton); + } + + /// + /// This method is used to hide the menu for current item + /// + public void HideMenu() + { + FlyoutButton?.Flyout?.Hide(); + } + + /// + protected override void OnApplyTemplate() + { + FlyoutButton = GetTemplateChild(FlyoutButtonName) as Button; + _parentMenu = this.FindAscendant(); + IsOpened = false; + + Items.VectorChanged -= Items_VectorChanged; + + if (_menuFlyout == null) + { + _menuFlyout = new MenuFlyout(); + } + else + { + _menuFlyout.Opened -= MenuFlyout_Opened; + _menuFlyout.Closed -= MenuFlyout_Closed; + } + + if (FlyoutButton != null) + { + FlyoutButton.PointerExited -= FlyoutButton_PointerExited; + Items.VectorChanged += Items_VectorChanged; + + _menuFlyout.Placement = _parentMenu.Orientation == Orientation.Horizontal + ? FlyoutPlacementMode.Bottom + : FlyoutPlacementMode.Right; + + _menuFlyout.Opened += MenuFlyout_Opened; + _menuFlyout.Closed += MenuFlyout_Closed; + FlyoutButton.PointerExited += FlyoutButton_PointerExited; + + _menuFlyout.MenuFlyoutPresenterStyle = _parentMenu.MenuFlyoutStyle; + ReAddItemsToFlyout(); + + FlyoutButton.Flyout = _menuFlyout; + + if (_isAccessKeySupported) + { + FlyoutButton.AccessKey = AccessKey; + AccessKey = string.Empty; + } + } + + base.OnApplyTemplate(); + } + + internal IEnumerable GetMenuFlyoutItems() + { + var allItems = new List(); + var menuFlyout = FlyoutButton.Flyout as MenuFlyout; + if (menuFlyout != null) + { + GetMenuFlyoutItemItems(menuFlyout.Items, allItems); + } + + return allItems; + } + + private void GetMenuFlyoutItemItems(IList menuFlyoutItems, List allItems) + { + foreach (var menuFlyoutItem in menuFlyoutItems) + { + allItems.Add(menuFlyoutItem); + + if (menuFlyoutItem is MenuFlyoutSubItem) + { + var menuItem = (MenuFlyoutSubItem)menuFlyoutItem; + GetMenuFlyoutItemItems(menuItem.Items, allItems); + } + } + } + + internal void ShowTooltip() + { + var inputGestureText = GetValue(Menu.InputGestureTextProperty) as string; + if (string.IsNullOrEmpty(inputGestureText)) + { + return; + } + + var tooltip = ToolTipService.GetToolTip(FlyoutButton) as ToolTip; + if (tooltip == null) + { + tooltip = new ToolTip(); + tooltip.Style = _parentMenu.TooltipStyle; + ToolTipService.SetToolTip(FlyoutButton, tooltip); + } + + tooltip.Placement = _parentMenu.TooltipPlacement; + tooltip.Content = RemoveAlt(inputGestureText); + tooltip.IsOpen = !tooltip.IsOpen; + } + + private string RemoveAlt(string inputGesture) + { + if (string.IsNullOrEmpty(inputGesture)) + { + return string.Empty; + } + + return inputGesture.Replace("Alt+", string.Empty); + } + + internal void HideTooltip() + { + var tooltip = ToolTipService.GetToolTip(FlyoutButton) as ToolTip; + if (tooltip != null) + { + tooltip.IsOpen = false; + } + } + + private void ReAddItemsToFlyout() + { + if (_menuFlyout == null) + { + return; + } + + _menuFlyout.Items.Clear(); + foreach (var item in Items) + { + AddItemToFlyout(item); + } + } + + private void AddItemToFlyout(object item) + { + var menuItem = item as MenuFlyoutItemBase; + if (menuItem != null) + { + _menuFlyout.Items.Add(menuItem); + } + else + { + var newMenuItem = new MenuFlyoutItem(); + newMenuItem.DataContext = item; + _menuFlyout.Items.Add(newMenuItem); + } + } + + private void InsertItemToFlyout(object item, int index) + { + var menuItem = item as MenuFlyoutItemBase; + if (menuItem != null) + { + _menuFlyout.Items.Insert(index, menuItem); + } + else + { + var newMenuItem = new MenuFlyoutItem(); + newMenuItem.DataContext = item; + _menuFlyout.Items.Insert(index, newMenuItem); + } + } + + private void Items_VectorChanged(IObservableVector sender, IVectorChangedEventArgs e) + { + var index = (int)e.Index; + switch (e.CollectionChange) + { + case CollectionChange.Reset: + ReAddItemsToFlyout(); + break; + case CollectionChange.ItemInserted: + AddItemToFlyout(sender.ElementAt(index)); + break; + case CollectionChange.ItemRemoved: + _menuFlyout.Items.RemoveAt(index); + break; + case CollectionChange.ItemChanged: + _menuFlyout.Items.RemoveAt(index); + InsertItemToFlyout(sender.ElementAt(index), index); + break; + } + } + + private void FlyoutButton_PointerExited(object sender, PointerRoutedEventArgs e) + { + if (IsOpened) + { + VisualStateManager.GoToState(this, "Opened", true); + } + } + + private void MenuFlyout_Closed(object sender, object e) + { + IsOpened = false; + VisualStateManager.GoToState(this, "Normal", true); + } + + private void MenuFlyout_Opened(object sender, object e) + { + IsOpened = true; + VisualStateManager.GoToState(this, "Opened", true); + _parentMenu.IsInTransitionState = false; + } + + /// + protected override void OnTapped(TappedRoutedEventArgs e) + { + _parentMenu.SelectedMenuItem = this; + base.OnTapped(e); + } + + /// + protected override void OnGotFocus(RoutedEventArgs e) + { + _parentMenu.SelectedMenuItem = this; + base.OnGotFocus(e); + } + } +} diff --git a/Microsoft.Toolkit.Uwp.UI.Controls/Microsoft.Toolkit.Uwp.UI.Controls.csproj b/Microsoft.Toolkit.Uwp.UI.Controls/Microsoft.Toolkit.Uwp.UI.Controls.csproj index 86b1301a0c2..604f3bfecaa 100644 --- a/Microsoft.Toolkit.Uwp.UI.Controls/Microsoft.Toolkit.Uwp.UI.Controls.csproj +++ b/Microsoft.Toolkit.Uwp.UI.Controls/Microsoft.Toolkit.Uwp.UI.Controls.csproj @@ -56,6 +56,10 @@ + + + + @@ -180,6 +184,11 @@ Designer XamlIntelliSenseFileGenerator + + MSBuild:Compile + Designer + PreserveNewest + MSBuild:Compile Designer diff --git a/Microsoft.Toolkit.Uwp.UI.Controls/Themes/Generic.xaml b/Microsoft.Toolkit.Uwp.UI.Controls/Themes/Generic.xaml index 5f1669137f2..c0a6e20ed19 100644 --- a/Microsoft.Toolkit.Uwp.UI.Controls/Themes/Generic.xaml +++ b/Microsoft.Toolkit.Uwp.UI.Controls/Themes/Generic.xaml @@ -22,5 +22,6 @@ + \ No newline at end of file diff --git a/docs/controls/Menu.md b/docs/controls/Menu.md new file mode 100644 index 00000000000..2fbd8e1570a --- /dev/null +++ b/docs/controls/Menu.md @@ -0,0 +1,140 @@ +# Menu Control + +The **Menu Control** defines a menu of choices for users to invoke, it is inheriting from `ItemsControl`. The default ItemsPanel for the menu control is `WrapPanel` and it only supports MenuItem as an item\children. + +### How it works + +The **Menu Control** positions it's items the way the WrapPanel does based on the selected orientation Virtical\Horizontal (Developers can change the control ItemsPanel). The Menu items must be of type MenuItem, each MenuItem can be opened using keyboard or pointer. + +**MenuItem** is inheriting from `ItemsControl` and the allowed controls must be derived from `MenuFlyoutItemBase` like `MenuFlyoutSubItem`, `MenuFlyoutItem`, etc... + +To invoke any command on any Menu, MenuItem or MenuFlyoutItem you must use property `InputGestureText` + +If the tooltip is allowed on the Menu control when clicking Alt a tooltip with the input gesture text will show\hide. + +![Menu Overview](../resources/images/Menu.png "Menu") + +## Syntax + +```xaml + + + + + + + + + + +``` + +## External Properties + +### InputGestureText +Sets the text describing an input gesture that will call the command tied to the specified item or to open the MenuItem FlyoutMenu. ex (Alt+F) + +`Note`: InputGestureText supports Ctrl, Alt or Shift. + +### AllowTooltip +Specify whether to allow tooltip on Alt click or not. + +## Menu Properties + +### Orientation +Gets or sets the orientation of the Menu, Horizontal or vertical means that child controls will be added horizontally until the width of the panel can't fit more control then a new row is added to fit new horizontal added child controls, vertical means that child will be added vertically until the height of the panel is received then a new column is added + +### MenuFlyoutStyle +Gets or sets the FlyoutMenu style for MenuItem. + +### TooltipStyle +Gets or sets the tooltip styles for MenuItem. + +### TooltipPlacement +Gets or sets the tooltip PlacementMode on MenuItem. (Bottom,Right,Mouse,Left and Top) + +### SelectedHeaderItem +Gets the current selected MenuItem. + +### IsOpened +Gets a value indicating whether the menu is opened or not. + +## Example Code + +[Menu Sample Page](https://github.com/Microsoft/UWPCommunityToolkit/tree/master/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/Menu) + +The following sample demonstrates how to add Menu Control. + +```xml + + + + + + + + + + + + + + + + + + + + +``` + +``` CSharp + +internal class NewProjectCommand : ICommand + { + public bool CanExecute(object parameter) + { + return true; + } + + public async void Execute(object parameter) + { + var dialog = new MessageDialog("Create New Project"); + await dialog.ShowAsync(); + } + + public event EventHandler CanExecuteChanged; + } + +``` + +## Default Template + +[Menu XAML File](https://github.com/Microsoft/UWPCommunityToolkit/blob/master/Microsoft.Toolkit.Uwp.UI.Controls/Menu/MenuPage.xaml) is the XAML template used in the toolkit for the default styling. + +## Requirements (Windows 10 Device Family) + +| [Device family](http://go.microsoft.com/fwlink/p/?LinkID=526370) | Universal, 10.0.10586.0 or higher | +| --- | --- | +| Namespace | Microsoft.Toolkit.Uwp.UI.Controls | + +## API + +* [Menu source code](https://github.com/Microsoft/UWPCommunityToolkit/tree/master/Microsoft.Toolkit.Uwp.UI.Controls/Menu) diff --git a/docs/resources/images/Menu.png b/docs/resources/images/Menu.png new file mode 100644 index 00000000000..61179a3e5c8 Binary files /dev/null and b/docs/resources/images/Menu.png differ diff --git a/readme.md b/readme.md index 01cd85a4d95..5719195a03c 100644 --- a/readme.md +++ b/readme.md @@ -74,6 +74,7 @@ Once you search you should see a list similar to the one below (versions may be * [TextBoxRegex](http://docs.uwpcommunitytoolkit.com/en/master/controls/TextBoxRegex/) * [TileControl](http://docs.uwpcommunitytoolkit.com/en/master/controls/TileControl/) * [WrapPanel](http://docs.uwpcommunitytoolkit.com/en/master/controls/WrapPanel/) +* [Menu](http://docs.uwpcommunitytoolkit.com/en/master/controls/Menu/) ### Code Helpers