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

Fix keyboard shortcuts not working as expected on non-QWERTY keyboard #5790

Open
wants to merge 47 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
6fb7adc
Add `KeyboardKey` and propagate `Character` trough the input hierarchy
Susko3 Sep 6, 2022
af66d0f
Update `InputManager`s with `KeyboardKeyInput` changes
Susko3 Sep 6, 2022
a28777e
Add native implementations for `KeyboardKey.Character`
Susko3 Sep 6, 2022
0782390
Update tests
Susko3 Sep 6, 2022
7324d3d
Update benchmarks
Susko3 Sep 6, 2022
9d50793
Add test for keyboard layout changing during key press
Susko3 Sep 6, 2022
2087808
Fix numpad keys giving wrong characters when numlock was off
Susko3 Sep 6, 2022
d0f002f
Merge branch 'master' into KeyboardEvent-Character
Susko3 Sep 7, 2022
7f8f26d
Merge branch 'master' into KeyboardEvent-Character
Susko3 May 11, 2023
f39fe32
Add way to use keycodes as `InputKey`s
Susko3 May 11, 2023
f07a37a
Change `PlatformKeyBindings` to use new Keycode keys
Susko3 May 11, 2023
b0bff4c
Add xmldoc
Susko3 May 11, 2023
fc318ae
Remove unused method
Susko3 May 11, 2023
be2d763
Inline trivial function
Susko3 May 11, 2023
03be244
Fix wrong xmldoc
Susko3 May 11, 2023
f13d6f4
Fix handling when the character changes mid-press
Susko3 May 13, 2023
d9fa4ca
Misc code style
Susko3 May 13, 2023
fa287ae
Add tests and xmldoc about non-working feature
Susko3 May 14, 2023
1fbb979
Refine xmldoc to add missing information
Susko3 May 14, 2023
4fbc7d9
Add `HeadlessTest` attribute
Susko3 May 21, 2023
71e2231
Show keycodes separately from regular InputKeys
Susko3 May 21, 2023
2616982
Merge branch 'master' into InputKey-Character
peppy May 22, 2023
6e4d9be
Add tests that show what works and what doesn't
Susko3 Jun 19, 2023
a325080
Merge branch 'master' into InputKey-Character
Susko3 Jun 26, 2023
ee64823
Merge branch 'master' into InputKey-Character
Susko3 Nov 5, 2023
2b008e2
Make tests correctly fail
Susko3 Nov 5, 2023
593d1cd
Fix `ManualInputManager` pressing invalid keys
Susko3 Nov 5, 2023
74b144a
Fix nullref (fixed in same way MidiKeyInput handles it)
Susko3 Nov 5, 2023
1ffb0bb
Merge branch 'master' into InputKey-Character
peppy Nov 29, 2023
7402c7b
Merge branch 'master' into InputKey-Character
Susko3 Feb 1, 2024
08f82c4
Add comment about why using `default` is fine
Susko3 Feb 1, 2024
51ac0a0
Merge branch 'master' into InputKey-Character
Susko3 Mar 22, 2024
c39c37b
Update tests so they compile
Susko3 Mar 22, 2024
1c88800
Remove stray #nullable disable
Susko3 Mar 27, 2024
54b49e1
Use `char?` and denote no keycode as `null` instead of `\0`
Susko3 Mar 27, 2024
9c370bc
Fix test
Susko3 Mar 27, 2024
b74b208
Add failing test
Susko3 Mar 27, 2024
76601d3
Merge branch 'master' into InputKey-Character
Susko3 May 23, 2024
85229d3
Copy SDL2 to SDL3 implementation
Susko3 May 23, 2024
e54a409
Consider Keycode[A-Z] as virtual InputKeys
Susko3 May 23, 2024
47b0b77
Update `getVirtualKey()` to return `Keycode[A-Z]`
Susko3 May 23, 2024
d912e12
Revert `KeyCombination.romKey()` to work with regular `Key`s
Susko3 May 23, 2024
a4a3e7a
Show pressed virtual keys in the test
Susko3 May 23, 2024
f8be52c
Fix pressed keys visualisation not accounting for virtual keys
Susko3 May 23, 2024
f82b9b7
Update `SDLKeyboardTest` to check virtual separately
Susko3 May 23, 2024
d32c46e
Update `SDLKeyboardTest` to SDL3
Susko3 May 23, 2024
1d35383
Fix 'fixme' test
Susko3 May 23, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 5 additions & 4 deletions osu.Framework.Benchmarks/BenchmarkButtonInput.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
using osu.Framework.Input.StateChanges.Events;
using osu.Framework.Input.States;
using osuTK.Input;
using KeyboardState = osu.Framework.Input.States.KeyboardState;

