2
\$\begingroup\$

I am trying to achieve the following, a behavior like Windows Explorer where mouse and keyboard play nice together.

Here's a GIF along explanations:

enter image description here

  • first I click one of the folders using the mouse
  • second I click the background area, folder is not focused but still selected
  • third I move to the next folder below using the keyboard

To sum it up, mouse and keyboard play well together.

Now in Unity it's an entirely different story, here is a fairly simple menu created using the actions in Hierarchy context menu, nothing special:

enter image description here

Here's what happens:

  • I click one of the buttons using the mouse
  • I move down to the next button using the keyboard
  • I click in the blank area, button loses focus
  • I try to move down to the next button using the keyboard, nothing happens

I have tried a couple of things like disabling Raycast target on images, disabling background panels or making them opaque but without success.

How can one make Unity UI behave like Windows Explorer's one ?

(where clicking in the blank area with the mouse does not break keyboard navigation)

\$\endgroup\$
2
\$\begingroup\$

You need to create some kind of manager class, which controls the logic of the control element. Add a script, which holds references to the buttons and create methods, which react to user input. In this manager you would build a dictionary of type (Button, int), or if it's easier for you a List of type (Button, int), which you fill at the Awake method of the class. You do this in order to create a relationship between a button and an index.

So, whenever the user clicks on a button, the manager saves the last clicked index. Now, when the user uses the keyboard, you check the last clicked index and depending on the direction of the keyboard input (up/down), you increase/decrease the index by one and look up the right button in the list. You then activate the respective button and update the last saved index. Make sure to catch edge-cases when the index reaches a value smaller than 0 or an index greater than the elements in the list minus one.

\$\endgroup\$
1
\$\begingroup\$

In turned out there was an utterly simple solution to this problem and that works really well, the trickiest part being to figure it out by thinking out of the box loud enough!

One extra script next to the ScrollRect which tracks selection and restores it after dragging but with visual feedback being preserved, with a bit of reflection, though.

In the following GIF:

  • dragging the scroller with the mouse
    • last control is saved but its state looks like selected
  • mouse is released
    • control is restored and really selected
  • navigation with the keyboard
  • dragging again

While I show a drag operation, it works when you just click on the back, like in Windows Explorer.

enter image description here

using System.Reflection;
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.UI;

namespace WIP
{
    public sealed class PreventLostFocus : MonoBehaviour, IPointerDownHandler, IPointerUpHandler
    {
        private GameObject _current;

        [Tooltip("Selectable should look as selected during drag?")]
        public bool SelectedLook;

        #region IPointerDownHandler Members

        public void OnPointerDown(PointerEventData eventData)
        {
            if (_current == null)
                return;

            if (!SelectedLook)
                return;

            var selectable = _current.GetComponent<Selectable>();
            if (selectable == null)
                return;

            var method = selectable.GetType().GetMethod("DoStateTransition", BindingFlags.Instance | BindingFlags.NonPublic);
            if (method == null)
            {
                Debug.LogWarning("Couldn't perform discreet state transition, something has probably changed under the hood.");
                return; // fail gracefully but let dev know it didn't work
            }

            method.Invoke(selectable, new object[] {3, true}); // 3 == Selectable.SelectionState.Selected
        }

        #endregion

        #region IPointerUpHandler Members

        public void OnPointerUp(PointerEventData eventData)
        {
            // restore last good selection

            if (_current == null)
                return;

            EventSystem.current.SetSelectedGameObject(_current);
        }

        #endregion

        private void Reset()
        {
            SelectedLook = true;
        }

        private void Update()
        {
            // track current selection

            var current = EventSystem.current.currentSelectedGameObject;
            if (current == null)
                return;

            _current = current;
        }
    }
}
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.