-
Notifications
You must be signed in to change notification settings - Fork 690
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
Casting inconsistent between Key
and KeyCode
#3071
Comments
Another strange behavior is the fact I can't debug this unit test in VS2022 with the [[Theory]
[InlineData ("A-Ctrl", KeyCode.A | KeyCode.CtrlMask)]
[InlineData ("Alt-A-Ctrl", KeyCode.A | KeyCode.CtrlMask | KeyCode.AltMask)]
public void TryParse_ShouldReturnTrue_WhenValidKey (string keyString, Key expected)
{
Key key;
Assert.True (Key.TryParse (keyString, out key));
Assert.Equal (((Key)expected).ToString (), key.ToString ());
} |
The reason why this is happening is because the [Theory]
[ClassData (typeof (MyTestClass))]
public void MyTestClassMethod (...parameters) {
...
} |
But in my opinion we don't need to pass the parameter as [InlineData ("A-Ctrl", KeyCode.A | KeyCode.CtrlMask)]
[InlineData ("Alt-A-Ctrl", KeyCode.A | KeyCode.CtrlMask | KeyCode.AltMask)]
public void TryParse_ShouldReturnTrue_WhenValidKey (string keyString, KeyCode expected)
{
Assert.True (Key.TryParse (keyString, out Key key));
Key expectedKey = (Key)expected;
Assert.Equal (expectedKey.ToString (), key.ToString ());
} |
[InlineData (KeyCode.CharMask, '\0')]
[InlineData (KeyCode.SpecialMask, '\0')]
public void AsRune_ShouldReturnCorrectIntValue (KeyCode key, Rune expected)
{
var eventArgs = new Key (key);
Assert.Equal (expected, eventArgs.AsRune);
}
[InlineData (KeyCode.CharMask, '\0')]
[InlineData (KeyCode.SpecialMask, '\0')]
public void AsRune_ShouldReturnCorrectIntValue (KeyCode key, uint expected)
{
var eventArgs = new Key (key);
Rune expectedRune = (Rune)expected;
Assert.Equal (expectedRune, eventArgs.AsRune);
} Or [InlineData (KeyCode.CharMask, '\0')]
[InlineData (KeyCode.SpecialMask, '\0')]
public void AsRune_ShouldReturnCorrectIntValue (KeyCode key, uint expected)
{
var eventArgs = new Key (key);
Assert.Equal ((Rune)expected, eventArgs.AsRune);
} |
Not 100% sure the char cast operator is correct. We should dig in. However the issue BDisp raises I did explore. In my 3rd attempt at this I decided not having to update as many unit tests as possible was important. So I tried to craft the operators (and constructors) to balance that. I think having the unit tests depend on "ToString" and "TryParse" is an ideal long term solution as it makes the tests the most readable. Or, put another way: for test inlinedata params, use KeyCode or string, never Key. |
No it isn't because isn't considering code points grater than 0xFFFF which is the max value of the char type. I think we should consider
I already changed many tests with the
You're absolute right.
You're also absolute right. 😃 |
Solution may be as simple as having both |
Not really. Well this is really ambiguous, because the
As it's returning the
Do you both agree? |
That makes sense to me. |
Maybe we should avoid to cast to |
Even better is using |
@tig what tis do? I also see this on other places. You are excluding the else if (wch <= 'z') {
k = (KeyCode)wch & ~KeyCode.Space;
} |
Without context it's hard to say. It may be a leftover I missed. |
This is the way. At least char, for sure. TerminalGuiDesigner has 53 tests broken by the changes to Key no longer consistently working with casts from char, due to case changes. Test code, for reference, since Terminal.Gui likely needs similar test cases, since this is largely a test of Terminal.Gui behavior:
The last line of the test will fail on any character that would have required a |
@dodexahedron can you convert it to |
It's TGD specific because it is testing the KeyMap and KeybooardManager, and involves other TGD-specific classes. It's just for reference of how it breaks. The last line of the test method and the line two above that are the most interesting for you. After having handled a key event, cast from a char, (third line from bottom), it then checks the last character in the view (last line). For anything requiring a shift (however that is defined) it will be broken. |
Basically, you probably want the helper method below it and then a proper test in Terminal.Gui would need to ensure the round trip from char to Key to char works without change, tested in multiple cultures and ui cultures. In NUnit, the way that is set up results in a bunch of individual test cases getting created: one per character output by the helper method shown there, all repeated once per type being tested (it currently tests TextBox and Label, but that's not relevant in the context of this issue). |
Take a look on the https://github.com/gui-cs/Terminal.Gui/pull/3078/files#diff-b06483ffe0f3297a11416013982e6ceb777c6a35cbcfec41afe180eaa5b15132 where I have some unit tests that probe it's working better. Thanks. |
I would strongly suggest using whatever equivalent xUnit has to the generation of all of the possible test cases, or else just testing them all inside a single test method, if that's not possible. The possibilities for problematic input handling are vast, when considering different keyboard layouts and cultures, which make the concept of a letter differing solely by the shift key non-portable. For the test in the context of Terminal.Gui to be valid, it needs to at least handle all printable 8-bit characters and whitespace, along with control characters and function keys. That single test method, for example, covers 124 cases in TGD, and we know exactly which ones fail because they are treated as individual cases. If I were to add additional cultures, it would multiply that 124 by the number of cultures tested. I am not familiar with xUnit, so I don't know what it has for that kind of thing, but input handling is so crucial that it needs to be provable across all elements of a reasonable range of inputs. Note the helper method I showed only provides what the current culture's concept of alphanumeric characters are. If you ran that in a spanish locale, I imagine it would generate more cases than it does on mine, which are US english. |
It covers more than 124 cases and I think it would work with some cultures. |
I think you might misunderstand my meaning, there. I mean to say they are not exhaustive tests. Each one spot-checks a few inputs, and the inputs checked are not consistent. The sample I showed covers all 62 alphanumeric characters in US english, across two possible View types that have a Text property. (thus 124). JUST for the case of round-tripping the char value from char to Key to char. 52 of those fail (26 for each View, with those 26 being every capital letter). It is therefore a proof that all of those characters round-trip as expected, which they used to, but now do not. A specific set of behaviors should be defined, and then a test theory should test that behavior and ensure that it is consistent across the entire input domain or at least a reasonable subset of it (such as 8-bit characters, as mentioned before). Non-exhaustive tests result in a lot of things slipping through the cracks (for example, I've discovered plenty of subtle things to fix in TGD as I've been formalizing the tests). |
I pointed out one example of a problematic test in #3078 , to provide an example and context for this discussion. |
This comment was marked as off-topic.
This comment was marked as off-topic.
This comment was marked as off-topic.
This comment was marked as off-topic.
Why are we using our own types for this?
ConsoleKeyInfo is cumbersome, it lacks many keys and has ambiguous entries
e.g. the Oem keys.
There are environmental issues too, for example some deployments will see
only 'virtual key' and the unicode char as input.
Terminal.Gui does a lot of standardisation at the console driver level.
I don't think changing to core types would improve the library user
experience. Current issue just seems to be tied up with the use of
shift/capital letters.
…On Sun, 24 Dec 2023, 03:22 dodexahedron, ***@***.***> wrote:
Also, it is important to note that caps lock also makes the "WithShift"
behavior potentially odd.
ConsoleKeyInfo already handles that.
Here's the output from the same code as above but with caps lock on:
Key: A | Modifiers: None | Unicode Value: 65(0x41) | Resulting Character: A
Key: A | Modifiers: Shift | Unicode Value: 97(0x61) | Resulting Character: a
Key: A | Modifiers: Alt | Unicode Value: 65(0x41) | Resulting Character: A
Key: A | Modifiers: Alt, Control | Unicode Value: 0(0x0) | Resulting Character:
Key: A | Modifiers: Alt, Shift, Control | Unicode Value: 0(0x0) | Resulting Character:
Key: A | Modifiers: Alt, Shift | Unicode Value: 97(0x61) | Resulting Character: a
—
Reply to this email directly, view it on GitHub
<#3071 (comment)>,
or unsubscribe
<https://github.com/notifications/unsubscribe-auth/AHO3C5HSCM42GH4CP3INU5LYK6NYVAVCNFSM6AAAAABBARZAJ6VHI2DSMVQWIX3LMV43OSLTON2WKQ3PNVWWK3TUHMYTQNRYGQZDEOJRHE>
.
You are receiving this because you authored the thread.Message ID:
***@***.***>
|
Here the same. It's limiting the caps lock to only characters lower or equal to if (((keyInfo.Modifiers == ConsoleModifiers.Shift) ^ (keyInfoEx.CapsLock))) {
if (keyInfo.KeyChar <= (uint)KeyCode.Z) {
return (KeyCode)((uint)KeyCode.A + delta) | KeyCode.ShiftMask;
}
} |
This is not correct. On a US keyboard CapsLock does NOT apply to D1-D9, This may be different on different keyboards. |
You are right, my bad. But it's only handling characters from A to Z and there is no need to add the conditional |
Yeah, i think so. |
This will get the same KeyChar e.g. Ç (199) will return Ç (199) or if it's Z (90) will return Z (90) anyway. I think you did want if (keyInfo.Modifiers == 0 && ((KeyCode)((uint)keyInfo.KeyChar) & KeyCode.Space) == 0) {
return (KeyCode)((uint)keyInfo.KeyChar) & ~KeyCode.Space;
} |
It occurred (and still does) to me that for Key, the KeyCode property could be immutable. I can't think of any case where after a Key object is created that KeyCode needs to be changed. Thus, in addition to changing the helper "constants" (Key.X) to be get-only, we can make KeyCode "get; init". This further reduces the chance of a static issue and makes the intent even more clear. |
Well we really don't want and we aren't re-event the base class library. What we are doing is using only only Key to manage all the keys combinations at once. With |
A note on cast. I noticed in another pr from BDisp, he changed the api doc comment to say "the cast is never lossy". This is not right. Casting a Key to anything other than a Key is definitely lossy. For example whatever Key.Handled is set to is lost in the cast. This is why those operators are "explicit" and not "implicit". |
I only was referring about cast from |
Sometimes in TGD I change the key e.g. when Enter is pressed in a menu I add another menu item and change the KeyCode to Down so that when it finishes resolving, the new menu item is highlighted. But I'll grant you its a pretty obscure use case and I currently make do with reflection so would be fine with immutableness. You can even call private void ChangeKeyTo(Key keystroke, Key newKey)
{
Type t = typeof(Key);
var p = t.GetProperty("KeyCode") ?? throw new Exception("Property somehow doesn't exist");
p.SetValue(keystroke, newKey.KeyCode);
} |
If caps lock is on and shift is also pressed, a yields a. |
Coolio. Then I think a good starting point to resolving this type of issue is to write a test that round-trips all possible raw keyboard inputs (at least that are representable by Key) to Key and back and ensures value equality of all fields/properties of the round-tripped input. Might smoke out other subtle things as well. |
You are absolute right and all drivers are doing the right behavior if you try it. |
This comment was marked as off-topic.
This comment was marked as off-topic.
Only the |
This comment was marked as off-topic.
This comment was marked as off-topic.
If lower case letters existed when computer keyboard standards were established you'd be right about correctness. But they didn't. ASCII 65 == VK_A which is upper case 'A'. On non-us keyboards the keys between VK_A and VK_Z are not always A..Z. But they always result in a lower case char unless shift is pressed or caps lock is on. We could choose to make KeyCode.A be KeyCode.a, but we already went down that path and it led to having BOTH KeyCode.A and KeyCode.a which just moved the complexity and masked it with more. So... terminal.gui's low level encoding of keys (KeyCode) special -cases the A..Z keys as is well documented in the Key docs in current v2_develop branch (I just noticed the KeyCode docs are incorrect as they say A..Z mean shifted). The high-level abstraction of Key carries this forward with the Key.A..Z helper properties. Users are free to use neither of these and just do new Key('a') or new Key('A') if they want. |
There are limitations placed by console protocol. We don't have hardware
access, only what the console terminal emulator sends.
So for example there is no key down, key up sent by Linux terminals. If
user pastes into terminal e.g. ctrl+shift+v - we don't see that either only
a sequence of characters that were pasted.
So Shift+A is the same as CapsLock A; is the same as pasting a capital A.
We just have to do the best we can as a library to standardise the input to
a common denominator across all the drivers and make it as easy to consume
as possible for API users.
…On Sun, 24 Dec 2023, 21:29 BDisp, ***@***.***> wrote:
Only the WindowsDriver handles caps lock and here where it's handled
https://github.com/BDisp/Terminal.Gui/blob/a938295eeb98950f7a65b4afb5377e1210c9d6a6/Terminal.Gui/ConsoleDrivers/WindowsDriver.cs#L1009-L1013.
The others drivers the terminal already provide the right character.
—
Reply to this email directly, view it on GitHub
<#3071 (comment)>,
or unsubscribe
<https://github.com/notifications/unsubscribe-auth/AHO3C5EQWYBUEWVSUKQG76TYLCNDLAVCNFSM6AAAAABBARZAJ6VHI2DSMVQWIX3LMV43OSLTON2WKQ3PNVWWK3TUHMYTQNRYGU4TMNRRGM>
.
You are receiving this because you authored the thread.Message ID:
***@***.***>
|
We are mapping the correct ConsoleKeyInfo into KeyCode. Remember that's A and a belong both to the ConsoleKey.A which is the upper case. The difference is that A have the shift modifier and the a don't. I admitted that my PR may have some incorrect code but the main code satisfy the needs. |
This comment was marked as off-topic.
This comment was marked as off-topic.
This comment was marked as off-topic.
This comment was marked as off-topic.
It seems you think we are changing the original keys from terminal and we are not. It's better present the bugs by showing trough a unit test. |
It's off-topic of the original issue, anyway, as it was only incidentally related. I had a sample demonstrating what I was originally getting at, last night, but I'll have to re-create it. I don't think I'm on the right/same track as what I was dealing with at that point (or could have missed something then in the first place), so I think I'm creating unnecessary confusion at the moment. I'll hide the off-topic tangent here for now until I figure out WTH I was specifically hitting and file a separate issue, if it really is relevant. Apologies for the de-rail. |
That constructor doesn't currently exist, though it would be better than the cast path from char to Key, if you ask me, since calling a constructor is a pretty intentional act. I would propose perhaps a modified version of your thought from earlier, about making almost everything about a Key immutable actually: Lots of ways to achieve it1, but making them all init-only gets that desired immutability while still allowing users to mess with it in an object initializer if desired. KeyCode already is, as it turns out. I don't think it needs further restriction than that. Maybe also provide optional parameters in the constructors, in case someone wants to explicitly build the Key with, for example, certain modifiers set (which would also cover a use case @tznind mentioned)? For example:
Footnotes
|
Still a WIP (Merry Christmas!) but I hope this helps! |
* Removed char->Key cast. Added Key(char) * Re-added char->key cast. Added unit tests * Fixed standard keys to always return new instead of being readonly * Re-fixed WindowsDriver to report shift/alt/ctrl as key/down/up * Re-fixed WindowsDriver to report shift/alt/ctrl as key/down/up * Adds string constructor to Key + tests. * Simplified Key json * Added string/Key cast operators.
…rrect (gui-cs#3089) * Removed char->Key cast. Added Key(char) * Re-added char->key cast. Added unit tests * Fixed standard keys to always return new instead of being readonly * Re-fixed WindowsDriver to report shift/alt/ctrl as key/down/up * Re-fixed WindowsDriver to report shift/alt/ctrl as key/down/up * Adds string constructor to Key + tests. * Simplified Key json * Added string/Key cast operators.
Describe the bug
Key
andKeyCode
support equality e.g.However when casting is involved things behave inconsistently. The most noticeable case is non character values e.g. F1
The int cast on
Key
is going into the 'char' explicit operator which returns 0But there is also inconsistent behaviour when it comes to lower case letters
I think the code is going down the
char
route in order to get touint
and I note that the docs describes this as 'lossy'.Expected behavior
Casting to uint should either be impossible or should be consistent
The text was updated successfully, but these errors were encountered: