Newer
Older
HoloAnatomy / Assets / HoloToolkit-Examples / UX / Scripts / Interactive.cs
SURFACEBOOK2\jackwynne on 25 May 2018 16 KB v1
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License. See LICENSE in the project root for license information.

using UnityEngine;
using UnityEngine.Events;
using System.Collections.Generic;
using HoloToolkit.Unity.InputModule;

#if UNITY_WSA || UNITY_STANDALONE_WIN
using UnityEngine.Windows.Speech;
#endif

namespace HoloToolkit.Examples.InteractiveElements
{
    /// <summary>
    /// Interactive exposes basic button type events to the Unity Editor and receives messages from the GestureManager and GazeManager.
    /// 
    /// Beyond the basic button functionality, Interactive also maintains the notion of selection and enabled, which allow for more robust UI features.
    /// InteractiveEffects are behaviors that listen for updates from Interactive, which allows for visual feedback to be customized and placed on
    /// individual elements of the Interactive GameObject
    /// </summary>
    public class Interactive : MonoBehaviour, IInputClickHandler, IFocusable, IInputHandler
    {

        public GameObject ParentObject;

        /// <summary>
        /// Should the button listen to input?
        /// </summary>
        public bool IsEnabled = true;

        /// <summary>
        /// Does the GameObject currently have focus?
        /// </summary>
        public bool HasGaze { get; protected set; }

        /// <summary>
        /// Is the Tap currently in the down state?
        /// </summary>
        public bool HasDown { get; protected set; }

        /// <summary>
        /// Should the button care about click and hold?
        /// </summary>
        public bool DetectHold = false;

        /// <summary>
        /// Configure the amount to time for the hold event to fire
        /// </summary>
        public float HoldTime = 0.5f;

        /// <summary>
        /// Configure the amount of time a roll off update should incur. When building more advanced UI,
        /// we may need to evaluate what the next gazed item is before updating.
        /// </summary>
        public float RollOffTime = 0.02f;

        /// <summary>
        /// Current selected state, can be set from the Unity Editor for default behavior
        /// </summary>
        public bool IsSelected { get; protected set; }

        [Tooltip("Set a keyword to invoke the OnSelect event")]
        public string Keyword = "";

        [Tooltip("Gaze is required for the keyword to trigger this Interactive.")]
        public bool KeywordRequiresGaze = true;

        /// <summary>
        /// Exposed Unity Events
        /// </summary>
        public UnityEvent OnSelectEvents;
        public UnityEvent OnDownEvent;
        public UnityEvent OnHoldEvent;

        /// <summary>
        /// A button typically has 8 potential states.
        /// We can update visual feedback based on state change, all the logic is already done, making InteractiveEffects behaviors less complex then comparing selected + Disabled.
        /// </summary>
        public enum ButtonStateEnum { Default, Focus, Press, Selected, FocusSelected, PressSelected, Disabled, DisabledSelected }
        protected ButtonStateEnum mState = ButtonStateEnum.Default;

        /// <summary>
        /// Timers
        /// </summary>
        protected float mRollOffTimer = 0;
        protected float mHoldTimer = 0;
        protected bool mCheckRollOff = false;
        protected bool mCheckHold = false;

#if UNITY_WSA || UNITY_STANDALONE_WIN
        protected KeywordRecognizer mKeywordRecognizer;
#endif
        protected Dictionary<string, int> mKeywordDictionary;
        protected string[] mKeywordArray;

        /// <summary>
        /// Internal comparison variables to allow for live state updates no matter the input method
        /// </summary>
        protected bool mIgnoreSelect = false;
        protected bool mCheckEnabled = false;
        protected bool mCheckSelected = false;
        protected bool UserInitiatedEvent = false;
        protected bool mAllowSelection = false;

        protected List<InteractiveWidget> mInteractiveWidgets = new List<InteractiveWidget>();

        protected virtual void Awake()
        {
            if (ParentObject == null)
            {
                ParentObject = this.gameObject;
            }

            CollectWidgets(forceCollection: true);
        }

        /// <summary>
        /// Set default visual states on Start
        /// </summary>
        protected virtual void Start()
        {
            if (Keyword != "")
            {
                mKeywordArray = new string[1] { Keyword };
                if (Keyword.IndexOf(',') > -1)
                {
                    mKeywordArray = Keyword.Split(',');

                    mKeywordDictionary = new Dictionary<string, int>();
                    for (int i = 0; i < mKeywordArray.Length; ++i)
                    {
                        mKeywordDictionary.Add(mKeywordArray[i], i);
                    }
                }

#if UNITY_WSA || UNITY_STANDALONE_WIN
                if (!KeywordRequiresGaze)
                {
                    mKeywordRecognizer = new KeywordRecognizer(mKeywordArray);
                    mKeywordRecognizer.OnPhraseRecognized += KeywordRecognizer_OnPhraseRecognized;
                    mKeywordRecognizer.Start();
                }
#endif

            }

            mCheckEnabled = IsEnabled;
            mCheckSelected = IsSelected;

            UpdateEffects();
        }

