Skip to content

Commit

Permalink
Merge pull request #6229 from Susko3/refactor-keycombination
Browse files Browse the repository at this point in the history
Refactor `KeyCombination.ContainsKey()` and `.ContainsKeyPermissive()` for better extensibility
  • Loading branch information
smoogipoo authored May 23, 2024
2 parents 11c2d23 + 759c48e commit f6fd752
Show file tree
Hide file tree
Showing 5 changed files with 110 additions and 98 deletions.
27 changes: 12 additions & 15 deletions osu.Framework.Tests/Input/KeyCombinationTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,37 +14,34 @@ public class KeyCombinationTest
// test single combination matches.
new object[] { new KeyCombination(InputKey.Shift), new KeyCombination(InputKey.LShift), KeyCombinationMatchingMode.Any, true },
new object[] { new KeyCombination(InputKey.Shift), new KeyCombination(InputKey.RShift), KeyCombinationMatchingMode.Any, true },
new object[] { new KeyCombination(InputKey.Shift), new KeyCombination(InputKey.Shift), KeyCombinationMatchingMode.Any, true },
new object[] { new KeyCombination(InputKey.LShift), new KeyCombination(InputKey.RShift), KeyCombinationMatchingMode.Any, false },
new object[] { new KeyCombination(InputKey.RShift), new KeyCombination(InputKey.RShift), KeyCombinationMatchingMode.Any, true },

new object[] { new KeyCombination(InputKey.Shift), new KeyCombination(InputKey.LShift), KeyCombinationMatchingMode.Exact, true },
new object[] { new KeyCombination(InputKey.Shift), new KeyCombination(InputKey.RShift), KeyCombinationMatchingMode.Exact, true },
new object[] { new KeyCombination(InputKey.Shift), new KeyCombination(InputKey.Shift), KeyCombinationMatchingMode.Exact, true },
new object[] { new KeyCombination(InputKey.LShift), new KeyCombination(InputKey.RShift), KeyCombinationMatchingMode.Exact, false },
new object[] { new KeyCombination(InputKey.RShift), new KeyCombination(InputKey.RShift), KeyCombinationMatchingMode.Exact, true },

new object[] { new KeyCombination(InputKey.Shift), new KeyCombination(InputKey.LShift), KeyCombinationMatchingMode.Modifiers, true },
new object[] { new KeyCombination(InputKey.Shift), new KeyCombination(InputKey.RShift), KeyCombinationMatchingMode.Modifiers, true },
new object[] { new KeyCombination(InputKey.Shift), new KeyCombination(InputKey.Shift), KeyCombinationMatchingMode.Modifiers, true },
new object[] { new KeyCombination(InputKey.LShift), new KeyCombination(InputKey.RShift), KeyCombinationMatchingMode.Modifiers, false },
new object[] { new KeyCombination(InputKey.RShift), new KeyCombination(InputKey.RShift), KeyCombinationMatchingMode.Modifiers, true },

// test multiple combination matches.
new object[] { new KeyCombination(InputKey.Shift), new KeyCombination(InputKey.Shift, InputKey.LShift), KeyCombinationMatchingMode.Any, true },
new object[] { new KeyCombination(InputKey.Shift), new KeyCombination(InputKey.Shift, InputKey.RShift), KeyCombinationMatchingMode.Any, true },
new object[] { new KeyCombination(InputKey.LShift), new KeyCombination(InputKey.Shift, InputKey.RShift), KeyCombinationMatchingMode.Any, false },
new object[] { new KeyCombination(InputKey.RShift), new KeyCombination(InputKey.Shift, InputKey.RShift), KeyCombinationMatchingMode.Any, true },
new object[] { new KeyCombination(InputKey.Shift), new KeyCombination(InputKey.LShift, InputKey.RShift), KeyCombinationMatchingMode.Any, true },
new object[] { new KeyCombination(InputKey.LShift), new KeyCombination(InputKey.LShift, InputKey.RShift), KeyCombinationMatchingMode.Any, true },
new object[] { new KeyCombination(InputKey.RShift), new KeyCombination(InputKey.LShift, InputKey.RShift), KeyCombinationMatchingMode.Any, true },
new object[] { new KeyCombination(InputKey.RShift), new KeyCombination(InputKey.RShift, InputKey.A), KeyCombinationMatchingMode.Any, true },