namespace osu.Framework.Benchmarks
{
Expand All @@ -20,14 +21,14 @@ public class BenchmarkButtonInput
[Benchmark]
public KeyboardKeyInput FromTwoStates(int count)
{
var state1 = new ButtonStates<Key>();
var state2 = new ButtonStates<Key>();
var state1 = new KeyboardState();
var state2 = new KeyboardState();

for (int i = 0; i < count; i++)
state1.SetPressed((Key)i, true);
state1.Keys.SetPressed((Key)i, true);

for (int i = count; i < 2 * count; i++)
state2.SetPressed((Key)i, true);
state2.Keys.SetPressed((Key)i, true);

return new KeyboardKeyInput(state1, state2);
}
Expand Down
33 changes: 32 additions & 1 deletion osu.Framework.Tests/Input/KeyBindingInputTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,35 @@ public void TestReleaseAlwaysPressedToOriginalTargets()
AddAssert("receptorBelow received release", () => receptorBelow.ReleasedReceived);
}

[Test]
public void TestKeycodeBindings()
{
InputReceptor receptor = null;

AddStep("setup", () =>
{
Child = new TestKeyBindingContainer
{
RelativeSizeAxes = Axes.Both,
Children = new Drawable[]
{
receptor = new InputReceptor(true)
{
Size = new Vector2(100),
},
}
};
});

AddStep("move mouse to receptor", () => InputManager.MoveMouseTo(receptor));
AddStep("press keybind3", () => InputManager.PressKey(new KeyboardKey(Key.B, 'a')));
AddAssert("receptor received press", () => receptor.PressedReceived);

// simulate changing keyboard layout mid-press (not sure how valid this test is, but it doesn't hurt to cover)
AddStep("release keybind3 with different character", () => InputManager.ReleaseKey(new KeyboardKey(Key.B, 'b')));
AddAssert("receptor received release", () => receptor.ReleasedReceived);
}

private partial class InputReceptor : Box, IKeyBindingHandler<TestKeyBinding>
{
public bool PressedReceived { get; private set; }
Expand Down Expand Up @@ -121,13 +150,15 @@ public TestKeyBindingContainer()
{
new KeyBinding(InputKey.Up, TestKeyBinding.Binding1),
new KeyBinding(InputKey.Down, TestKeyBinding.Binding2),
new KeyBinding(InputKey.KeycodeA, TestKeyBinding.Binding3),
};
}

private enum TestKeyBinding
{
Binding1,
Binding2
Binding2,
Binding3,
}
}
}
147 changes: 147 additions & 0 deletions osu.Framework.Tests/Input/KeyBindingKeycodeTest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
// 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 System.Linq;
using NUnit.Framework;
using osu.Framework.Input;
using osu.Framework.Input.Bindings;
using osu.Framework.Testing;
using osuTK.Input;

