ENB: fixing Leo's key handling

35 views
Skip to first unread message

Edward K. Ream

unread,
Apr 19, 2018, 6:11:53 AM4/19/18
to leo-editor
This is an engineering notebook post concerning Leo's key-handling code.  Please feel free to ignore unless you might someday maintain this code.

This is a "thinking out loud post", inspired by the recent difficulties in the "keys2" branch. Leo's key-handling code is complex and difficult.  Some of this difficulty is inherent.  Some is likely self-inflicted, so simplifications may be possible.

Status

I have installed Greek, German and Chinese language packs on Windows, so I can easily type σ, or ß  or 都. 

At present (at least in the "keys2" branch), Leo does not handle most (all?) Greek characters.  Traces show that proper key events are generated, but somehow these characters are "disqualified" later.

Similarly, Leo handles regular German characters properly, but not ß.

Otoh, Leo does handle 都 properly.  And I see why.  When I type a key, a popup appears with a menu of items.  When I select the item Qt (or Windows?) inserts the Chinese character directly in the widget without generating a key event!

Initial thoughts

Last night I realized (Doh!) that I hadn't read the QKeyEvent docs in a long time. This produced some surprises.

Even before reading the docs, I realized that modifier keys other than "Shift" are problematic.  It's not entirely clear what they mean.

Let's define a bare shift as a Shift modifier no accompanied by any other modifier.

Let's define a bare key as a key that has (at most) the Shift modifier.

The separation between event.binding and event.char causes a great deal of confusion.  I would like to clear that up.

At present, k.masterKeyHandler uses event.char and event.stroke, but this is strange.  One would think that the stroke would contain a char ivar.

Reading the docs

Here are what appear to be the essential excerpts:

QQQ
event.text(): Returns the Unicode text that this key generated.

Return values when modifier keys such as Shift, Control, Alt, and Meta are pressed differ among platforms and could return an empty string.

event.key(): See Qt::Key for the list of keyboard codes. These codes are independent of the underlying window system.

This function does not distinguish between capital and non-capital letters, use the text() function.

From Qt::Key: On Windows, when the KeyDown event for [Key_AltGr] is sent, the Ctrl+Alt modifiers are also set.
QQQ

There is a lot to chew on here.

Generating characters

filter.toBinding returns (binding, char).  At present, the distinction between binding and char is muddled.

It's imperative that this confusion be cleaned up.  Thinking out loud, let's see if the following plan might work:

At present: filter.qtKey does use event.key() to generate the keynum field, but does not test for Key_AltGr.  This likely causes a lot problems/bug later.

1. For bare keys, char should just be event.text().  Indeed, this contains the desired (single) unicode character, with the proper case.

At present, various parts of code contain case-adjusting code.  Wherever they appear, these lines are extremely difficult to get right.  But not all of the case-adjusting code can disappear.  In particular, there must be case-adjusting code to handle user settings.

2. For keys containing modifiers other than Shift, char should probably be empty.

But there is a crucial complication.  For keys such as Home, Right, etc. event.text() generates multi-character (unicode) text.

3. In any case, the eventFilter logic should "adjust" event modifiers as follows:

A: Remove the Shift modifier from bare events.  This should eliminate the case-adjusting logic that presently exists in filter.tweak.

B. Remove the Alt and Ctrl modifiers if keynum == Key_AltGr.

Ensuring that user settings match bindings

At present, this kinda happens automatically, because both key binding and user create g.KeyStroke events.  In both cases, the ctor, via ks.finalize_binding, converts the incoming binding to stroke.s.

In essence, this is the canonical form of either the key binding or the user setting. And in particular, ks.finalize_char (a helper of ks.finalize_binding) has the dreaded case-adjustment code.

What modifiers are allowed?

But now we come to a new question in Leo's history! What are we to do about Alt, Ctrl, Meta and Command modifiers? That is, to which characters are these modifiers allowed to be applied?

This question has tricky ramifications. It seems to me, that regardless of what modifiers the QKeyEvent might deliver, it makes no sense to allow user settings like Alt-σ or Ctrl-ß or Meta+都.

Otoh, we definitely do want to allow user settings like Shift-Tab, Shift-Home, Shift-Ctrl-End, etc. And the present code does allow such things.  There are already lists of such characters in g.KeyStroke.  Perhaps more should be added.

Summary

Leo's key handling must be perfect.  Nothing else is acceptable.

Surprisingly, Greek and German letters appear to be a better test of the code than Chinese.

Modifiers probably should be adjusted more than they are now.

Case-adjusting code can never be completely eliminated, but is should be possible to eliminate it from the eventFilter code.

At present, the exact meaning of the (binding, char) tuple returned from filter.toBinding is unclear.  This has ripple effects in the code.

When this confusion is gone, it should be possible clean the code further. This would be a separate project, but now is the time to clean the code as much as possible, when all the details are fresh in my mind.

More details may come to mind, but I have been working on this post most of the night.  Time for a break.

Edward

Edward K. Ream

unread,
Apr 19, 2018, 6:17:36 AM4/19/18
to leo-editor
On Thursday, April 19, 2018 at 5:11:53 AM UTC-5, Edward K. Ream wrote:

It seems to me, that regardless of what modifiers the QKeyEvent might deliver, it makes no sense to allow user settings like Alt-σ or Ctrl-ß or Meta+都.

I misspoke. Alt-σ or Ctrl-ß certainly should be allowed, because (unless I am mistaken) σ and ß are, in fact, ascii characters.

I had intended to say that the modifiers should be allowed if, say, 32 <= ord(ch) < 128.  And to repeat, modifiers should be allowed for keys such as Home, End, etc.

Edward

Edward K. Ream

unread,
Apr 19, 2018, 6:24:07 AM4/19/18
to leo-editor
On Thursday, April 19, 2018 at 5:17:36 AM UTC-5, Edward K. Ream wrote:


I misspoke. Alt-σ or Ctrl-ß certainly should be allowed, because (unless I am mistaken) σ and ß are, in fact, ascii characters.

I had intended to say that the modifiers should be allowed if, say, 32 <= ord(ch) < 128.  And to repeat, modifiers should be allowed for keys such as Home, End, etc.

Note that apparently simple tests can be fraught.  There is a bug against ks.isPlainKey.  It ends with:
 
    if s in string.printable:
        return True
    if len(s) > 1:
        return False
    return unicodedata.category(s).startswith('C')

Why this fails is mysterious.  Perhaps the test should be:

   return unicodedata.category(s).startswith('C') if len(s) == 1 else False

But I rather doubt that this will work.  Or maybe the test against 'C' is the culprit.

In any event, these kinds of unicode tests are trickier than I hoped.

Edward

Karsten Wolf

unread,
Apr 19, 2018, 7:12:46 AM4/19/18
to leo-editor
... 
Note that apparently simple tests can be fraught.  There is a bug against ks.isPlainKey.  It ends with:
 
    if s in string.printable:
        return True
    if len(s) > 1:
        return False
    return unicodedata.category(s).startswith('C')

Why this fails is mysterious.  Perhaps the test should be:

s  could still be a str (string.printable is a subset) and make unicodedata.category(s) fail.

The question is: Why is s not unicode?

Edward K. Ream

unread,
Apr 19, 2018, 1:07:10 PM4/19/18
to leo-editor

​An excellent question.  Happily, the soon-to-be-pushed version of ks.isPlainKey bypasses unicodedata entirely:

def isPlainKey(self):
    '''
    Return True if self.s represents a plain key.
   
    **Note**: The caller is responsible for handling Alt-Ctrl keys.
    '''
    s = self.s
    if s in g.ignoreChars:
        # For unit tests.
        return False
    if self.find_mods(s) or self.isFKey():
        return False
    if s in self.specialChars:
        return False
    return True

Once a few special cases are handled, the interesting assumption is that all keys without modifiers are "plain" keys, even (especially) Chinese characters.  It looks like this is working now.

Edward
Reply all
Reply to author
Forward
0 new messages