-
Notifications
You must be signed in to change notification settings - Fork 423
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
Refactor KeyCombination.ContainsKey()
and .ContainsKeyPermissive()
for better extensibility
#6229
Conversation
These are invalid because they have the virtual `InputKey.Shift` as a pressed key, which never happens in real usage. This is because `KeyCombination.FromKey` will never return a virtual key like `Shift` or `Control`.
… concept of virtual keys Where and how exactly `getVirtualKey()` works is subject to change. This is just a MVP.
This is so difficult to make sense of that I am trying to approach this PR a second time and still not understanding any of it.
|
These may have been useful at some point, but not anymore.
Could be better but it's a start.
This is needed to unblock #5790. That PR currently takes a naive approach to adding virtual keys: it just adds them in |
{ | ||
Debug.Assert(pressedKeys.All(k => k.IsPhysical())); | ||
ImmutableArray<(InputKey Physical, InputKey? Virtual)> pressed = pressedKeys.Select(k => (k, getVirtualKey(k))).ToImmutableArray(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is really bad for perf. Should be bubbled up to KeyBindingContainer.pressedInputKeys
(currently a HashSet<InputKey>
, but should be changed to a Dictionary<InputKey, InputKey?>
that maps [physical key -> virtual key]).
Leaving for later, when the code makes it past the first round of review.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't get it, if that's a concern, why isn't there a static dictionary in this class then? Why involve KeyBindingContainer
at all?
Idk, it's really hard to explain and reason as the current code is broken in subtle ways and does things that don't make sense. But together it works out and the errors are not visible to the outside world. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
seems better than before but i still think it could be better yet
Per bdach's suggestion.
Debug.Assert(pressedPhysicalKeys.All(k => k.IsPhysical())); | ||
ImmutableArray<(InputKey Physical, InputKey? Virtual)> pressed = pressedPhysicalKeys.Select(k => (k, getVirtualKey(k))).ToImmutableArray(); | ||
|
||
Debug.Assert(pressed.All(k => k.Virtual == null || k.Virtual.Value.IsVirtual())); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I suppose it's not a huge deal, but is there a reason this was structured to map physical/virtual keys in a separate array here rather than using getVirtualKey()
at the points where it's required?
There are two minor concerns:
- Obviously this will allocate an array every time. It's a small and infrequent allocation though, so I'm willing to let this pass.
- The API of
KeyBindingContains
andIsPressed
become a little bit weird now that the consumer is able to specify a separate physical and virtual key with what appears to be no validation. There's nothing stopping users from providing(Physical: A, Virtual: Shift)
as an argument, for example, which would be an undefined operation.
Maybe I'm misunderstanding something, but it sounds like given your commit message about the invalid test cases that the user should never be dealing with virtual keys outside ofKeyCombination
. The APIs I'd expect are:KeyBindingContains(ImmutableArray<InputKey> candidateKeyBinding, InputKey physicalKey)
IsPressed(InputKey virtualOrPhysicalKey, InputKey candidateKey)
Again please let me know if I've missed something.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Here's my attempt at refactoring this to better explain what I mean: https://gist.github.com/smoogipoo/af35edc6492de8d5ea47ca4d64b7fc42
I'm wondering if there's some part of the extensibility argument that I'm missing with this.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
2. The API of
KeyBindingContains
andIsPressed
become a little bit weird now that the consumer is able to specify a separate physical and virtual key with what appears to be no validation. There's nothing stopping users from providing(Physical: A, Virtual: Shift)
as an argument, for example, which would be an undefined operation.
The methods you're talking about are internal
(and can even be made private
) so that's not part of the public API.
Here's my attempt at refactoring this to better explain what I mean: https://gist.github.com/smoogipoo/af35edc6492de8d5ea47ca4d64b7fc42
Yes, your refactor is perfectly fine if the virtual keys can be statically determined. But in #5790 that will not the the case. Eg. (Physical: Y, Virtual: KeycodeY)
on a QWERTY, and (Physical: Y, Virtual: KeycodeZ)
on a QWERTZ keyboard. The virtual key can only be inferred from InputState.Keyboard
.
So the public API of KeyCombination
should ultimately be changed from
public bool IsPressed(KeyCombination pressedKeys, KeyCombinationMatchingMode matchingMode)
to one of these options:
// Pass in InputState, so the (Physical, Virtual) key array is created each time
// [or getVirtualKey(InputKey physical, InputState state) is called when needed].
// Hard to test directly. But we can always test an internal method like how
// `KeyCombinationTest` tests `KeyCombination.ContainsAll`.
public bool IsPressed(KeyCombination pressedKeys, InputState inputState, KeyCombinationMatchingMode matchingMode)
// KeyBindingContainer builds a dictionary of (Physical, Virtual)
// and reuses the same instance for each checked KeyCombination.
// This ensures that there are no duplicate physical keys.
// But the meaning of <InputKey, InputKey?> is not apparent.
public bool IsPressed(ImmutableDictionary<InputKey, InputKey?> pressedPhysicalAndVirtualMapping, KeyCombinationMatchingMode matchingMode)
// KeyBindingContainer builds an array of (Physical, Virtual)
// and reuses the same instance for each checked KeyCombination.
// Undefined if someone passes in duplicate physical keys.
public bool IsPressed(ImmutableArray<(InputKey Physical, InputKey? Virtual)> pressedKeys, KeyCombinationMatchingMode matchingMode)
I think the first one makes the most sense, as we can have the getVirtualKey()
logic contained inside KeyCombination
. Your refactor works well with that.
Do we want to make the breaking change to KeyCombination.IsPressed()
now or in #5790?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The methods you're talking about are
internal
Oh, I didn't notice that.
I think the first one makes the most sense, as we can have the
getVirtualKey()
logic contained insideKeyCombination
. Your refactor works well with that.
That one makes the most sense to me too for a public API. Maybe the breaking change should be in this PR because I don't know how it's going to be used - I'm only reviewing purely from a code-quality and isolation perspective because you definitely have a better vision for how this all works.
Unless the breaking change + implementation is significant :)
Co-authored-by: Dan Balasescu <smoogipoo@smgi.me>
To be used in the future.
return ContainsAll(Keys, pressedKeys.Keys, inputState, matchingMode); | ||
} | ||
|
||
private static InputKey? getVirtualKey(InputKey key, InputState inputState) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why does this receive the full InputState
? Is the goal here to eventually read the Characters
thing from #5790, then I'd rather just pass that in specifically when it exists.
The reason why I have an issue with this is that the InputState
is propagated through methods multiple levels and when I look at the method signature of ContainsAll()
I get very uneasy because there's three key collections there now - there's candidateKeyBinding
and pressedPhysicalKeys
which you are supposed to be checking, but then there is also InputState
which really just allows you to read everything from everywhere and that feels like terrible layering. KeyBindingContains()
is similar but not as egregious imo.
I'm fine with the IsPressed()
signature fwiw, it's everything below that that I have an issue with.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah that's what I was also hoping to figure out - how the InputState
is actually going to be used here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The InputState
will be used to read the Characters
of the passed in InputKey
. And if the character is [a-z]
, it'll return the corresponding InputKey.Keycode{A-Z}
.
I'd rather just pass that in specifically when it exists.
Alright, I'll do that.
It's still there in `public` `IsPressed` for the breaking change.
461acb1
to
dbfed58
Compare
Sure, the tests may have been "invalid" due to specifying a pressed virtual key, but the actual coverage they were trying to exercise is useful, and can be recovered just by adjusting the test to actually press both physical modifiers rather than just straight-up removing.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
lgtm
Found this while checking something else. After ppy#6229, `KeyCombination.IsPressed()` can trip on an assertion: System.AggregateException : One or more errors occurred. (: ) ----> NUnit.Framework.AssertionException : : at osu.Framework.Testing.TestScene.checkForErrors() in /home/dachb/Documents/opensource/osu-framework/osu.Framework/Testing/TestScene.cs:line 502 at osu.Framework.Testing.TestScene.UseTestSceneRunnerAttribute.AfterTest(ITest test) in /home/dachb/Documents/opensource/osu-framework/osu.Framework/Testing/TestScene.cs:line 563 at NUnit.Framework.Internal.Commands.TestActionCommand.<>c__DisplayClass0_0.<.ctor>b__1(TestExecutionContext context) at NUnit.Framework.Internal.Commands.BeforeAndAfterTestCommand.<>c__DisplayClass1_0.<Execute>b__1() at NUnit.Framework.Internal.Commands.DelegatingTestCommand.RunTestMethodInThreadAbortSafeZone(TestExecutionContext context, Action action) --AssertionException at osu.Framework.Logging.ThrowingTraceListener.Fail(String message1, String message2) in /home/dachb/Documents/opensource/osu-framework/osu.Framework/Logging/ThrowingTraceListener.cs:line 27 at System.Diagnostics.TraceInternal.Fail(String message, String detailMessage) at System.Diagnostics.Debug.Fail(String message, String detailMessage) at osu.Framework.Input.Bindings.KeyCombination.IsPressed(ImmutableArray`1 pressedPhysicalKeys, InputKey candidateKey) in /home/dachb/Documents/opensource/osu-framework/osu.Framework/Input/Bindings/KeyCombination.cs:line 206 at osu.Framework.Input.Bindings.KeyCombination.IsPressed(KeyCombination pressedKeys, InputState inputState, KeyCombinationMatchingMode matchingMode) in /home/dachb/Documents/opensource/osu-framework/osu.Framework/Input/Bindings/KeyCombination.cs:line 105 at osu.Framework.Input.Bindings.KeyBindingContainer`1.handleNewPressed(InputState state, InputKey newKey, Nullable`1 scrollDelta, Boolean isPrecise) in /home/dachb/Documents/opensource/osu-framework/osu.Framework/Input/Bindings/KeyBindingContainer.cs:line 227 at osu.Framework.Input.Bindings.KeyBindingContainer`1.Handle(UIEvent e) in /home/dachb/Documents/opensource/osu-framework/osu.Framework/Input/Bindings/KeyBindingContainer.cs:line 125 at osu.Framework.Graphics.Drawable.OnMouseDown(MouseDownEvent e) in /home/dachb/Documents/opensource/osu-framework/osu.Framework/Graphics/Drawable.cs:line 2157 at osu.Framework.Graphics.Drawable.TriggerEvent(UIEvent e) in /home/dachb/Documents/opensource/osu-framework/osu.Framework/Graphics/Drawable.cs:line 2033 This is happening game-side when a keybinding is vacated, at which point it is represented by `KeyCombination.none` or equivalent, i.e. a `KeyCombination` with its only key being `InputKey.None`. `InputKey.None` is defined to be neither physical or virtual, which trips the aforementioned assertion. To bypass that entire debacle altogether, add an early return path that just returns false if the key combination being tested for being pressed is equivalent to `none`. (It cannot be a reference equality check, I checked against game. Sequence equality is required. Something in game is probably instantiating anew. Probably something to do with how keybindings are materialised from realm.)
Terminology
osuTK.Input.Key
,MouseButton
,JoystickButton
and their direct mappings toInputKeys
(Input)Key.LControl
and(Input)Key.RControl
map to the virtual keyInputKey.Control
SDL_Keycode
(the keyboard layout-dependant unicode character) that is used in Fix keyboard shortcuts not working as expected on non-QWERTY keyboard #5790Problems
KeyCombination.ContainsKey()
is used for two different purposesThese two purposes have very different semantics:
KeyCombinationMatchingMode.{Exact,Modifiers}
, it's used to check that there are no excessInputKey
s from pressedKeys present in the combination.This is evident from the xmldoc which explains 1. in
<summary>
and 2. in<param name=.../>
osu-framework/osu.Framework/Input/Bindings/KeyCombination.cs
Lines 198 to 205 in 51d8215
This is fixed in e05083a and invalid tests are removed in 3e021dd (check commit message as to why).
Adding additional virtual keys is complicated
Since definitions of virtual keys are spread across
ContainsKeyPermissive()
andContainsKey()
, it's hard to add new ones, and impossible to do it programmatically.This is fixed by 0ab0adc without changing semantics. The added
getVirtualKey()
static method is subject to change and will need to have theInputState
propagated to it to make it work for #5790.I'll be doing some follow-up cleanup after this PR is merged.
Breaking changes
KeyCombination.IsPressed
now takes in anInputState
parameter