diff --git a/Microsoft.Toolkit.Uwp.SampleApp/Microsoft.Toolkit.Uwp.SampleApp.csproj b/Microsoft.Toolkit.Uwp.SampleApp/Microsoft.Toolkit.Uwp.SampleApp.csproj index 5a4e88729a1..0fc1a59113a 100644 --- a/Microsoft.Toolkit.Uwp.SampleApp/Microsoft.Toolkit.Uwp.SampleApp.csproj +++ b/Microsoft.Toolkit.Uwp.SampleApp/Microsoft.Toolkit.Uwp.SampleApp.csproj @@ -264,6 +264,7 @@ + @@ -328,6 +329,9 @@ + + Designer + @@ -419,6 +423,9 @@ ObjectStoragePage.xaml + + SurfaceDialTextboxHelperPage.xaml + SystemInformationPage.xaml @@ -594,6 +601,10 @@ Designer MSBuild:Compile + + Designer + MSBuild:Compile + MSBuild:Compile Designer diff --git a/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/SurfaceDialTextboxHelper/SurfaceDialTextboxHelper.png b/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/SurfaceDialTextboxHelper/SurfaceDialTextboxHelper.png new file mode 100644 index 00000000000..9c57d5f46e9 Binary files /dev/null and b/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/SurfaceDialTextboxHelper/SurfaceDialTextboxHelper.png differ diff --git a/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/SurfaceDialTextboxHelper/SurfaceDialTextboxHelperCode.bind b/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/SurfaceDialTextboxHelper/SurfaceDialTextboxHelperCode.bind new file mode 100644 index 00000000000..1f3d89a1653 --- /dev/null +++ b/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/SurfaceDialTextboxHelper/SurfaceDialTextboxHelperCode.bind @@ -0,0 +1,29 @@ + + + + + + + + + diff --git a/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/SurfaceDialTextboxHelper/SurfaceDialTextboxHelperPage.xaml b/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/SurfaceDialTextboxHelper/SurfaceDialTextboxHelperPage.xaml new file mode 100644 index 00000000000..079cb78d62b --- /dev/null +++ b/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/SurfaceDialTextboxHelper/SurfaceDialTextboxHelperPage.xaml @@ -0,0 +1,55 @@ + + + + + + + + + + + + + diff --git a/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/SurfaceDialTextboxHelper/SurfaceDialTextboxHelperPage.xaml.cs b/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/SurfaceDialTextboxHelper/SurfaceDialTextboxHelperPage.xaml.cs new file mode 100644 index 00000000000..6e123fa1bff --- /dev/null +++ b/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/SurfaceDialTextboxHelper/SurfaceDialTextboxHelperPage.xaml.cs @@ -0,0 +1,41 @@ +// ****************************************************************** +// 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 Microsoft.Toolkit.Uwp.SampleApp.Models; +using Windows.UI.Xaml.Controls; +using Windows.UI.Xaml.Navigation; + +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 SurfaceDialTextboxHelperPage : Page + { + public SurfaceDialTextboxHelperPage() + { + InitializeComponent(); + } + + protected override void OnNavigatedTo(NavigationEventArgs e) + { + base.OnNavigatedTo(e); + + var propertyDesc = e.Parameter as PropertyDescriptor; + + if (propertyDesc != null) + { + DataContext = propertyDesc.Expando; + } + } + } +} diff --git a/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/samples.json b/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/samples.json index 603ffa41507..ae16047c469 100644 --- a/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/samples.json +++ b/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/samples.json @@ -122,6 +122,14 @@ "CodeUrl": "https://github.com/Microsoft/UWPCommunityToolkit/tree/master/Microsoft.Toolkit.Uwp.UI.Controls/TextBoxMask", "XamlCodeFile": "TextBoxMask.bind", "Icon": "/SamplePages/TextBoxMask/TextBoxMask.png" + }, + { + "Name": "SurfaceDialTextboxHelper", + "Type": "SurfaceDialTextboxHelperPage", + "About": "Enables support for Surface Dial on any given Textbox. Rotate the Dial to change the numeric value of the Textbox.", + "CodeUrl": "https://github.com/Microsoft/UWPCommunityToolkit/tree/master/Microsoft.Toolkit.Uwp.UI.Controls/SurfaceDialTextboxHelper", + "XamlCodeFile": "SurfaceDialTextboxHelperCode.bind", + "Icon": "/SamplePages/SurfaceDialTextboxHelper/SurfaceDialTextboxHelper.png" } ] }, 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 6e268f8276f..065d65700d1 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 @@ -74,6 +74,7 @@ + diff --git a/Microsoft.Toolkit.Uwp.UI.Controls/SurfaceDialTextboxHelper/SurfaceDialTextboxHelper.cs b/Microsoft.Toolkit.Uwp.UI.Controls/SurfaceDialTextboxHelper/SurfaceDialTextboxHelper.cs new file mode 100644 index 00000000000..11b1842b26d --- /dev/null +++ b/Microsoft.Toolkit.Uwp.UI.Controls/SurfaceDialTextboxHelper/SurfaceDialTextboxHelper.cs @@ -0,0 +1,403 @@ +// ****************************************************************** +// 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.Diagnostics; +using Windows.Foundation.Metadata; +using Windows.UI.Input; +using Windows.UI.Xaml; +using Windows.UI.Xaml.Controls; +using Windows.UI.Xaml.Input; + +namespace Microsoft.Toolkit.Uwp.UI.Controls +{ + /// + /// Helper class that provides attached properties to enable any TextBox with the Surface Dial. Rotate to change the value by StepValue between MinValue and MaxValue, and tap to go to the Next focus element from a TextBox + /// + public static class SurfaceDialTextboxHelper + { + /// + /// If you provide the Controller yourself, set this to true so you won't add new menu items. + /// + public static readonly DependencyProperty ForceMenuItemProperty = + DependencyProperty.RegisterAttached("ForceMenuItem", typeof(bool), typeof(SurfaceDialTextboxHelper), new PropertyMetadata(false)); + + /// + /// Set the default icon of the menu item that gets added. A user will most likely not see this. Defaults to the Ruler icon. + /// + public static readonly DependencyProperty IconProperty = + DependencyProperty.RegisterAttached("Icon", typeof(RadialControllerMenuKnownIcon), typeof(SurfaceDialTextboxHelper), new PropertyMetadata(RadialControllerMenuKnownIcon.Ruler)); + + /// + /// The amount the TextBox will be modified for each rotation step on the Surface Dial. This can be any double value. + /// + public static readonly DependencyProperty StepValueProperty = + DependencyProperty.RegisterAttached("StepValue", typeof(double), typeof(SurfaceDialTextboxHelper), new PropertyMetadata(0d, new PropertyChangedCallback(StepValueChanged))); + + /// + /// A flag to enable or disable haptic feedback when rotating the dial for the give TextBox. This is enabled by default. + /// + public static readonly DependencyProperty EnableHapticFeedbackProperty = + DependencyProperty.RegisterAttached("EnableHapticFeedback", typeof(bool), typeof(SurfaceDialTextboxHelper), new PropertyMetadata(true)); + + /// + /// Sets the minimum value the TextBox can have when modifying it using a Surface Dial. Default is -100.0 + /// + public static readonly DependencyProperty MinValueProperty = + DependencyProperty.RegisterAttached("MinValue", typeof(double), typeof(SurfaceDialTextboxHelper), new PropertyMetadata(-100d)); + + /// + /// Sets the maxium value the TextBox can have when modifying it using a Surface Dial. Default is 100.0 + /// + public static readonly DependencyProperty MaxValueProperty = + DependencyProperty.RegisterAttached("MaxValue", typeof(double), typeof(SurfaceDialTextboxHelper), new PropertyMetadata(100d)); + + /// + /// TapToNext is a feature you can set to automatically try to focus the next focusable element from the Surface Dial enabled TextBox. This is on dy default. + /// + public static readonly DependencyProperty EnableTapToNextControlProperty = + DependencyProperty.RegisterAttached("EnableTapToNextControl", typeof(bool), typeof(SurfaceDialTextboxHelper), new PropertyMetadata(true)); + + /// + /// EnableMinMax limits the value in the textbox to your spesificed Min and Max values, see the other properties. + /// + public static readonly DependencyProperty EnableMinMaxValueProperty = + DependencyProperty.RegisterAttached("EnableMinMaxValue", typeof(bool), typeof(SurfaceDialTextboxHelper), new PropertyMetadata(false)); + + /// + /// Getter of the EnableMinMax property + /// + /// The Depenency Object we are dealing with, like a TextBox. + /// Return value of property + public static bool GetEnableMinMaxValue(DependencyObject obj) + { + return (bool)obj.GetValue(EnableMinMaxValueProperty); + } + + /// + /// Setter of the EnableMinMax property + /// + /// The Depenency Object we are dealing with, like a TextBox. + /// The value to set the property to. + public static void SetEnableMinMaxValue(DependencyObject obj, bool value) + { + obj.SetValue(EnableMinMaxValueProperty, value); + } + + /// + /// Getter of the TapToNext flag. + /// + /// The Depenency Object we are dealing with, like a TextBox. + /// Return value of property + public static bool GetEnableTapToNextControl(DependencyObject obj) + { + return (bool)obj.GetValue(EnableTapToNextControlProperty); + } + + /// + /// Setter of the TapToNext flag. + /// + /// The Depenency Object we are dealing with, like a TextBox. + /// The value to set the property to. + public static void SetEnableTapToNextControl(DependencyObject obj, bool value) + { + obj.SetValue(EnableTapToNextControlProperty, value); + } + + /// + /// Getter of the MaxValue + /// + /// The Depenency Object we are dealing with, like a TextBox. + /// Return value of property + public static double GetMaxValue(DependencyObject obj) + { + return (double)obj.GetValue(MaxValueProperty); + } + + /// + /// Setter of the MaxValue + /// + /// The Depenency Object we are dealing with, like a TextBox. + /// The value to set the property to. + public static void SetMaxValue(DependencyObject obj, double value) + { + obj.SetValue(MaxValueProperty, value); + } + + /// + /// Getter of the MinValue + /// + /// The Depenency Object we are dealing with, like a TextBox. + /// Return value of property + public static double GetMinValue(DependencyObject obj) + { + return (double)obj.GetValue(MinValueProperty); + } + + /// + /// Setter of the MinValue + /// + /// The Depenency Object we are dealing with, like a TextBox. + /// The value to set the property to. + public static void SetMinValue(DependencyObject obj, double value) + { + obj.SetValue(MinValueProperty, value); + } + + /// + /// Setter of the StepValue. + /// + /// The Depenency Object we are dealing with, like a TextBox. + /// Return value of property + public static double GetStepValue(DependencyObject obj) + { + return (double)obj.GetValue(StepValueProperty); + } + + /// + /// Getter of the StepValue + /// + /// The Depenency Object we are dealing with, like a TextBox. + /// The value to set the property to. + public static void SetStepValue(DependencyObject obj, double value) + { + obj.SetValue(StepValueProperty, value); + } + + /// + /// Getter of the Icon + /// + /// The Depenency Object we are dealing with, like a TextBox. + /// Return value of property + public static RadialControllerMenuKnownIcon GetIcon(DependencyObject obj) + { + return (RadialControllerMenuKnownIcon)obj.GetValue(IconProperty); + } + + /// + /// Setter of the Icon + /// + /// The Depenency Object we are dealing with, like a TextBox. + /// The value to set the property to. + public static void SetIcon(DependencyObject obj, RadialControllerMenuKnownIcon value) + { + obj.SetValue(IconProperty, value); + } + + /// + /// Setter of the Haptic Feedback property + /// + /// The Depenency Object we are dealing with, like a TextBox. + /// Return value of property + public static bool GetEnableHapticFeedback(DependencyObject obj) + { + return (bool)obj.GetValue(EnableHapticFeedbackProperty); + } + + /// + /// Getter of the Haptic Feedback property + /// + /// The Depenency Object we are dealing with, like a TextBox. + /// The value to set the property to. + public static void SetEnableHapticFeedback(DependencyObject obj, bool value) + { + obj.SetValue(EnableHapticFeedbackProperty, value); + } + + /// + /// Getter of the Force Menu Item flag + /// + /// The Depenency Object we are dealing with, like a TextBox. + /// Return value of property + public static bool GetForceMenuItem(DependencyObject obj) + { + return (bool)obj?.GetValue(ForceMenuItemProperty); + } + + /// + /// Setter of the Force Menu Item flag + /// + /// The Depenency Object we are dealing with, like a TextBox. + /// The value to set the property to. + public static void SetForceMenuItem(DependencyObject obj, bool value) + { + obj.SetValue(ForceMenuItemProperty, value); + } + + /// + /// The Surface Dial controller instance itself + /// + private static RadialController _controller; + + /// + /// A default menu item that will be used for this to function. It will automatically be cleaned up when you move away from the TextBox, and created on Focus. + /// + private static RadialControllerMenuItem _stepTextMenuItem; + + /// + /// The textbox itself needed to refernece the current TextBox that is being modified + /// + private static TextBox _textBox; + + /// + /// Gets or sets the controller for the Surface Dial. The RadialController can be set from your app logic in case you use Surface Dial in other custom cases than on a TextBox. + /// This helper class will do everything for you, but if you want to control the Menu Items and/or wish to use the same Surface Dial insta + /// This is the property for the static controller so you can access it if needed. + /// + public static RadialController Controller + { + get + { + return _controller; + } + + set + { + _controller = value; + } + } + + /// + /// This function gets called every time there is a rotational change on the connected Surface Dial while a Surface Dial enabled TextBox is in focus. + /// This function ensures that the TextBox stays within the set range between MinValue and MaxValue while rotating the Surface Dial. + /// It defaults the content of the TextBox to 0.0 if a non-numerical value is detected. + /// + /// The RadialController being used. + /// The arguments of the changed event. + private static void Controller_RotationChanged(RadialController sender, RadialControllerRotationChangedEventArgs args) + { + if (_textBox == null) + { + return; + } + + string t = _textBox.Text; + double nr; + + if (double.TryParse(t, out nr)) + { + nr += args.RotationDeltaInDegrees * GetStepValue(_textBox); + if (GetEnableMinMaxValue(_textBox)) + { + if (nr < GetMinValue(_textBox)) + { + nr = GetMinValue(_textBox); + } + + if (nr > GetMaxValue(_textBox)) + { + nr = GetMaxValue(_textBox); + } + } + } + else + { + // default to zero if content is not a number + nr = 0.0d; + } + + _textBox.Text = nr.ToString("0.00"); + } + + /// + /// Sets up the events needed for the current TextBox so it can trigger on GotFocus and LostFocus + /// + /// The Depenency Object we are dealing with, like a TextBox. + /// The arguments of the changed event. + private static void StepValueChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + if (!ApiInformation.IsTypePresent("Windows.UI.Input.RadialController")) + { + return; + } + + var textBox = d as TextBox; + + if (textBox == null) + { + return; + } + + textBox.GotFocus -= TextBox_GotFocus; + textBox.LostFocus -= TextBox_LostFocus; + textBox.GotFocus += TextBox_GotFocus; + textBox.LostFocus += TextBox_LostFocus; + } + + /// + /// When the focus of the TextBox is lost, ensure we clean up the events and Surface Dial menu item. + /// + /// The TextBox in being affected. + /// The event arguments. + private static void TextBox_LostFocus(object sender, RoutedEventArgs e) + { + if (_textBox == null) + { + return; + } + + if (GetForceMenuItem(_textBox)) + { + _controller.Menu.Items.Remove(_stepTextMenuItem); + } + + _controller.RotationChanged -= Controller_RotationChanged; + if (GetEnableTapToNextControl(_textBox)) + { + _controller.ButtonClicked -= Controller_ButtonClicked; + } + + _textBox = null; + } + + /// + /// When a Surface Dial TextBox gets focus, ensure the proper events are setup, and connect the Surface Dial itself. + /// + /// The TextBox in being affected. + /// The event arguments. + private static void TextBox_GotFocus(object sender, RoutedEventArgs e) + { + _textBox = sender as TextBox; + + if (_textBox == null) + { + return; + } + + _controller = _controller ?? RadialController.CreateForCurrentView(); + + if (GetForceMenuItem(_textBox)) + { + _stepTextMenuItem = RadialControllerMenuItem.CreateFromKnownIcon("Step Text Box", GetIcon(_textBox)); + _controller.Menu.Items.Add(_stepTextMenuItem); + _controller.Menu.SelectMenuItem(_stepTextMenuItem); + } + + _controller.UseAutomaticHapticFeedback = GetEnableHapticFeedback(_textBox); + _controller.RotationResolutionInDegrees = 1; + _controller.RotationChanged += Controller_RotationChanged; + if (GetEnableTapToNextControl(_textBox)) + { + _controller.ButtonClicked += Controller_ButtonClicked; + } + } + + /// + /// If the TapToNext flag is enabled, this function will try to take the focus to the next focusable element. + /// + /// The RadialController being used. + /// The arguments of the changed event. + private static void Controller_ButtonClicked(RadialController sender, RadialControllerButtonClickedEventArgs args) + { + FocusManager.TryMoveFocus(FocusNavigationDirection.Next); + } + } +} diff --git a/docs/controls/SurfaceDialTextboxHelper.md b/docs/controls/SurfaceDialTextboxHelper.md new file mode 100644 index 00000000000..5db1e103251 --- /dev/null +++ b/docs/controls/SurfaceDialTextboxHelper.md @@ -0,0 +1,59 @@ +# SurfaceDialTextboxHelper Control + +The **SurfaceDialTextboxHelper Control** adds features from the Surface Dial control to a numeric TextBox. This enables you to modify the content of the TextBox when rotating the Surface Dial (increasing or decreasing the value) and optionally go to the next focus element by tapping the Surface Dial click button. + +You can set the following properties to control the behaviour: +**StepValue**: Required +This property enables the Textbox with the Surface Dial controller, and control the amount each rotation step of the Surface Dial modifies the TextBox. + +**ForceMenuItem**: Required +The Surface Dial requires the app to add a menu item to the Surface Dial context menu for it to function in the app. If you don't have a Surface Dial controller elsewhere in the app, set this field to True. This will add a menu item to the Contect Menu when you modify a Surface Dial enabled Textbox. If you already have a Surface Dial controller elsewhere in the app, you can set the Controller property to the same one to reuse it. Use the Icon property to modify the icon you wish to use. Typically, a user will not see this item unless you open the Context Menu while a Textbox is in focus. + +**EnableHapticFeedback**: Optional +This property makes it possible to turn on or off the haptic feedback on the Surface Dial hardware while rotating. + +**EnableMinMaxValue**: Optional +This property enables the MinValue and MaxValue limits of the Textbox. These values are used to limit the value in the TextBox while rotating the Surface Dial. + +**EnableTapToNextControl**: Optional +Enables you to click the Surface Dial Control to move to the next focus item in your UI. Good for quickly navigating between Textbox elements on your UI. + + +**NOTE:** Windows Anniversary Update (10.0.14393.0) is needed to support correctly this helper. + +## Syntax + +```xml + + + +``` + +## Example Image + +![SurfaceDialTextboxHelper animation](../resources/images/SurfaceDialTextboxAnim.gif "SurfaceDialTextboxHelper") + +## Example Code + +[SurfaceDialTextboxHelper Sample Page](https://github.com/Microsoft/UWPCommunityToolkit/tree/master/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/SurfaceDialTextboxHelper) + +## Requirements (Windows 10 Device Family) + +| [Device family](http://go.microsoft.com/fwlink/p/?LinkID=526370) | Universal, 10.0.14393.0 or higher | +| --- | --- | +| Namespace | Microsoft.Toolkit.Uwp.UI.Controls | + +## API + +* [SurfaceDialTextboxHelper source code](https://github.com/Microsoft/UWPCommunityToolkit/tree/master/Microsoft.Toolkit.Uwp.UI.Controls/SurfaceDialTextboxHelper) \ No newline at end of file diff --git a/docs/resources/images/SurfaceDialTextboxAnim.gif b/docs/resources/images/SurfaceDialTextboxAnim.gif new file mode 100644 index 00000000000..b2f92a60f55 Binary files /dev/null and b/docs/resources/images/SurfaceDialTextboxAnim.gif differ diff --git a/docs/resources/images/SurfaeDial.jpg b/docs/resources/images/SurfaeDial.jpg new file mode 100644 index 00000000000..925b3f6a72b Binary files /dev/null and b/docs/resources/images/SurfaeDial.jpg differ