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).
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.