diff --git a/Microsoft.Toolkit.Uwp.Input.GazeInteraction/GazeInput.cpp b/Microsoft.Toolkit.Uwp.Input.GazeInteraction/GazeInput.cpp index dd4ce104589..272e54b378e 100644 --- a/Microsoft.Toolkit.Uwp.Input.GazeInteraction/GazeInput.cpp +++ b/Microsoft.Toolkit.Uwp.Input.GazeInteraction/GazeInput.cpp @@ -10,6 +10,7 @@ #include "GazeTargetItem.h" using namespace Platform; +using namespace Windows::Foundation::Collections; using namespace Windows::UI; BEGIN_NAMESPACE_GAZE_INPUT @@ -85,6 +86,11 @@ static void OnCursorRadiusChanged(DependencyObject^ ob, DependencyPropertyChange GazePointer::Instance->CursorRadius = safe_cast(args->NewValue); } +static void OnIsSwitchEnabledChanged(DependencyObject^ ob, DependencyPropertyChangedEventArgs^ args) +{ + GazePointer::Instance->IsSwitchEnabled = 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, @@ -98,6 +104,8 @@ static DependencyProperty^ s_repeatDelayDurationProperty = DependencyProperty::R 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))); +static DependencyProperty^ s_isSwitchEnabledProperty = DependencyProperty::RegisterAttached("IsSwitchEnabled", bool::typeid, GazeInput::typeid, + ref new PropertyMetadata(false, ref new PropertyChangedCallback(&OnIsSwitchEnabledChanged))); DependencyProperty^ GazeInput::InteractionProperty::get() { return s_interactionProperty; } DependencyProperty^ GazeInput::IsCursorVisibleProperty::get() { return s_isCursorVisibleProperty; } @@ -109,6 +117,7 @@ DependencyProperty^ GazeInput::RepeatDelayDurationProperty::get() { return s_rep DependencyProperty^ GazeInput::DwellRepeatDurationProperty::get() { return s_dwellRepeatDurationProperty; } DependencyProperty^ GazeInput::ThresholdDurationProperty::get() { return s_thresholdDurationProperty; } DependencyProperty^ GazeInput::MaxDwellRepeatCountProperty::get() { return s_maxRepeatCountProperty; } +DependencyProperty^ GazeInput::IsSwitchEnabledProperty::get() { return s_isSwitchEnabledProperty; } Interaction GazeInput::GetInteraction(UIElement^ element) { return safe_cast(element->GetValue(s_interactionProperty)); } bool GazeInput::GetIsCursorVisible(UIElement^ element) { return safe_cast(element->GetValue(s_isCursorVisibleProperty)); } @@ -120,6 +129,7 @@ TimeSpan GazeInput::GetRepeatDelayDuration(UIElement^ element) { return safe_cas 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)); } +bool GazeInput::GetIsSwitchEnabled(UIElement^ element) { return safe_cast(element->GetValue(s_isSwitchEnabledProperty)); } void GazeInput::SetInteraction(UIElement^ element, GazeInteraction::Interaction value) { element->SetValue(s_interactionProperty, value); } void GazeInput::SetIsCursorVisible(UIElement^ element, bool value) { element->SetValue(s_isCursorVisibleProperty, value); } @@ -131,6 +141,7 @@ void GazeInput::SetRepeatDelayDuration(UIElement^ element, TimeSpan span) { elem 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); } +void GazeInput::SetIsSwitchEnabled(UIElement^ element, bool value) { element->SetValue(s_isSwitchEnabledProperty, value); } GazePointer^ GazeInput::GetGazePointer(Page^ page) { @@ -143,6 +154,11 @@ void GazeInput::Invoke(UIElement^ element) item->Invoke(); } +void GazeInput::LoadSettings(ValueSet^ settings) +{ + GazePointer::Instance->LoadSettings(settings); +} + bool GazeInput::IsDeviceAvailable::get() { return GazePointer::Instance->IsDeviceAvailable; diff --git a/Microsoft.Toolkit.Uwp.Input.GazeInteraction/GazeInput.h b/Microsoft.Toolkit.Uwp.Input.GazeInteraction/GazeInput.h index cf7f4472fba..1b722a1ee41 100644 --- a/Microsoft.Toolkit.Uwp.Input.GazeInteraction/GazeInput.h +++ b/Microsoft.Toolkit.Uwp.Input.GazeInteraction/GazeInput.h @@ -5,6 +5,7 @@ #include "Interaction.h" +using namespace Windows::Foundation::Collections; using namespace Windows::UI::Xaml; using namespace Windows::UI::Xaml::Controls; @@ -71,6 +72,11 @@ public ref class GazeInput sealed /// static property DependencyProperty^ MaxDwellRepeatCountProperty { DependencyProperty^ get(); } + /// + /// Identifyes the IsSwitchEnabled dependency property + /// + static property DependencyProperty^ IsSwitchEnabledProperty { DependencyProperty^ get(); } + /// /// Gets or sets the brush to use when displaying the default indication that gaze entered a control /// @@ -141,6 +147,11 @@ public ref class GazeInput sealed /// static int GetMaxDwellRepeatCount(UIElement^ element); + /// + /// Gets the Boolean indicating whether gaze plus switch is enabled. + /// + static bool GetIsSwitchEnabled(UIElement^ element); + /// /// Sets the status of gaze interaction over that particular XAML element. /// @@ -191,6 +202,11 @@ public ref class GazeInput sealed /// static void SetMaxDwellRepeatCount(UIElement^ element, int value); + /// + /// Sets the Boolean indicating whether gaze plus switch is enabled. + /// + static void SetIsSwitchEnabled(UIElement^ element, bool value); + /// /// Gets the GazePointer object. /// @@ -215,6 +231,13 @@ public ref class GazeInput sealed void remove(EventRegistrationToken token); } + /// + /// Loads a settings collection into GazeInput. + /// Note: This must be loaded from a UI thread to be valid, since the GazeInput + /// instance is tied to the UI thread. + /// + static void LoadSettings(ValueSet^ settings); + internal: static TimeSpan UnsetTimeSpan; diff --git a/Microsoft.Toolkit.Uwp.Input.GazeInteraction/GazePointer.cpp b/Microsoft.Toolkit.Uwp.Input.GazeInteraction/GazePointer.cpp index 76c6f87b79f..49daaf1a668 100644 --- a/Microsoft.Toolkit.Uwp.Input.GazeInteraction/GazePointer.cpp +++ b/Microsoft.Toolkit.Uwp.Input.GazeInteraction/GazePointer.cpp @@ -11,6 +11,7 @@ #include "StateChangedEventArgs.h" using namespace Platform; +using namespace Windows::Foundation; using namespace Windows::UI::Xaml::Automation::Peers; BEGIN_NAMESPACE_GAZE_INPUT @@ -150,14 +151,14 @@ void GazePointer::LoadSettings(ValueSet^ settings) _defaultDwell = TimeSpanFromMicroseconds((int)(settings->Lookup("GazePointer.DwellDelay"))); } - if (settings->HasKey("GazePointer.DwellDelay")) + if (settings->HasKey("GazePointer.DwellRepeatDelay")) { - _defaultDwellRepeatDelay = TimeSpanFromMicroseconds((int)(settings->Lookup("GazePointer.DwellRepeatDelay"))); + _defaultDwellRepeatDelay = TimeSpanFromMicroseconds((int)(settings->Lookup("GazePointer.RepeatDelay"))); } if (settings->HasKey("GazePointer.RepeatDelay")) { - _defaultRepeat = TimeSpanFromMicroseconds((int)(settings->Lookup("GazePointer.RepeatDelay"))); + _defaultRepeatDelay = TimeSpanFromMicroseconds((int)(settings->Lookup("GazePointer.RepeatDelay"))); } if (settings->HasKey("GazePointer.ThresholdDelay")) @@ -179,6 +180,11 @@ void GazePointer::LoadSettings(ValueSet^ settings) { EyesOffDelay = TimeSpanFromMicroseconds((int)(settings->Lookup("GazePointer.GazeIdleTime"))); } + + if (settings->HasKey("GazePointer.IsSwitchEnabled")) + { + IsSwitchEnabled = (bool)(settings->Lookup("GazePointer.IsSwitchEnabled")); + } } void GazePointer::InitializeHistogram() @@ -241,7 +247,7 @@ TimeSpan GazePointer::GetDefaultPropertyValue(PointerState state) { case PointerState::Fixation: return _defaultFixation; case PointerState::Dwell: return _defaultDwell; - case PointerState::DwellRepeat: return _defaultRepeat; + case PointerState::DwellRepeat: return _defaultRepeatDelay; case PointerState::Enter: return _defaultThreshold; case PointerState::Exit: return _defaultThreshold; default: throw ref new NotImplementedException(); @@ -480,33 +486,6 @@ GazeTargetItem^ GazePointer::ResolveHitTarget(Point gazePoint, TimeSpan timestam 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(); @@ -527,7 +506,10 @@ void GazePointer::CheckIfExiting(TimeSpan curTimestamp) if (targetItem->ElementState != PointerState::PreEnter && idleDuration > exitDelay) { targetItem->ElementState = PointerState::PreEnter; - GotoState(targetElement, PointerState::Exit); + + // Transitioning to exit - clear the cached fixated element + _currentlyFixatedElement = nullptr; + RaiseGazePointerEvent(targetItem, PointerState::Exit, targetItem->ElapsedTime); targetItem->GiveFeedback(); @@ -689,7 +671,20 @@ void GazePointer::ProcessGazePoint(TimeSpan timestamp, Point position) } } - GotoState(targetItem->TargetElement, targetItem->ElementState); + if (targetItem->ElementState == PointerState::Fixation) + { + // Cache the fixated item + _currentlyFixatedElement = targetItem; + + // We are about to transition into the Dwell state + // If switch input is enabled, make sure dwell never completes + // via eye gaze + if (_isSwitchEnabled) + { + // Don't allow the next state (Dwell) to progress + targetItem->NextStateTime = TimeSpan{ MAXINT64 }; + } + } RaiseGazePointerEvent(targetItem, targetItem->ElementState, targetItem->ElapsedTime); } @@ -700,4 +695,16 @@ void GazePointer::ProcessGazePoint(TimeSpan timestamp, Point position) _lastTimestamp = fa->Timestamp; } +/// +/// When in switch mode, will issue a click on the currently fixated element +/// +void GazePointer::Click() +{ + if (_isSwitchEnabled && + _currentlyFixatedElement != nullptr) + { + _currentlyFixatedElement->Invoke(); + } +} + END_NAMESPACE_GAZE_INPUT \ No newline at end of file diff --git a/Microsoft.Toolkit.Uwp.Input.GazeInteraction/GazePointer.h b/Microsoft.Toolkit.Uwp.Input.GazeInteraction/GazePointer.h index a106ec7cbe9..5c41ea6579c 100644 --- a/Microsoft.Toolkit.Uwp.Input.GazeInteraction/GazePointer.h +++ b/Microsoft.Toolkit.Uwp.Input.GazeInteraction/GazePointer.h @@ -44,6 +44,11 @@ public ref class GazePointer sealed /// void LoadSettings(ValueSet^ settings); + /// + /// When in switch mode, will issue a click on the currently fixated element + /// + void Click(); + internal: Brush^ _enterBrush = nullptr; @@ -95,6 +100,12 @@ public ref class GazePointer sealed void set(int value) { _gazeCursor->CursorRadius = value; } } + property bool IsSwitchEnabled + { + bool get() { return _isSwitchEnabled; } + void set(bool value) { _isSwitchEnabled = value; } + } + internal: static property GazePointer^ Instance { GazePointer^ get(); } @@ -126,7 +137,6 @@ public ref class GazePointer sealed GazeTargetItem^ ResolveHitTarget(Point gazePoint, TimeSpan timestamp); void CheckIfExiting(TimeSpan curTimestamp); - void GotoState(UIElement^ control, PointerState state); void RaiseGazePointerEvent(GazeTargetItem^ target, PointerState state, TimeSpan elapsedTime); void OnGazeEntered( @@ -183,8 +193,11 @@ public ref class GazePointer sealed TimeSpan _defaultFixation = DEFAULT_FIXATION_DELAY; TimeSpan _defaultDwell = DEFAULT_DWELL_DELAY; TimeSpan _defaultDwellRepeatDelay = DEFAULT_DWELL_REPEAT_DELAY; - TimeSpan _defaultRepeat = DEFAULT_REPEAT_DELAY; + TimeSpan _defaultRepeatDelay = DEFAULT_REPEAT_DELAY; TimeSpan _defaultThreshold = DEFAULT_THRESHOLD_DELAY; + + bool _isSwitchEnabled; + GazeTargetItem^ _currentlyFixatedElement; }; END_NAMESPACE_GAZE_INPUT \ No newline at end of file diff --git a/Microsoft.Toolkit.Uwp.Input.GazeInteraction/GazeSettingsHelper.cpp b/Microsoft.Toolkit.Uwp.Input.GazeInteraction/GazeSettingsHelper.cpp index 03c599f1440..2f2cc14fd35 100644 --- a/Microsoft.Toolkit.Uwp.Input.GazeInteraction/GazeSettingsHelper.cpp +++ b/Microsoft.Toolkit.Uwp.Input.GazeInteraction/GazeSettingsHelper.cpp @@ -16,37 +16,21 @@ GazeSettingsHelper::GazeSettingsHelper() Windows::Foundation::IAsyncAction^ GazeSettingsHelper::RetrieveSharedSettings(ValueSet^ settings) { - return create_async([settings]{ + return create_async([settings] { // Setup a new app service connection AppServiceConnection^ connection = ref new AppServiceConnection(); connection->AppServiceName = "com.microsoft.ectksettings"; connection->PackageFamilyName = "Microsoft.EyeControlToolkitSettings_s9y1p3hwd5qda"; // open the connection - create_task(connection->OpenAsync()).then([settings, connection](AppServiceConnectionStatus status) + return create_task(connection->OpenAsync()).then([settings, connection](AppServiceConnectionStatus status) { switch (status) { case AppServiceConnectionStatus::Success: // The new connection opened successfully // Set up the inputs and send a message to the service - create_task(connection->SendMessageAsync(ref new ValueSet())).then([settings](AppServiceResponse^ response) - { - switch (response->Status) - { - case AppServiceResponseStatus::Success: - for each (auto item in response->Message) - { - settings->Insert(item->Key, item->Value); - } - break; - default: - case AppServiceResponseStatus::Failure: - case AppServiceResponseStatus::ResourceLimitsExceeded: - case AppServiceResponseStatus::Unknown: - break; - } - }); // create_task(connection->SendMessageAsync(inputs)) + return create_task(connection->SendMessageAsync(ref new ValueSet())); break; default: @@ -54,9 +38,32 @@ Windows::Foundation::IAsyncAction^ GazeSettingsHelper::RetrieveSharedSettings(Va case AppServiceConnectionStatus::AppUnavailable: case AppServiceConnectionStatus::AppServiceUnavailable: case AppServiceConnectionStatus::Unknown: + // All return paths need to return a task of type AppServiceResponse, so fake it + AppServiceResponse ^ response = nullptr; + return task_from_result(response); + } + }).then([settings](AppServiceResponse^ response) + { + if (response == nullptr) + { + return; + } + + switch (response->Status) + { + case AppServiceResponseStatus::Success: + for each (auto item in response->Message) + { + settings->Insert(item->Key, item->Value); + } + break; + default: + case AppServiceResponseStatus::Failure: + case AppServiceResponseStatus::ResourceLimitsExceeded: + case AppServiceResponseStatus::Unknown: break; } - }); // create_task(connection->OpenAsync()) + }); }); // create_async() } diff --git a/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/GazeInteraction/GazeInteractionPage.xaml b/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/GazeInteraction/GazeInteractionPage.xaml index 28a27717ffe..9bec18d1004 100644 --- a/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/GazeInteraction/GazeInteractionPage.xaml +++ b/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/GazeInteraction/GazeInteractionPage.xaml @@ -4,11 +4,12 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:local="using:Microsoft.Toolkit.Uwp.SampleApp.Common" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" - xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" - xmlns:g="using:Microsoft.Toolkit.Uwp.Input.GazeInteraction" - g:GazeInput.Interaction="Enabled" - g:GazeInput.IsCursorVisible="True" - g:GazeInput.CursorRadius="20" + xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" + xmlns:g="using:Microsoft.Toolkit.Uwp.Input.GazeInteraction" + g:GazeInput.Interaction="Enabled" + g:GazeInput.IsCursorVisible="True" + g:GazeInput.CursorRadius="20" + g:GazeInput.IsSwitchEnabled="False" mc:Ignorable="d"> @@ -135,8 +136,8 @@ - - + + diff --git a/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/GazeInteraction/GazeInteractionPage.xaml.cs b/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/GazeInteraction/GazeInteractionPage.xaml.cs index 66252bcf3ea..b8b0364c80f 100644 --- a/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/GazeInteraction/GazeInteractionPage.xaml.cs +++ b/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/GazeInteraction/GazeInteractionPage.xaml.cs @@ -5,6 +5,7 @@ using System; using Microsoft.Toolkit.Uwp.Input.GazeInteraction; using Microsoft.Toolkit.Uwp.UI.Extensions; +using Windows.UI.Core; using Windows.UI.Xaml; using Windows.UI.Xaml.Controls; using Windows.UI.Xaml.Shapes; @@ -17,6 +18,7 @@ namespace Microsoft.Toolkit.Uwp.SampleApp.SamplePages public sealed partial class GazeInteractionPage : IXamlRenderListener { private GazeElement gazeButtonControl; + private GazePointer gazePointer; private int dwellCount = 0; @@ -32,6 +34,7 @@ public void OnXamlRendered(FrameworkElement control) WarnUserToPlugInDevice(); var buttonControl = control.FindChildByName("TargetButton") as Button; + buttonControl.Click += TargetButton_Click; if (buttonControl != null) { @@ -50,6 +53,13 @@ public void OnXamlRendered(FrameworkElement control) gazeButtonControl.StateChanged += GazeButtonControl_StateChanged; } } + + gazePointer = GazeInput.GetGazePointer(null); + + CoreWindow.GetForCurrentThread().KeyDown += new Windows.Foundation.TypedEventHandler(delegate(CoreWindow sender, KeyEventArgs args) + { + gazePointer.Click(); + }); } private void GazeInput_IsDeviceAvailableChanged(object sender, object e) @@ -126,5 +136,12 @@ private void GazeButtonControl_StateChanged(object sender, StateChangedEventArgs DwellProgressBar.Value = 0; } } + + private int _targetButtonClickCount = 0; + + private void TargetButton_Click(object sender, RoutedEventArgs e) + { + ClickCount.Text = $"Number of clicks = {++_targetButtonClickCount}"; + } } } diff --git a/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/GazeInteraction/GazeInteractionXaml.bind b/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/GazeInteraction/GazeInteractionXaml.bind index be23433bbad..33792d0a1bf 100644 --- a/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/GazeInteraction/GazeInteractionXaml.bind +++ b/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/GazeInteraction/GazeInteractionXaml.bind @@ -1,20 +1,20 @@ - - - +