new object[] { new KeyCombination(InputKey.Shift), new KeyCombination(InputKey.Shift, InputKey.LShift), KeyCombinationMatchingMode.Exact, true },
new object[] { new KeyCombination(InputKey.Shift), new KeyCombination(InputKey.Shift, InputKey.RShift), KeyCombinationMatchingMode.Exact, true },
new object[] { new KeyCombination(InputKey.LShift), new KeyCombination(InputKey.Shift, InputKey.RShift), KeyCombinationMatchingMode.Exact, false },
new object[] { new KeyCombination(InputKey.RShift), new KeyCombination(InputKey.Shift, InputKey.RShift), KeyCombinationMatchingMode.Exact, true },
new object[] { new KeyCombination(InputKey.Shift), new KeyCombination(InputKey.LShift, InputKey.RShift), KeyCombinationMatchingMode.Exact, true },
new object[] { new KeyCombination(InputKey.LShift), new KeyCombination(InputKey.LShift, InputKey.RShift), KeyCombinationMatchingMode.Exact, false },
new object[] { new KeyCombination(InputKey.RShift), new KeyCombination(InputKey.LShift, InputKey.RShift), KeyCombinationMatchingMode.Exact, false },
new object[] { new KeyCombination(InputKey.RShift), new KeyCombination(InputKey.RShift, InputKey.A), KeyCombinationMatchingMode.Exact, false },

new object[] { new KeyCombination(InputKey.Shift), new KeyCombination(InputKey.Shift, InputKey.LShift), KeyCombinationMatchingMode.Modifiers, true },
new object[] { new KeyCombination(InputKey.Shift), new KeyCombination(InputKey.Shift, InputKey.RShift), KeyCombinationMatchingMode.Modifiers, true },
new object[] { new KeyCombination(InputKey.LShift), new KeyCombination(InputKey.Shift, InputKey.RShift), KeyCombinationMatchingMode.Modifiers, false },
new object[] { new KeyCombination(InputKey.RShift), new KeyCombination(InputKey.Shift, InputKey.RShift), KeyCombinationMatchingMode.Modifiers, true },
new object[] { new KeyCombination(InputKey.Shift), new KeyCombination(InputKey.LShift, InputKey.RShift), KeyCombinationMatchingMode.Modifiers, true },
new object[] { new KeyCombination(InputKey.LShift), new KeyCombination(InputKey.LShift, InputKey.RShift), KeyCombinationMatchingMode.Modifiers, false },
new object[] { new KeyCombination(InputKey.RShift), new KeyCombination(InputKey.LShift, InputKey.RShift), KeyCombinationMatchingMode.Modifiers, false },
new object[] { new KeyCombination(InputKey.RShift), new KeyCombination(InputKey.RShift, InputKey.A), KeyCombinationMatchingMode.Modifiers, true },
};

[TestCaseSource(nameof(key_combination_display_test_cases))]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -139,15 +139,15 @@ protected override void LoadComplete()

protected override bool OnKeyDown(KeyDownEvent e)
{
if (keyCombination.IsPressed(new KeyCombination(KeyCombination.FromKey(e.Key)), KeyCombinationMatchingMode.Any))
if (keyCombination.IsPressed(new KeyCombination(KeyCombination.FromKey(e.Key)), e.CurrentState, KeyCombinationMatchingMode.Any))
box.Colour = Color4.Navy;

return base.OnKeyDown(e);
}

