Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement focus traps #6286

Merged
merged 13 commits into from
May 19, 2024
12 changes: 6 additions & 6 deletions osu.Framework.Tests/Visual/Drawables/TestSceneFocus.cs
Original file line number Diff line number Diff line change
Expand Up @@ -135,35 +135,35 @@ public void RequestsFocusLosesFocusOnClickingFocused()
}

/// <summary>
/// Ensures that performing <see cref="InputManager.ChangeFocus(Drawable)"/> to a drawable with disabled <see cref="Drawable.AcceptsFocus"/> returns <see langword="false"/>.
/// Ensures that performing <see cref="IFocusManager.ChangeFocus(Drawable)"/> to a drawable with disabled <see cref="Drawable.AcceptsFocus"/> returns <see langword="false"/>.
/// </summary>
[Test]
public void DisabledFocusDrawableCannotReceiveFocusViaChangeFocus()
{
checkFocused(() => requestingFocus);

AddStep("disable focus from top left", () => focusTopLeft.AllowAcceptingFocus = false);
AddAssert("cannot switch focus to top left", () => !InputManager.ChangeFocus(focusTopLeft));
AddAssert("cannot switch focus to top left", () => !((IFocusManager)InputManager).ChangeFocus(focusTopLeft));

checkFocused(() => requestingFocus);
}

/// <summary>
/// Ensures that performing <see cref="InputManager.ChangeFocus(Drawable)"/> to a non-present drawable returns <see langword="false"/>.
/// Ensures that performing <see cref="IFocusManager.ChangeFocus(Drawable)"/> to a non-present drawable returns <see langword="false"/>.
/// </summary>
[Test]
public void NotPresentDrawableCannotReceiveFocusViaChangeFocus()
{
checkFocused(() => requestingFocus);

AddStep("hide top left", () => focusTopLeft.Alpha = 0);
AddAssert("cannot switch focus to top left", () => !InputManager.ChangeFocus(focusTopLeft));
AddAssert("cannot switch focus to top left", () => !((IFocusManager)InputManager).ChangeFocus(focusTopLeft));

checkFocused(() => requestingFocus);
}

/// <summary>
/// Ensures that performing <see cref="InputManager.ChangeFocus(Drawable)"/> to a drawable of a non-present parent returns <see langword="false"/>.
/// Ensures that performing <see cref="IFocusManager.ChangeFocus(Drawable)"/> to a drawable of a non-present parent returns <see langword="false"/>.
/// </summary>
[Test]
public void DrawableOfNotPresentParentCannotReceiveFocusViaChangeFocus()
Expand All @@ -183,7 +183,7 @@ public void DrawableOfNotPresentParentCannotReceiveFocusViaChangeFocus()
Remove(focusTopLeft, false);
container.Add(focusTopLeft);
});
AddAssert("cannot switch focus to top left", () => !InputManager.ChangeFocus(focusTopLeft));
AddAssert("cannot switch focus to top left", () => !((IFocusManager)InputManager).ChangeFocus(focusTopLeft));

checkFocused(() => requestingFocus);
}
Expand Down
3 changes: 2 additions & 1 deletion osu.Framework.Tests/Visual/Input/TestSceneHandleInput.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Input;
using osu.Framework.Testing;
using osuTK;
using osuTK.Graphics;
Expand Down Expand Up @@ -37,7 +38,7 @@ public TestSceneHandleInput()
{
handleNonPositionalInput.Enabled = true;
InputManager.MoveMouseTo(handleNonPositionalInput);
InputManager.TriggerFocusContention(null);
((IFocusManager)InputManager).TriggerFocusContention(null);
});
AddAssert($"check {nameof(handleNonPositionalInput)}", () => !handleNonPositionalInput.IsHovered && handleNonPositionalInput.HasFocus);

Expand Down
142 changes: 142 additions & 0 deletions osu.Framework.Tests/Visual/UserInterface/TestSceneDropdown.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text.

using System;
using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
Expand All @@ -13,6 +14,8 @@
using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.UserInterface;
using osu.Framework.Input;
using osu.Framework.Input.Bindings;
using osu.Framework.Input.Events;
using osu.Framework.Localisation;
using osu.Framework.Testing;
using osu.Framework.Testing.Input;
Expand Down Expand Up @@ -487,6 +490,22 @@ public void TestReleaseFocusAfterSearching()
AddAssert("search bar hidden", () => dropdown.ChildrenOfType<DropdownSearchBar>().Single().State.Value, () => Is.EqualTo(Visibility.Hidden));
}

