Welcome, guest | Sign In | My Account | Store | Cart

Some people have a burning need for emacs-like multiple key sequence hotkeys. What do I mean? Ctrl+x Ctrl+s to save the currently open document and exit emacs.

Included as code is a sample wxPython program which implements multi-group keypress combinations, and will print 'hello!' in the statusbar if the combination Ctrl+Y Alt+3 Shift+B is entered. As you are entering a valid sequence of hotkeys, it will print your current combination in the status bar. If you make a mistake, it will print out your failed keyboard combination.

I use variants of the menu manupulation and keymap generation code in PyPE (http://pype.sf.net).

Python, 172 lines
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
'''
multi_hotkey.py

A few simple methods which implement multiple hotkey support for emacs-like
hotkey sequences.

Feel free to use this code as you desire, though please cite the source.

Josiah carlson
http://come.to/josiah
'''

import wx
import time

keyMap = {}

def gen_keymap():
    keys = ("BACK", "TAB", "RETURN", "ESCAPE", "SPACE", "DELETE", "START",
        "LBUTTON", "RBUTTON", "CANCEL", "MBUTTON", "CLEAR", "PAUSE",
        "CAPITAL", "PRIOR", "NEXT", "END", "HOME", "LEFT", "UP", "RIGHT",
        "DOWN", "SELECT", "PRINT", "EXECUTE", "SNAPSHOT", "INSERT", "HELP",
        "NUMPAD0", "NUMPAD1", "NUMPAD2", "NUMPAD3", "NUMPAD4", "NUMPAD5",
        "NUMPAD6", "NUMPAD7", "NUMPAD8", "NUMPAD9", "MULTIPLY", "ADD",
        "SEPARATOR", "SUBTRACT", "DECIMAL", "DIVIDE", "F1", "F2", "F3", "F4",
        "F5", "F6", "F7", "F8", "F9", "F10", "F11", "F12", "F13", "F14",
        "F15", "F16", "F17", "F18", "F19", "F20", "F21", "F22", "F23", "F24",
        "NUMLOCK", "SCROLL", "PAGEUP", "PAGEDOWN", "NUMPAD_SPACE",
        "NUMPAD_TAB", "NUMPAD_ENTER", "NUMPAD_F1", "NUMPAD_F2", "NUMPAD_F3",
        "NUMPAD_F4", "NUMPAD_HOME", "NUMPAD_LEFT", "NUMPAD_UP",
        "NUMPAD_RIGHT", "NUMPAD_DOWN", "NUMPAD_PRIOR", "NUMPAD_PAGEUP",
        "NUMPAD_NEXT", "NUMPAD_PAGEDOWN", "NUMPAD_END", "NUMPAD_BEGIN",
        "NUMPAD_INSERT", "NUMPAD_DELETE", "NUMPAD_EQUAL", "NUMPAD_MULTIPLY",
        "NUMPAD_ADD", "NUMPAD_SEPARATOR", "NUMPAD_SUBTRACT", "NUMPAD_DECIMAL",
        "NUMPAD_DIVIDE")
    
    for i in keys:
        keyMap[getattr(wx, "WXK_"+i)] = i
    for i in ("SHIFT", "ALT", "CONTROL", "MENU"):
        keyMap[getattr(wx, "WXK_"+i)] = ''

def GetKeyPress(evt):
    keycode = evt.GetKeyCode()
    keyname = keyMap.get(keycode, None)
    modifiers = ""
    for mod, ch in ((evt.ControlDown(), 'Ctrl+'),
                    (evt.AltDown(),     'Alt+'),
                    (evt.ShiftDown(),   'Shift+'),
                    (evt.MetaDown(),    'Meta+')):
        if mod:
            modifiers += ch

    if keyname is None:
        if 27 < keycode < 256:
            keyname = chr(keycode)
        else:
            keyname = "(%s)unknown" % keycode
    return modifiers + keyname

def _spl(st):
    if '\t' in st:
        return st.split('\t', 1)
    return st, ''

class StatusUpdater:
    def __init__(self, frame, message):
        self.frame = frame
        self.message = message
    def __call__(self, evt):
        self.frame.SetStatusText(self.message)

class MainFrame(wx.Frame):
    def __init__(self):
        wx.Frame.__init__(self, None, -1, "test")
        self.CreateStatusBar()
        ctrl = self.ctrl = wx.TextCtrl(self, -1, style=wx.TE_MULTILINE|wx.WANTS_CHARS|wx.TE_RICH2)
        ctrl.SetFocus()
        ctrl.Bind(wx.EVT_KEY_DOWN, self.KeyPressed, ctrl)
        
        self.lookup = {}
        
        menuBar = wx.MenuBar()
        self.SetMenuBar(menuBar)  # Adding the MenuBar to the Frame content.
        self.menuBar = menuBar
        
        testmenu = wx.Menu()
        self.menuAddM(menuBar, testmenu, "TestMenu", "help")
        self.menuAdd(testmenu, "testitem\tCtrl+Y\tAlt+3\tShift+B", "testdesc", StatusUpdater(self, "hello!"))
        
        print self.lookup
        
        self._reset()
        self.Show(1)
    
    def addHotkey(self, acc, fcn):
        hotkeys = self.lookup
        x = [i for i in acc.split('\t') if i]
        x = [(i, j==len(x)-1) for j,i in enumerate(x)]
        for name, last in x:
            if last:
                if name in hotkeys:
                    raise Exception("Some other hotkey shares a prefix with this hotkey: %s"%acc)
                hotkeys[name] = fcn
            else:
                if name in hotkeys:
                    if not isinstance(hotkeys[name], dict):
                        raise Exception("Some other hotkey shares a prefix with this hotkey: %s"%acc)
                else:
                    hotkeys[name] = {}
                hotkeys = hotkeys[name]

    def menuAdd(self, menu, name, desc, fcn, id=-1, kind=wx.ITEM_NORMAL):
        if id == -1:
            id = wx.NewId()
        a = wx.MenuItem(menu, id, 'TEMPORARYNAME', desc, kind)
        menu.AppendItem(a)
        wx.EVT_MENU(self, id, fcn)
        ns, acc = _spl(name)
        
        if acc:
            self.addHotkey(acc, fcn)
        
        menu.SetLabel(id, '%s\t%s'%(ns, acc.replace('\t', ' ')))
        menu.SetHelpString(id, desc)

    def menuAddM(self, parent, menu, name, help=''):
        if isinstance(parent, wx.Menu) or isinstance(parent, wx.MenuPtr):
            id = wx.NewId()
            parent.AppendMenu(id, "TEMPORARYNAME", menu, help)

            self.menuBar.SetLabel(id, name)
            self.menuBar.SetHelpString(id, help)
        else:
            parent.Append(menu, name)

    def _reset(self):
        self.sofar = ''
        self.cur = self.lookup
        self.SetStatusText('')
    
    def _add(self, key):
        self.cur = self.cur[key]
        self.sofar += ' ' + key
        self.SetStatusText(self.sofar)

    def KeyPressed(self, evt):
        key = GetKeyPress(evt)
        print key
        
        if key == 'ESCAPE':
            self._reset()
        elif key.endswith('+') and len(key) > 1 and not key.endswith('++'):
            #only modifiers
            evt.Skip()
        elif key in self.cur:
            self._add(key)
            if not isinstance(self.cur, dict):
                sc = self.cur
                self._reset()
                sc(evt)
        elif self.cur is not self.lookup:
            sf = "%s %s  <- Unknown sequence"%(self.sofar, key)
            self._reset()
            self.SetStatusText(sf)
        else:
            evt.Skip()

if __name__ == '__main__':
    gen_keymap()
    app = wx.PySimpleApp()
    frame = MainFrame()
    app.MainLoop()

People who want multiple hotkey support in their menu items or program know what they want with them. I personally wouldn't suggest people use them, if only because over-use of multi-command hotkeys have a tendency to lead to 'emacs pinky'. Use with caution, but know that you can use it if necessary.

Astute observers will note that you can implement arbitrary keyboard command sequences with the above. With a little reworking, it would even be possible to add 'cheat code'-like support to your application, but I'll leave that up to the rest of you.

Created by Josiah Carlson on Mon, 21 Nov 2005 (PSF)
Python recipes (4591)
Josiah Carlson's recipes (9)

Required Modules

Other Information and Tasks