protected override void OnKeyUp(KeyUpEvent e)
{
if (keyCombination.IsPressed(new KeyCombination(KeyCombination.FromKey(e.Key)), KeyCombinationMatchingMode.Any))
if (keyCombination.IsPressed(new KeyCombination(KeyCombination.FromKey(e.Key)), e.CurrentState, KeyCombinationMatchingMode.Any))
box.Colour = Color4.DarkGray;

base.OnKeyUp(e);
Expand Down
42 changes: 42 additions & 0 deletions osu.Framework/Extensions/InputKeyExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
// Copyright (c) ppy Pty Ltd <[email protected]>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.

using System;
using osu.Framework.Input.Bindings;

namespace osu.Framework.Extensions
{
public static class InputKeyExtensions
{
public static bool IsPhysical(this InputKey key)
{
if (!Enum.IsDefined(key) || IsVirtual(key))
return false;

switch (key)
{
case InputKey.None:
case InputKey.LastKey:
return false;

default:
return true;
}
}

public static bool IsVirtual(this InputKey key)
{
switch (key)
{
case InputKey.Shift:
case InputKey.Control:
case InputKey.Alt:
case InputKey.Super:
return true;

default:
return false;
}
}
}
}
6 changes: 3 additions & 3 deletions osu.Framework/Input/Bindings/KeyBindingContainer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -224,7 +224,7 @@ private bool handleNewPressed(InputState state, InputKey newKey, Vector2? scroll
if (pressedBindings.Contains(binding))
continue;

if (binding.KeyCombination.IsPressed(pressedCombination, matchingMode))
if (binding.KeyCombination.IsPressed(pressedCombination, state, matchingMode))
newlyPressed.Add(binding);
}
}
Expand All @@ -251,7 +251,7 @@ private bool handleNewPressed(InputState state, InputKey newKey, Vector2? scroll
if (simultaneousMode == SimultaneousBindingMode.None && (matchingMode == KeyCombinationMatchingMode.Exact || matchingMode == KeyCombinationMatchingMode.Modifiers))
{
// only want to release pressed actions if no existing bindings would still remain pressed
if (pressedBindings.Count > 0 && !pressedBindings.Any(m => m.KeyCombination.IsPressed(pressedCombination, matchingMode)))
if (pressedBindings.Count > 0 && !pressedBindings.Any(m => m.KeyCombination.IsPressed(pressedCombination, state, matchingMode)))
releasePressedActions(state);
}

Expand Down Expand Up @@ -365,7 +365,7 @@ private void handleNewReleased(InputState state, InputKey releasedKey)
{
var binding = pressedBindings[i];

if (pressedInputKeys.Count == 0 || !binding.KeyCombination.IsPressed(pressedCombination, KeyCombinationMatchingMode.Any))
if (pressedInputKeys.Count == 0 || !binding.KeyCombination.IsPressed(pressedCombination, state, KeyCombinationMatchingMode.Any))
{
pressedBindings.RemoveAt(i--);
PropagateReleased(getInputQueue(binding).Where(d => d.IsRootedAt(this)), state, binding.GetAction<T>());
Expand Down
129 changes: 51 additions & 78 deletions osu.Framework/Input/Bindings/KeyCombination.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
using System.Diagnostics;
using System.Linq;
using System.Runtime.CompilerServices;
using osu.Framework.Extensions;
using osu.Framework.Input.States;
using osuTK;
using osuTK.Input;
Expand Down Expand Up @@ -91,9 +92,10 @@ private KeyCombination(ImmutableArray<InputKey> keys)
/// Check whether the provided pressed keys are valid for this <see cref="KeyCombination"/>.
/// </summary>
/// <param name="pressedKeys">The potential pressed keys for this <see cref="KeyCombination"/>.</param>
/// <param name="inputState">The current input state.</param>
/// <param name="matchingMode">The method for handling exact key matches.</param>
/// <returns>Whether the pressedKeys keys are valid.</returns>
public bool IsPressed(KeyCombination pressedKeys, KeyCombinationMatchingMode matchingMode)
public bool IsPressed(KeyCombination pressedKeys, InputState inputState, KeyCombinationMatchingMode matchingMode)
{
Debug.Assert(!pressedKeys.Keys.Contains(InputKey.None)); // Having None in pressed keys will break IsPressed

Expand All @@ -103,42 +105,68 @@ public bool IsPressed(KeyCombination pressedKeys, KeyCombinationMatchingMode mat
return ContainsAll(Keys, pressedKeys.Keys, matchingMode);
}

private static InputKey? getVirtualKey(InputKey key)
{
switch (key)
{
case InputKey.LShift:
case InputKey.RShift:
return InputKey.Shift;

case InputKey.LControl:
case InputKey.RControl:
return InputKey.Control;

case InputKey.LAlt:
case InputKey.RAlt:
return InputKey.Alt;

case InputKey.LSuper:
case InputKey.RSuper:
return InputKey.Super;
}

return null;
}

/// <summary>
/// Check whether the provided set of pressed keys matches the candidate binding.
/// </summary>
/// <param name="candidateKey">The candidate key binding to match against.</param>
/// <param name="pressedKey">The keys which have been pressed by a user.</param>
/// <param name="candidateKeyBinding">The candidate key binding to match against.</param>
/// <param name="pressedPhysicalKeys">The keys which have been pressed by a user.</param>
/// <param name="matchingMode">The matching mode to be used when checking.</param>
/// <returns>Whether this is a match.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal static bool ContainsAll(ImmutableArray<InputKey> candidateKey, ImmutableArray<InputKey> pressedKey, KeyCombinationMatchingMode matchingMode)
internal static bool ContainsAll(ImmutableArray<InputKey> candidateKeyBinding, ImmutableArray<InputKey> pressedPhysicalKeys, KeyCombinationMatchingMode matchingMode)
{
Debug.Assert(pressedPhysicalKeys.All(k => k.IsPhysical()));

// first, check that all the candidate keys are contained in the provided pressed keys.
// regardless of the matching mode, every key needs to at least be present (matching modes only change
// the behaviour of excess keys).
foreach (var key in candidateKey)
foreach (var key in candidateKeyBinding)
{
if (!ContainsKey(pressedKey, key))
if (!IsPressed(pressedPhysicalKeys, key))
return false;
}

switch (matchingMode)
{
case KeyCombinationMatchingMode.Exact:
foreach (var key in pressedKey)
foreach (var key in pressedPhysicalKeys)
{
// in exact matching mode, every pressed key needs to be in the candidate.
if (!ContainsKeyPermissive(candidateKey, key))
if (!KeyBindingContains(candidateKeyBinding, key))
return false;
}

break;

case KeyCombinationMatchingMode.Modifiers:
foreach (var key in pressedKey)
foreach (var key in pressedPhysicalKeys)
{
// in modifiers match mode, the same check applies as exact but only for modifier keys.
if (IsModifierKey(key) && !ContainsKeyPermissive(candidateKey, key))
if (IsModifierKey(key) && !KeyBindingContains(candidateKeyBinding, key))
return false;
}

Expand All @@ -154,84 +182,29 @@ internal static bool ContainsAll(ImmutableArray<InputKey> candidateKey, Immutabl

/// <summary>
/// Check whether the provided key is part of the candidate binding.
/// This will match bidirectionally for modifier keys (LShift and Shift being present in both of the two parameters in either order will return true).
/// </summary>
/// <param name="candidate">The candidate key binding to match against.</param>
/// <param name="key">The key which has been pressed by a user.</param>
/// <param name="candidateKeyBinding">The candidate key binding to match against.</param>
/// <param name="physicalKey">The physical key that has been pressed.</param>
/// <returns>Whether this is a match.</returns>
internal static bool ContainsKeyPermissive(ImmutableArray<InputKey> candidate, InputKey key)
internal static bool KeyBindingContains(ImmutableArray<InputKey> candidateKeyBinding, InputKey physicalKey)
{
switch (key)
{
case InputKey.LControl:
case InputKey.RControl:
if (candidate.Contains(InputKey.Control))
return true;

break;

case InputKey.LShift:
case InputKey.RShift:
if (candidate.Contains(InputKey.Shift))
return true;

break;

case InputKey.RAlt:
case InputKey.LAlt:
if (candidate.Contains(InputKey.Alt))
return true;

break;

case InputKey.LSuper:
case InputKey.RSuper:
if (candidate.Contains(InputKey.Super))
return true;

break;
}

return ContainsKey(candidate, key);
return candidateKeyBinding.Contains(physicalKey) ||
(getVirtualKey(physicalKey) is InputKey vKey && candidateKeyBinding.Contains(vKey));
}

/// <summary>
/// Check whether a single key from a candidate binding is relevant to the currently pressed keys.
/// If the <paramref name="key"/> contains a left/right specific modifier, the <paramref name="candidate"/> must also for this to match.
/// Check whether a single physical or virtual key from a candidate binding is relevant to the currently pressed keys.
/// </summary>
/// <param name="candidate">The candidate key binding to match against.</param>
/// <param name="key">The key which has been pressed by a user.</param>
/// <param name="pressedPhysicalKeys">The currently pressed keys to match against.</param>
/// <param name="candidateKey">The candidate key to check.</param>
/// <returns>Whether this is a match.</returns>
internal static bool ContainsKey(ImmutableArray<InputKey> candidate, InputKey key)
internal static bool IsPressed(ImmutableArray<InputKey> pressedPhysicalKeys, InputKey candidateKey)
{
switch (key)
{
case InputKey.Control:
if (candidate.Contains(InputKey.LControl) || candidate.Contains(InputKey.RControl))
return true;

break;

case InputKey.Shift:
if (candidate.Contains(InputKey.LShift) || candidate.Contains(InputKey.RShift))
return true;

break;

case InputKey.Alt:
if (candidate.Contains(InputKey.LAlt) || candidate.Contains(InputKey.RAlt))
return true;

break;

case InputKey.Super:
if (candidate.Contains(InputKey.LSuper) || candidate.Contains(InputKey.RSuper))
return true;

break;
}
if (candidateKey.IsPhysical())
return pressedPhysicalKeys.Contains(candidateKey);

return candidate.Contains(key);
Debug.Assert(candidateKey.IsVirtual());
return pressedPhysicalKeys.Any(k => getVirtualKey(k) == candidateKey);
}

public bool Equals(KeyCombination other) => Keys.SequenceEqual(other.Keys);
Expand Down

0 comments on commit f6fd752

Please sign in to comment.