@@ -1,5 +1,7 @@
|
||||
// Copyright (c) 2012-2021 Wojciech Figat. All rights reserved.
|
||||
|
||||
using System;
|
||||
|
||||
namespace FlaxEngine.GUI
|
||||
{
|
||||
/// <summary>
|
||||
@@ -11,6 +13,8 @@ namespace FlaxEngine.GUI
|
||||
{
|
||||
private UICanvas _canvas;
|
||||
private Vector2 _mousePosition;
|
||||
private float _navigationHeldTimeUp, _navigationHeldTimeDown, _navigationHeldTimeLeft, _navigationHeldTimeRight, _navigationHeldTimeSubmit;
|
||||
private float _navigationRateTimeUp, _navigationRateTimeDown, _navigationRateTimeLeft, _navigationRateTimeRight, _navigationRateTimeSubmit;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the owning canvas.
|
||||
@@ -137,6 +141,77 @@ namespace FlaxEngine.GUI
|
||||
&& (_canvas.TestCanvasIntersection == null || _canvas.TestCanvasIntersection(ref location));
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void Update(float deltaTime)
|
||||
{
|
||||
// UI navigation
|
||||
if (_canvas.ReceivesEvents)
|
||||
{
|
||||
UpdateNavigation(deltaTime, _canvas.NavigationInputActionUp, NavDirection.Up, ref _navigationHeldTimeUp, ref _navigationRateTimeUp);
|
||||
UpdateNavigation(deltaTime, _canvas.NavigationInputActionDown, NavDirection.Down, ref _navigationHeldTimeDown, ref _navigationRateTimeDown);
|
||||
UpdateNavigation(deltaTime, _canvas.NavigationInputActionLeft, NavDirection.Left, ref _navigationHeldTimeLeft, ref _navigationRateTimeLeft);
|
||||
UpdateNavigation(deltaTime, _canvas.NavigationInputActionRight, NavDirection.Right, ref _navigationHeldTimeRight, ref _navigationRateTimeRight);
|
||||
UpdateNavigation(deltaTime, _canvas.NavigationInputActionSubmit, ref _navigationHeldTimeSubmit, ref _navigationRateTimeSubmit, SubmitFocused);
|
||||
}
|
||||
else
|
||||
{
|
||||
_navigationHeldTimeUp = _navigationHeldTimeDown = _navigationHeldTimeLeft = _navigationHeldTimeRight = 0;
|
||||
_navigationRateTimeUp = _navigationRateTimeDown = _navigationRateTimeLeft = _navigationRateTimeRight = 0;
|
||||
}
|
||||
|
||||
base.Update(deltaTime);
|
||||
}
|
||||
|
||||
private void UpdateNavigation(float deltaTime, string actionName, NavDirection direction, ref float heldTime, ref float rateTime)
|
||||
{
|
||||
if (Input.GetAction(actionName))
|
||||
{
|
||||
if (heldTime <= Mathf.Epsilon)
|
||||
{
|
||||
Navigate(direction);
|
||||
}
|
||||
if (heldTime > _canvas.NavigationInputRepeatDelay)
|
||||
{
|
||||
rateTime += deltaTime;
|
||||
}
|
||||
if (rateTime > _canvas.NavigationInputRepeatRate)
|
||||
{
|
||||
Navigate(direction);
|
||||
rateTime = 0;
|
||||
}
|
||||
heldTime += deltaTime;
|
||||
}
|
||||
else
|
||||
{
|
||||
heldTime = rateTime = 0;
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateNavigation(float deltaTime, string actionName, ref float heldTime, ref float rateTime, Action action)
|
||||
{
|
||||
if (Input.GetAction(actionName))
|
||||
{
|
||||
if (heldTime <= Mathf.Epsilon)
|
||||
{
|
||||
action();
|
||||
}
|
||||
if (heldTime > _canvas.NavigationInputRepeatDelay)
|
||||
{
|
||||
rateTime += deltaTime;
|
||||
}
|
||||
if (rateTime > _canvas.NavigationInputRepeatRate)
|
||||
{
|
||||
action();
|
||||
rateTime = 0;
|
||||
}
|
||||
heldTime += deltaTime;
|
||||
}
|
||||
else
|
||||
{
|
||||
heldTime = rateTime = 0;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override bool OnCharInput(char c)
|
||||
{
|
||||
|
||||
@@ -155,7 +155,7 @@ namespace FlaxEngine.GUI
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called when mouse clicks the button.
|
||||
/// Called when mouse or touch clicks the button.
|
||||
/// </summary>
|
||||
protected virtual void OnClick()
|
||||
{
|
||||
@@ -164,7 +164,7 @@ namespace FlaxEngine.GUI
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called when buttons starts to be pressed by the used (via mouse or touch).
|
||||
/// Called when button starts to be pressed by the used (via mouse or touch).
|
||||
/// </summary>
|
||||
protected virtual void OnPressBegin()
|
||||
{
|
||||
@@ -174,7 +174,7 @@ namespace FlaxEngine.GUI
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called when buttons ends to be pressed by the used (via mouse or touch).
|
||||
/// Called when button ends to be pressed by the used (via mouse or touch).
|
||||
/// </summary>
|
||||
protected virtual void OnPressEnd()
|
||||
{
|
||||
@@ -215,7 +215,7 @@ namespace FlaxEngine.GUI
|
||||
backgroundColor = BackgroundColorSelected;
|
||||
borderColor = BorderColorSelected;
|
||||
}
|
||||
else if (IsMouseOver)
|
||||
else if (IsMouseOver || IsNavFocused)
|
||||
{
|
||||
backgroundColor = BackgroundColorHighlighted;
|
||||
borderColor = BorderColorHighlighted;
|
||||
@@ -322,5 +322,13 @@ namespace FlaxEngine.GUI
|
||||
|
||||
base.OnLostFocus();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void OnSubmit()
|
||||
{
|
||||
OnClick();
|
||||
|
||||
base.OnSubmit();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,9 +32,9 @@ namespace FlaxEngine.GUI
|
||||
public class CheckBox : Control
|
||||
{
|
||||
/// <summary>
|
||||
/// The mouse is down.
|
||||
/// True if checked is being pressed (by mouse or touch).
|
||||
/// </summary>
|
||||
protected bool _mouseDown;
|
||||
protected bool _isPressed;
|
||||
|
||||
/// <summary>
|
||||
/// The current state.
|
||||
@@ -186,6 +186,32 @@ namespace FlaxEngine.GUI
|
||||
_box = new Rectangle(0, (Height - _boxSize) * 0.5f, _boxSize, _boxSize);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called when mouse or touch clicks the checkbox.
|
||||
/// </summary>
|
||||
protected virtual void OnClick()
|
||||
{
|
||||
Toggle();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called when checkbox starts to be pressed by the used (via mouse or touch).
|
||||
/// </summary>
|
||||
protected virtual void OnPressBegin()
|
||||
{
|
||||
_isPressed = true;
|
||||
if (AutoFocus)
|
||||
Focus();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called when checkbox ends to be pressed by the used (via mouse or touch).
|
||||
/// </summary>
|
||||
protected virtual void OnPressEnd()
|
||||
{
|
||||
_isPressed = false;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void Draw()
|
||||
{
|
||||
@@ -197,7 +223,7 @@ namespace FlaxEngine.GUI
|
||||
Color borderColor = BorderColor;
|
||||
if (!enabled)
|
||||
borderColor *= 0.5f;
|
||||
else if (_mouseDown || _mouseOverBox)
|
||||
else if (_isPressed || _mouseOverBox || IsNavFocused)
|
||||
borderColor = BorderColorHighlighted;
|
||||
Render2D.DrawRectangle(_box.MakeExpanded(-2.0f), borderColor);
|
||||
|
||||
@@ -226,11 +252,9 @@ namespace FlaxEngine.GUI
|
||||
/// <inheritdoc />
|
||||
public override bool OnMouseDown(Vector2 location, MouseButton button)
|
||||
{
|
||||
if (button == MouseButton.Left)
|
||||
if (button == MouseButton.Left && !_isPressed)
|
||||
{
|
||||
// Set flag
|
||||
_mouseDown = true;
|
||||
Focus();
|
||||
OnPressBegin();
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -240,16 +264,12 @@ namespace FlaxEngine.GUI
|
||||
/// <inheritdoc />
|
||||
public override bool OnMouseUp(Vector2 location, MouseButton button)
|
||||
{
|
||||
if (button == MouseButton.Left && _mouseDown)
|
||||
if (button == MouseButton.Left && _isPressed)
|
||||
{
|
||||
// Clear flag
|
||||
_mouseDown = false;
|
||||
|
||||
// Check if mouse is still over the box
|
||||
if (_mouseOverBox)
|
||||
OnPressEnd();
|
||||
if (_box.Contains(ref location))
|
||||
{
|
||||
Toggle();
|
||||
Focus();
|
||||
OnClick();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -260,11 +280,57 @@ namespace FlaxEngine.GUI
|
||||
/// <inheritdoc />
|
||||
public override void OnMouseLeave()
|
||||
{
|
||||
base.OnMouseLeave();
|
||||
|
||||
// Clear flags
|
||||
if (_isPressed)
|
||||
OnPressEnd();
|
||||
_mouseOverBox = false;
|
||||
_mouseDown = false;
|
||||
|
||||
base.OnMouseLeave();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override bool OnTouchDown(Vector2 location, int pointerId)
|
||||
{
|
||||
if (!_isPressed)
|
||||
{
|
||||
OnPressBegin();
|
||||
return true;
|
||||
}
|
||||
|
||||
return base.OnTouchDown(location, pointerId);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override bool OnTouchUp(Vector2 location, int pointerId)
|
||||
{
|
||||
if (_isPressed)
|
||||
{
|
||||
OnPressEnd();
|
||||
if (_box.Contains(ref location))
|
||||
{
|
||||
OnClick();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return base.OnTouchUp(location, pointerId);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void OnTouchLeave()
|
||||
{
|
||||
if (_isPressed)
|
||||
OnPressEnd();
|
||||
|
||||
base.OnTouchLeave();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void OnLostFocus()
|
||||
{
|
||||
if (_isPressed)
|
||||
OnPressEnd();
|
||||
|
||||
base.OnLostFocus();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -274,5 +340,13 @@ namespace FlaxEngine.GUI
|
||||
|
||||
CacheBox();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void OnSubmit()
|
||||
{
|
||||
OnClick();
|
||||
|
||||
base.OnSubmit();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,11 +24,57 @@ namespace FlaxEngine.GUI
|
||||
public Action LostFocus;
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void OnLostFocus()
|
||||
public override void OnEndContainsFocus()
|
||||
{
|
||||
base.OnLostFocus();
|
||||
base.OnEndContainsFocus();
|
||||
|
||||
LostFocus?.Invoke();
|
||||
// Call event after this 'focus contains flag' propagation ends to prevent focus issues
|
||||
if (LostFocus != null)
|
||||
Scripting.RunOnUpdate(LostFocus);
|
||||
}
|
||||
|
||||
private static DropdownLabel FindItem(Control control)
|
||||
{
|
||||
if (control is DropdownLabel item)
|
||||
return item;
|
||||
if (control is ContainerControl containerControl)
|
||||
{
|
||||
foreach (var child in containerControl.Children)
|
||||
{
|
||||
item = FindItem(child);
|
||||
if (item != null)
|
||||
return item;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override Control OnNavigate(NavDirection direction, Vector2 location, Control caller, List<Control> visited)
|
||||
{
|
||||
if (IsFocused)
|
||||
{
|
||||
// Dropdown root is focused
|
||||
if (direction == NavDirection.Down)
|
||||
{
|
||||
// Pick the first item
|
||||
return FindItem(this);
|
||||
}
|
||||
|
||||
// Close popup
|
||||
Defocus();
|
||||
return null;
|
||||
}
|
||||
|
||||
return base.OnNavigate(direction, location, caller, visited);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void OnSubmit()
|
||||
{
|
||||
Defocus();
|
||||
|
||||
base.OnSubmit();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -43,14 +89,13 @@ namespace FlaxEngine.GUI
|
||||
[HideInEditor]
|
||||
private class DropdownLabel : Label
|
||||
{
|
||||
public int Index;
|
||||
public Action<int> ItemClicked;
|
||||
public Action<Label> ItemClicked;
|
||||
|
||||
public override bool OnMouseDown(Vector2 location, MouseButton button)
|
||||
{
|
||||
if (base.OnMouseDown(location, button))
|
||||
return true;
|
||||
ItemClicked?.Invoke(Index);
|
||||
ItemClicked?.Invoke(this);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -58,10 +103,18 @@ namespace FlaxEngine.GUI
|
||||
{
|
||||
if (base.OnTouchDown(location, pointerId))
|
||||
return true;
|
||||
ItemClicked?.Invoke(Index);
|
||||
ItemClicked?.Invoke(this);
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void OnSubmit()
|
||||
{
|
||||
ItemClicked?.Invoke(this);
|
||||
|
||||
base.OnSubmit();
|
||||
}
|
||||
|
||||
public override void OnDestroy()
|
||||
{
|
||||
ItemClicked = null;
|
||||
@@ -81,6 +134,7 @@ namespace FlaxEngine.GUI
|
||||
protected DropdownRoot _popup;
|
||||
|
||||
private bool _touchDown;
|
||||
private bool _hadNavFocus;
|
||||
|
||||
/// <summary>
|
||||
/// The selected index of the item (-1 for no selection).
|
||||
@@ -229,8 +283,6 @@ namespace FlaxEngine.GUI
|
||||
public Dropdown()
|
||||
: base(0, 0, 120, 18.0f)
|
||||
{
|
||||
AutoFocus = false;
|
||||
|
||||
var style = Style.Current;
|
||||
Font = new FontReference(style.FontMedium);
|
||||
TextColor = style.Foreground;
|
||||
@@ -346,8 +398,9 @@ namespace FlaxEngine.GUI
|
||||
|
||||
for (int i = 0; i < _items.Count; i++)
|
||||
{
|
||||
var item = new Spacer
|
||||
var item = new ContainerControl
|
||||
{
|
||||
AutoFocus = false,
|
||||
Height = itemsHeight,
|
||||
Width = itemsWidth,
|
||||
Parent = container,
|
||||
@@ -355,6 +408,7 @@ namespace FlaxEngine.GUI
|
||||
|
||||
var label = new DropdownLabel
|
||||
{
|
||||
AutoFocus = true,
|
||||
X = itemsMargin,
|
||||
Size = new Vector2(itemsWidth - itemsMargin, itemsHeight),
|
||||
Font = Font,
|
||||
@@ -363,11 +417,11 @@ namespace FlaxEngine.GUI
|
||||
HorizontalAlignment = TextAlignment.Near,
|
||||
Text = _items[i],
|
||||
Parent = item,
|
||||
Index = i,
|
||||
Tag = i,
|
||||
};
|
||||
label.ItemClicked += index =>
|
||||
label.ItemClicked += c =>
|
||||
{
|
||||
OnItemClicked(index);
|
||||
OnItemClicked((int)c.Tag);
|
||||
DestroyPopup();
|
||||
};
|
||||
height += itemsHeight;
|
||||
@@ -416,6 +470,10 @@ namespace FlaxEngine.GUI
|
||||
OnPopupHide();
|
||||
_popup.Dispose();
|
||||
_popup = null;
|
||||
if (_hadNavFocus)
|
||||
NavigationFocus();
|
||||
else
|
||||
Focus();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -430,6 +488,7 @@ namespace FlaxEngine.GUI
|
||||
|
||||
// Setup popup
|
||||
DestroyPopup();
|
||||
_hadNavFocus = IsNavFocused;
|
||||
_popup = CreatePopup();
|
||||
_popup.UnlockChildrenRecursive();
|
||||
_popup.PerformLayout();
|
||||
@@ -488,7 +547,7 @@ namespace FlaxEngine.GUI
|
||||
borderColor = BorderColorSelected;
|
||||
arrowColor = ArrowColorSelected;
|
||||
}
|
||||
else if (IsMouseOver)
|
||||
else if (IsMouseOver || IsNavFocused)
|
||||
{
|
||||
backgroundColor = BackgroundColorHighlighted;
|
||||
borderColor = BorderColorHighlighted;
|
||||
@@ -590,5 +649,13 @@ namespace FlaxEngine.GUI
|
||||
|
||||
base.OnTouchLeave(pointerId);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void OnSubmit()
|
||||
{
|
||||
ShowPopup();
|
||||
|
||||
base.OnSubmit();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -114,7 +114,7 @@ namespace FlaxEngine.GUI
|
||||
|
||||
Margin.ShrinkRectangle(ref rect);
|
||||
|
||||
var color = IsMouseOver ? MouseOverColor : Color;
|
||||
var color = IsMouseOver || IsNavFocused ? MouseOverColor : Color;
|
||||
if (!Enabled)
|
||||
color *= DisabledTint;
|
||||
Brush.Draw(rect, color);
|
||||
|
||||
@@ -210,7 +210,7 @@ namespace FlaxEngine.GUI
|
||||
if (ClipText)
|
||||
Render2D.PushClip(new Rectangle(Vector2.Zero, Size));
|
||||
|
||||
var color = IsMouseOver ? TextColorHighlighted : TextColor;
|
||||
var color = IsMouseOver || IsNavFocused ? TextColorHighlighted : TextColor;
|
||||
|
||||
if (!EnabledInHierarchy)
|
||||
color *= 0.6f;
|
||||
|
||||
@@ -228,7 +228,7 @@ namespace FlaxEngine.GUI
|
||||
|
||||
// Background
|
||||
Color backColor = BackgroundColor;
|
||||
if (IsMouseOver)
|
||||
if (IsMouseOver || IsNavFocused)
|
||||
backColor = BackgroundSelectedColor;
|
||||
Render2D.FillRectangle(rect, backColor);
|
||||
Render2D.DrawRectangle(rect, IsFocused ? BorderSelectedColor : BorderColor);
|
||||
|
||||
@@ -152,7 +152,7 @@ namespace FlaxEngine.GUI
|
||||
|
||||
// Background
|
||||
Color backColor = BackgroundColor;
|
||||
if (IsMouseOver)
|
||||
if (IsMouseOver || IsNavFocused)
|
||||
backColor = BackgroundSelectedColor;
|
||||
Render2D.FillRectangle(rect, backColor);
|
||||
Render2D.DrawRectangle(rect, IsFocused ? BorderSelectedColor : BorderColor);
|
||||
|
||||
@@ -431,7 +431,6 @@ namespace FlaxEngine.GUI
|
||||
_isMultiline = isMultiline;
|
||||
_maxLength = 2147483646;
|
||||
_selectionStart = _selectionEnd = -1;
|
||||
AutoFocus = false;
|
||||
|
||||
var style = Style.Current;
|
||||
CaretColor = style.Foreground;
|
||||
@@ -1015,6 +1014,28 @@ namespace FlaxEngine.GUI
|
||||
_isSelecting = false;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void NavigationFocus()
|
||||
{
|
||||
base.NavigationFocus();
|
||||
|
||||
if (IsNavFocused)
|
||||
SelectAll();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void OnSubmit()
|
||||
{
|
||||
OnEditEnd();
|
||||
if (IsNavFocused)
|
||||
{
|
||||
OnEditBegin();
|
||||
SelectAll();
|
||||
}
|
||||
|
||||
base.OnSubmit();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void OnMouseMove(Vector2 location)
|
||||
{
|
||||
@@ -1215,7 +1236,8 @@ namespace FlaxEngine.GUI
|
||||
SetSelection(-1);
|
||||
_text = _onStartEditValue;
|
||||
|
||||
Defocus();
|
||||
if (!IsNavFocused)
|
||||
Defocus();
|
||||
OnTextChanged();
|
||||
|
||||
return true;
|
||||
@@ -1226,7 +1248,7 @@ namespace FlaxEngine.GUI
|
||||
// Insert new line
|
||||
Insert('\n');
|
||||
}
|
||||
else
|
||||
else if (!IsNavFocused)
|
||||
{
|
||||
// End editing
|
||||
Defocus();
|
||||
|
||||
@@ -491,6 +491,153 @@ namespace FlaxEngine.GUI
|
||||
return child.IntersectsContent(ref location, out childSpaceLocation);
|
||||
}
|
||||
|
||||
#region Navigation
|
||||
|
||||
/// <inheritdoc />
|
||||
public override Control OnNavigate(NavDirection direction, Vector2 location, Control caller, List<Control> visited)
|
||||
{
|
||||
// Try to focus itself first (only if navigation focus can enter this container)
|
||||
if (AutoFocus && !ContainsFocus)
|
||||
return this;
|
||||
|
||||
// Try to focus children
|
||||
if (_children.Count != 0 && !visited.Contains(this))
|
||||
{
|
||||
visited.Add(this);
|
||||
|
||||
// Perform automatic navigation based on the layout
|
||||
var result = NavigationRaycast(direction, location, visited);
|
||||
if (result == null && direction == NavDirection.Next)
|
||||
{
|
||||
// Try wrap the navigation over the layout based on the direction
|
||||
var visitedWrap = new List<Control>(visited);
|
||||
result = NavigationWrap(direction, location, visitedWrap);
|
||||
}
|
||||
if (result != null)
|
||||
{
|
||||
result = result.OnNavigate(direction, result.PointFromParent(location), this, visited);
|
||||
if (result != null)
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
// Try to focus itself
|
||||
if (AutoFocus && !IsFocused || caller == this)
|
||||
return this;
|
||||
|
||||
// Route navigation to parent
|
||||
var parent = Parent;
|
||||
if (AutoFocus && Visible)
|
||||
{
|
||||
// Focusable container controls use own nav origin instead of the provided one
|
||||
location = GetNavOrigin(direction);
|
||||
}
|
||||
return parent?.OnNavigate(direction, PointToParent(location), caller, visited);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if this container control can more with focus navigation into the given child control.
|
||||
/// </summary>
|
||||
/// <param name="child">The child.</param>
|
||||
/// <returns>True if can navigate to it, otherwise false.</returns>
|
||||
protected virtual bool CanNavigateChild(Control child)
|
||||
{
|
||||
return !child.IsFocused && child.Enabled && child.Visible && CanGetAutoFocus(child);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Wraps the navigation over the layout.
|
||||
/// </summary>
|
||||
/// <param name="direction">The navigation direction.</param>
|
||||
/// <param name="location">The navigation start location (in the control-space).</param>
|
||||
/// <param name="visited">The list with visited controls. Used to skip recursive navigation calls when doing traversal across the UI hierarchy.</param>
|
||||
/// <returns>The target navigation control or null if didn't performed any navigation.</returns>
|
||||
protected virtual Control NavigationWrap(NavDirection direction, Vector2 location, List<Control> visited)
|
||||
{
|
||||
// This searches form a child that calls this navigation event (see Control.OnNavigate) to determinate the layout wrapping size based on that child size
|
||||
var currentChild = RootWindow?.FocusedControl;
|
||||
visited.Add(this);
|
||||
if (currentChild != null)
|
||||
{
|
||||
var layoutSize = currentChild.Size;
|
||||
var predictedLocation = Vector2.Minimum;
|
||||
switch (direction)
|
||||
{
|
||||
case NavDirection.Next:
|
||||
predictedLocation = new Vector2(0, location.Y + layoutSize.Y);
|
||||
break;
|
||||
}
|
||||
if (new Rectangle(Vector2.Zero, Size).Contains(ref predictedLocation))
|
||||
{
|
||||
var result = NavigationRaycast(direction, predictedLocation, visited);
|
||||
if (result != null)
|
||||
return result;
|
||||
}
|
||||
}
|
||||
return Parent?.NavigationWrap(direction, PointToParent(ref location), visited);
|
||||
}
|
||||
|
||||
private static bool CanGetAutoFocus(Control c)
|
||||
{
|
||||
if (c.AutoFocus)
|
||||
return true;
|
||||
if (c is ContainerControl cc)
|
||||
{
|
||||
for (int i = 0; i < cc.Children.Count; i++)
|
||||
{
|
||||
if (cc.CanNavigateChild(cc.Children[i]))
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private Control NavigationRaycast(NavDirection direction, Vector2 location, List<Control> visited)
|
||||
{
|
||||
Vector2 uiDir1 = Vector2.Zero, uiDir2 = Vector2.Zero;
|
||||
switch (direction)
|
||||
{
|
||||
case NavDirection.Up:
|
||||
uiDir1 = uiDir2 = new Vector2(0, -1);
|
||||
break;
|
||||
case NavDirection.Down:
|
||||
uiDir1 = uiDir2 = new Vector2(0, 1);
|
||||
break;
|
||||
case NavDirection.Left:
|
||||
uiDir1 = uiDir2 = new Vector2(-1, 0);
|
||||
break;
|
||||
case NavDirection.Right:
|
||||
uiDir1 = uiDir2 = new Vector2(1, 0);
|
||||
break;
|
||||
case NavDirection.Next:
|
||||
uiDir1 = new Vector2(1, 0);
|
||||
uiDir2 = new Vector2(0, 1);
|
||||
break;
|
||||
}
|
||||
Control result = null;
|
||||
var minDistance = float.MaxValue;
|
||||
for (var i = 0; i < _children.Count; i++)
|
||||
{
|
||||
var child = _children[i];
|
||||
if (!CanNavigateChild(child) || visited.Contains(child))
|
||||
continue;
|
||||
var childNavLocation = child.Center;
|
||||
var childBounds = child.Bounds;
|
||||
var childNavDirection = Vector2.Normalize(childNavLocation - location);
|
||||
var childNavCoherence1 = Vector2.Dot(ref uiDir1, ref childNavDirection);
|
||||
var childNavCoherence2 = Vector2.Dot(ref uiDir2, ref childNavDirection);
|
||||
var distance = Rectangle.Distance(childBounds, location);
|
||||
if (childNavCoherence1 > Mathf.Epsilon && childNavCoherence2 > Mathf.Epsilon && distance < minDistance)
|
||||
{
|
||||
minDistance = distance;
|
||||
result = child;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
/// <summary>
|
||||
/// Update contain focus state and all it's children
|
||||
/// </summary>
|
||||
@@ -501,10 +648,10 @@ namespace FlaxEngine.GUI
|
||||
|
||||
for (int i = 0; i < _children.Count; i++)
|
||||
{
|
||||
if (_children[i] is ContainerControl child)
|
||||
var control = _children[i];
|
||||
if (control is ContainerControl child)
|
||||
child.UpdateContainsFocus();
|
||||
|
||||
if (_children[i].ContainsFocus)
|
||||
if (control.ContainsFocus)
|
||||
result = true;
|
||||
}
|
||||
|
||||
|
||||
@@ -51,7 +51,7 @@ namespace FlaxEngine.GUI
|
||||
|
||||
private ContainerControl _parent;
|
||||
private RootControl _root;
|
||||
private bool _isDisposing, _isFocused;
|
||||
private bool _isDisposing, _isFocused, _isNavFocused;
|
||||
|
||||
// State
|
||||
|
||||
@@ -456,9 +456,9 @@ namespace FlaxEngine.GUI
|
||||
#region Focus
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether the control can receive automatic focus on user events (eg. mouse down).
|
||||
/// If checked, the control can receive automatic focus (eg. on user click or UI navigation).
|
||||
/// </summary>
|
||||
[HideInEditor, NoSerialize]
|
||||
[EditorOrder(512), Tooltip("If checked, the control can receive automatic focus (eg. on user click or UI navigation).")]
|
||||
public bool AutoFocus
|
||||
{
|
||||
get => _autoFocus;
|
||||
@@ -473,7 +473,12 @@ namespace FlaxEngine.GUI
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether the control has input focus
|
||||
/// </summary>
|
||||
public virtual bool IsFocused => _isFocused;
|
||||
public bool IsFocused => _isFocused;
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether the control has UI navigation focus.
|
||||
/// </summary>
|
||||
public bool IsNavFocused => _isNavFocused;
|
||||
|
||||
/// <summary>
|
||||
/// Sets input focus to the control
|
||||
@@ -505,6 +510,7 @@ namespace FlaxEngine.GUI
|
||||
{
|
||||
// Cache flag
|
||||
_isFocused = true;
|
||||
_isNavFocused = false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -515,6 +521,7 @@ namespace FlaxEngine.GUI
|
||||
{
|
||||
// Clear flag
|
||||
_isFocused = false;
|
||||
_isNavFocused = false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -575,6 +582,118 @@ namespace FlaxEngine.GUI
|
||||
|
||||
#endregion
|
||||
|
||||
#region Navigation
|
||||
|
||||
/// <summary>
|
||||
/// The explicitly specified target navigation control for <see cref="NavDirection.Up"/> direction.
|
||||
/// </summary>
|
||||
[HideInEditor, NoSerialize]
|
||||
public Control NavTargetUp;
|
||||
|
||||
/// <summary>
|
||||
/// The explicitly specified target navigation control for <see cref="NavDirection.Down"/> direction.
|
||||
/// </summary>
|
||||
[HideInEditor, NoSerialize]
|
||||
public Control NavTargetDown;
|
||||
|
||||
/// <summary>
|
||||
/// The explicitly specified target navigation control for <see cref="NavDirection.Left"/> direction.
|
||||
/// </summary>
|
||||
[HideInEditor, NoSerialize]
|
||||
public Control NavTargetLeft;
|
||||
|
||||
/// <summary>
|
||||
/// The explicitly specified target navigation control for <see cref="NavDirection.Right"/> direction.
|
||||
/// </summary>
|
||||
[HideInEditor, NoSerialize]
|
||||
public Control NavTargetRight;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the next navigation control to focus for the given direction. Returns null for automated direction resolving.
|
||||
/// </summary>
|
||||
/// <param name="direction">The navigation direction.</param>
|
||||
/// <returns>The target navigation control or null to use automatic navigation.</returns>
|
||||
[NoAnimate]
|
||||
public virtual Control GetNavTarget(NavDirection direction)
|
||||
{
|
||||
switch (direction)
|
||||
{
|
||||
case NavDirection.Up: return NavTargetUp;
|
||||
case NavDirection.Down: return NavTargetDown;
|
||||
case NavDirection.Left: return NavTargetLeft;
|
||||
case NavDirection.Right: return NavTargetRight;
|
||||
default: return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the navigation origin location for this control. It's the starting anchor point for searching navigable controls in the nearby area. By default the origin points are located on the control bounds edges.
|
||||
/// </summary>
|
||||
/// <param name="direction">The navigation direction.</param>
|
||||
/// <returns>The navigation origin for the automatic navigation.</returns>
|
||||
[NoAnimate]
|
||||
public virtual Vector2 GetNavOrigin(NavDirection direction)
|
||||
{
|
||||
var size = Size;
|
||||
switch (direction)
|
||||
{
|
||||
case NavDirection.Up: return new Vector2(size.X * 0.5f, 0);
|
||||
case NavDirection.Down: return new Vector2(size.X * 0.5f, size.Y);
|
||||
case NavDirection.Left: return new Vector2(0, size.Y * 0.5f);
|
||||
case NavDirection.Right: return new Vector2(size.X, size.Y * 0.5f);
|
||||
case NavDirection.Next: return Vector2.Zero;
|
||||
default: return size * 0.5f;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Performs the UI navigation for this control.
|
||||
/// </summary>
|
||||
/// <param name="direction">The navigation direction.</param>
|
||||
/// <param name="location">The navigation start location (in the control-space).</param>
|
||||
/// <param name="caller">The control that calls the event.</param>
|
||||
/// <param name="visited">The list with visited controls. Used to skip recursive navigation calls when doing traversal across the UI hierarchy.</param>
|
||||
/// <returns>The target navigation control or null if didn't performed any navigation.</returns>
|
||||
public virtual Control OnNavigate(NavDirection direction, Vector2 location, Control caller, List<Control> visited)
|
||||
{
|
||||
if (caller == _parent && AutoFocus && Visible)
|
||||
return this;
|
||||
return _parent?.OnNavigate(direction, PointToParent(GetNavOrigin(direction)), caller, visited);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Focuses the control by the UI navigation system. Called during navigating around UI with gamepad/keyboard navigation. Focuses the control and sets the <see cref="IsNavFocused"/> flag.
|
||||
/// </summary>
|
||||
public virtual void NavigationFocus()
|
||||
{
|
||||
Focus();
|
||||
if (IsFocused)
|
||||
{
|
||||
_isNavFocused = true;
|
||||
|
||||
// Ensure to be in a view
|
||||
var parent = Parent;
|
||||
while (parent != null)
|
||||
{
|
||||
if (parent is Panel panel && ((panel.VScrollBar != null && panel.VScrollBar.Enabled) || (panel.HScrollBar != null && panel.HScrollBar.Enabled)))
|
||||
{
|
||||
panel.ScrollViewTo(this);
|
||||
break;
|
||||
}
|
||||
parent = parent.Parent;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generic user interaction event for a control used by UI navigation (eg. user submits on the currently focused control).
|
||||
/// </summary>
|
||||
public virtual void OnSubmit()
|
||||
{
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Mouse
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -167,4 +167,40 @@ namespace FlaxEngine.GUI
|
||||
/// </summary>
|
||||
Vertical = 1,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The navigation directions in the user interface layout.
|
||||
/// </summary>
|
||||
public enum NavDirection
|
||||
{
|
||||
/// <summary>
|
||||
/// No direction to skip navigation.
|
||||
/// </summary>
|
||||
None,
|
||||
|
||||
/// <summary>
|
||||
/// The up direction.
|
||||
/// </summary>
|
||||
Up,
|
||||
|
||||
/// <summary>
|
||||
/// The down direction.
|
||||
/// </summary>
|
||||
Down,
|
||||
|
||||
/// <summary>
|
||||
/// The left direction.
|
||||
/// </summary>
|
||||
Left,
|
||||
|
||||
/// <summary>
|
||||
/// The right direction.
|
||||
/// </summary>
|
||||
Right,
|
||||
|
||||
/// <summary>
|
||||
/// The next item (right with layout wrapping).
|
||||
/// </summary>
|
||||
Next,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,6 +19,14 @@ namespace FlaxEngine.GUI
|
||||
[EditorOrder(10), Tooltip("Whether or not we should ignore previous alphas.")]
|
||||
public bool IgnoreStack;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="AlphaPanel"/> class.
|
||||
/// </summary>
|
||||
public AlphaPanel()
|
||||
{
|
||||
AutoFocus = false;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void Draw()
|
||||
{
|
||||
|
||||
@@ -19,6 +19,7 @@ namespace FlaxEngine.GUI
|
||||
/// </summary>
|
||||
public BlurPanel()
|
||||
{
|
||||
AutoFocus = false;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
|
||||
@@ -560,5 +560,14 @@ namespace FlaxEngine.GUI
|
||||
{
|
||||
Arrange();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override bool CanNavigateChild(Control child)
|
||||
{
|
||||
// Closed panel skips navigation for hidden children
|
||||
if (IsClosed && child.IsScrollable)
|
||||
return false;
|
||||
return base.CanNavigateChild(child);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -73,6 +73,7 @@ namespace FlaxEngine.GUI
|
||||
/// <param name="slotPadding">The slot padding.</param>
|
||||
public GridPanel(float slotPadding)
|
||||
{
|
||||
AutoFocus = false;
|
||||
SlotPadding = new Margin(slotPadding);
|
||||
_cellsH = new[]
|
||||
{
|
||||
|
||||
@@ -75,6 +75,7 @@ namespace FlaxEngine.GUI
|
||||
/// <param name="slotPadding">The slot padding.</param>
|
||||
public UniformGridPanel(float slotPadding = 2)
|
||||
{
|
||||
AutoFocus = false;
|
||||
SlotPadding = new Margin(slotPadding);
|
||||
_slotsH = _slotsV = 5;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
// Copyright (c) 2012-2021 Wojciech Figat. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace FlaxEngine.GUI
|
||||
@@ -69,6 +70,61 @@ namespace FlaxEngine.GUI
|
||||
AutoFocus = false;
|
||||
}
|
||||
|
||||
#region Navigation
|
||||
|
||||
/// <summary>
|
||||
/// The custom callback function for UI navigation. Can be used to override the default behaviour.
|
||||
/// </summary>
|
||||
public Action<NavDirection> CustomNavigation;
|
||||
|
||||
/// <summary>
|
||||
/// Performs the UI navigation.
|
||||
/// </summary>
|
||||
/// <param name="direction">The navigation direction.</param>
|
||||
public void Navigate(NavDirection direction)
|
||||
{
|
||||
if (direction == NavDirection.None)
|
||||
return;
|
||||
|
||||
if (CustomNavigation != null)
|
||||
{
|
||||
// Custom
|
||||
CustomNavigation.Invoke(direction);
|
||||
return;
|
||||
}
|
||||
|
||||
var focused = FocusedControl;
|
||||
if (focused == null)
|
||||
{
|
||||
// Nothing is focused so go to the first control
|
||||
focused = OnNavigate(direction, Vector2.Zero, this, new List<Control>());
|
||||
focused?.NavigationFocus();
|
||||
return;
|
||||
}
|
||||
|
||||
var target = focused.GetNavTarget(direction);
|
||||
if (target != null)
|
||||
{
|
||||
// Explicitly specified focus target
|
||||
target.NavigationFocus();
|
||||
return;
|
||||
}
|
||||
|
||||
// Automatic navigation routine
|
||||
target = focused.OnNavigate(direction, focused.GetNavOrigin(direction), this, new List<Control>());
|
||||
target?.NavigationFocus();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Submits the currently focused control.
|
||||
/// </summary>
|
||||
public void SubmitFocused()
|
||||
{
|
||||
FocusedControl?.OnSubmit();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void Update(float deltaTime)
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user