private async Task ProcessMultiKeySelectionResult(
            IList<Timestamped<PointAndKeyValue>> pointsAndKeyValues, 
            TriggerSignal startSelectionTriggerSignal)
        {
            Log.DebugFormat("Multi-key selection captured a set of '{0}' PointAndKeyValues.", pointsAndKeyValues.Count);

            RequestSuspend(); //Pause everything (i.e. processing new points) while we perform the (CPU bound) word matching

            try
            {
                if (pointsAndKeyValues.Any())
                {
                    var timeSpan = pointsAndKeyValues.Last().Timestamp.Subtract(pointsAndKeyValues.First().Timestamp);

                    var sequenceThreshold = (int)Math.Round(
                        ((double)pointsAndKeyValues.Count / (double)timeSpan.TotalMilliseconds)
                        * Settings.Default.MultiKeySelectionFixationMinDwellTime.TotalMilliseconds);

                    Log.DebugFormat(
                        "Multi-key selection capture lasted {0}ms. Minimum dwell time is {1}ms, or {2} points.",
                        timeSpan.TotalMilliseconds,
                        Settings.Default.MultiKeySelectionFixationMinDwellTime.TotalMilliseconds,
                        sequenceThreshold);

                    //Always assume the start trigger is reliable if it occurs on a letter
                    string reliableFirstLetter =
                        startMultiKeySelectionTriggerSignal != null
                        && startMultiKeySelectionTriggerSignal.Value.PointAndKeyValue != null
                        && startMultiKeySelectionTriggerSignal.Value.PointAndKeyValue.Value.StringIsLetter
                            ? startMultiKeySelectionTriggerSignal.Value.PointAndKeyValue.Value.String
                            : null;

                    Log.DebugFormat(
                        "First letter ('{0}') of multi-key selection capture {1} reliable.",
                        reliableFirstLetter,
                        reliableFirstLetter != null ? "IS" : "IS NOT");

                    //If we are using a fixation trigger and the stop trigger has occurred on a letter then it is reliable - use it
                    string reliableLastLetter = selectionTriggerSource is IFixationTriggerSource
                        && stopMultiKeySelectionTriggerSignal != null
                        && stopMultiKeySelectionTriggerSignal.Value.PointAndKeyValue != null
                        && stopMultiKeySelectionTriggerSignal.Value.PointAndKeyValue.Value.StringIsLetter
                            ? stopMultiKeySelectionTriggerSignal.Value.PointAndKeyValue.Value.String
                            : null;

                    Log.DebugFormat(
                            "Last letter ('{0}') of multi-key selection capture {1} reliable.",
                            reliableLastLetter,
                            reliableLastLetter != null ? "IS" : "IS NOT");

                    if (reliableLastLetter != null)
                    {
                        Log.Debug("Publishing selection event on last letter of multi-key selection capture.");

                        PublishSelection(stopMultiKeySelectionTriggerSignal.Value.PointAndKeyValue.Value);
                    }

                    //Why am I wrapping this call in a Task.Run? Internally the MapCaptureToEntries method uses PLINQ which also blocks the UI thread - this frees it up.
                    //This cannot be done inside the MapCaptureToEntries method as the method takes a ref param, which cannot be used inside an anonymous delegate or lambda.
                    //The method cannot be made awaitable as async/await also does not support ref params.
                    Tuple<List<Point>, FunctionKeys?, string, List<string>> result = null;
                    await Task.Run(() =>
                    {
                        result = dictionaryService.MapCaptureToEntries(
                            pointsAndKeyValues.ToList(), sequenceThreshold,
                            reliableFirstLetter, reliableLastLetter,
                            ref mapToDictionaryMatchesCancellationTokenSource,
                            exception => PublishError(this, exception));
                    });

                    if (result != null)
                    {
                        if (result.Item2 == null && result.Item3 == null &&
                            (result.Item4 == null || !result.Item4.Any()))
                        {
                            //Nothing useful in the result - play error message. Publish anyway as the points can be rendered in debugging mode.
                            audioService.PlaySound(Settings.Default.ErrorSoundFile, Settings.Default.ErrorSoundVolume);
                        }

                        PublishSelectionResult(result);
                    }
                }
            }
            finally
            {
                RequestResume();
            }
        }
        private void ProcessSelectionTrigger(TriggerSignal triggerSignal)
        {
            if (triggerSignal.Signal >= 1
                && !CapturingMultiKeySelection)
            {
                //We are not currently capturing a multikey selection and have received a high (start) trigger signal
                if (triggerSignal.PointAndKeyValue != null)
                {
                    Log.Debug("Selection trigger signal (with relevent PointAndKeyValue) detected.");

                    if (SelectionMode == SelectionModes.Key)
                    {
                        if (triggerSignal.PointAndKeyValue.Value.KeyValue != null
                            && (keyStateService.KeyEnabledStates == null || keyStateService.KeyEnabledStates[triggerSignal.PointAndKeyValue.Value.KeyValue.Value]))
                        {
                            Log.Debug("Selection mode is KEY and the key on which the trigger occurred is enabled.");

                            if (MultiKeySelectionSupported
                                && keyStateService.KeyDownStates[KeyValues.MultiKeySelectionKey].Value.IsDownOrLockedDown()
                                && triggerSignal.PointAndKeyValue.Value.KeyValue != null
                                && KeyValues.MultiKeySelectionKeys.Contains(triggerSignal.PointAndKeyValue.Value.KeyValue.Value))
                            {
                                Log.Debug("Multi-key selection is currently enabled and the key on which the trigger occurred is a letter. Publishing the selection and beginning a new multi-key selection capture.");

                                //Multi-key selection is allowed and the trigger occurred on a letter - start a capture
                                startMultiKeySelectionTriggerSignal = triggerSignal;
                                stopMultiKeySelectionTriggerSignal = null;

                                CapturingMultiKeySelection = true;

                                PublishSelection(triggerSignal.PointAndKeyValue.Value);

                                multiKeySelectionSubscription =
                                    CreateMultiKeySelectionSubscription()
                                        .ObserveOnDispatcher()
                                        .Subscribe(
                                            async pointsAndKeyValues => await ProcessMultiKeySelectionResult(pointsAndKeyValues, triggerSignal),
                                            (exception =>
                                            {
                                                PublishError(this, exception);

                                                stopMultiKeySelectionTriggerSignal = null;
                                                CapturingMultiKeySelection = false;
                                            }),
                                            () =>
                                            {
                                                Log.Debug("Multi-key selection capture has completed.");

                                                stopMultiKeySelectionTriggerSignal = null;
                                                CapturingMultiKeySelection = false;
                                            });
                            }
                            else
                            {
                                PublishSelection(triggerSignal.PointAndKeyValue.Value);

                                PublishSelectionResult(new Tuple<List<Point>, FunctionKeys?, string, List<string>>(
                                    new List<Point> { triggerSignal.PointAndKeyValue.Value.Point },
                                    triggerSignal.PointAndKeyValue.Value.KeyValue.Value.FunctionKey,
                                    triggerSignal.PointAndKeyValue.Value.KeyValue.Value.String,
                                    null));
                            }
                        }
                        else
                        {
                            Log.Debug("Selection mode is KEY, but the trigger occurred off a key or over a disabled key.");
                            audioService.PlaySound(Settings.Default.ErrorSoundFile, Settings.Default.ErrorSoundVolume);
                        }
                    }
                    else if (SelectionMode == SelectionModes.Point)
                    {
                        PublishSelection(triggerSignal.PointAndKeyValue.Value);

                        PublishSelectionResult(new Tuple<List<Point>, FunctionKeys?, string, List<string>>(
                            new List<Point> { triggerSignal.PointAndKeyValue.Value.Point }, null, null, null));
                    }
                }
                else
                {
                    Log.Error("TriggerSignal.Signal==1, but TriggerSignal.PointAndKeyValue is null. "
                            + "Discarding trigger as point source is down, or producing stale points. "
                            + "Publishing error instead.");

                    PublishError(this, new ApplicationException(Resources.TRIGGER_WITHOUT_POSITION_ERROR));
                }
            }
            else if (CapturingMultiKeySelection)
            {
                //We are capturing and may have received the stop capturing signal
                if ((triggerSignal.Signal >= 1 && Settings.Default.MultiKeySelectionTriggerStopSignal == TriggerStopSignals.NextHigh)
                    || (triggerSignal.Signal <= -1 && Settings.Default.MultiKeySelectionTriggerStopSignal == TriggerStopSignals.NextLow))
                {
                    //If we are using a fixation trigger source then the stop signal must occur on a letter
                    if (!(selectionTriggerSource is IFixationTriggerSource)
                        || (triggerSignal.PointAndKeyValue != null && triggerSignal.PointAndKeyValue.Value.StringIsLetter))
                    {
                        Log.Debug("Trigger signal to stop the current multi-key selection capture detected.");

                        stopMultiKeySelectionTriggerSignal = triggerSignal;
                    }
                }
            }
        }