== Introduction == Sometimes people have the desire and/or need to require multiple keyboard commands to signal an event. For example, Emacs users use Ctrl+x followed by Ctrl+s to save the file they are currently editing. This recipe will show how to create menu items that have this feature, and the base class also supports creating keyboard shortcuts without menu items. == Making It Happen == The base wx.Menu classes do not support all of the keys that our keyboards support. If we are going to be overriding the keyboard shortcut handling of the menu, we may as well support all of the possible keys that our keyboards do. We are first going to get all of the ids for the 'special' keyboard keys and their names. {{{ #!python import wx 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)] = '' }}} Now that we know all of the keyboard keys, we are going to need to generate specific names for each keyboard keypress combination that we come upon in our event processing. {{{ #!python 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 }}} The above !GetKeyPress() function will, if given an ''wx.EVT_KEY_DOWN'' event, return a string that describes the keyboard keys currently being pressed. Because the following code is pretty ugly, and I'm not very good at step-by-step tutorials, you get it all in one big block. {{{ #!python #a utility function and class 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) #The frame with hotkey chaining. 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\tH\tE\tL\tP", "helptext", StatusUpdater(self, "Did you want help?")) self.menuAdd(testmenu, "testitem\tCtrl+C\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, 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('++'): #modifiers only, if we don't skip these events, then when people #hold down modifier keys, things get ugly evt.Skip() elif key in self.cur: self._add(key) if not isinstance(self.cur, dict): sc = self.cur self._reset() sc(evt) #comment the next line if you don't want the partial keyboard #command to continue on to the other controls evt.Skip() 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() }}} === Comments === This is just a simple example which uses nested dictionaries and keyboard key down events to implement multi-stage keyboard shortcuts. This was strictly a proof-of-concept. If you plan on using something like this I would encourage you to do a few things: 1. seriously reconsider whether you want to use multi-stage keyboard shortcuts, you don't want people to get emacs pinky 2. move all of your keyboard handling off into a mixin 3. examine the !KeyPressed() method and play with it to verify that it does what you want it to do 4. remember that if you are going to use a standard character, use the uppercase version, not the lowercase (A instead of a) [[http://pype.sf.net|PyPE]] uses a variant of the above to handle its keyboard shortcuts, though does not support multi-stage keyboard shortcuts. ---- Nice recipe! In testing it, I found that putting the accelerator in the menu causes the wxMenu to respond to the last keystroke in the accelerator text as a command independent of the keyboard processing. For instance, in the example above pressing "Shift-B" at any time will display "hello!" on the status bar, even in the middle of keystroke processing. Changing the accelerator text to Emacs style keystroke definitions (i.e. from "Ctrl-C" to "C-C", "Shift-B" to "S-B", etc.) works around this problem on Windows: {{{ #!python 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) # unix doesn't allow displaying arbitrary text as the accelerator key. acc=acc.replace('Ctrl','C').replace('Shift','S').replace('Alt','M') acc=acc.replace('+','-').replace('\t',' ') print "acc=%s" % acc menu.SetLabel(id, '%s\t%s'%(ns,acc)) menu.SetHelpString(id, desc) }}} but as noted in the comment, the wxGTK port doesn't allow arbirtary text as a menu accelerator. On unix it apparently checks the accelerator text to see if it is a valid keystroke combination before it will place it in the menu. --RobMcMullen ---- I've enhanced this recipe to include additional Emacs style processing, including global and local keymaps, numeric arguments, the ESC-ESC-ESC cancel sequence, and more. See EmacsStyleKeybindings for the implementation. --RobMcMullen