namespace osu.Framework.Tests.Input
{
[HeadlessTest]
public partial class KeyBindingKeycodeTest : ManualInputManagerTestScene
{
private TestKeyBindingContainer keyBindingContainer = null!;

private void create(IEnumerable<IKeyBinding> keyBindings, SimultaneousBindingMode simultaneousMode, KeyCombinationMatchingMode matchingMode)
{
AddStep("create hierarchy", () =>
{
Child = keyBindingContainer = new TestKeyBindingContainer(keyBindings, simultaneousMode, matchingMode);
});
}

[Test]
public void TestConflictingKeys()
{
create(test_bindings, SimultaneousBindingMode.Unique, KeyCombinationMatchingMode.Modifiers);

press(new KeyboardKey(Key.A, 'a'));
check(TestKeyBinding.A, TestKeyBinding.KeycodeA);
release(new KeyboardKey(Key.A, 'a'));

press(KeyboardKey.FromKey(Key.LControl));
press(new KeyboardKey(Key.A, 'a'));
check(TestKeyBinding.CtrlA, TestKeyBinding.CtrlKeycodeA);
release(new KeyboardKey(Key.A, 'a'));
release(KeyboardKey.FromKey(Key.LControl));

check();
}

[TestCase(KeyCombinationMatchingMode.Any)]
[TestCase(KeyCombinationMatchingMode.Exact)]
[TestCase(KeyCombinationMatchingMode.Modifiers)]
public void TestExactMode(KeyCombinationMatchingMode mode)
{
create(test_bindings, SimultaneousBindingMode.Unique, mode);

press(new KeyboardKey(Key.U, 'a'));
check(TestKeyBinding.KeycodeA);

release(new KeyboardKey(Key.U, 'a'));
check();
}

[TestCase('d', 'm')]
[TestCase('m', 'd')] // swap D and M
public void TestSimpleGame(char keycodeForD, char keycodeForM)
{
create(simple_game_bindings, SimultaneousBindingMode.Unique, KeyCombinationMatchingMode.Modifiers);

var keyD = new KeyboardKey(Key.D, keycodeForD);
var keyM = new KeyboardKey(Key.M, keycodeForM);

press(keyD);
if (keyD.Character == 'm')
check(TestKeyBinding.Right, TestKeyBinding.Map); // map is also triggered, even if the developer wouldn't expect it
else
check(TestKeyBinding.Right);
release(keyD);

press(keyM);
if (keyM.Character == 'm')
check(TestKeyBinding.Map);
else
check(); // empty is expected here
release(keyM);

var keyForMenu = keyD.Character == 'm' ? keyD : keyM;

AddAssert("keycode 'm' will be pressed", () => keyForMenu.Character == 'm');

press(KeyboardKey.FromKey(Key.ControlLeft));
press(keyForMenu);
check(TestKeyBinding.Menu); // since Menu is using modifier keys different from Up/Down/Left/Right, it'll always work as expected
release(keyForMenu);
release(KeyboardKey.FromKey(Key.ControlLeft));

check();
}

private void press(KeyboardKey key) => AddStep($"press {key}", () => InputManager.PressKey(key));
private void release(KeyboardKey key) => AddStep($"release {key}", () => InputManager.ReleaseKey(key));

private void check(params TestKeyBinding[] bindings) => AddAssert($"check {(bindings.Any() ? string.Join(", ", bindings) : "<empty>")}", () => keyBindingContainer.PressedActions, () => Is.EquivalentTo(bindings));

private partial class TestKeyBindingContainer : KeyBindingContainer<TestKeyBinding>
{
public TestKeyBindingContainer(IEnumerable<IKeyBinding> keyBindings, SimultaneousBindingMode simultaneousMode, KeyCombinationMatchingMode matchingMode)
: base(simultaneousMode, matchingMode)
{
DefaultKeyBindings = keyBindings;
}

public override IEnumerable<IKeyBinding> DefaultKeyBindings { get; }
}

private static readonly IEnumerable<IKeyBinding> test_bindings = new[]
{
new KeyBinding(InputKey.A, TestKeyBinding.A),
new KeyBinding(InputKey.KeycodeA, TestKeyBinding.KeycodeA),
new KeyBinding(InputKey.KeycodeB, TestKeyBinding.KeycodeB),
new KeyBinding(new KeyCombination(InputKey.Control, InputKey.A), TestKeyBinding.CtrlA),
new KeyBinding(new KeyCombination(InputKey.Control, InputKey.KeycodeA), TestKeyBinding.CtrlKeycodeA),
};

/// sample keybinds for a simple game that will not as expected on some layouts
private static readonly IEnumerable<IKeyBinding> simple_game_bindings = new[]
{
new KeyBinding(InputKey.W, TestKeyBinding.Up),
new KeyBinding(InputKey.A, TestKeyBinding.Left),
new KeyBinding(InputKey.S, TestKeyBinding.Down),
new KeyBinding(InputKey.D, TestKeyBinding.Right),
new KeyBinding(InputKey.KeycodeM, TestKeyBinding.Map),
new KeyBinding(new KeyCombination(InputKey.Control, InputKey.KeycodeM), TestKeyBinding.Menu),
};

private enum TestKeyBinding
{
A,
KeycodeA,
KeycodeB,
CtrlA,
CtrlKeycodeA,

Up,
Left,
Down,
Right,
Map,
Menu,
}
}
}
4 changes: 3 additions & 1 deletion osu.Framework.Tests/Input/KeyCombinationTest.cs
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
// 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 NUnit.Framework;
using osu.Framework.Input.Bindings;
using osuTK.Input;

namespace osu.Framework.Tests.Input
{
Expand Down Expand Up @@ -46,7 +48,7 @@ public class KeyCombinationTest

[TestCaseSource(nameof(key_combination_display_test_cases))]
public void TestLeftRightModifierHandling(KeyCombination candidate, KeyCombination pressed, KeyCombinationMatchingMode matchingMode, bool shouldContain)
=> Assert.AreEqual(shouldContain, KeyCombination.ContainsAll(candidate.Keys, pressed.Keys, matchingMode));
=> Assert.AreEqual(shouldContain, KeyCombination.ContainsAll(candidate.Keys, pressed.Keys, matchingMode, new Dictionary<Key, char?>()));

[Test]
public void TestCreationNoDuplicates()
Expand Down
16 changes: 16 additions & 0 deletions osu.Framework.Tests/Input/KeyboardInputTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Input;
using osu.Framework.Input.Events;
using osu.Framework.Testing;
using osuTK;
Expand Down Expand Up @@ -168,6 +169,21 @@ public void TestReValidatedDrawableReceivesRepeat()
AddUntilStep("wait for repeat on receptor 0", () => receptors[0].RepeatReceived);
}

[Test]
public void TestKeyCharacterChangingDuringPress()
{
AddStep("press key as 'a'", () => InputManager.PressKey(new KeyboardKey(Key.A, 'a')));

AddAssert("key pressed", () => InputManager.CurrentState.Keyboard.IsPressed(Key.A));
AddAssert("char pressed", () => InputManager.CurrentState.Keyboard.IsPressed('a'));

AddStep("release same key as 'b'", () => InputManager.ReleaseKey(new KeyboardKey(Key.A, 'b')));

AddAssert("key released", () => InputManager.CurrentState.Keyboard.IsPressed(Key.A), () => Is.False);
AddAssert("char 'a' not pressed", () => InputManager.CurrentState.Keyboard.IsPressed('a'), () => Is.False);
AddAssert("char 'b' not pressed", () => InputManager.CurrentState.Keyboard.IsPressed('b'), () => Is.False);
}

private partial class InputReceptor : Box
{
public bool DownReceived { get; set; }
Expand Down
Loading
Loading