diff --git a/.vsts-ci.yml b/.vsts-ci.yml index ba627db415a..47893c4691c 100644 --- a/.vsts-ci.yml +++ b/.vsts-ci.yml @@ -4,7 +4,7 @@ variables: steps: - task: BatchScript@1 inputs: - filename: "C:\\Program Files (x86)\\Microsoft Visual Studio\\Preview\\Enterprise\\Common7\\Tools\\VsDevCmd.bat" + filename: "C:\\Program Files (x86)\\Microsoft Visual Studio\\2017\\Community\\Common7\\Tools\\VsDevCmd.bat" arguments: -no_logo modifyEnvironment: true displayName: Setup Environment Variables diff --git a/GazeInputTest/App.xaml b/GazeInputTest/App.xaml new file mode 100644 index 00000000000..760dafe33dc --- /dev/null +++ b/GazeInputTest/App.xaml @@ -0,0 +1,8 @@ + + + diff --git a/GazeInputTest/App.xaml.cs b/GazeInputTest/App.xaml.cs new file mode 100644 index 00000000000..7aba0f3c809 --- /dev/null +++ b/GazeInputTest/App.xaml.cs @@ -0,0 +1,102 @@ +// ****************************************************************** +// 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 Windows.ApplicationModel; +using Windows.ApplicationModel.Activation; +using Windows.UI.Xaml; +using Windows.UI.Xaml.Controls; +using Windows.UI.Xaml.Navigation; + +namespace GazeInputTest +{ + /// + /// Provides application-specific behavior to supplement the default Application class. + /// + sealed partial class App : Application + { + /// + /// Initializes the singleton application object. This is the first line of authored code + /// executed, and as such is the logical equivalent of main() or WinMain(). + /// + public App() + { + this.InitializeComponent(); + this.Suspending += OnSuspending; + } + + /// + /// Invoked when the application is launched normally by the end user. Other entry points + /// will be used such as when the application is launched to open a specific file. + /// + /// Details about the launch request and process. + protected override void OnLaunched(LaunchActivatedEventArgs e) + { + Frame rootFrame = Window.Current.Content as Frame; + + // Do not repeat app initialization when the Window already has content, + // just ensure that the window is active + if (rootFrame == null) + { + // Create a Frame to act as the navigation context and navigate to the first page + rootFrame = new Frame(); + + rootFrame.NavigationFailed += OnNavigationFailed; + + if (e.PreviousExecutionState == ApplicationExecutionState.Terminated) + { + //TODO: Load state from previously suspended application + } + + // Place the frame in the current Window + Window.Current.Content = rootFrame; + } + + if (e.PrelaunchActivated == false) + { + if (rootFrame.Content == null) + { + // When the navigation stack isn't restored navigate to the first page, + // configuring the new page by passing required information as a navigation + // parameter + rootFrame.Navigate(typeof(MainPage), e.Arguments); + } + // Ensure the current window is active + Window.Current.Activate(); + } + } + + /// + /// Invoked when Navigation to a certain page fails + /// + /// The Frame which failed navigation + /// Details about the navigation failure + void OnNavigationFailed(object sender, NavigationFailedEventArgs e) + { + throw new Exception("Failed to load Page " + e.SourcePageType.FullName); + } + + /// + /// Invoked when application execution is being suspended. Application state is saved + /// without knowing whether the application will be terminated or resumed with the contents + /// of memory still intact. + /// + /// The source of the suspend request. + /// Details about the suspend request. + private void OnSuspending(object sender, SuspendingEventArgs e) + { + var deferral = e.SuspendingOperation.GetDeferral(); + //TODO: Save application state and stop any background activity + deferral.Complete(); + } + } +} diff --git a/GazeInputTest/Assets/LockScreenLogo.scale-200.png b/GazeInputTest/Assets/LockScreenLogo.scale-200.png new file mode 100644 index 00000000000..735f57adb5d Binary files /dev/null and b/GazeInputTest/Assets/LockScreenLogo.scale-200.png differ diff --git a/GazeInputTest/Assets/SplashScreen.scale-200.png b/GazeInputTest/Assets/SplashScreen.scale-200.png new file mode 100644 index 00000000000..023e7f1feda Binary files /dev/null and b/GazeInputTest/Assets/SplashScreen.scale-200.png differ diff --git a/GazeInputTest/Assets/Square150x150Logo.scale-200.png b/GazeInputTest/Assets/Square150x150Logo.scale-200.png new file mode 100644 index 00000000000..af49fec1a54 Binary files /dev/null and b/GazeInputTest/Assets/Square150x150Logo.scale-200.png differ diff --git a/GazeInputTest/Assets/Square44x44Logo.scale-200.png b/GazeInputTest/Assets/Square44x44Logo.scale-200.png new file mode 100644 index 00000000000..ce342a2ec8a Binary files /dev/null and b/GazeInputTest/Assets/Square44x44Logo.scale-200.png differ diff --git a/GazeInputTest/Assets/Square44x44Logo.targetsize-24_altform-unplated.png b/GazeInputTest/Assets/Square44x44Logo.targetsize-24_altform-unplated.png new file mode 100644 index 00000000000..f6c02ce97e0 Binary files /dev/null and b/GazeInputTest/Assets/Square44x44Logo.targetsize-24_altform-unplated.png differ diff --git a/GazeInputTest/Assets/StoreLogo.png b/GazeInputTest/Assets/StoreLogo.png new file mode 100644 index 00000000000..7385b56c0e4 Binary files /dev/null and b/GazeInputTest/Assets/StoreLogo.png differ diff --git a/GazeInputTest/Assets/Wide310x150Logo.scale-200.png b/GazeInputTest/Assets/Wide310x150Logo.scale-200.png new file mode 100644 index 00000000000..288995b397f Binary files /dev/null and b/GazeInputTest/Assets/Wide310x150Logo.scale-200.png differ diff --git a/GazeInputTest/GazeInputTest.csproj b/GazeInputTest/GazeInputTest.csproj new file mode 100644 index 00000000000..20eee273052 --- /dev/null +++ b/GazeInputTest/GazeInputTest.csproj @@ -0,0 +1,151 @@ + + + + + Debug + x86 + {A122EA02-4DE7-413D-BFBF-AF7DFC668DD6} + AppContainerExe + Properties + GazeInputTest + GazeInputTest + en-US + UAP + 10.0.17134.0 + 10.0.17134.0 + 14 + 512 + {A5A43C5B-DE2A-4C0C-9213-0A381AF9435A};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} + true + GazeInputTest_TemporaryKey.pfx + + + true + bin\x86\Debug\ + DEBUG;TRACE;NETFX_CORE;WINDOWS_UWP + ;2008 + full + x86 + false + prompt + true + + + bin\x86\Release\ + TRACE;NETFX_CORE;WINDOWS_UWP + true + ;2008 + pdbonly + x86 + false + prompt + true + true + + + true + bin\ARM\Debug\ + DEBUG;TRACE;NETFX_CORE;WINDOWS_UWP + ;2008 + full + ARM + false + prompt + true + + + bin\ARM\Release\ + TRACE;NETFX_CORE;WINDOWS_UWP + true + ;2008 + pdbonly + ARM + false + prompt + true + true + + + true + bin\x64\Debug\ + DEBUG;TRACE;NETFX_CORE;WINDOWS_UWP + ;2008 + full + x64 + false + prompt + true + + + bin\x64\Release\ + TRACE;NETFX_CORE;WINDOWS_UWP + true + ;2008 + pdbonly + x64 + false + prompt + true + true + + + PackageReference + + + + App.xaml + + + MainPage.xaml + + + + + + Designer + + + + + + + + + + + + + + + + MSBuild:Compile + Designer + + + MSBuild:Compile + Designer + + + + + 6.0.1 + + + + + {a5e98964-45b1-442d-a07a-298a3221d81e} + Microsoft.Toolkit.Uwp.Input.GazeInteraction + + + + 14.0 + + + + \ No newline at end of file diff --git a/GazeInputTest/MainPage.xaml b/GazeInputTest/MainPage.xaml new file mode 100644 index 00000000000..863a2efa955 --- /dev/null +++ b/GazeInputTest/MainPage.xaml @@ -0,0 +1,48 @@ + + + + + + + + + + + + + + Show Cursor + + + + + + + + + + + + + + + + + diff --git a/GazeInputTest/MainPage.xaml.cs b/GazeInputTest/MainPage.xaml.cs new file mode 100644 index 00000000000..8d5f4a6802b --- /dev/null +++ b/GazeInputTest/MainPage.xaml.cs @@ -0,0 +1,83 @@ +// ****************************************************************** +// 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.Input.GazeInteraction; +using System; +using Windows.UI.Xaml; +using Windows.UI.Xaml.Controls; + +namespace GazeInputTest +{ + /// + /// An empty page that can be used on its own or navigated to within a Frame. + /// + public sealed partial class MainPage : Page + { + public MainPage() + { + this.InitializeComponent(); + + ShowCursor.IsChecked = GazeInput.GetIsCursorVisible(this); + + GazeInput.IsDeviceAvailableChanged += GazeInput_IsDeviceAvailableChanged; + GazeInput_IsDeviceAvailableChanged(null, null); + } + + private void GazeInput_IsDeviceAvailableChanged(object sender, object e) + { + DeviceAvailable.Text = GazeInput.IsDeviceAvailable ? "Eye tracker device available" : "No eye tracker device available"; + } + + private void OnStateChanged(object sender, StateChangedEventArgs ea) + { + Dwell.Content = ea.PointerState.ToString(); + } + + private void Dwell_Click(object sender, RoutedEventArgs e) + { + Dwell.Content = "Clicked"; + } + + private void ShowCursor_Toggle(object sender, RoutedEventArgs e) + { + if (ShowCursor.IsChecked.HasValue) + { + GazeInput.SetIsCursorVisible(this, ShowCursor.IsChecked.Value); + } + } + + int clickCount; + + private void OnLegacyInvoked(object sender, RoutedEventArgs e) + { + clickCount++; + HowButton.Content = string.Format("{0}: Legacy click", clickCount); + } + + private void OnGazeInvoked(object sender, DwellInvokedRoutedEventArgs e) + { + clickCount++; + HowButton.Content = string.Format("{0}: Accessible click", clickCount); + e.Handled = true; + } + + private void OnInvokeProgress(object sender, DwellProgressEventArgs e) + { + if (e.State == DwellProgressState.Progressing) + { + ProgressShow.Value = 100.0 * e.Progress; + } + ProgressShow.IsIndeterminate = e.State == DwellProgressState.Complete; + e.Handled = true; + } + } +} diff --git a/GazeInputTest/Package.appxmanifest b/GazeInputTest/Package.appxmanifest new file mode 100644 index 00000000000..232aef12182 --- /dev/null +++ b/GazeInputTest/Package.appxmanifest @@ -0,0 +1,50 @@ + + + + + + + + + + GazeInputTest + harishsk + Assets\StoreLogo.png + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/GazeInputTest/Properties/AssemblyInfo.cs b/GazeInputTest/Properties/AssemblyInfo.cs new file mode 100644 index 00000000000..08fe6b28abd --- /dev/null +++ b/GazeInputTest/Properties/AssemblyInfo.cs @@ -0,0 +1,40 @@ +// ****************************************************************** +// 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.Reflection; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("GazeInputTest")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("GazeInputTest")] +[assembly: AssemblyCopyright("Copyright © 2017")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +//[assembly: AssemblyVersion("1.0.0.0")] +//[assembly: AssemblyFileVersion("1.0.0.0")] +[assembly: ComVisible(false)] \ No newline at end of file diff --git a/GazeInputTest/Properties/Default.rd.xml b/GazeInputTest/Properties/Default.rd.xml new file mode 100644 index 00000000000..af00722cdf9 --- /dev/null +++ b/GazeInputTest/Properties/Default.rd.xml @@ -0,0 +1,31 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/Microsoft.Toolkit.Uwp.Input.GazeInteraction/DwellInvokedRoutedEventArgs.h b/Microsoft.Toolkit.Uwp.Input.GazeInteraction/DwellInvokedRoutedEventArgs.h new file mode 100644 index 00000000000..7ef31be8bb5 --- /dev/null +++ b/Microsoft.Toolkit.Uwp.Input.GazeInteraction/DwellInvokedRoutedEventArgs.h @@ -0,0 +1,28 @@ +//Copyright (c) Microsoft. All rights reserved. Licensed under the MIT license. +//See LICENSE in the project root for license information. + +#pragma once + +BEGIN_NAMESPACE_GAZE_INPUT + +/// +/// This parameter is passed to the GazeElement::Invoked event and allows +/// the application to prevent default invocation when the user dwells on a control +/// +public ref class DwellInvokedRoutedEventArgs : public RoutedEventArgs +{ +public: + + /// + /// The application should set this value to true to prevent invoking the control when the user dwells on a control + /// + property bool Handled; + +internal: + + DwellInvokedRoutedEventArgs() + { + } +}; + +END_NAMESPACE_GAZE_INPUT \ No newline at end of file diff --git a/Microsoft.Toolkit.Uwp.Input.GazeInteraction/DwellProgressEventArgs.h b/Microsoft.Toolkit.Uwp.Input.GazeInteraction/DwellProgressEventArgs.h new file mode 100644 index 00000000000..86c5b2ef5eb --- /dev/null +++ b/Microsoft.Toolkit.Uwp.Input.GazeInteraction/DwellProgressEventArgs.h @@ -0,0 +1,43 @@ +//Copyright (c) Microsoft. All rights reserved. Licensed under the MIT license. +//See LICENSE in the project root for license information. + +#pragma once + +#include "DwellProgressState.h" + +BEGIN_NAMESPACE_GAZE_INPUT + +/// +/// This parameter is passed to the GazeElement.DwellProgressFeedback event. The event is fired to inform the application of the user's progress towards completing dwelling on a control +/// +public ref class DwellProgressEventArgs : public RoutedEventArgs +{ +public: + + /// + /// An enum that reflects the current state of dwell progress + /// + property DwellProgressState State { DwellProgressState get() { return _state; }} + + /// + /// A value between 0 and 1 that reflects the fraction of progress towards completing dwell + /// + property double Progress { double get() { return _progress; }} + + /// + /// A parameter for the application to set to true if it handles the event. If this parameter is set to true, the library suppresses default animation for dwell feedback on the control + /// + property bool Handled; + +internal: + DwellProgressEventArgs(DwellProgressState state, TimeSpan elapsedDuration, TimeSpan triggerDuration) + { + _state = state; + _progress = ((double)elapsedDuration.Duration) / triggerDuration.Duration; + } +private: + DwellProgressState _state; + double _progress; +}; + +END_NAMESPACE_GAZE_INPUT \ No newline at end of file diff --git a/Microsoft.Toolkit.Uwp.Input.GazeInteraction/DwellProgressState.h b/Microsoft.Toolkit.Uwp.Input.GazeInteraction/DwellProgressState.h new file mode 100644 index 00000000000..0ff7716193d --- /dev/null +++ b/Microsoft.Toolkit.Uwp.Input.GazeInteraction/DwellProgressState.h @@ -0,0 +1,26 @@ +#pragma once + +BEGIN_NAMESPACE_GAZE_INPUT + +/// +/// An enum that reflects the current state of progress towards dwell when a user is focused on a control +/// +public enum class DwellProgressState +{ + /// + /// User is not looking at the control + /// + Idle, + + /// + /// User is continuing to focus on a control with an intent to dwell and invoke + /// + Progressing, + + /// + /// User has completed dwelling on a control + /// + Complete +}; + +END_NAMESPACE_GAZE_INPUT \ No newline at end of file diff --git a/Microsoft.Toolkit.Uwp.Input.GazeInteraction/GazeCursor.cpp b/Microsoft.Toolkit.Uwp.Input.GazeInteraction/GazeCursor.cpp new file mode 100644 index 00000000000..96396ae831e --- /dev/null +++ b/Microsoft.Toolkit.Uwp.Input.GazeInteraction/GazeCursor.cpp @@ -0,0 +1,89 @@ +//Copyright (c) Microsoft. All rights reserved. Licensed under the MIT license. +//See LICENSE in the project root for license information. + +#include "pch.h" +#include "GazeCursor.h" + +BEGIN_NAMESPACE_GAZE_INPUT + +GazeCursor::GazeCursor() +{ + _cursorRadius = DEFAULT_CURSOR_RADIUS; + + _isCursorVisible = DEFAULT_CURSOR_VISIBILITY; + + _gazePopup = ref new Popup(); + _gazePopup->IsHitTestVisible = false; + + _gazeCanvas = ref new Canvas(); + _gazeCanvas->IsHitTestVisible = false; + + _gazeCursor = ref new Shapes::Ellipse(); + _gazeCursor->Fill = ref new SolidColorBrush(Colors::IndianRed); + _gazeCursor->VerticalAlignment = Windows::UI::Xaml::VerticalAlignment::Top; + _gazeCursor->HorizontalAlignment = Windows::UI::Xaml::HorizontalAlignment::Left; + _gazeCursor->Width = 2 * CursorRadius; + _gazeCursor->Height = 2 * CursorRadius; + _gazeCursor->IsHitTestVisible = false; + + _origSignalCursor = ref new Shapes::Ellipse(); + _origSignalCursor->Fill = ref new SolidColorBrush(Colors::Green); + _origSignalCursor->VerticalAlignment = Windows::UI::Xaml::VerticalAlignment::Top; + _origSignalCursor->HorizontalAlignment = Windows::UI::Xaml::HorizontalAlignment::Left; + _origSignalCursor->Width = 2 * CursorRadius; + _origSignalCursor->Height = 2 * CursorRadius; + _origSignalCursor->IsHitTestVisible = false; + + _gazeRect = ref new Shapes::Rectangle(); + _gazeRect->IsHitTestVisible = false; + + _gazeCanvas->Children->Append(_gazeCursor); + _gazeCanvas->Children->Append(_gazeRect); + + // TODO: Reenable this once GazeCursor is refactored correctly + //_gazeCanvas->Children->Append(_origSignalCursor); + + _gazePopup->Child = _gazeCanvas; +} + +void GazeCursor::CursorRadius::set(int value) +{ + _cursorRadius = value; + if (_gazeCursor != nullptr) + { + _gazeCursor->Width = 2 * _cursorRadius; + _gazeCursor->Height = 2 * _cursorRadius; + } +} + +void GazeCursor::IsCursorVisible::set(bool value) +{ + _isCursorVisible = value; + if (_gazePopup != nullptr) + { + _gazePopup->IsOpen = _isCursorVisible && _isGazeEntered; + } +} + +void GazeCursor::IsGazeEntered::set(bool value) +{ + _isGazeEntered = value; + if (_gazePopup != nullptr) + { + _gazePopup->IsOpen = _isCursorVisible && _isGazeEntered; + } +} + +void GazeCursor::LoadSettings(ValueSet^ settings) +{ + if (settings->HasKey("GazeCursor.CursorRadius")) + { + CursorRadius = (int)(settings->Lookup("GazeCursor.CursorRadius")); + } + if (settings->HasKey("GazeCursor.CursorVisibility")) + { + IsCursorVisible = (bool)(settings->Lookup("GazeCursor.CursorVisibility")); + } +} + +END_NAMESPACE_GAZE_INPUT \ No newline at end of file diff --git a/Microsoft.Toolkit.Uwp.Input.GazeInteraction/GazeCursor.h b/Microsoft.Toolkit.Uwp.Input.GazeInteraction/GazeCursor.h new file mode 100644 index 00000000000..0c427626716 --- /dev/null +++ b/Microsoft.Toolkit.Uwp.Input.GazeInteraction/GazeCursor.h @@ -0,0 +1,93 @@ +//Copyright (c) Microsoft. All rights reserved. Licensed under the MIT license. +//See LICENSE in the project root for license information. + +#pragma once + +using namespace Windows::Foundation::Collections; +using namespace Windows::UI; +using namespace Windows::UI::Xaml::Controls; +using namespace Windows::UI::Xaml::Controls::Primitives; + +BEGIN_NAMESPACE_GAZE_INPUT + +private ref class GazeCursor sealed +{ +private: + const int DEFAULT_CURSOR_RADIUS = 5; + const bool DEFAULT_CURSOR_VISIBILITY = true; + +public: + static property GazeCursor^ Instance + { + GazeCursor^ get() + { + static GazeCursor^ cursor = ref new GazeCursor(); + return cursor; + } + } + + void LoadSettings(ValueSet^ settings); + property int CursorRadius + { + int get() { return _cursorRadius; } + void set(int value); + } + + property bool IsCursorVisible + { + bool get() { return _isCursorVisible; } + void set(bool value); + } + + property bool IsGazeEntered + { + bool get() { return _isGazeEntered; } + void set(bool value); + } + + property Point Position + { + Point get() + { + return _cursorPosition; + } + + void set(Point value) + { + _cursorPosition = value; + _gazeCursor->Margin = Thickness(value.X - CursorRadius, value.Y - CursorRadius, 0, 0); + } + } + + property Point PositionOriginal + { + Point get() + { + return _originalCursorPosition; + } + + void set(Point value) + { + _originalCursorPosition = value; + _origSignalCursor->Margin = Thickness(value.X - CursorRadius, value.Y - CursorRadius, 0, 0); + } + } + +private: + GazeCursor(); + +private: + Popup^ _gazePopup; + Canvas^ _gazeCanvas; + Shapes::Ellipse^ _gazeCursor; + Shapes::Ellipse^ _origSignalCursor; + Shapes::Rectangle^ _gazeRect; + Point _cursorPosition = {}; + Point _originalCursorPosition = {}; + int _cursorRadius = DEFAULT_CURSOR_RADIUS; + bool _isCursorVisible = DEFAULT_CURSOR_VISIBILITY; + bool _isGazeEntered; + +}; + +END_NAMESPACE_GAZE_INPUT \ No newline at end of file diff --git a/Microsoft.Toolkit.Uwp.Input.GazeInteraction/GazeElement.h b/Microsoft.Toolkit.Uwp.Input.GazeInteraction/GazeElement.h new file mode 100644 index 00000000000..cabd7df89d9 --- /dev/null +++ b/Microsoft.Toolkit.Uwp.Input.GazeInteraction/GazeElement.h @@ -0,0 +1,54 @@ +//Copyright (c) Microsoft. All rights reserved. Licensed under the MIT license. +//See LICENSE in the project root for license information. + +#pragma once + +#include "DwellInvokedRoutedEventArgs.h" +#include "DwellProgressEventArgs.h" +#include "StateChangedEventArgs.h" + +using namespace Windows::Foundation; +using namespace Windows::UI::Xaml; + +BEGIN_NAMESPACE_GAZE_INPUT + +/// +/// Surrogate object attached to controls allowing subscription to per-control gaze events. +/// +public ref class GazeElement sealed : public DependencyObject +{ +public: + + /// + /// This event is fired when the state of the user's gaze on a control has changed + /// + event EventHandler^ StateChanged; + + /// + /// This event is fired when the user completed dwelling on a control and the control is about to be invoked by default. This event is fired to give the application an opportunity to prevent default invocation + /// + event EventHandler^ Invoked; + + /// + /// This event is fired to inform the application of the progress towards dwell + /// + event EventHandler^ DwellProgressFeedback; + +internal: + + void RaiseStateChanged(Object^ sender, StateChangedEventArgs^ args) { StateChanged(sender, args); } + + void RaiseInvoked(Object^ sender, DwellInvokedRoutedEventArgs^ args) + { + Invoked(sender, args); + } + + bool RaiseProgressFeedback(Object^ sender, DwellProgressState state, TimeSpan elapsedTime, TimeSpan triggerTime) + { + auto args = ref new DwellProgressEventArgs(state, elapsedTime, triggerTime); + DwellProgressFeedback(sender, args); + return args->Handled; + } +}; + +END_NAMESPACE_GAZE_INPUT \ No newline at end of file diff --git a/Microsoft.Toolkit.Uwp.Input.GazeInteraction/GazeFeedbackPopupFactory.cpp b/Microsoft.Toolkit.Uwp.Input.GazeInteraction/GazeFeedbackPopupFactory.cpp new file mode 100644 index 00000000000..99691b12021 --- /dev/null +++ b/Microsoft.Toolkit.Uwp.Input.GazeInteraction/GazeFeedbackPopupFactory.cpp @@ -0,0 +1,38 @@ +#include "pch.h" +#include "GazeFeedbackPopupFactory.h" + +using namespace Platform::Collections; + +static Vector^ s_cache = ref new Vector(); + +BEGIN_NAMESPACE_GAZE_INPUT + +Popup^ GazeFeedbackPopupFactory::Get() +{ + Popup^ popup; + + if (s_cache->Size != 0) + { + popup = s_cache->GetAt(0); + s_cache->RemoveAt(0); + } + else + { + popup = ref new Popup(); + + auto rectangle = ref new Rectangle(); + rectangle->StrokeThickness = 2; + + popup->Child = rectangle; + } + + return popup; +} + +void GazeFeedbackPopupFactory::Return(Popup^ popup) +{ + popup->IsOpen = false; + s_cache->Append(popup); +} + +END_NAMESPACE_GAZE_INPUT \ No newline at end of file diff --git a/Microsoft.Toolkit.Uwp.Input.GazeInteraction/GazeFeedbackPopupFactory.h b/Microsoft.Toolkit.Uwp.Input.GazeInteraction/GazeFeedbackPopupFactory.h new file mode 100644 index 00000000000..d2fe6722003 --- /dev/null +++ b/Microsoft.Toolkit.Uwp.Input.GazeInteraction/GazeFeedbackPopupFactory.h @@ -0,0 +1,16 @@ +#pragma once + +using namespace Windows::UI::Xaml::Controls::Primitives; + +BEGIN_NAMESPACE_GAZE_INPUT + +private ref class GazeFeedbackPopupFactory +{ +public: + + static Popup^ Get(); + + static void Return(Popup^ popup); +}; + +END_NAMESPACE_GAZE_INPUT diff --git a/Microsoft.Toolkit.Uwp.Input.GazeInteraction/GazeHistoryItem.h b/Microsoft.Toolkit.Uwp.Input.GazeInteraction/GazeHistoryItem.h new file mode 100644 index 00000000000..b1c1121bb4f --- /dev/null +++ b/Microsoft.Toolkit.Uwp.Input.GazeInteraction/GazeHistoryItem.h @@ -0,0 +1,19 @@ +//Copyright (c) Microsoft. All rights reserved. Licensed under the MIT license. +//See LICENSE in the project root for license information. + +#pragma once + +using namespace Windows::Foundation; + +BEGIN_NAMESPACE_GAZE_INPUT + +ref class GazeTargetItem; + +private ref struct GazeHistoryItem +{ + property GazeTargetItem^ HitTarget; + property TimeSpan Timestamp; + property TimeSpan Duration; +}; + +END_NAMESPACE_GAZE_INPUT \ No newline at end of file diff --git a/Microsoft.Toolkit.Uwp.Input.GazeInteraction/GazeInput.cpp b/Microsoft.Toolkit.Uwp.Input.GazeInteraction/GazeInput.cpp new file mode 100644 index 00000000000..15380934338 --- /dev/null +++ b/Microsoft.Toolkit.Uwp.Input.GazeInteraction/GazeInput.cpp @@ -0,0 +1,127 @@ +//Copyright (c) Microsoft. All rights reserved. Licensed under the MIT license. +//See LICENSE in the project root for license information. + +#include "pch.h" +#include "GazeInput.h" + +#include "GazeElement.h" +#include "GazePointer.h" +#include "GazePointerProxy.h" + +using namespace Platform; +using namespace Windows::UI; + + +BEGIN_NAMESPACE_GAZE_INPUT + +static Brush^ s_progressBrush = ref new SolidColorBrush(Colors::Green); + +Brush^ GazeInput::DwellFeedbackProgressBrush::get() +{ + return s_progressBrush; +} + +void GazeInput::DwellFeedbackCompleteBrush::set(Brush^ value) +{ + s_progressBrush = value; +} + +static Brush^ s_completeBrush = ref new SolidColorBrush(Colors::Red); + +Brush^ GazeInput::DwellFeedbackCompleteBrush::get() +{ + return s_completeBrush; +} + +void GazeInput::DwellFeedbackProgressBrush::set(Brush^ value) +{ + s_completeBrush = value; +} + +TimeSpan GazeInput::UnsetTimeSpan = { -1 }; + +static void OnInteractionChanged(DependencyObject^ ob, DependencyPropertyChangedEventArgs^ args) +{ + auto element = safe_cast(ob); + auto interaction = safe_cast(args->NewValue); + GazePointerProxy::SetGazeInteraction(element, interaction); +} + +static void OnIsCursorVisibleChanged(DependencyObject^ ob, DependencyPropertyChangedEventArgs^ args) +{ + GazePointer::Instance->IsCursorVisible = safe_cast(args->NewValue); +} + +static void OnCursorRadiusChanged(DependencyObject^ ob, DependencyPropertyChangedEventArgs^ args) +{ + GazePointer::Instance->CursorRadius = safe_cast(args->NewValue); +} + +static DependencyProperty^ s_interactionProperty = DependencyProperty::RegisterAttached("Interaction", Interaction::typeid, GazeInput::typeid, + ref new PropertyMetadata(Interaction::Inherited, ref new PropertyChangedCallback(&OnInteractionChanged))); +static DependencyProperty^ s_isCursorVisibleProperty = DependencyProperty::RegisterAttached("IsCursorVisible", bool::typeid, GazeInput::typeid, + ref new PropertyMetadata(true, ref new PropertyChangedCallback(&OnIsCursorVisibleChanged))); +static DependencyProperty^ s_cursorRadiusProperty = DependencyProperty::RegisterAttached("CursorRadius", int::typeid, GazeInput::typeid, + ref new PropertyMetadata(6, ref new PropertyChangedCallback(&OnCursorRadiusChanged))); +static DependencyProperty^ s_gazeElementProperty = DependencyProperty::RegisterAttached("GazeElement", GazeElement::typeid, GazeInput::typeid, ref new PropertyMetadata(nullptr)); +static DependencyProperty^ s_fixationDurationProperty = DependencyProperty::RegisterAttached("FixationDuration", TimeSpan::typeid, GazeInput::typeid, ref new PropertyMetadata(GazeInput::UnsetTimeSpan)); +static DependencyProperty^ s_dwellDurationProperty = DependencyProperty::RegisterAttached("DwellDuration", TimeSpan::typeid, GazeInput::typeid, ref new PropertyMetadata(GazeInput::UnsetTimeSpan)); +static DependencyProperty^ s_repeatDelayDurationProperty = DependencyProperty::RegisterAttached("RepeatDelayDuration", TimeSpan::typeid, GazeInput::typeid, ref new PropertyMetadata(GazeInput::UnsetTimeSpan)); +static DependencyProperty^ s_dwellRepeatDurationProperty = DependencyProperty::RegisterAttached("DwellRepeatDuration", TimeSpan::typeid, GazeInput::typeid, ref new PropertyMetadata(GazeInput::UnsetTimeSpan)); +static DependencyProperty^ s_thresholdDurationProperty = DependencyProperty::RegisterAttached("ThresholdDuration", TimeSpan::typeid, GazeInput::typeid, ref new PropertyMetadata(GazeInput::UnsetTimeSpan)); +static DependencyProperty^ s_maxRepeatCountProperty = DependencyProperty::RegisterAttached("MaxDwellRepeatCount", int::typeid, GazeInput::typeid, ref new PropertyMetadata(safe_cast(0))); + +DependencyProperty^ GazeInput::InteractionProperty::get() { return s_interactionProperty; } +DependencyProperty^ GazeInput::IsCursorVisibleProperty::get() { return s_isCursorVisibleProperty; } +DependencyProperty^ GazeInput::CursorRadiusProperty::get() { return s_cursorRadiusProperty; } +DependencyProperty^ GazeInput::GazeElementProperty::get() { return s_gazeElementProperty; } +DependencyProperty^ GazeInput::FixationDurationProperty::get() { return s_fixationDurationProperty; } +DependencyProperty^ GazeInput::DwellDurationProperty::get() { return s_dwellDurationProperty; } +DependencyProperty^ GazeInput::RepeatDelayDurationProperty::get() { return s_repeatDelayDurationProperty; } +DependencyProperty^ GazeInput::DwellRepeatDurationProperty::get() { return s_dwellRepeatDurationProperty; } +DependencyProperty^ GazeInput::ThresholdDurationProperty::get() { return s_thresholdDurationProperty; } +DependencyProperty^ GazeInput::MaxDwellRepeatCountProperty::get() { return s_maxRepeatCountProperty; } + +Interaction GazeInput::GetInteraction(UIElement^ element) { return safe_cast(element->GetValue(s_interactionProperty)); } +bool GazeInput::GetIsCursorVisible(UIElement^ element) { return safe_cast(element->GetValue(s_isCursorVisibleProperty)); } +int GazeInput::GetCursorRadius(UIElement^ element) { return safe_cast(element->GetValue(s_cursorRadiusProperty)); } +GazeElement^ GazeInput::GetGazeElement(UIElement^ element) { return safe_cast(element->GetValue(s_gazeElementProperty)); } +TimeSpan GazeInput::GetFixationDuration(UIElement^ element) { return safe_cast(element->GetValue(s_fixationDurationProperty)); } +TimeSpan GazeInput::GetDwellDuration(UIElement^ element) { return safe_cast(element->GetValue(s_dwellDurationProperty)); } +TimeSpan GazeInput::GetRepeatDelayDuration(UIElement^ element) { return safe_cast(element->GetValue(s_repeatDelayDurationProperty)); } +TimeSpan GazeInput::GetDwellRepeatDuration(UIElement^ element) { return safe_cast(element->GetValue(s_dwellRepeatDurationProperty)); } +TimeSpan GazeInput::GetThresholdDuration(UIElement^ element) { return safe_cast(element->GetValue(s_thresholdDurationProperty)); } +int GazeInput::GetMaxDwellRepeatCount(UIElement^ element) { return safe_cast(element->GetValue(s_maxRepeatCountProperty)); } + +void GazeInput::SetInteraction(UIElement^ element, Interaction value) { element->SetValue(s_interactionProperty, value); } +void GazeInput::SetIsCursorVisible(UIElement^ element, bool value) { element->SetValue(s_isCursorVisibleProperty, value); } +void GazeInput::SetCursorRadius(UIElement^ element, int value) { element->SetValue(s_cursorRadiusProperty, value); } +void GazeInput::SetGazeElement(UIElement^ element, GazeElement^ value) { element->SetValue(s_gazeElementProperty, value); } +void GazeInput::SetFixationDuration(UIElement^ element, TimeSpan span) { element->SetValue(s_fixationDurationProperty, span); } +void GazeInput::SetDwellDuration(UIElement^ element, TimeSpan span) { element->SetValue(s_dwellDurationProperty, span); } +void GazeInput::SetRepeatDelayDuration(UIElement^ element, TimeSpan span) { element->SetValue(s_repeatDelayDurationProperty, span); } +void GazeInput::SetDwellRepeatDuration(UIElement^ element, TimeSpan span) { element->SetValue(s_dwellRepeatDurationProperty, span); } +void GazeInput::SetThresholdDuration(UIElement^ element, TimeSpan span) { element->SetValue(s_thresholdDurationProperty, span); } +void GazeInput::SetMaxDwellRepeatCount(UIElement^ element, int value) { element->SetValue(s_maxRepeatCountProperty, value); } + +GazePointer^ GazeInput::GetGazePointer(Page^ page) +{ + return GazePointer::Instance; +} + +bool GazeInput::IsDeviceAvailable::get() +{ + return GazePointer::Instance->IsDeviceAvailable; +} + +EventRegistrationToken GazeInput::IsDeviceAvailableChanged::add(EventHandler^ handler) +{ + return GazePointer::Instance->IsDeviceAvailableChanged += handler; +} + +void GazeInput::IsDeviceAvailableChanged::remove(EventRegistrationToken token) +{ + GazePointer::Instance->IsDeviceAvailableChanged -= token; +} + +END_NAMESPACE_GAZE_INPUT \ No newline at end of file diff --git a/Microsoft.Toolkit.Uwp.Input.GazeInteraction/GazeInput.h b/Microsoft.Toolkit.Uwp.Input.GazeInteraction/GazeInput.h new file mode 100644 index 00000000000..e7fb588fe12 --- /dev/null +++ b/Microsoft.Toolkit.Uwp.Input.GazeInteraction/GazeInput.h @@ -0,0 +1,209 @@ +//Copyright (c) Microsoft. All rights reserved. Licensed under the MIT license. +//See LICENSE in the project root for license information. + +#pragma once + +#include "Interaction.h" + +using namespace Windows::UI::Xaml; +using namespace Windows::UI::Xaml::Controls; + +BEGIN_NAMESPACE_GAZE_INPUT + +ref class GazeElement; +ref class GazePointer; + +/// +/// Static class primarily providing access to attached properties controlling gaze behavior. +/// +[Windows::Foundation::Metadata::WebHostHidden] +public ref class GazeInput sealed +{ +public: + + /// + /// Identifyes the Interaction dependency property + /// + static property DependencyProperty^ InteractionProperty { DependencyProperty^ get(); } + + /// + /// Identifyes the IsCursorVisible dependency property + /// + static property DependencyProperty^ IsCursorVisibleProperty { DependencyProperty^ get(); } + + /// + /// Identifyes the CursorRadius dependency property + /// + static property DependencyProperty^ CursorRadiusProperty { DependencyProperty^ get(); } + + /// + /// Identifyes the GazeElement dependency property + /// + static property DependencyProperty^ GazeElementProperty { DependencyProperty^ get(); } + + /// + /// Identifyes the FixationDuration dependency property + /// + static property DependencyProperty^ FixationDurationProperty { DependencyProperty^ get(); } + + /// + /// Identifies the DwellDuration dependency property + /// + static property DependencyProperty^ DwellDurationProperty { DependencyProperty^ get(); } + + /// + /// Identifies the RepeatDelayDuration dependency property + /// + static property DependencyProperty^ RepeatDelayDurationProperty { DependencyProperty^ get(); } + + /// + /// Identifies the DwellRepeatDuration dependency property + /// + static property DependencyProperty^ DwellRepeatDurationProperty { DependencyProperty^ get(); } + + /// + /// Identifies the ThresholdDuration dependency property + /// + static property DependencyProperty^ ThresholdDurationProperty { DependencyProperty^ get(); } + + /// + /// Identifies the MaxDwellRepeatCount dependency property + /// + static property DependencyProperty^ MaxDwellRepeatCountProperty { DependencyProperty^ get(); } + + /// + /// Gets or sets the brush to use when displaying the default animation for dwell press + /// + static property Brush^ DwellFeedbackProgressBrush { Brush^ get(); void set(Brush^ value); } + + /// + /// Gets or sets the brush to use when displaying the default animation for dwell complete + /// + static property Brush^ DwellFeedbackCompleteBrush { Brush^ get(); void set(Brush^ value); } + + /// + /// Gets the status of gaze interaction over that particular XAML element. + /// + static Interaction GetInteraction(UIElement^ element); + + /// + /// Gets Boolean indicating whether cursor is shown while user is looking at the school. + /// + static bool GetIsCursorVisible(UIElement^ element); + + /// + /// Gets the size of the gaze cursor radius. + /// + static int GetCursorRadius(UIElement^ element); + + /// + /// Gets the GazeElement associated with an UIElement. + /// + static GazeElement^ GetGazeElement(UIElement^ element); + + /// + /// Gets the duration for the control to transition from the Enter state to the Fixation state. At this point, a StateChanged event is fired with PointerState set to Fixation. This event should be used to control the earliest visual feedback the application needs to provide to the user about the gaze location. The default is 350ms. + /// + static TimeSpan GetFixationDuration(UIElement^ element); + + /// + /// Gets the duration for the control to transition from the Fixation state to the Dwell state. At this point, a StateChanged event is fired with PointerState set to Dwell. The Enter and Fixation states are typicaly achieved too rapidly for the user to have much control over. In contrast Dwell is conscious event. This is the point at which the control is invoked, e.g. a button click. The application can modify this property to control when a gaze enabled UI element gets invoked after a user starts looking at it. + /// + static TimeSpan GetDwellDuration(UIElement^ element); + + /// + /// Gets the additional duration for the first repeat to occur.This prevents inadvertent repeated invocation. + /// + static TimeSpan GetRepeatDelayDuration(UIElement^ element); + + /// + /// Gets the duration of repeated dwell invocations, should the user continue to dwell on the control. The first repeat will occur after an additional delay specified by RepeatDelayDuration. Subsequent repeats happen after every period of DwellRepeatDuration. A control is invoked repeatedly only if MaxDwellRepeatCount is set to greater than zero. + /// + static TimeSpan GetDwellRepeatDuration(UIElement^ element); + + /// + /// Gets the duration that controls when the PointerState moves to either the Enter state or the Exit state. When this duration has elapsed after the user's gaze first enters a control, the PointerState is set to Enter. And when this duration has elapsed after the user's gaze has left the control, the PointerState is set to Exit. In both cases, a StateChanged event is fired. The default is 50ms. + /// + static TimeSpan GetThresholdDuration(UIElement^ element); + + /// + /// Gets the maximum times the control will invoked repeatedly without the user's gaze having to leave and re-enter the control. The default value is zero which disables repeated invocation of a control. Developers can set a higher value to enable repeated invocation. + /// + static int GetMaxDwellRepeatCount(UIElement^ element); + + /// + /// Sets the status of gaze interaction over that particular XAML element. + /// + static void SetInteraction(UIElement^ element, Interaction value); + + /// + /// Sets Boolean indicating whether cursor is shown while user is looking at the school. + /// + static void SetIsCursorVisible(UIElement^ element, bool value); + + /// + /// Sets the size of the gaze cursor radius. + /// + static void SetCursorRadius(UIElement^ element, int value); + + /// + /// Sets the GazeElement associated with an UIElement. + /// + static void SetGazeElement(UIElement^ element, GazeElement^ value); + + /// + /// Sets the duration for the control to transition from the Enter state to the Fixation state. At this point, a StateChanged event is fired with PointerState set to Fixation. This event should be used to control the earliest visual feedback the application needs to provide to the user about the gaze location. The default is 350ms. + /// + static void SetFixationDuration(UIElement^ element, TimeSpan span); + + /// + /// Sets the duration for the control to transition from the Fixation state to the Dwell state. At this point, a StateChanged event is fired with PointerState set to Dwell. The Enter and Fixation states are typicaly achieved too rapidly for the user to have much control over. In contrast Dwell is conscious event. This is the point at which the control is invoked, e.g. a button click. The application can modify this property to control when a gaze enabled UI element gets invoked after a user starts looking at it. + /// + static void SetDwellDuration(UIElement^ element, TimeSpan span); + + /// + /// Sets the additional duration for the first repeat to occur.This prevents inadvertent repeated invocation. + /// + static void SetRepeatDelayDuration(UIElement^ element, TimeSpan span); + + /// + /// Sets the duration of repeated dwell invocations, should the user continue to dwell on the control. The first repeat will occur after an additional delay specified by RepeatDelayDuration. Subsequent repeats happen after every period of DwellRepeatDuration. A control is invoked repeatedly only if MaxDwellRepeatCount is set to greater than zero. + /// + static void SetDwellRepeatDuration(UIElement^ element, TimeSpan span); + + /// + /// Sets the duration that controls when the PointerState moves to either the Enter state or the Exit state. When this duration has elapsed after the user's gaze first enters a control, the PointerState is set to Enter. And when this duration has elapsed after the user's gaze has left the control, the PointerState is set to Exit. In both cases, a StateChanged event is fired. The default is 50ms. + /// + static void SetThresholdDuration(UIElement^ element, TimeSpan span); + + /// + /// Sets the maximum times the control will invoked repeatedly without the user's gaze having to leave and re-enter the control. The default value is zero which disables repeated invocation of a control. Developers can set a higher value to enable repeated invocation. + /// + static void SetMaxDwellRepeatCount(UIElement^ element, int value); + + /// + /// Gets the GazePointer object. + /// + static GazePointer^ GetGazePointer(Page^ page); + + /// + /// Reports whether a gaze input device is available, and hence whether there is any possibility of gaze events occurring in the application. + /// + static property bool IsDeviceAvailable { bool get(); } + + + /// + /// Event triggered whenever IsDeviceAvailable changes value. + /// + static event EventHandler^ IsDeviceAvailableChanged + { + EventRegistrationToken add(EventHandler^ handler); + void remove(EventRegistrationToken token); + } + +internal: + + static TimeSpan UnsetTimeSpan; +}; + +END_NAMESPACE_GAZE_INPUT \ No newline at end of file diff --git a/Microsoft.Toolkit.Uwp.Input.GazeInteraction/GazePointer.cpp b/Microsoft.Toolkit.Uwp.Input.GazeInteraction/GazePointer.cpp new file mode 100644 index 00000000000..a39fd03f47e --- /dev/null +++ b/Microsoft.Toolkit.Uwp.Input.GazeInteraction/GazePointer.cpp @@ -0,0 +1,615 @@ +//Copyright (c) Microsoft. All rights reserved. Licensed under the MIT license. +//See LICENSE in the project root for license information. + +#include "pch.h" + +#include "GazePointer.h" + +#include "GazeElement.h" +#include "GazeHistoryItem.h" +#include "GazeTargetItem.h" +#include "StateChangedEventArgs.h" + +using namespace Platform; + +BEGIN_NAMESPACE_GAZE_INPUT + +GazePointer^ GazePointer::Instance::get() +{ + static auto value = ref new GazePointer(); + return value; +} + +void GazePointer::AddRoot(FrameworkElement^ element) +{ + _roots->InsertAt(0, element); + + if (_roots->Size == 1) + { + _isShuttingDown = false; + InitializeGazeInputSource(); + } +} + +void GazePointer::RemoveRoot(FrameworkElement^ element) +{ + unsigned int index = 0; + while (index < _roots->Size && _roots->GetAt(index) != element) + { + index++; + } + if (index < _roots->Size) + { + _roots->RemoveAt(index); + } + + if (_roots->Size == 0) + { + _isShuttingDown = true; + _gazeCursor->IsGazeEntered = false; + DeinitializeGazeInputSource(); + } +} + +GazePointer::GazePointer() +{ + _coreDispatcher = CoreWindow::GetForCurrentThread()->Dispatcher; + + // Default to not filtering sample data + Filter = ref new NullFilter(); + + _gazeCursor = GazeCursor::Instance; + + // timer that gets called back if there gaze samples haven't been received in a while + _eyesOffTimer = ref new DispatcherTimer(); + _eyesOffTimer->Tick += ref new EventHandler(this, &GazePointer::OnEyesOff); + + // provide a default of GAZE_IDLE_TIME microseconds to fire eyes off + EyesOffDelay = GAZE_IDLE_TIME; + + InitializeHistogram(); + + auto view = GazeInputSourcePreview::GetForCurrentView(); + _watcher = view->CreateWatcher(); + _watcher->Added += ref new TypedEventHandler(this, &GazePointer::OnDeviceAdded); + _watcher->Removed += ref new TypedEventHandler(this, &GazePointer::OnDeviceRemoved); + _watcher->Start(); +} + +void GazePointer::OnDeviceAdded(GazeDeviceWatcherPreview^ sender, GazeDeviceWatcherAddedPreviewEventArgs^ args) +{ + _deviceCount++; + + if (_deviceCount == 1) + { + IsDeviceAvailableChanged(nullptr, nullptr); + } +} + +void GazePointer::OnDeviceRemoved(GazeDeviceWatcherPreview^ sender, GazeDeviceWatcherRemovedPreviewEventArgs^ args) +{ + _deviceCount--; + + if (_deviceCount == 0) + { + IsDeviceAvailableChanged(nullptr, nullptr); + } +} + +GazePointer::~GazePointer() +{ + _watcher->Added -= _deviceAddedToken; + _watcher->Removed -= _deviceRemovedToken; + + if (_gazeInputSource != nullptr) + { + _gazeInputSource->GazeEntered -= _gazeEnteredToken; + _gazeInputSource->GazeMoved -= _gazeMovedToken; + _gazeInputSource->GazeExited -= _gazeExitedToken; + } +} + +void GazePointer::LoadSettings(ValueSet^ settings) +{ + _gazeCursor->LoadSettings(settings); + Filter->LoadSettings(settings); + + // TODO Add logic to protect against missing settings + + if (settings->HasKey("GazePointer.FixationDelay")) + { + _defaultFixation = TimeSpanFromMicroseconds((int)(settings->Lookup("GazePointer.FixationDelay"))); + } + + if (settings->HasKey("GazePointer.DwellDelay")) + { + _defaultDwell = TimeSpanFromMicroseconds((int)(settings->Lookup("GazePointer.DwellDelay"))); + } + + if (settings->HasKey("GazePointer.DwellDelay")) + { + _defaultDwellRepeatDelay = TimeSpanFromMicroseconds((int)(settings->Lookup("GazePointer.DwellRepeatDelay"))); + } + + if (settings->HasKey("GazePointer.RepeatDelay")) + { + _defaultRepeat = TimeSpanFromMicroseconds((int)(settings->Lookup("GazePointer.RepeatDelay"))); + } + + if (settings->HasKey("GazePointer.ThresholdDelay")) + { + _defaultThreshold = TimeSpanFromMicroseconds((int)(settings->Lookup("GazePointer.ThresholdDelay"))); + } + + // TODO need to set fixation and dwell for all elements + if (settings->HasKey("GazePointer.FixationDelay")) + { + SetElementStateDelay(_offScreenElement, PointerState::Fixation, TimeSpanFromMicroseconds((int)(settings->Lookup("GazePointer.FixationDelay")))); + } + if (settings->HasKey("GazePointer.DwellDelay")) + { + SetElementStateDelay(_offScreenElement, PointerState::Dwell, TimeSpanFromMicroseconds((int)(settings->Lookup("GazePointer.DwellDelay")))); + } + + if (settings->HasKey("GazePointer.GazeIdleTime")) + { + EyesOffDelay = TimeSpanFromMicroseconds((int)(settings->Lookup("GazePointer.GazeIdleTime"))); + } +} + +void GazePointer::InitializeHistogram() +{ + _activeHitTargetTimes = ref new Vector(); + + _offScreenElement = ref new UserControl(); + SetElementStateDelay(_offScreenElement, PointerState::Fixation, _defaultFixation); + SetElementStateDelay(_offScreenElement, PointerState::Dwell, _defaultDwell); + + _maxHistoryTime = DEFAULT_MAX_HISTORY_DURATION; // maintain about 3 seconds of history (in microseconds) + _gazeHistory = ref new Vector(); +} + +void GazePointer::InitializeGazeInputSource() +{ + _gazeInputSource = GazeInputSourcePreview::GetForCurrentView(); + if (_gazeInputSource != nullptr) + { + _gazeEnteredToken = _gazeInputSource->GazeEntered += ref new TypedEventHandler< + GazeInputSourcePreview^, GazeEnteredPreviewEventArgs^>(this, &GazePointer::OnGazeEntered); + _gazeMovedToken = _gazeInputSource->GazeMoved += ref new TypedEventHandler< + GazeInputSourcePreview^, GazeMovedPreviewEventArgs^>(this, &GazePointer::OnGazeMoved); + _gazeExitedToken = _gazeInputSource->GazeExited += ref new TypedEventHandler< + GazeInputSourcePreview^, GazeExitedPreviewEventArgs^>(this, &GazePointer::OnGazeExited); + } +} + +void GazePointer::DeinitializeGazeInputSource() +{ + if (_gazeInputSource != nullptr) + { + _gazeInputSource->GazeEntered -= _gazeEnteredToken; + _gazeInputSource->GazeMoved -= _gazeMovedToken; + _gazeInputSource->GazeExited -= _gazeExitedToken; + + _gazeInputSource = nullptr; + } +} + +static DependencyProperty^ GetProperty(PointerState state) +{ + switch (state) + { + case PointerState::Fixation: return GazeInput::FixationDurationProperty; + case PointerState::Dwell: return GazeInput::DwellDurationProperty; + case PointerState::DwellRepeat: return GazeInput::DwellRepeatDurationProperty; + case PointerState::Enter: return GazeInput::ThresholdDurationProperty; + case PointerState::Exit: return GazeInput::ThresholdDurationProperty; + default: return nullptr; + } +} + +TimeSpan GazePointer::GetDefaultPropertyValue(PointerState state) +{ + switch (state) + { + case PointerState::Fixation: return _defaultFixation; + case PointerState::Dwell: return _defaultDwell; + case PointerState::DwellRepeat: return _defaultRepeat; + case PointerState::Enter: return _defaultThreshold; + case PointerState::Exit: return _defaultThreshold; + default: throw ref new NotImplementedException(); + } +} + +void GazePointer::SetElementStateDelay(UIElement ^element, PointerState relevantState, TimeSpan stateDelay) +{ + auto property = GetProperty(relevantState); + element->SetValue(property, stateDelay); + + // fix up _maxHistoryTime in case the new param exceeds the history length we are currently tracking + auto dwellTime = GetElementStateDelay(element, PointerState::Dwell); + auto repeatTime = GetElementStateDelay(element, PointerState::DwellRepeat); + _maxHistoryTime = 2 * max(dwellTime, repeatTime); +} + +TimeSpan GazePointer::GetElementStateDelay(UIElement ^element, DependencyProperty^ property, TimeSpan defaultValue) +{ + DependencyObject^ walker = element; + Object^ valueAtWalker = walker->GetValue(property); + + while (GazeInput::UnsetTimeSpan.Equals(valueAtWalker) && walker != nullptr) + { + walker = VisualTreeHelper::GetParent(walker); + + if (walker != nullptr) + { + valueAtWalker = walker->GetValue(property); + } + } + + auto ticks = GazeInput::UnsetTimeSpan.Equals(valueAtWalker) ? defaultValue : safe_cast(valueAtWalker); + + return ticks; +} + +TimeSpan GazePointer::GetElementStateDelay(UIElement ^element, PointerState pointerState) +{ + auto property = GetProperty(pointerState); + auto defaultValue = GetDefaultPropertyValue(pointerState); + auto ticks = GetElementStateDelay(element, property, defaultValue); + + switch (pointerState) + { + case PointerState::Dwell: + case PointerState::DwellRepeat: + _maxHistoryTime = max(_maxHistoryTime, 2 * ticks); + break; + } + + return ticks; +} + +void GazePointer::Reset() +{ + _activeHitTargetTimes->Clear(); + _gazeHistory->Clear(); + + _maxHistoryTime = DEFAULT_MAX_HISTORY_DURATION; +} + +GazeTargetItem^ GazePointer::GetHitTarget(Point gazePoint) +{ + for each (auto rootElement in _roots) + { + auto targets = VisualTreeHelper::FindElementsInHostCoordinates(gazePoint, rootElement, false); + GazeTargetItem^ invokable = nullptr; + for each (auto target in targets) + { + if (invokable == nullptr) + { + auto item = GazeTargetItem::GetOrCreate(target); + if (item->IsInvokable) + { + invokable = item; + } + } + + switch (GazeInput::GetInteraction(target)) + { + case Interaction::Enabled: + if (invokable != nullptr) + { + return invokable; + } + break; + + case Interaction::Disabled: + return GazeTargetItem::NonInvokable; + } + } + assert(invokable == nullptr); + } + // TODO : Check if the location is offscreen + return GazeTargetItem::NonInvokable; +} + +void GazePointer::ActivateGazeTargetItem(GazeTargetItem^ target) +{ + unsigned int index; + if (!_activeHitTargetTimes->IndexOf(target, &index)) + { + _activeHitTargetTimes->Append(target); + + // calculate the time that the first DwellRepeat needs to be fired after. this will be updated every time a DwellRepeat is + // fired to keep track of when the next one is to be fired after that. + auto nextStateTime = GetElementStateDelay(target->TargetElement, PointerState::Enter); + + target->Reset(nextStateTime); + } +} + +GazeTargetItem^ GazePointer::ResolveHitTarget(Point gazePoint, TimeSpan timestamp) +{ + // TODO: The existance of a GazeTargetItem should be used to indicate that + // the target item is invokable. The method of invokation should be stored + // within the GazeTargetItem when it is created and not recalculated when + // subsequently needed. + + // create GazeHistoryItem to deal with this sample + auto target = GetHitTarget(gazePoint); + auto historyItem = ref new GazeHistoryItem(); + historyItem->HitTarget = target; + historyItem->Timestamp = timestamp; + historyItem->Duration = TimeSpanZero; + assert(historyItem->HitTarget != nullptr); + + // create new GazeTargetItem with a (default) total elapsed time of zero if one does not exist already. + // this ensures that there will always be an entry for target elements in the code below. + ActivateGazeTargetItem(target); + target->LastTimestamp = timestamp; + + // find elapsed time since we got the last hit target + historyItem->Duration = timestamp - _lastTimestamp; + if (historyItem->Duration > MAX_SINGLE_SAMPLE_DURATION) + { + historyItem->Duration = MAX_SINGLE_SAMPLE_DURATION; + } + _gazeHistory->Append(historyItem); + + // update the time this particular hit target has accumulated + target->DetailedTime += historyItem->Duration; + + // drop the oldest samples from the list until we have samples only + // within the window we are monitoring + // + // historyItem is the last item we just appended a few lines above. + for (auto evOldest = _gazeHistory->GetAt(0); + historyItem->Timestamp - evOldest->Timestamp > _maxHistoryTime; + evOldest = _gazeHistory->GetAt(0)) + { + _gazeHistory->RemoveAt(0); + + // subtract the duration obtained from the oldest sample in _gazeHistory + auto targetItem = evOldest->HitTarget; + assert(targetItem->DetailedTime - evOldest->Duration >= TimeSpanZero); + targetItem->DetailedTime -= evOldest->Duration; + if (targetItem->ElementState != PointerState::PreEnter) + { + targetItem->OverflowTime += evOldest->Duration; + } + } + + _lastTimestamp = timestamp; + + // Return the most recent hit target + // Intuition would tell us that we should return NOT the most recent + // hitTarget, but the one with the most accumulated time in + // in the maintained history. But the effect of that is that + // the user will feel that they have clicked on the wrong thing + // when they are looking at something else. + // That is why we return the most recent hitTarget so that + // when its dwell time has elapsed, it will be invoked + return target; +} + +void GazePointer::GotoState(UIElement^ control, PointerState state) +{ + Platform::String^ stateName; + + switch (state) + { + case PointerState::Enter: + return; + case PointerState::Exit: + stateName = "Normal"; + break; + case PointerState::Fixation: + stateName = "Fixation"; + break; + case PointerState::DwellRepeat: + case PointerState::Dwell: + stateName = "Dwell"; + break; + default: + //assert(0); + return; + } + + // TODO: Implement proper support for visual states + // VisualStateManager::GoToState(dynamic_cast(control), stateName, true); +} + +void GazePointer::OnEyesOff(Object ^sender, Object ^ea) +{ + _eyesOffTimer->Stop(); + + CheckIfExiting(_lastTimestamp + EyesOffDelay); + RaiseGazePointerEvent(nullptr, PointerState::Enter, EyesOffDelay); +} + +void GazePointer::CheckIfExiting(TimeSpan curTimestamp) +{ + for (unsigned int index = 0; index < _activeHitTargetTimes->Size; index++) + { + auto targetItem = _activeHitTargetTimes->GetAt(index); + auto targetElement = targetItem->TargetElement; + auto exitDelay = GetElementStateDelay(targetElement, PointerState::Exit); + + auto idleDuration = curTimestamp - targetItem->LastTimestamp; + if (targetItem->ElementState != PointerState::PreEnter && idleDuration > exitDelay) + { + targetItem->ElementState = PointerState::PreEnter; + GotoState(targetElement, PointerState::Exit); + RaiseGazePointerEvent(targetItem, PointerState::Exit, targetItem->ElapsedTime); + targetItem->GiveFeedback(); + + _activeHitTargetTimes->RemoveAt(index); + + // remove all history samples referring to deleted hit target + for (unsigned i = 0; i < _gazeHistory->Size; ) + { + auto hitTarget = _gazeHistory->GetAt(i)->HitTarget; + if (hitTarget->TargetElement == targetElement) + { + _gazeHistory->RemoveAt(i); + } + else + { + i++; + } + } + + // return because only one element can be exited at a time and at this point + // we have done everything that we can do + return; + } + } +} + +wchar_t *PointerStates[] = { + L"Exit", + L"PreEnter", + L"Enter", + L"Fixation", + L"Dwell", + L"DwellRepeat" +}; + +void GazePointer::RaiseGazePointerEvent(GazeTargetItem^ target, PointerState state, TimeSpan elapsedTime) +{ + auto control = target != nullptr ? safe_cast(target->TargetElement) : nullptr; + //assert(target != _rootElement); + auto gpea = ref new StateChangedEventArgs(control, state, elapsedTime); + //auto buttonObj = dynamic_cast +``` + +```c# +private void OnInvokeProgress(object sender, GazeProgressEventArgs e) +{ + e.Handled = true; +} +``` + + + + + +## Sample Code + +[GazeInteractionPage](https://github.com/Microsoft/UWPCommunityToolkit/tree/master/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/GazeInteraction/). You can see this in action in [UWP Community Toolkit Sample App](https://www.microsoft.com/store/apps/9NBLGGH4TLCQ). + +## Requirements + +| Device family | Universal,10.0.17133.0 or higher | +| -- | -- | +| Namespace | Microsoft.Toolkit.Uwp.Input.Gaze | +| NuGet package | [NuGet package](https://www.nuget.org/packages/Microsoft.Toolkit.Uwp.Input.Gaze/) | + +## API Source Code + +* [control/helper name source code](https://github.com/Microsoft/UWPCommunityToolkit/tree/master/Microsoft.Toolkit.Uwp.Input.Gaze) + +## Related Topics + +* [Windows 10 eye gaze API Preview](https://docs.microsoft.com/en-us/uwp/api/windows.devices.input.preview)