[Test]
public void TestSelectSearchedItem()
{
ManualTextDropdown dropdown = null!;

AddStep("setup dropdown", () => dropdown = createDropdowns<ManualTextDropdown>(1)[0]);
toggleDropdownViaClick(() => dropdown);

AddStep("trigger text", () => dropdown.TextInput.Text("test 4"));
AddAssert("search bar visible", () => dropdown.ChildrenOfType<DropdownSearchBar>().Single().State.Value, () => Is.EqualTo(Visibility.Visible));

AddStep("press enter", () => InputManager.Key(Key.Enter));
AddAssert("search bar hidden", () => dropdown.ChildrenOfType<DropdownSearchBar>().Single().State.Value, () => Is.EqualTo(Visibility.Hidden));
AddAssert("dropdown closed", () => dropdown.Menu.State == MenuState.Closed);
}

[Test]
public void TestAlwaysShowSearchBar()
{
Expand Down Expand Up @@ -527,6 +546,90 @@ public void TestAlwaysShowSearchBar()
AddAssert("search bar hidden", () => dropdown.ChildrenOfType<DropdownSearchBar>().Single().State.Value, () => Is.EqualTo(Visibility.Hidden));
}

[Test]
public void TestKeyBindingIsolation()
{
ManualTextDropdown dropdown = null!;
TestKeyBindingHandler keyBindingHandler = null!;

AddStep("setup dropdown", () =>
{
dropdown = createDropdowns<ManualTextDropdown>(1)[0];
dropdown.AlwaysShowSearchBar = true;
});

AddStep("setup key binding handler", () =>
{
Add(new TestKeyBindingContainer
{
RelativeSizeAxes = Axes.Both,
Child = keyBindingHandler = new TestKeyBindingHandler
{
RelativeSizeAxes = Axes.Both,
},
});
});

AddAssert("search bar hidden", () => dropdown.ChildrenOfType<DropdownSearchBar>().Single().State.Value, () => Is.EqualTo(Visibility.Hidden));
toggleDropdownViaClick(() => dropdown);

AddStep("press space", () =>
{
InputManager.Key(Key.Space);
// we must send something via the text input path for TextBox to block the space key press above,
// we're not supposed to do this here, but we don't have a good way of simulating text input from ManualInputManager so let's just do this for now.
// todo: add support for simulating text typing at a ManualInputManager level for more realistic results.
dropdown.TextInput.Text(" ");
});
AddAssert("handler did not receive press", () => !keyBindingHandler.ReceivedPress);

toggleDropdownViaClick(() => dropdown);

AddStep("press space", () =>
{
InputManager.Key(Key.Space);
// we must send something via the text input path for TextBox to block the space key press above,
// we're not supposed to do this here, but we don't have a good way of simulating text input from ManualInputManager so let's just do this for now.
dropdown.TextInput.Text(" ");
});
AddAssert("handler did not receive press", () => !keyBindingHandler.ReceivedPress);
}

[Test]
public void TestMouseFromTouch()
{
ManualTextDropdown dropdown = null!;
TestClickHandler clickHandler = null!;

AddStep("setup dropdown", () =>
{
dropdown = createDropdowns<ManualTextDropdown>(1)[0];
dropdown.AlwaysShowSearchBar = true;
});

AddStep("setup click handler", () => Add(clickHandler = new TestClickHandler
{
RelativeSizeAxes = Axes.Both
}));

AddAssert("search bar hidden", () => dropdown.ChildrenOfType<DropdownSearchBar>().Single().State.Value, () => Is.EqualTo(Visibility.Hidden));
AddStep("begin touch", () => InputManager.BeginTouch(new Touch(TouchSource.Touch1, dropdown.Header.ScreenSpaceDrawQuad.Centre)));
AddStep("end touch", () => InputManager.EndTouch(new Touch(TouchSource.Touch1, dropdown.Header.ScreenSpaceDrawQuad.Centre)));

AddAssert("search bar still hidden", () => dropdown.ChildrenOfType<DropdownSearchBar>().Single().State.Value, () => Is.EqualTo(Visibility.Hidden));
AddAssert("handler received click", () => clickHandler.ReceivedClick);

AddStep("type something", () => dropdown.TextInput.Text("something"));
AddAssert("search bar still hidden", () => dropdown.ChildrenOfType<DropdownSearchBar>().Single().State.Value, () => Is.EqualTo(Visibility.Hidden));
AddAssert("search bar empty", () => dropdown.Header.SearchTerm.Value, () => Is.Null.Or.Empty);

AddStep("hide click handler", () => clickHandler.Hide());
AddStep("begin touch", () => InputManager.BeginTouch(new Touch(TouchSource.Touch1, dropdown.Header.ScreenSpaceDrawQuad.Centre)));
AddStep("end touch", () => InputManager.EndTouch(new Touch(TouchSource.Touch1, dropdown.Header.ScreenSpaceDrawQuad.Centre)));

AddAssert("search bar visible", () => dropdown.ChildrenOfType<DropdownSearchBar>().Single().State.Value, () => Is.EqualTo(Visibility.Visible));
}

