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 concept of multiple focus targets #6095

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
60 changes: 59 additions & 1 deletion osu.Framework.Tests/Visual/Drawables/TestSceneFocus.cs
Expand Up @@ -4,6 +4,7 @@
#nullable disable

using System;
using System.Collections.Generic;
using NUnit.Framework;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
Expand Down Expand Up @@ -243,6 +244,49 @@ public void InputPropagation()
focusBottomRight.JoystickPressCount == 1 && focusBottomRight.JoystickReleaseCount == 1);
}

[Test]
public void TestAdditionalFocusTargets()
{
FocusBoxWithTargets parent = null;
FocusBox target = null;
FocusBox other = null;

AddStep("setup", () =>
{
Child = parent = new FocusBoxWithTargets
{
RelativeSizeAxes = Axes.Both,
Children = new[]
{
target = new FocusBox { Colour = Color4.Red, AllowAcceptingFocus = false },
other = new FocusBox { Colour = Color4.Green, AllowAcceptingFocus = false },
}
};

parent.FocusTargets.Add(target);
});

AddStep("focus by click", () =>
{
InputManager.MoveMouseTo(parent);
InputManager.Click(MouseButton.Left);
});

checkFocused(() => parent);
checkFocused(() => target);
checkNotFocused(() => other);

AddStep("click away", () =>
{
InputManager.MoveMouseTo(Vector2.Zero);
InputManager.Click(MouseButton.Left);
});

checkNotFocused(() => parent);
checkNotFocused(() => target);
checkNotFocused(() => other);
}

private void checkFocused(Func<Drawable> d) => AddAssert("check focus", () => d().HasFocus);
private void checkNotFocused(Func<Drawable> d) => AddAssert("check not focus", () => !d().HasFocus);

Expand Down Expand Up @@ -340,11 +384,13 @@ public RequestingFocusBox()
}
}

public partial class FocusBox : CompositeDrawable
public partial class FocusBox : Container
{
protected Box Box;
public int KeyDownCount, KeyUpCount, JoystickPressCount, JoystickReleaseCount;

protected override Container<Drawable> Content { get; }

public FocusBox()
{
AddInternal(Box = new Box
Expand All @@ -354,6 +400,11 @@ public FocusBox()
Colour = Color4.Red
});

AddInternal(Content = new Container
{
RelativeSizeAxes = Axes.Both,
});

RelativeSizeAxes = Axes.Both;
Size = new Vector2(0.4f);
}
Expand Down Expand Up @@ -401,5 +452,12 @@ protected override void OnJoystickRelease(JoystickReleaseEvent e)
base.OnJoystickRelease(e);
}
}

public partial class FocusBoxWithTargets : FocusBox
{
public readonly List<Drawable> FocusTargets = new List<Drawable>();

protected internal override IEnumerable<Drawable> AdditionalFocusTargets => FocusTargets;
}
}
}
6 changes: 6 additions & 0 deletions osu.Framework/Graphics/Drawable.cs
Expand Up @@ -2384,6 +2384,12 @@ public bool TriggerEvent(UIEvent e)
/// </summary>
public virtual bool AcceptsFocus => false;

/// <summary>
/// A list of <see cref="Drawable"/>s that will gain/lose focus according to this <see cref="Drawable"/>.
/// Such drawables do not require to inherit <see cref="AcceptsFocus"/>.
/// </summary>
protected internal virtual IEnumerable<Drawable> AdditionalFocusTargets => Enumerable.Empty<Drawable>();

/// <summary>
/// Whether this Drawable is currently hovered over.
/// </summary>
Expand Down
12 changes: 6 additions & 6 deletions osu.Framework/Graphics/UserInterface/TextBox.cs
Expand Up @@ -1316,7 +1316,7 @@ protected override void OnFocusLost(FocusLostEvent e)
// let's say that a focus loss is not a user event as focus is commonly indirectly lost.
FinalizeImeComposition(false);

unbindInput(e.NextFocused is TextBox);
unbindInput(e.NextFocused.Any(d => d is TextBox));

updateCaretVisibility();

Expand All @@ -1336,7 +1336,7 @@ protected override bool OnClick(ClickEvent e)

protected override void OnFocus(FocusEvent e)
{
bindInput(e.PreviouslyFocused is TextBox);
bindInput(e.PreviouslyFocused.Any(d => d is TextBox));

updateCaretVisibility();
}
Expand All @@ -1350,7 +1350,7 @@ protected override void OnFocus(FocusEvent e)
/// </summary>
private bool textInputBound;