        /// <summary>
        /// An OnTap event occurred
        /// </summary>
        public virtual void OnInputClicked(InputClickedEventData eventData)
        {
            if (!IsEnabled)
            {
                return;
            }

            UserInitiatedEvent = true;

            if (mIgnoreSelect)
            {
                mIgnoreSelect = false;
                return;
            }

            UpdateEffects();

            OnSelectEvents.Invoke();
        }

        /// <summary>
        /// The gameObject received gaze
        /// </summary>
        public virtual void OnFocusEnter()
        {
            if (!IsEnabled)
            {
                return;
            }

            HasGaze = true;

            SetKeywordListener(true);

            UpdateEffects();
        }

        /// <summary>
        /// The gameObject no longer has gaze
        /// </summary>
        public virtual void OnFocusExit()
        {
            HasGaze = false;
            EndHoldDetection();
            mRollOffTimer = 0;
            mCheckRollOff = true;
            SetKeywordListener(false);
            UpdateEffects();
        }

        private void SetKeywordListener(bool listen)
        {
#if UNITY_WSA || UNITY_STANDALONE_WIN
            if (listen)
            {
                if (KeywordRequiresGaze && mKeywordArray != null)
                {
                    if (mKeywordRecognizer == null)
                    {
                        mKeywordRecognizer = new KeywordRecognizer(mKeywordArray);
                        mKeywordRecognizer.OnPhraseRecognized += KeywordRecognizer_OnPhraseRecognized;
                        mKeywordRecognizer.Start();
                    }
                    else
                    {
                        if (!mKeywordRecognizer.IsRunning)
                        {
                            mKeywordRecognizer.Start();
                        }
                    }
                }
            }
            else
            {
                if (mKeywordRecognizer != null && KeywordRequiresGaze)
                {
                    if (mKeywordRecognizer.IsRunning)
                    {
                        mKeywordRecognizer.Stop();
                        mKeywordRecognizer.OnPhraseRecognized -= KeywordRecognizer_OnPhraseRecognized;
                        mKeywordRecognizer.Dispose();
                        mKeywordRecognizer = null;
                    }
                }
            }
#endif
        }

        /// <summary>
        /// shortcut to set title
        /// (assuming this Interactive has a LabelTheme and a TextMesh attached to it)
        /// </summary>
        /// <param name="title"></param>
        public void SetTitle(string title)
        {
            LabelTheme lblTheme = gameObject.GetComponent<LabelTheme>();
            if (lblTheme != null)
            {
                lblTheme.Default = title;
            }
            TextMesh textMesh = gameObject.GetComponentInChildren<TextMesh>();
            if (textMesh != null)
            {
                textMesh.text = title;
            }
        }

        /// <summary>
        /// The user is initiating a tap or hold
        /// </summary>
        public virtual void OnInputDown(InputEventData eventData)
        {
            if (!HasGaze)
            {
                return;
            }

            HasDown = true;
            mCheckRollOff = false;

            if (DetectHold)
            {
                mHoldTimer = 0;
                mCheckHold = true;
            }
            UpdateEffects();

            OnDownEvent.Invoke();
        }

        /// <summary>
        /// All tab, hold, and gesture events are completed
        /// </summary>
        public virtual void OnInputUp(InputEventData eventData)
        {
            mCheckHold = false;
            HasDown = false;
            mIgnoreSelect = false;
            EndHoldDetection();
            mCheckRollOff = false;

            UpdateEffects();
        }

        /// <summary>
        /// The hold timer has finished
        /// </summary>
        public virtual void OnHold()
        {
            mIgnoreSelect = true;
            EndHoldDetection();

            UpdateEffects();

            OnHoldEvent.Invoke();
        }

        /// <summary>
        /// The percentage of hold time completed
        /// </summary>
        /// <returns>percentage 0 - 1</returns>
        public float GetHoldPercentage()
        {
            return mHoldTimer / HoldTime;
        }

        protected void EndHoldDetection()
        {
            mHoldTimer = 0;
            mCheckHold = false;
        }

