Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add gaze plus switch support #2270

Merged
merged 22 commits into from
Jun 27, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions Microsoft.Toolkit.Uwp.Input.GazeInteraction/GazeInput.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
#include "GazeTargetItem.h"

using namespace Platform;
using namespace Windows::Foundation::Collections;
using namespace Windows::UI;

BEGIN_NAMESPACE_GAZE_INPUT
Expand Down Expand Up @@ -85,6 +86,11 @@ static void OnCursorRadiusChanged(DependencyObject^ ob, DependencyPropertyChange
GazePointer::Instance->CursorRadius = safe_cast<int>(args->NewValue);
}

static void OnIsSwitchEnabledChanged(DependencyObject^ ob, DependencyPropertyChangedEventArgs^ args)
{
GazePointer::Instance->IsSwitchEnabled = safe_cast<bool>(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,
Expand All @@ -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<Object^>(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; }
Expand All @@ -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<GazeInteraction::Interaction>(element->GetValue(s_interactionProperty)); }
bool GazeInput::GetIsCursorVisible(UIElement^ element) { return safe_cast<bool>(element->GetValue(s_isCursorVisibleProperty)); }
Expand All @@ -120,6 +129,7 @@ TimeSpan GazeInput::GetRepeatDelayDuration(UIElement^ element) { return safe_cas
TimeSpan GazeInput::GetDwellRepeatDuration(UIElement^ element) { return safe_cast<TimeSpan>(element->GetValue(s_dwellRepeatDurationProperty)); }
TimeSpan GazeInput::GetThresholdDuration(UIElement^ element) { return safe_cast<TimeSpan>(element->GetValue(s_thresholdDurationProperty)); }
int GazeInput::GetMaxDwellRepeatCount(UIElement^ element) { return safe_cast<int>(element->GetValue(s_maxRepeatCountProperty)); }
bool GazeInput::GetIsSwitchEnabled(UIElement^ element) { return safe_cast<bool>(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); }
Expand All @@ -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)
{
Expand All @@ -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;
Expand Down
23 changes: 23 additions & 0 deletions Microsoft.Toolkit.Uwp.Input.GazeInteraction/GazeInput.h
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

#include "Interaction.h"

using namespace Windows::Foundation::Collections;
using namespace Windows::UI::Xaml;
using namespace Windows::UI::Xaml::Controls;

Expand Down Expand Up @@ -71,6 +72,11 @@ public ref class GazeInput sealed
/// </summary>
static property DependencyProperty^ MaxDwellRepeatCountProperty { DependencyProperty^ get(); }

/// <summary>
/// Identifyes the IsSwitchEnabled dependency property
/// </summary>
static property DependencyProperty^ IsSwitchEnabledProperty { DependencyProperty^ get(); }

/// <summary>
/// Gets or sets the brush to use when displaying the default indication that gaze entered a control
/// </summary>
Expand Down Expand Up @@ -141,6 +147,11 @@ public ref class GazeInput sealed
/// </summary>
static int GetMaxDwellRepeatCount(UIElement^ element);

/// <summary>
/// Gets the Boolean indicating whether gaze plus switch is enabled.
/// </summary>
static bool GetIsSwitchEnabled(UIElement^ element);

/// <summary>
/// Sets the status of gaze interaction over that particular XAML element.
/// </summary>
Expand Down Expand Up @@ -191,6 +202,11 @@ public ref class GazeInput sealed
/// </summary>
static void SetMaxDwellRepeatCount(UIElement^ element, int value);

/// <summary>
/// Sets the Boolean indicating whether gaze plus switch is enabled.
/// </summary>
static void SetIsSwitchEnabled(UIElement^ element, bool value);

/// <summary>
/// Gets the GazePointer object.
/// </summary>
Expand All @@ -215,6 +231,13 @@ public ref class GazeInput sealed
void remove(EventRegistrationToken token);
}

/// <summary>
/// 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.
/// </summary>
static void LoadSettings(ValueSet^ settings);

internal:

static TimeSpan UnsetTimeSpan;
Expand Down
73 changes: 40 additions & 33 deletions Microsoft.Toolkit.Uwp.Input.GazeInteraction/GazePointer.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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"))
Expand All @@ -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()
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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^>(control), stateName, true);
}

void GazePointer::OnEyesOff(Object ^sender, Object ^ea)
{
_eyesOffTimer->Stop();
Expand All @@ -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();

Expand Down Expand Up @@ -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);
}
Expand All @@ -700,4 +695,16 @@ void GazePointer::ProcessGazePoint(TimeSpan timestamp, Point position)
_lastTimestamp = fa->Timestamp;
}

/// <summary>
/// When in switch mode, will issue a click on the currently fixated element
/// </summary>
void GazePointer::Click()
{
if (_isSwitchEnabled &&
_currentlyFixatedElement != nullptr)
{
_currentlyFixatedElement->Invoke();
}
}

END_NAMESPACE_GAZE_INPUT
17 changes: 15 additions & 2 deletions Microsoft.Toolkit.Uwp.Input.GazeInteraction/GazePointer.h
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,11 @@ public ref class GazePointer sealed
/// </summary>
void LoadSettings(ValueSet^ settings);

/// <summary>
/// When in switch mode, will issue a click on the currently fixated element
/// </summary>
void Click();

internal:
Brush^ _enterBrush = nullptr;

Expand Down Expand Up @@ -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(); }
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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
47 changes: 27 additions & 20 deletions Microsoft.Toolkit.Uwp.Input.GazeInteraction/GazeSettingsHelper.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -16,47 +16,54 @@ 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:
case AppServiceConnectionStatus::AppNotInstalled:
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()
}

Expand Down
Loading