A new idea how to enable Emacs-like multi-stroke keymap in wxPython and hint to boost menubar and accelerators

Hi there :wave:

I’ve read some posts about key bindings and accelerators on this list. I think many folks are interested in the topic so that I’d like to post an idea to improve key-bindings and to enable multi-stroke keymaps like Emacs in wxPython.

1. Mimic the accelerator table (on the Panel)

The first idea is to mimic the accelerator table using SSM (single state machine).

Suppose hotkey is a function that converts the event.KeyCode like C-M-S-key string and overwrite event.key. For example, if the Control key and ‘c’ are pressed, the event.key becomes ‘C-c’.

Also, suppose the modifier keys are arranged in the same order as matplotlib, such as ctrl + alt(meta) + shift. (The matplotlib has recently changed the order from version 3.4.x to shift+alt+ctrl, but I’m using the old way here)

The sample code below works as a mimic of the accelerator table.
When you press (and release) the key ‘c’ or ‘C-c’ on the panel, the event is passed to the SSM handler. Then, the handler performs the specified action in the list specified by the key c pressed or C-c pressed.

keys.py (4.0 KB)

from keys import hotkey
import wx

class SSM(dict):
    def __call__(self, event, *args):
        return [act(*args) for act in self.get(event) or ()]

class Panel(wx.Panel):
    def __init__(self, parent):
        wx.Panel.__init__(self, parent)
        
        self.handler = SSM({
                'c pressed' : [ lambda v: print('{} is pressed'.format(v.key)) ],
               'c released' : [ lambda v: print('{} is released'.format(v.key)) ],
              'C-c pressed' : [ lambda v: print('{} is pressed'.format(v.key)) ],
             'C-c released' : [ lambda v: print('{} is released'.format(v.key)) ],
        })
        self.Bind(wx.EVT_KEY_DOWN, self.on_key_press)
        self.Bind(wx.EVT_KEY_UP, self.on_key_release)
    
    def on_key_press(self, evt):
        self.handler('{} pressed'.format(hotkey(evt)), evt)
    
    def on_key_release(self, evt):
        self.handler('{} released'.format(hotkey(evt)), evt)

if __name__ == "__main__":
    app = wx.App()
    frm = wx.Frame(None)
    frm.panel = Panel(frm)
    frm.Show()
    app.MainLoop()

2. Improve the handler

In the next step, we extend SSM to FSM (Finite state machine).

The theory of FSM, developed in mid 20th century, is one of the most useful models in system design.
This concept is very simple and I don’t explain here, but if your want to learn this concept more, see Wikipedia.

The way sample code works is still quite simple. If you press ‘C-c’, the handler transits from the initial state 0 to the next state 1 and performs the specified actions in the list. Then, if you press ‘C-c’ again, the handler transits 1 to 0 and performs the specified actions. This behavior is a mimic of the two-stroke key like emacs.

from keys import hotkey
import fnmatch
import wx

class FSM(dict):
    def __init__(self, contexts=None, default=None):
        dict.__init__(self, contexts or {})
        self.state = default
    
    def __call__(self, event, *args):
        return self.call(event, *args)
    
    def call(self, event, *args):
        context = self[self.state]
        if event in context:
            transaction = context[event]
            self.state = transaction[0] # the state transits here
            for act in transaction[1:]: # then execute the actions
                act(*args)
            return True
        for pat in context:
            if fnmatch.fnmatchcase(event, pat): # if not found, search for matching
                return self.call(pat, *args)    # then recursive call with matched pattern

class Panel(wx.Panel):
    def __init__(self, parent):
        wx.Panel.__init__(self, parent)
        
        self.handler = FSM({
                0 : {
                  'C-c pressed' : [ 1, lambda v: print('{} is pressed'.format(v.key))],
                    '* pressed' : [ 0, lambda v: v.Skip() ], # skip to parent menu
                },
                1 : {
                  'C-c pressed' : [ 0, lambda v: print('[C-c {}] is pressed'.format(v.key))],
                 '*alt pressed' : ( 1, ),
                '*ctrl pressed' : ( 1, ),
               '*shift pressed' : ( 1, ),
             '*[LR]win pressed' : ( 1, ),
                    '* pressed' : [ 0, lambda v: print('[C-c {}] is skipped'.format(v.key))],
                },
            },
            default=0
        )
        ## self.Bind(wx.EVT_KEY_DOWN, self.on_key_press)
        self.Bind(wx.EVT_CHAR_HOOK, self.on_key_press)
        self.Bind(wx.EVT_KEY_UP, self.on_key_release)
    
    def on_key_press(self, evt):
        self.handler('{} pressed'.format(hotkey(evt)), evt)
    
    def on_key_release(self, evt):
        self.handler('{} released'.format(hotkey(evt)), evt)

if __name__ == "__main__":
    app = wx.App()
    frm = wx.Frame(None)
    frm.panel = Panel(frm)
    frm.Show()
    app.MainLoop()

Note that there are two more improvements.

  1. The handler accepts keys including wildcards as fnmatch (Unix filename pattern matching). This allows the users to omit a lot of typing to define keys mapping. For example, 4 lines above including keys *alt, *ctrl, *shift, and *[LR]Win describes that any key events containing no character code and only modifiers just make the transition to the original state and do nothing.

  2. EVT_CHAR_HOOK is bound to the handler to grab the key events before the frame menubar does, and prevent menubar to invoke the defined shortcut key events. If you skip the event, it is passed to the parent frame and the shortcut keys defined on the menubar (and maybe the accelerator table) are processed normally.

That’s it!

Remarks

Thank you for reading this long post. :grinning_face_with_smiling_eyes: I’m happy if I can share this idea with you and I hope this could be a help for the next improvement of the menubar and accelerator!