        private void CollectWidgets(bool forceCollection = false)
        {
            if (mInteractiveWidgets.Count == 0 || forceCollection)
            {
                if (ParentObject != null)
                {
                    ParentObject.GetComponentsInChildren(mInteractiveWidgets);
                }
                for (int i = 0; i < mInteractiveWidgets.Count; ++i)
                {
                    if (mInteractiveWidgets[i].InteractiveHost == null)
                    {
                        mInteractiveWidgets[i].InteractiveHost = this;
                    }
                    else
                    {
                        mInteractiveWidgets.RemoveAt(i);
                        --i;
                    }
                }
            }
        }

        /// <summary>
        /// Loop through all InteractiveEffects on child elements and update their states
        /// </summary>
        protected void UpdateEffects()
        {
            CollectWidgets();

            CompareStates();

            int interactiveCount = mInteractiveWidgets.Count;
            for (int i = 0; i < interactiveCount; ++i)
            {
                InteractiveWidget widget = mInteractiveWidgets[i];
                widget.SetState(mState);

                int currentCount = mInteractiveWidgets.Count;
                if (currentCount < interactiveCount)
                {
                    Debug.LogWarningFormat("Call to {0}'s SetState removed other interactive widgets. GameObject name: {1}.", widget.GetType(), widget.name);
                    interactiveCount = currentCount;
                }
            }
        }

        public void RegisterWidget(InteractiveWidget widget)
        {
            CollectWidgets();
            if (mInteractiveWidgets.Contains(widget))
            {
                return;
            }

            mInteractiveWidgets.Add(widget);
            widget.SetState(mState);
        }

        public void UnregisterWidget(InteractiveWidget widget)
        {
            if (mInteractiveWidgets != null)
            {
                mInteractiveWidgets.Remove(widget);
            }
        }

#if UNITY_WSA || UNITY_STANDALONE_WIN
        protected virtual void KeywordRecognizer_OnPhraseRecognized(PhraseRecognizedEventArgs args)
        {

            // Check to make sure the recognized keyword matches, then invoke the corresponding method.
            if (args.text == Keyword && (!KeywordRequiresGaze || HasGaze) && IsEnabled)
            {
                if (mKeywordDictionary == null)
                {
                    OnInputClicked(null);
                }
            }
        }
#endif

        /// <summary>
        /// Check if any state changes have occurred, from alternate input sources
        /// </summary>
        protected void CompareStates()
        {
            if (IsEnabled)
            {
                // all states
                if (IsSelected)
                {
                    if (HasGaze)
                    {
                        if (HasDown)
                        {
                            mState = ButtonStateEnum.PressSelected;
                        }
                        else
                        {
                            mState = ButtonStateEnum.FocusSelected;
                        }
                    }
                    else
                    {
                        if (HasDown)
                        {
                            mState = ButtonStateEnum.PressSelected;
                        }
                        else
                        {
                            mState = ButtonStateEnum.Selected;
                        }
                    }
                }
                else
                {
                    if (HasGaze)
                    {
                        if (HasDown)
                        {
                            mState = ButtonStateEnum.Press;
                        }
                        else
                        {
                            mState = ButtonStateEnum.Focus;
                        }
                    }
                    else
                    {
                        if (HasDown)
                        {
                            mState = ButtonStateEnum.Press;
                        }
                        else
                        {
                            mState = ButtonStateEnum.Default;
                        }
                    }
                }

            }
            else
            {
                if (IsSelected)
                {
                    mState = ButtonStateEnum.DisabledSelected;
                }
                else
                {
                    mState = ButtonStateEnum.Disabled;
                }
            }
            mCheckSelected = IsSelected;
            mCheckEnabled = IsEnabled;
        }

        /// <summary>
        /// Run timers and check for updates
        /// </summary>
        protected virtual void Update()
        {

            if (mCheckRollOff && HasDown)
            {
                if (mRollOffTimer < RollOffTime)
                {
                    mRollOffTimer += Time.deltaTime;
                }
                else
                {
                    mCheckRollOff = false;
                    OnInputUp(null);
                }
            }
            if (mCheckHold)
            {
                if (mHoldTimer < HoldTime)
                {
                    mHoldTimer += Time.deltaTime;
                }
                else
                {
                    mCheckHold = false;
                    OnHold();
                }
            }

            if (!UserInitiatedEvent && (mCheckEnabled != IsEnabled || mCheckSelected != IsSelected))
            {
                UpdateEffects();
            }

            UserInitiatedEvent = false;
        }

        protected virtual void OnDestroy()
        {
            SetKeywordListener(false);
        }

        protected virtual void OnEnable()
        {
#if UNITY_WSA || UNITY_STANDALONE_WIN
            if (mKeywordRecognizer != null && !KeywordRequiresGaze)
            {
                SetKeywordListener(true);
            }
#endif
        }

        protected virtual void OnDisable()
        {
            OnFocusExit();
        }
    }
}