-
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
Fix keyboard shortcuts not working as expected on non-QWERTY keyboard #5790
base: master
Are you sure you want to change the base?
Conversation
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.
That's to be expected, as both I've updated the tests to show them separately now, hopefully this clears the confusion. |
/// <summary> | ||
/// Key that produces the 'a' character when pressed. Depends on the current keyboard layout. | ||
/// </summary> | ||
/// <remarks>Different from <see cref="A"/>.</remarks> | ||
KeycodeA = 10752 + 1, |
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 feel like this is missing two details in this xmldoc:
- first, that there might be no physical key that produces this
InputKey
on the current keyboard layout, - what
InputKey.A
really means now (because this says that they're "different" now, but doesn't say how) - the best approximation of which would be "the key on the user's keyboard that is closest to the position of the A key on a QWERTY US keyboard", I guess?
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.
first, that there might be no physical key that produces this
InputKey
on the current keyboard layout
I thought that all keyboard layouts would have ascii alpha keys, and any non-Latin languages (eg. Japanese) would have a standard US keyboard and have an IME do all the work. Turns out some keyboard layouts don't have all the ascii, but windows lies to SDL about what the characters are, so it works out fine. (Typing in a textbox is not affected by this lie.)
Take Azerbaijani (Standard)
, which has some A-Z characters. Windows lies to SDL (in hopes of compatibility I guess) and overlays the A-Z of the US layout over the actual keyboard. This works in our favour and ensures all InputKey.Keycode*
keys are accessible.
(Notice how the Z
keycode is pressed twice, since the US A-Z doesn't overlap the original Z.)
what
InputKey.A
really means now (because this says that they're "different" now, but doesn't say how) - the best approximation of which would be "the key on the user's keyboard that is closest to the position of the A key on a QWERTY US keyboard", I guess?
Maybe better to have "Not to be confused with A"?
I'll expand the xmldoc of InputKey.[A-Z]
to mention something like
/// <summary>
/// The physical key corresponding to the A key on a US QWERTY keyboard.
/// </summary>
/// <remarks>Not to be confused with <see cref="KeycodeA"/></remarks>
A = 83,
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'll expand the xmldoc of InputKey.[A-Z] to mention something like
Doesn't look to have been applied yet.
Also I'm not sure what to think about the naming here. Keycode
is ambiguous to me. I'd probably ideally see KeyA
for the "scancode" key and CharacterA
for the "keycode" key, but that's very breaking.....
/// Mapping of <see cref="Key"/>s to <see cref="KeyboardKey.Character"/>s printed on them. | ||
/// </summary> | ||
/// <seealso cref="KeyboardKey"/> | ||
internal readonly Dictionary<Key, char> Characters = new Dictionary<Key, char>(); |
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.
What is responsible for cleaning up potentially-stale key-to-character mappings after a key is released? Does anything even do that?
Or, to reword the question: what's the guarantee that this mapping is fully correct at any given moment? Because that's what I would expect from KeyboardState
.
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 mapping may contain stale values and those are not cleaned up, but it's guaranteed to have valid values for currently pressed keys. This mapping is deliberately made internal
as it's an implementation detail.
The public methods/properties of KeyboardState
guarantee correctness as they access the mapping via PressedKeys
.
Usage of the mapping outside KeyboardState
requires more scrutiny. I'll make some tests for that, especially for PassThroughInputManager
.
case SDL.SDL_Keycode.SDLK_RETURN: // SDLK_RETURN is '\r' | ||
return '\n'; |
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'm not even sure how to react to this. So is it \r
or \n
? Does anything even produce \n
if not SDLK_RETURN
? Does this mean there's a possibility that two \n
s can be emitted? If yes then in what circumstances?
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.
SDLK_RETURN
is defined in SDL as \r
. It makes more sense to have it map to \n
as that is the ubiquitous newline character (eg. \n
makes line breaks in TextFlowContainer
).
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.
It makes more sense to have it map to
\n
I'd say whether it "makes more sense" depends on the answers to my other questions, namely
Does anything even produce
\n
if notSDLK_RETURN
? Does this mean there's a possibility that two\n
s can be emitted? If yes then in what circumstances?
which I don't see the answer to 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.
It's possible for two physical keys to produce the same keycode. Eg. regular enter and numpad enter.
I've added a .Distinct()
to cover that in KeyboardState
, but it's not a silver bullet.
public IEnumerable<char> PressedCharacters => PressedKeys.Select(key => Characters[key]).Where(c => c != null).Select(c => c!.Value).Distinct(); |
Two keys having the same keycode means that KeyBindingContainer.pressedInputKeys
can get desynchronised from KeyboardState.PressedCharacters
, but that whole logic is pending changes. I'll add a failing test for that.
|
||
case SDL.SDL_Keycode.SDLK_KP_PERIOD: | ||
// potentially return ',' as decimal separator based on keyboard layout. | ||
return numLockOn ? '.' : '\x7f'; // char for delete |
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 would this method produce a control character at all anywhere? By that logic, where's U+0008
(backspace)? U+001B
(escape)?
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 would this method produce a control character at all anywhere?
SDL already produces control characters, so I made this to match. I don't see the problem with providing control characters.
By that logic, where's
U+0008
(backspace)?U+001B
(escape)?
This is already handled by SDL. You can check by pressing those keys in TestSceneInputManager
.
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.
Can we just not handle control characters at all?
// in exact matching mode, every pressed key except the "virtual" keycode keys needs to be in the candidate. | ||
if (!IsKeycode(key) && !ContainsKeyPermissive(candidateKey, key)) |
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.
An explanation of the implications of this PR on the exact matching mode is required somewhere because there is none in the OP and I'm not sure what they are myself at this point.
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.
Thinking more about the relationship between physical keys and "virtual keys" (char?
/Keycode) in our case. It seems that the relationship between a physical key and its keycode is similar to eg. InputKey.{L,R}Control
and InputKey.Control
. Same as joystick buttons - there's Joystick1
, Joystick2
and also the virtual ABXY
face buttons (joystick buttons are out of scope).
If the key combination is InputKey.Control, InputKey.Number9
and the user presses InputKey.LControl, InputKey.RControl, InputKey.Number9
- that is an exact match.
Maybe it's wrong to have the virtual keys present in the "keys pressed by user" array, and it's the job of ContainsKey
and ContainsKeyPermissive
to unmap any InputKey.Keycode*
found in the candidate combination.
I'll have to think more about 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.
What's the status on this?
/// </summary> | ||
/// <param name="character"></param> | ||
/// <returns><c>string</c> with form of <c>'\0'</c>, <c>'a'</c>, <c>'\x7f'</c>, etc.</returns> | ||
public static string StringRepresentation(this char character) |
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.
What even is this method? Why does it exist?
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.
Mostly for debugging, to have control characters be readable in text (eg. input event logging, TestSceneInputManager
). The o!f font rendered would just displays them as ?
otherwise.
Possible failure cases: - `KeyCombination` contains a physical key and its mapping, eg. LControl-Control-Z - keyboard handler reports a dubious combination, like (LControl, KeyCodeA) - only relevant after ppy#5790 makes it in This is bad because a single key should only match to one `InputKey` in the `KeyCombination`.
Doesn't compile
This broke the Ctrl, Shift and Alt visualiser, if releasing one physical key when the opposite hand-side was pressed.
/// Gets the <see cref="Key"/> equavalent to this <see cref="InputKey"/>. | ||
/// </summary> | ||
/// <remarks>Inverse of <see cref="KeyCombination.FromKey"/>.</remarks> | ||
public static bool IsKeyboardKey(this InputKey inputKey, out Key key) |
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 method is all sorts of confusing now.
- called
IsKeyboardKey
(water wet anyone?) - has a weird
out
parameter that nobody really knows what it's doing
Does this method even do anything at this point? Its only caller is KeyCombination.GetVirtualKey()
, which - as far as I can tell -
- ensures that the incoming key is physical, so the first conditional is dead code
- handles modifiers before the switch in this method, so the entire switch is dead code
- depending on the range check this either returns the key but pushed through cast, or
Key.Unknown
Can this just be inlined at its only usage?
@@ -135,9 +141,11 @@ public bool IsPressed(KeyCombination pressedKeys, InputState inputState, KeyComb | |||
/// <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> | |||
/// <param name="keyboardCharacters"><see cref="States.KeyboardState.Characters"/> of the current <see cref="InputState"/>.</param> |
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.
Param needs to be named better. Something like keyToChararacterMapping
or whatever.
@@ -9,6 +11,26 @@ public class KeyboardState | |||
{ | |||
public readonly ButtonStates<Key> Keys = new ButtonStates<Key>(); | |||
|
|||
/// <summary> | |||
/// Mapping of <see cref="Key"/>s to <see cref="KeyboardKey.Character"/>s printed on them. |
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.
/// Mapping of <see cref="Key"/>s to <see cref="KeyboardKey.Character"/>s printed on them. | |
/// Mapping of <see cref="Key"/>s to <see cref="KeyboardKey.Character"/>s which are produced when the key is pressed. |
public bool IsPressed(Key key) => Keys.IsPressed(key); | ||
|
||
/// <summary> | ||
/// Whether a specific <see cref="KeyboardKey.Character"/> is pressed. |
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.
/// Whether a specific <see cref="KeyboardKey.Character"/> is pressed. | |
/// Whether a key that produces the specific <paramref name="character"/> is pressed. |
internal readonly Dictionary<Key, char?> Characters = new Dictionary<Key, char?>(); | ||
|
||
/// <summary> | ||
/// Whether a specific <see cref="Key"/> is pressed. |
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.
/// Whether a specific <see cref="Key"/> is pressed. | |
/// Whether a specific <paramref name="key"/> is pressed. |
|
||
case SDL.SDL_Keycode.SDLK_KP_PERIOD: | ||
// potentially return ',' as decimal separator based on keyboard layout. | ||
return numLockOn ? '.' : '\x7f'; // char for delete |
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.
Can we just not handle control characters at all?
Re-reading this diff yet again I am yet again struck by the amount of complexity this introduces to everything. I am wondering whether we can change direction here and sacrifice backwards compatibility for a simpler diff. I'd begin by not bothering to add all of that @Susko3 did you explore the possibility of doing something like the above? @smoogipoo would you be fine with such a hard break? |
This was the previous way of doing things, but it lead to problems, see the fix: #3950. We could explore that option, but there are multiple issues with it.
The implementation is complex, but I really tried to make the API understandable and simple to use. Please tell what failures do you see in those. |
My primary issue at this stage wrt API is the There's also the part where attempting to handle both keycode and scancode is an inherent increase in complexity. You might say it's unavoidable but it makes it a tough sell in general. I dunno, I really need more of @ppy/team-client to sound off here. I just want to avoid another pooling situation where we go in a direction that seems logical one step at a time and then wake up a year later having no earthly idea how anything in pooling works anymore. |
the French guys were on my ass about this issue the entire COE so i'd like to see movement on this. yes there are merge conflicts but i wanna rack people's brains about the general direction first. bump @ppy/team-client |
This PR does two things combined into one PR (to help see the full picture, as it's hopefully not too big):
SDL_Keycode
s trough the input hierarchy (implementation idea taken from MidiEvent/StateVelocity
)struct KeyboardKey
, andchar Character
properties on events / where neededmaster
)KeyCombination
/InputKey
InputKey
s: fromKeycodeA
troughKeycodeZ
While fixing the above issues, this PR also adds new things, mainly in [1.]:
The new
KeyboardEvent.Character
property would be useful for access keys (old docs, ref for underlining) that are seen in newer osu! figma designs (...
underline on Mods, Random, Options, etc).For access keys, I'd like to have some level of localisation support (and ways to check that the localisations don't have conflicts).
Main considerations when making this PR
Don't break existing behaviour
Consumers using
KeyboardEvent
will not notice any changes, as the newCharacter
property is added in a way that only augments the existing information and no behaviour is changed.Existing
KeyBinding
s are not affected, even in cases where the new keycode feature doesn't work.Simple to use for consumers
KeyboardEvent
s have minimal API where it makes sense for the newly addedchar Character
property.Changing existing
KeyBinding
s to keycode ones is as simple as:Only cover the most important use cases and don't have API footguns
Support has been added where it makes sense and where it would be used.
That's why the new keycodes don't work with(This now works properly as a side effect of an unrelated fix.)KeyCombinationMatchingMode.Exact
. While it would be possible to support this, there would need to be a way to correlate anInputKey.Keycode{}
with anInputKey.{}
(like theCharacter
and theKey
in aKeyboardKey
). This is deemed unnecessary to implement as it's complex andKeyCombinationMatchingMode.Exact
isn't used.Even though
KeyboardKey.Character
has support for all Unicode BMP characters,InputKey
only has support forKeycode[A-Z]
. This is because only those keys can reliably be used on all (or most, I cannot check them all) keyboard layouts. Other characters, like punctuation, may not be available. Number keycode are not available as they would be functionally identical toInputKey.Number#
+InputKey.Keypad#
.It would be nice to have support for plus and minus keys, but SDL doesn't work like that. Windows does have virtual keys for those (
VK_OEM_PLUS
,VK_OEM_MINUS
), but SDL does not map them in this way (VK_OEM_PLUS
would beSDL_SCANCODE_EQUALS
,SDLK_EQUALS
on en_us keyboard).Stuff to test and check