[UE4] フォーカスとUIナビゲーション

対象Ver：UE4.25.0
#1. フォーカス
　Widgetにはフォーカスが可能かどうかを指定するフラグ`UUserWidget::bIsFocusable`があります。フォーカスが当たっているウィジェットはインタラクションが可能になり、例えばButtonにフォーカスが当たっている場合はスペースキーを押すとボタンのPressを実行し、テキストボックスにフォーカスされている場合は文字を入力することが可能です。

![2020-06-02_22h13_24.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/101019/c6903cb4-07b0-8d72-aab5-9dc536194916.png)

　UserWidgetの場合、このフラグに応じてフォーカス可能かの判定`UUserWidget::NativeSupportsKeyboardFocus`を行いますが、`SObjectWidget::SupportsKeyboardFocus`の実装を見るとButtonやListViewなどといった特定のWidgetへのフォーカス可否については、独自の実装`SObjectWidget::SupportsKeyboardFocus`によって決まっています。Button, CheckBox, Slider, ListViewなどは、ユーザー操作によってフォーカスの移動が可能であるものであるため、有効になっています。

#2. フォーカスの判定
　キーボードやコントローラなどの入力によりフォーカスを適用するかは最初に`FSlateApplication::AttemptNavigation`を実行します。ここでTabやShift+Tabなど「フォーカス送り」の処理は`FWeakWidgetPath::ToNextFocusedPath`で実行し、割り当てられたインデックス番号を元に次の番号か前の番号のWidgetにフォーカスを移動します。最終的にフォーカスの移動は`FWidgetPath::MoveFocus`で行います。
![video_01.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/101019/58b4592d-3aff-d88b-a46f-e0912a704279.gif)

*図：1,2,3,4の順に生成されたら1→2→3→4の順にタブ送りされる*

　一方、キー操作など「フォーカスのフリー移動」処理は`FHittestGrid::FindFocusableWidget`で行います。こちらの方法では、入力に応じて視覚的に上下右左のいずれかのWidgetにフォーカスをうつすことをします。最終的なフォーカスの送り先はWidgetの位置や大きさなどから判断して移動しますが、移動できないようなケースも存在します。
![video_01.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/101019/ede0a558-7845-a66c-4b64-96022f3091ed.gif)
*図：キー操作の方向にあるUIが次の移動となる*

　例えば下の二つの図は、2→4のボタンにフォーカスを移動させたいとした場合、2のボタンにフォーカスが当たっている状態でキーボードの右を押下した時にフォーカスが移動できるか？というものです。左は高さの部分が重複しているのでフォーカスの移動ができます。右の図では高さが不一致なので右を押してもフォーカスは移動しません。フォーカスが移動できるのはその方向にあるWidgetの領域が交差する場合です。

![2020-06-02_22h15_17.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/101019/3bf01cda-98ef-b993-62b7-10eeb723f06f.png)　　![2020-06-02_22h17_00.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/101019/4973db67-5ae5-9571-1c55-ee0c13d36379.png)
※左：2→4にフォーカス移動可能、右：2→4にフォーカス移動不可

#3. UIナビゲーション
　AIのナビゲーション機能と名前が被るので明示的に示すためにUIナビゲーションとしています。上記でフォーカスが移動する/しないという話をしましたが、ユーザーの操作によってUIの選択位置が移動するのがナビゲーションの機能です。以下のブログでは詳しく説明されています。

[ヒストリア様ブログ：UMGのNavigation機能を使ってみる](http://historia.co.jp/archives/8521/)

#4. ナビゲーション操作

![2020-06-02_22h39_08.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/101019/d6dd6c39-bfc5-36a2-665a-95416e1f4fb2.png)

| 項目			|概要			|
|:-----------------	|:------------------	|
| Escape		|操作方向/Index指定にフォーカスは移動します	|
| Stop			|操作方向/Index指定への移動を抑制します		|
| Wrap			|ループさせたい時などに逆方向に移動します		|
| Explicit		|指定のWidgetにフォーカスを移動します		|
| Custom		|指定のデリゲート関数を呼び出します		|
| CustomBoundary	|境界線判定移動＋指定のデリゲート関数を呼び出します	|

　**Escape**は"逃亡, 脱出"という意味を持つように、現在のフォーカスから脱出して指定された方向に移動します。デフォルトではこの設定によってカーソル, フォーカスの移動ができるようになっています。
![video_01(2).gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/101019/374358ce-9b9e-b6a2-ed9b-c3468e44ea6c.gif)


　**Stop**は"停止, 中断"という意味を持つように、現在のフォーカスから脱出することを抑制します。つまりキーが押されたとしてもカーソルやフォーカスは移動しません。それ以上移動させたくない時などに利用します。

　**Wrap**は"包括"の意味を持ちますが、反対側に移動する際に利用します。また、これは子widgetに対して適用されるので、同一レイヤにあるサンプルのボタンに対しては適用されません。以下の例では親widget(HorizontalBox)内のコンテンツに対して適用されるWrapの例です。カーソルが端のコンテンツに当たってない場合はLeft/Right共に順番に移動します。①の左端にカーソルがある場合、LeftにWrapで設定していると入力後は反対の②に移ります。同様に②の右端にカーソルがある場合、RightにWrapで設定していると入力後は反対の①に移ります。
![wrap.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/101019/e85440f5-1040-d650-6d89-1bd71f0b9f02.png)

　**Explicit**は指定するWidgetにフォーカスを移動させます。例えば以下の例では、右が押された時に順番や方向の移動を無視して"Button_10"に強制的にカーソルを移動させることができます。ただし、この機能には現在不具合があるので[この修正](https://github.com/EpicGames/UnrealEngine/pull/6375/commits/7a919839226e7f6bb5cc778a35ce9cf71a8bdca9)を適用する必要があります。
![2020-06-03_15h28_55.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/101019/41324661-158b-6bea-5a31-c9182291436b.png)


**Custom**はWrapと同様、Custom/Custom Boundaryは子widgetに対して適用されるので、同一レイヤにあるサンプルのボタンに対しては適用されません。カーソル移動の境界線判定を行わずにデリゲート関数を呼び出します。①にカーソルがある時、Leftを入力するとCustomに登録されたデリゲート関数は呼び出されます。

**Custom Boundary**はカーソル移動の境界線判定を行ってから移動デリゲート関数を呼び出します。
①にカーソルがある時、Rightを入力すると境界線判定を行って移動可能なためカーソルを移動してデリゲート関数は呼び出されません。右端に到達した時、Rightを入力すると境界線判定を行って移動できるジオメトリが無い場合はデリゲート関数を呼び出します。
![custom.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/101019/efd9cf18-4df8-8481-5138-165dd858e6fd.png)