#endregion

private TestDropdown createDropdown() => createDropdowns(1).Single();
Expand Down Expand Up @@ -631,5 +734,44 @@ protected override LocalisableString GenerateItemText(TestModel? item)
return $"{text}: {base.GenerateItemText(item)}";
}
}

private partial class TestKeyBindingContainer : KeyBindingContainer<TestAction>
{
public override IEnumerable<IKeyBinding> DefaultKeyBindings => new[]
{
new KeyBinding(InputKey.Space, TestAction.SpaceAction)
};
}

private partial class TestKeyBindingHandler : Drawable, IKeyBindingHandler<TestAction>
{
public bool ReceivedPress;

public bool OnPressed(KeyBindingPressEvent<TestAction> e)
{
ReceivedPress = true;
return true;
}

public void OnReleased(KeyBindingReleaseEvent<TestAction> e)
{
}
}

private partial class TestClickHandler : Drawable
{
public bool ReceivedClick;

protected override bool OnClick(ClickEvent e)
{
ReceivedClick = true;
return true;
}
}

private enum TestAction
{
SpaceAction,
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.UserInterface;
using osu.Framework.Input;
using osu.Framework.Input.Events;
using osu.Framework.Testing;
using osuTK;
Expand Down Expand Up @@ -395,7 +396,7 @@ public void TestExternalPopoverControl()
});
AddAssert("popover shown", () => this.ChildrenOfType<Popover>().Any());

AddStep("take away text box focus", () => InputManager.ChangeFocus(null));
AddStep("take away text box focus", () => ((IFocusManager)InputManager).ChangeFocus(null));
AddAssert("popover hidden", () => !this.ChildrenOfType<Popover>().Any());
}

Expand Down
4 changes: 2 additions & 2 deletions osu.Framework/Graphics/Containers/FocusedOverlayContainer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,11 @@ protected override void UpdateState(ValueChangedEvent<Visibility> state)
{
case Visibility.Hidden:
if (HasFocus)
GetContainingInputManager().ChangeFocus(null);
GetContainingFocusManager().ChangeFocus(null);
break;

case Visibility.Visible:
Schedule(() => GetContainingInputManager().TriggerFocusContention(this));
Schedule(() => GetContainingFocusManager().TriggerFocusContention(this));
break;
}
}
Expand Down
2 changes: 1 addition & 1 deletion osu.Framework/Graphics/Containers/TabbableContainer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ protected override bool OnKeyDown(KeyDownEvent e)
return false;

var nextTab = nextTabStop(TabbableContentContainer, e.ShiftPressed);
if (nextTab != null) GetContainingInputManager().ChangeFocus(nextTab);
if (nextTab != null) GetContainingFocusManager().ChangeFocus(nextTab);
return true;
}

Expand Down
7 changes: 7 additions & 0 deletions osu.Framework/Graphics/Drawable.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1492,6 +1492,13 @@ protected internal virtual bool ShouldBeAlive
/// <returns>The first parent <see cref="InputManager"/>.</returns>
protected InputManager GetContainingInputManager() => this.FindClosestParent<InputManager>();

/// <summary>
/// Retrieve the first parent in the tree which implements <see cref="IFocusManager"/>.
/// As this is performing an upward tree traversal, avoid calling every frame.
/// </summary>
/// <returns>The first parent <see cref="IFocusManager"/>.</returns>
protected IFocusManager GetContainingFocusManager() => this.FindClosestParent<IFocusManager>();

private CompositeDrawable parent;

/// <summary>
Expand Down
Loading
Loading