private void bindInput(bool previousFocusWasTextBox)
private void bindInput(bool textBoxFocusedPreviously)
{
if (textInputBound)
{
Expand All @@ -1361,7 +1361,7 @@ private void bindInput(bool previousFocusWasTextBox)
// TextBox has special handling of text input activation when focus is changed directly from one TextBox to another.
// We don't deactivate and activate, but instead keep text input active during the focus handoff, so that virtual keyboards on phones don't flicker.

if (previousFocusWasTextBox)
if (textBoxFocusedPreviously)
textInput.EnsureActivated(AllowIme);
else
textInput.Activate(AllowIme);
Expand All @@ -1373,15 +1373,15 @@ private void bindInput(bool previousFocusWasTextBox)
textInputBound = true;
}

private void unbindInput(bool nextFocusIsTextBox)
private void unbindInput(bool textBoxFocusedNext)
{
if (!textInputBound)
return;

textInputBound = false;

// see the comment above, in `bindInput(bool)`.
if (!nextFocusIsTextBox)
if (!textBoxFocusedNext)
textInput.Deactivate();

textInput.OnTextInput -= handleTextInput;
Expand Down
7 changes: 4 additions & 3 deletions osu.Framework/Input/Events/FocusEvent.cs
@@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.

using System.Collections.Generic;
using osu.Framework.Graphics;
using osu.Framework.Input.States;

Expand All @@ -12,11 +13,11 @@ namespace osu.Framework.Input.Events
public class FocusEvent : UIEvent
{
/// <summary>
/// The <see cref="Drawable"/> that has lost focus, or <c>null</c> if nothing was previously focused.
/// A list of <see cref="Drawable"/>s that lost focus. The list is empty if nothing was previously focused.
/// </summary>
public readonly Drawable? PreviouslyFocused;
public readonly IReadOnlyList<Drawable> PreviouslyFocused;

public FocusEvent(InputState state, Drawable? previouslyFocused)
public FocusEvent(InputState state, IReadOnlyList<Drawable> previouslyFocused)
: base(state)
{
PreviouslyFocused = previouslyFocused;
Expand Down
7 changes: 4 additions & 3 deletions osu.Framework/Input/Events/FocusLostEvent.cs
@@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.

using System.Collections.Generic;
using osu.Framework.Graphics;
using osu.Framework.Input.States;

Expand All @@ -12,11 +13,11 @@ namespace osu.Framework.Input.Events
public class FocusLostEvent : UIEvent
{
/// <summary>
/// The <see cref="Drawable"/> that will gain focus, or <c>null</c> if nothing will gain focus.
/// A list of <see cref="Drawable"/>s that will gain focus. The list is empty if nothing will gain focus.
/// </summary>
public readonly Drawable? NextFocused;
public readonly IReadOnlyList<Drawable> NextFocused;

public FocusLostEvent(InputState state, Drawable? nextFocused)
public FocusLostEvent(InputState state, IReadOnlyList<Drawable> nextFocused)
: base(state)
{
NextFocused = nextFocused;
Expand Down
54 changes: 44 additions & 10 deletions osu.Framework/Input/InputManager.cs
Expand Up @@ -7,6 +7,7 @@
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics;
using System.Linq;
using JetBrains.Annotations;
using osu.Framework.Allocation;
using osu.Framework.Extensions.ListExtensions;
Expand Down Expand Up @@ -51,6 +52,8 @@ public abstract partial class InputManager : Container, IInputStateChangeHandler
/// </summary>
public Drawable FocusedDrawable { get; internal set; }

private readonly List<Drawable> focusedDrawables = new List<Drawable>();

protected abstract ImmutableArray<InputHandler> InputHandlers { get; }

private double keyboardRepeatTime;
Expand Down Expand Up @@ -405,31 +408,50 @@ protected bool ChangeFocus(Drawable potentialFocusTarget, InputState state)
if (potentialFocusTarget != null && (!isDrawableValidForFocus(potentialFocusTarget) || !potentialFocusTarget.AcceptsFocus))
return false;

var previousFocus = FocusedDrawable;
var previousTarget = FocusedDrawable;

var previousDrawables = focusedDrawables.ToArray();
var nextDrawables = getFocusDrawables(potentialFocusTarget).ToArray();

FocusedDrawable = null;
focusedDrawables.Clear();

if (previousFocus != null)
foreach (var previous in previousDrawables)
{
previousFocus.HasFocus = false;
previousFocus.TriggerEvent(new FocusLostEvent(state, potentialFocusTarget));
previous.HasFocus = false;
previous.TriggerEvent(new FocusLostEvent(state, nextDrawables));

if (FocusedDrawable != null) throw new InvalidOperationException($"Focus cannot be changed inside {nameof(OnFocusLost)}");
}

FocusedDrawable = potentialFocusTarget;
focusedDrawables.AddRange(nextDrawables);

Logger.Log($"Focus changed from {previousFocus?.ToString() ?? "nothing"} to {FocusedDrawable?.ToString() ?? "nothing"}.", LoggingTarget.Runtime, LogLevel.Debug);
Logger.Log($"Focus changed from {previousTarget?.ToString() ?? "nothing"} to {FocusedDrawable?.ToString() ?? "nothing"}.", LoggingTarget.Runtime, LogLevel.Debug);

if (FocusedDrawable != null)
foreach (var next in nextDrawables)
{
FocusedDrawable.HasFocus = true;
FocusedDrawable.TriggerEvent(new FocusEvent(state, previousFocus));
next.HasFocus = true;
next.TriggerEvent(new FocusEvent(state, previousDrawables));
}

return true;
}

private IEnumerable<Drawable> getFocusDrawables(Drawable drawable)
{
if (drawable == null)
yield break;

yield return drawable;

foreach (var additional in drawable.AdditionalFocusTargets)
{
foreach (var t in getFocusDrawables(additional))
yield return t;
}
}

internal override bool BuildNonPositionalInputQueue(List<Drawable> queue, bool allowBlocking = true)
{
if (!allowBlocking)
Expand Down Expand Up @@ -602,8 +624,20 @@ private SlimReadOnlyListWrapper<Drawable> buildNonPositionalInputQueue()

if (!unfocusIfNoLongerValid())
{
inputQueue.Remove(FocusedDrawable);
inputQueue.Add(FocusedDrawable);
int count = inputQueue.Count;

for (int i = 0; i < count; i++)
{
Drawable d = inputQueue[i];

if (d.HasFocus)
{
inputQueue.Remove(d);
inputQueue.Add(d);
i--;
count--;
}
}
}

// queues were created in back-to-front order.
Expand Down