diff --git a/Microsoft.Toolkit.Uwp.SampleApp/Microsoft.Toolkit.Uwp.SampleApp.csproj b/Microsoft.Toolkit.Uwp.SampleApp/Microsoft.Toolkit.Uwp.SampleApp.csproj index b0e9f84eb42..1ae55ca4c84 100644 --- a/Microsoft.Toolkit.Uwp.SampleApp/Microsoft.Toolkit.Uwp.SampleApp.csproj +++ b/Microsoft.Toolkit.Uwp.SampleApp/Microsoft.Toolkit.Uwp.SampleApp.csproj @@ -358,6 +358,7 @@ + @@ -546,6 +547,11 @@ IsNullOrEmptyStateTriggerPage.xaml + + + VoiceCommandsPage.xaml + + WrapLayoutPage.xaml @@ -1350,6 +1356,10 @@ Designer MSBuild:Compile + + MSBuild:Compile + Designer + Designer MSBuild:Compile diff --git a/Microsoft.Toolkit.Uwp.SampleApp/Package.appxmanifest b/Microsoft.Toolkit.Uwp.SampleApp/Package.appxmanifest index c9118ddbe4f..5cea81f4dcb 100644 --- a/Microsoft.Toolkit.Uwp.SampleApp/Package.appxmanifest +++ b/Microsoft.Toolkit.Uwp.SampleApp/Package.appxmanifest @@ -48,5 +48,6 @@ + \ No newline at end of file diff --git a/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/VoiceCommands/ClickAction.cs b/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/VoiceCommands/ClickAction.cs new file mode 100644 index 00000000000..15f40edab1a --- /dev/null +++ b/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/VoiceCommands/ClickAction.cs @@ -0,0 +1,27 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Microsoft.Xaml.Interactivity; +using Windows.UI.Xaml; +using Windows.UI.Xaml.Automation.Peers; +using Windows.UI.Xaml.Automation.Provider; +using Windows.UI.Xaml.Controls; + +namespace Microsoft.Toolkit.Uwp.SampleApp.SamplePages +{ + public class ClickAction : DependencyObject, IAction + { + public object Execute(object sender, object parameter) + { + if (sender is Button btn && btn.IsEnabled) + { + var peer = new ButtonAutomationPeer(btn); + var invokeProv = peer.GetPattern(PatternInterface.Invoke) as IInvokeProvider; + invokeProv?.Invoke(); + } + + return null; + } + } +} diff --git a/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/VoiceCommands/SelectListBoxItemAction.cs b/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/VoiceCommands/SelectListBoxItemAction.cs new file mode 100644 index 00000000000..7e016bab64d --- /dev/null +++ b/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/VoiceCommands/SelectListBoxItemAction.cs @@ -0,0 +1,56 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Microsoft.Xaml.Interactivity; +using Windows.UI.Xaml; +using Windows.UI.Xaml.Controls; + +namespace Microsoft.Toolkit.Uwp.SampleApp.SamplePages +{ + public class SelectListBoxItemAction : DependencyObject, IAction + { + public Direction MoveDirection + { + get => (Direction)GetValue(MoveDirectionProperty); + set => SetValue(MoveDirectionProperty, value); + } + + public static readonly DependencyProperty MoveDirectionProperty = DependencyProperty.Register(nameof(MoveDirection), typeof(Direction), typeof(SelectListBoxItemAction), new PropertyMetadata(default(Direction))); + + public object Execute(object sender, object parameter) + { + if (sender is ListBox lb) + { + var selectedIndex = lb.SelectedIndex; + switch (MoveDirection) + { + case Direction.Up when selectedIndex > 0: + lb.SelectedIndex--; + break; + case Direction.Down when selectedIndex < lb.Items.Count - 1: + lb.SelectedIndex++; + break; + case Direction.First when lb.Items.Count > 0: + lb.SelectedIndex = 0; + break; + case Direction.Last when lb.Items.Count > 0: + lb.SelectedIndex = lb.Items.Count - 1; + break; + default: + break; + } + } + + return null; + } + + public enum Direction + { + Up, + Down, + First, + Last + } + } +} diff --git a/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/VoiceCommands/VoiceCommands.png b/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/VoiceCommands/VoiceCommands.png new file mode 100644 index 00000000000..06aa81ad292 Binary files /dev/null and b/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/VoiceCommands/VoiceCommands.png differ diff --git a/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/VoiceCommands/VoiceCommandsPage.xaml b/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/VoiceCommands/VoiceCommandsPage.xaml new file mode 100644 index 00000000000..fad852ff5ac --- /dev/null +++ b/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/VoiceCommands/VoiceCommandsPage.xaml @@ -0,0 +1,179 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/VoiceCommands/VoiceCommandsPage.xaml.cs b/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/VoiceCommands/VoiceCommandsPage.xaml.cs new file mode 100644 index 00000000000..a1b03843ea1 --- /dev/null +++ b/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/VoiceCommands/VoiceCommandsPage.xaml.cs @@ -0,0 +1,95 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using Microsoft.Toolkit.Uwp.UI.Behaviors; +using Windows.UI.Popups; +using Windows.UI.Xaml; +using Windows.UI.Xaml.Controls; + +namespace Microsoft.Toolkit.Uwp.SampleApp.SamplePages +{ + /// + /// An empty page that can be used on its own or navigated to within a Frame. + /// + public sealed partial class VoiceCommandsPage : Page + { + public VoiceCommandsPage() + { + this.InitializeComponent(); + this.Loaded += this.VoiceCommandsPage_Loaded; + } + + private async void VoiceCommandsPage_Loaded(object sender, RoutedEventArgs e) + { + if (VoiceCommandTrigger.SpeechRecognizer is null) + { + VoiceCommandTrigger.SpeechRecognizer = await WindowsMediaSpeechRecognizer.CreateAsync(Window.Current); + } + } + + private void ButtonAdd_Click(object sender, RoutedEventArgs e) + { + MoveItem(listBoxAvailable, listBoxSelected); + } + + private void MoveItem(ListBox from, ListBox to) + { + var selectedIndex = from.SelectedIndex; + var selectedItem = from.SelectedItem; + if (selectedIndex > -1) + { + from.Items.RemoveAt(selectedIndex); + from.SelectedIndex = Math.Min(selectedIndex, from.Items.Count - 1); + to.Items.Add(selectedItem); + to.SelectedIndex = to.Items.Count - 1; + } + } + + private void ButtonDelete_Click(object sender, RoutedEventArgs e) + { + MoveItem(listBoxSelected, listBoxAvailable); + } + + private void ButtonAppend_Click(object sender, RoutedEventArgs e) + { + if (!string.IsNullOrWhiteSpace(textBoxExtraItem.Text)) + { + listBoxSelected.Items.Add(new ListBoxItem + { + Content = textBoxExtraItem.Text + }); + listBoxSelected.SelectedIndex = listBoxSelected.Items.Count - 1; + textBoxExtraItem.Text = string.Empty; + } + } + + public void WhatCanISay() + { + string content = "You can speak the following voice commands: \r\n" + + "- What can I say\r\n" + + "- Help \r\n" + + "- Add\r\n" + + "- Delete\r\n" + + "- Move avaiable up\r\n" + + "- Move avaiable down\r\n" + + "- Move avaiable to First\r\n" + + "- Move avaiable to Last\r\n" + + "- Move selected up\r\n" + + "- Move selected down\r\n" + + "- Move selected to First\r\n" + + "- Move selected to Last"; + _ = new MessageDialog(content, "What can I Say").ShowAsync(); + } + + private void ToggleListning_Toggled(object sender, RoutedEventArgs e) + { + if (triggerListning is object) + { + triggerListning.Text = toggleListning.IsOn ? "Voice Off" : "Voice On"; + actionListning.Value = !toggleListning.IsOn; + } + } + } +} diff --git a/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/samples.json b/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/samples.json index 5b356756f2d..464eb217664 100644 --- a/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/samples.json +++ b/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/samples.json @@ -864,7 +864,7 @@ { "Name": "ViewportBehavior", "Type": "ViewportBehaviorPage", - "Subcategory": "Systems", + "Subcategory": "Behaviors", "About": "Behavior for listening element enter or exit viewport", "CodeUrl": "https://github.com/windows-toolkit/WindowsCommunityToolkit/tree/master/Microsoft.Toolkit.Uwp.UI/Behaviors", "CodeFile": "ViewportBehaviorCode.bind", @@ -875,7 +875,7 @@ { "Name": "AutoFocusBehavior", "Type": "AutoFocusBehaviorPage", - "Subcategory": "Systems", + "Subcategory": "Behaviors", "About": "Behavior to automatically set the focus on a control when it loads", "CodeUrl": "https://github.com/windows-toolkit/WindowsCommunityToolkit/tree/master/Microsoft.Toolkit.Uwp.UI/Behaviors/", "XamlCodeFile": "AutoFocusBehaviorXaml.bind", @@ -885,13 +885,22 @@ { "Name": "FocusBehavior", "Type": "FocusBehaviorPage", - "Subcategory": "Systems", + "Subcategory": "Behaviors", "About": "Behavior to automatically set the focus on the first control which accepts it", "CodeUrl": "https://github.com/windows-toolkit/WindowsCommunityToolkit/tree/master/Microsoft.Toolkit.Uwp.UI/Behaviors/", "CodeFile": "FocusBehaviorXaml.bind", "Icon": "/Assets/Helpers.png", "DocumentationUrl": "https://raw.githubusercontent.com/MicrosoftDocs/WindowsCommunityToolkitDocs/master/docs/behaviors/FocusBehavior.md" }, + { + "Name": "VoiceCommands", + "Type": "VoiceCommandsPage", + "Subcategory": "Behaviors", + "About": "Behavior to trigger Actions using a voice command", + "CodeUrl": "https://github.com/windows-toolkit/WindowsCommunityToolkit/tree/master/Microsoft.Toolkit.Uwp.UI/Behaviors/", + "Icon": "/SamplePages/VoiceCommands/VoiceCommands.png", + "DocumentationUrl": "https://raw.githubusercontent.com/MicrosoftDocs/WindowsCommunityToolkitDocs/master/docs/behaviors/FocusBehavior.md" + }, { "Name": "Markdown Parser", "Type": "MarkdownParserPage", diff --git a/Microsoft.Toolkit.Uwp.UI.Behaviors/VoiceCommands/ISpeechRecognitionResult.cs b/Microsoft.Toolkit.Uwp.UI.Behaviors/VoiceCommands/ISpeechRecognitionResult.cs new file mode 100644 index 00000000000..ddfe3d18c06 --- /dev/null +++ b/Microsoft.Toolkit.Uwp.UI.Behaviors/VoiceCommands/ISpeechRecognitionResult.cs @@ -0,0 +1,22 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace Microsoft.Toolkit.Uwp.UI.Behaviors +{ + /// + /// Service used to report a recognized speech result + /// + public interface ISpeechRecognitionResult + { + /// + /// Gets the Text of the recognized speech + /// + string Text { get; } + + /// + /// Gets the RawConfidence of the recognized speech + /// + double RawConfidence { get; } + } +} diff --git a/Microsoft.Toolkit.Uwp.UI.Behaviors/VoiceCommands/ISpeechRecognizer.cs b/Microsoft.Toolkit.Uwp.UI.Behaviors/VoiceCommands/ISpeechRecognizer.cs new file mode 100644 index 00000000000..ecaf111225f --- /dev/null +++ b/Microsoft.Toolkit.Uwp.UI.Behaviors/VoiceCommands/ISpeechRecognizer.cs @@ -0,0 +1,17 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace Microsoft.Toolkit.Uwp.UI.Behaviors +{ + /// + /// Service used to recongize speech + /// + public interface ISpeechRecognizer + { + /// + /// Occurs when a speech is recognized + /// + event RecognizedEventHandler Recognized; + } +} diff --git a/Microsoft.Toolkit.Uwp.UI.Behaviors/VoiceCommands/RecognizedEventArgs.cs b/Microsoft.Toolkit.Uwp.UI.Behaviors/VoiceCommands/RecognizedEventArgs.cs new file mode 100644 index 00000000000..c8a995ff810 --- /dev/null +++ b/Microsoft.Toolkit.Uwp.UI.Behaviors/VoiceCommands/RecognizedEventArgs.cs @@ -0,0 +1,35 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; + +namespace Microsoft.Toolkit.Uwp.UI.Behaviors +{ + /// + /// used to report the recognized speech result. + /// + public class RecognizedEventArgs : EventArgs + { + /// + /// Gets the result of a recognized speech + /// + public ISpeechRecognitionResult Result { get; } + + /// + /// Initializes a new instance of the class. + /// + /// The result which is speech recognized + public RecognizedEventArgs(ISpeechRecognitionResult result) + { + this.Result = result; + } + } + + /// + /// The Delegate for a Recognized Event. + /// + /// Sender ThemeListener + /// The event arguments. + public delegate void RecognizedEventHandler(ISpeechRecognizer sender, RecognizedEventArgs e); +} diff --git a/Microsoft.Toolkit.Uwp.UI.Behaviors/VoiceCommands/SpeechRecognitionResult.cs b/Microsoft.Toolkit.Uwp.UI.Behaviors/VoiceCommands/SpeechRecognitionResult.cs new file mode 100644 index 00000000000..358a1553679 --- /dev/null +++ b/Microsoft.Toolkit.Uwp.UI.Behaviors/VoiceCommands/SpeechRecognitionResult.cs @@ -0,0 +1,33 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace Microsoft.Toolkit.Uwp.UI.Behaviors +{ + /// + /// This class reports a recognized speech + /// + public class SpeechRecognitionResult : ISpeechRecognitionResult + { + /// + /// Gets the Text of the recognized speech + /// + public string Text { get; } + + /// + /// Gets the RawConfidence of the recognized speech + /// + public double RawConfidence { get; } + + /// + /// Initializes a new instance of the class. + /// + /// the speech recognized text + /// the rawConfidence of recognized speech + public SpeechRecognitionResult(string text, double rawConfidence) + { + Text = text; + RawConfidence = rawConfidence; + } + } +} diff --git a/Microsoft.Toolkit.Uwp.UI.Behaviors/VoiceCommands/VoiceCommandTrigger.cs b/Microsoft.Toolkit.Uwp.UI.Behaviors/VoiceCommands/VoiceCommandTrigger.cs new file mode 100644 index 00000000000..44f116eee94 --- /dev/null +++ b/Microsoft.Toolkit.Uwp.UI.Behaviors/VoiceCommands/VoiceCommandTrigger.cs @@ -0,0 +1,156 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using Microsoft.Xaml.Interactivity; +using Windows.Gaming.Input.ForceFeedback; +using Windows.UI.Core; +using Windows.UI.Xaml; + +namespace Microsoft.Toolkit.Uwp.UI.Behaviors +{ + /// + /// A behavior that listens spoken voice commands and executes its actions when that event is fired. + /// + public class VoiceCommandTrigger : Trigger + { + private static readonly Dictionary> _triggers = new Dictionary>(StringComparer.InvariantCultureIgnoreCase); + private static ISpeechRecognizer _speechRecognizer; + + /// + /// Gets or sets the SpeechRecognizer object used to recongize the voice commands + /// + public static ISpeechRecognizer SpeechRecognizer + { + get => _speechRecognizer; + set + { + if (value != _speechRecognizer) + { + if (_speechRecognizer is object) + { + _speechRecognizer.Recognized -= SpeechRecognizer_Recognized; + } + + _speechRecognizer = value; + if (_speechRecognizer is object) + { + _speechRecognizer.Recognized += SpeechRecognizer_Recognized; + } + } + } + } + + private static void SpeechRecognizer_Recognized(ISpeechRecognizer sender, RecognizedEventArgs e) + { + Debug.WriteLine(e.Result.Text); + if (_triggers.TryGetValue(e.Result.Text, out var list)) + { + foreach (var trigger in list) + { + _ = trigger.Dispatcher.RunAsync(CoreDispatcherPriority.Normal, () => + { + if (trigger.IsEnabled) + { + Interaction.ExecuteActions(trigger.AssociatedObject, trigger.Actions, e); + } + }); + } + } + } + + /// + /// Gets or sets the spoken Text to trigger the Actions + /// + /// + /// Use the | to seperate alternative texts + /// + public string Text + { + get => (string)GetValue(TextProperty); + set => SetValue(TextProperty, value); + } + + /// + /// Identifies the dependency property. + /// + public static readonly DependencyProperty TextProperty = DependencyProperty.Register(nameof(Text), typeof(string), typeof(VoiceCommandTrigger), new PropertyMetadata(default(string), OnTextPropertyChanged)); + + /// + /// Gets or sets a value indicating whether the VoiceCommand is enabled. Default is true + /// + public bool IsEnabled + { + get => (bool)GetValue(IsEnabledProperty); + set => SetValue(IsEnabledProperty, value); + } + + /// + /// Identifies the dependency property. + /// + public static readonly DependencyProperty IsEnabledProperty = DependencyProperty.Register(nameof(IsEnabled), typeof(bool), typeof(VoiceCommandTrigger), new PropertyMetadata(true)); + + private static void OnTextPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + if (d is VoiceCommandTrigger source) + { + var newValue = (string)e.NewValue; + var oldValue = (string)e.OldValue; + if (!string.IsNullOrEmpty(oldValue)) + { + source.Remove(oldValue); + } + + if (!string.IsNullOrEmpty(newValue)) + { + source.Add(newValue); + } + } + } + + /// + protected override void OnDetaching() + { + Remove(Text); + base.OnDetaching(); + } + + private void Add(string text) + { + foreach (var item in text.Split('|')) + { + if (_triggers.TryGetValue(item, out var list)) + { + list.Add(this); + } + else + { + list = new List() + { + this + }; + _triggers[item] = list; + } + } + } + + private void Remove(string text) + { + foreach (var item in text.Split('|')) + { + if (_triggers.TryGetValue(item, out var list)) + { + list.Remove(this); + if (list.Count == 0) + { + _triggers.Remove(item); + } + } + } + } + } +} diff --git a/Microsoft.Toolkit.Uwp.UI.Behaviors/VoiceCommands/WindowsMediaSpeechRecognizer.cs b/Microsoft.Toolkit.Uwp.UI.Behaviors/VoiceCommands/WindowsMediaSpeechRecognizer.cs new file mode 100644 index 00000000000..c982cb18d4f --- /dev/null +++ b/Microsoft.Toolkit.Uwp.UI.Behaviors/VoiceCommands/WindowsMediaSpeechRecognizer.cs @@ -0,0 +1,99 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Diagnostics; +using System.Threading.Tasks; +using Windows.Globalization; +using Windows.Media.SpeechRecognition; +using Windows.UI.Core; +using Windows.UI.Xaml; + +namespace Microsoft.Toolkit.Uwp.UI.Behaviors +{ + /// + /// This class used the Windows to recognize speech for the + /// + public class WindowsMediaSpeechRecognizer : ISpeechRecognizer + { + private readonly SpeechRecognizer _speechRecognizer; + + private WindowsMediaSpeechRecognizer(SpeechRecognizer speechRecognizer) + { + _speechRecognizer = speechRecognizer; + } + + /// + /// Creates a which can be used for a + /// + /// The is used for triggering the Activated event. This will restart the speech recongition. + /// The created + public static async Task CreateAsync(Window window) + { + SpeechRecognizer sr = null; + foreach (var item in Windows.System.UserProfile.GlobalizationPreferences.Languages) + { + var language = new Language(item); + try + { + sr = new SpeechRecognizer(language); + break; + } + catch (Exception ex) + { + Debug.WriteLine(ex); + } + } + + if (sr is null) + { + sr = new SpeechRecognizer(); + } + + var d = new WindowsMediaSpeechRecognizer(sr); + Debug.WriteLine($"SpeechRecognizer Language: {sr.CurrentLanguage.DisplayName}"); + + sr.ContinuousRecognitionSession.AutoStopSilenceTimeout = TimeSpan.MaxValue; + await sr.CompileConstraintsAsync(); + sr.ContinuousRecognitionSession.ResultGenerated += d.ContinuousRecognitionSession_ResultGenerated; + try + { + await sr.ContinuousRecognitionSession.StartAsync(); + window.Activated += d.Window_Activated; + return d; + } + catch + { + return null; + } + } + + private void Window_Activated(object sender, Windows.UI.Core.WindowActivatedEventArgs e) + { + if (e.WindowActivationState == CoreWindowActivationState.CodeActivated && _speechRecognizer.State == SpeechRecognizerState.Idle) + { + _ = _speechRecognizer.ContinuousRecognitionSession.StartAsync(); + } + } + + private void ContinuousRecognitionSession_ResultGenerated(SpeechContinuousRecognitionSession sender, SpeechContinuousRecognitionResultGeneratedEventArgs args) + { + OnRecognized(new RecognizedEventArgs(new SpeechRecognitionResult(args.Result.Text, args.Result.RawConfidence))); + } + + /// + /// Occurs when a speech is recognized + /// + public event RecognizedEventHandler Recognized; + + /// + /// Called when speech is recognized + /// + /// used to report the + protected virtual void OnRecognized(RecognizedEventArgs args) + { + Recognized?.Invoke(this, args); + } + } +}