Foreward

I'm addicted to XEmacs, but hate trying to learn elisp to extend it. I have a pie-in-the-sky project to implement an XEmacs workalike in python, peppy, and use the following code to process emacs-style keybindings.

Introduction

I based this code on Josiah Carlson's Using Multi-key Shortcuts recipe here in the wxPython wiki.

There are two classes that handle the key processing, the KeyMap class that represents a collection of keybindings, and the KeyProcessor class that processes individual keystrokes and attempts to match them with a command in the keybinding lists.

Mac Help

If you have a Mac, I'd be really interested for you to try this out. I have no idea how it's going to work on the Mac, or what the proper mapping of the Apple key should be.

Quirks

If you put the accelerator text in a wxMenu, the key processing code of the menu causes the application to respond to the last keystroke in the accelerator text as a command independent of the keyboard processing. I had to add an ascii zero to the end of the accelerator text to kill this effect. On Unix, non standard modifiers don't even get displayed properly in the wxMenu items. "C-X C-S" gets displayed as "X S", and processed as only "S", which means every time you pressed a single "S" character the menu gets activated. Not what we want, so I hacked around that by displaying the accelerator in the label part. Doesn't look as nice, but it avoids the problem of the menu interpreting the keystrokes.

KeyMap Class

This class represents a group of key mappings. Each key mapping consists of a sequence of keystrokes and a function to call when that sequence is matched. Keystrokes are defined by a text string that is made up of a sequence of modifier keys applied to key names, for instance:

Internally, all modifiers are represented by the abbreviations C-, S-, A-, and M- for Ctrl, Shift, Alt, and Meta respectively. When specifying keystrokes, the longer names may be used.

The function that is called when the keystroke is matched should take two parameters, the second of which is an optional numeric argument. I tend to implement these as Command objects:

   1 class Command(object):
   2     def __init__(self, stuff):
   3         self.stuff=stuff
   4 
   5     def __call__(self, evt, number=None):
   6         print "%s called by keybindings" % self
   7         # perform some cool function
   8         self.stuff.performCoolThing()

so that the keybinding is defined by:

   1 from wxemacskeybindings import *
   2 
   3 class UserFrame(wx.Frame):
   4 
   5     def __init__(...)
   6         # [...]
   7         keymap=KeyMap()
   8         keymap.define("C-C C-C", Command(self))
   9 
  10     def performCoolThing(self):
  11         # cool thing done here

KeyProcessor class

The KeyProcessor handles the keystrokes and searches through all the KeyMap classes to find a match. It has the concept of global and local keymaps, as well as "minor mode" keymaps, with the search order of minor mode keymaps first, then local, and finally global if nothing else matches.

The keymaps can be changed at any time using the provided set*Keymap methods, although if they're replaced in the middle of a keystroke sequence, you'll lose the previous keystrokes.

All keyboard processing must be diverted to the KeyProcessor, which means intercepting the EVT_KEY_DOWN event of whatever wx objects need to handle the keypresses. For example:

   1 class UserFrame(wx.Frame):
   2 
   3     def __init__(...)
   4         # [...]
   5         self.globalKeyMap=KeyMap()
   6         self.globalKeyMap.define("C-C C-C", Command(self))
   7         self.keys=KeyProcessor(status=self)
   8         self.keys.setGlobalKeyMap(self.globalKeyMap)
   9         self.Bind(wx.EVT_KEY_DOWN, self.KeyPressed, ctrl)
  10 
  11     def KeyPressed(self, evt):
  12         self.keys.process(evt)

Implementation

Here's the source code for the two classes, with lots of comments and an example program. Save as wxemacskeybindings.py and run it to start the demo.

   1 #!/usr/bin/env python
   2 
   3 import sys
   4 import wx
   5 
   6 # Based on demo program by Josiah Carlson found at
   7 # http://wiki.wxpython.org/index.cgi/Using_Multi-key_Shortcuts
   8 
   9 wxkeynames = (
  10     "BACK", "TAB", "RETURN", "ESCAPE", "SPACE", "DELETE", "START",
  11     "LBUTTON", "RBUTTON", "CANCEL", "MBUTTON", "CLEAR", "PAUSE",
  12     "CAPITAL", "PRIOR", "NEXT", "END", "HOME", "LEFT", "UP", "RIGHT",
  13     "DOWN", "SELECT", "PRINT", "EXECUTE", "SNAPSHOT", "INSERT", "HELP",
  14     "NUMPAD0", "NUMPAD1", "NUMPAD2", "NUMPAD3", "NUMPAD4", "NUMPAD5",
  15     "NUMPAD6", "NUMPAD7", "NUMPAD8", "NUMPAD9", "MULTIPLY", "ADD",
  16     "SEPARATOR", "SUBTRACT", "DECIMAL", "DIVIDE", "F1", "F2", "F3", "F4",
  17     "F5", "F6", "F7", "F8", "F9", "F10", "F11", "F12", "F13", "F14",
  18     "F15", "F16", "F17", "F18", "F19", "F20", "F21", "F22", "F23", "F24",
  19     "NUMLOCK", "SCROLL", "PAGEUP", "PAGEDOWN", "NUMPAD_SPACE",
  20     "NUMPAD_TAB", "NUMPAD_ENTER", "NUMPAD_F1", "NUMPAD_F2", "NUMPAD_F3",
  21     "NUMPAD_F4", "NUMPAD_HOME", "NUMPAD_LEFT", "NUMPAD_UP",
  22     "NUMPAD_RIGHT", "NUMPAD_DOWN", "NUMPAD_PRIOR", "NUMPAD_PAGEUP",
  23     "NUMPAD_NEXT", "NUMPAD_PAGEDOWN", "NUMPAD_END", "NUMPAD_BEGIN",
  24     "NUMPAD_INSERT", "NUMPAD_DELETE", "NUMPAD_EQUAL", "NUMPAD_MULTIPLY",
  25     "NUMPAD_ADD", "NUMPAD_SEPARATOR", "NUMPAD_SUBTRACT", "NUMPAD_DECIMAL",
  26     "NUMPAD_DIVIDE",
  27     )
  28 
  29 ##
  30 # This class represents a group of key mappings.  The KeyProcessor
  31 # class below uses multiple groups, one to represent global keymaps,
  32 # one for local keymaps, and an arbitrary number of other keymaps for
  33 # any additional minor modes that need other keymappings.
  34 class KeyMap(object):
  35     def __init__(self):
  36         self.debug=False
  37         
  38         self.lookup={}
  39         self.reset()
  40         
  41         self.modifiers=['C-','S-','A-','M-']
  42         self.modaliases={'Ctrl-':'C-',
  43                          'Shift-':'S-',
  44                          'Alt-':'A-',
  45                          'Meta-':'M-',
  46                          'Ctrl+':'C-',
  47                          'Shift+':'S-',
  48                          'Alt+':'A-',
  49                          'Meta+':'M-',
  50                          }
  51         self.keyaliases={'RET':'RETURN',
  52                          'SPC':'SPACE',
  53                          'ESC':'ESCAPE',
  54                          }
  55 
  56         self.function=None
  57 
  58         # if this is true, it will throw an exception when finding a
  59         # duplicate keystroke.  If false, it silently overwrites any
  60         # previously defined keystroke with the new one.
  61         self.exceptionsWhenDuplicate=False
  62 
  63     def reset(self):
  64         self.cur=self.lookup
  65         self.function=None
  66 
  67     ##
  68     # return True if keystroke is processed by the handler
  69     def add(self, key):
  70         if self.cur:
  71             if key in self.cur:
  72                 # get next item, either a dict of more possible
  73                 # choices or a function to execute
  74                 self.cur=self.cur[key]
  75                 if not isinstance(self.cur, dict):
  76                     self.function = self.cur
  77                     self.cur=None
  78                 return True
  79             elif self.cur is not self.lookup:
  80                 # if we get here, we have processed a partial match,
  81                 # but the most recent keystroke doesn't match
  82                 # anything.  Flag as unknown keystroke combo
  83                 self.cur=None
  84                 return True
  85             else:
  86                 # OK, this is the first keystroke and it doesn't match
  87                 # any of the first keystrokes in our keymap.  It's
  88                 # probably a regular character, so flag it as
  89                 # unprocessed by our handler.
  90                 self.cur=None
  91         return False
  92 
  93     ##
  94     # Convience function to check whether the keystroke combo is an
  95     # unknown combo.
  96     def isUnknown(self):
  97         return self.cur==None and self.function==None
  98 
  99     ##
 100     # Find a modifier in the accerelator string
 101     def matchModifier(self,str):
 102         for m in self.modifiers:
 103             if str.startswith(m):
 104                 return len(m),m
 105         for m in self.modaliases.keys():
 106             if str.startswith(m):
 107                 return len(m),self.modaliases[m]
 108         return 0,None
 109 
 110     ##
 111     # Find a keyname (not modifier name) in the accelerator string,
 112     # matching any special keys or abbreviations of the special keys
 113     def matchKey(self,str):
 114         key=None
 115         i=0
 116         for name in self.keyaliases:
 117             if str.startswith(name):
 118                 val=self.keyaliases[name]
 119                 return i+len(val),val
 120         for name in wxkeynames:
 121             if str.startswith(name):
 122                 return i+len(name),name
 123         if i<len(str) and not str[i].isspace():
 124             return i+1,str[i].upper()
 125         return i,None
 126 
 127     ##
 128     # Split the accelerator string (e.g. "C-X C-S") into individual
 129     # keystrokes, expanding abbreviations and standardizing the order
 130     # of modifier keys
 131     def split(self,acc):
 132         if acc.find('\t')>=0:
 133             # match the original format from the wxpython wiki, where
 134             # keystrokes are delimited by tab characters
 135             keystrokes = [i for i in acc.split('\t') if i]
 136         else:
 137             # find the individual keystrokes from a more emacs style
 138             # list, where the keystrokes are separated by whitespace.
 139             keystrokes=[]
 140             i=0
 141             flags={}
 142             while i<len(acc):
 143                 while acc[i].isspace() and i<len(acc): i+=1
 144 
 145                 # check all modifiers in any order.  C-S-key and
 146                 # S-C-key mean the same thing.
 147                 j=i
 148                 for m in self.modifiers: flags[m]=False
 149                 while j<len(acc):
 150                     chars,m=self.matchModifier(acc[j:])
 151                     if m:
 152                         j+=chars
 153                         flags[m]=True
 154                     else:
 155                         break
 156                 if self.debug: print "modifiers found: %s" % flags
 157                 
 158                 chars,key=self.matchKey(acc[j:])
 159                 if key is not None:
 160                     if self.debug: print "key found: %s" % key
 161                     keys="".join([m for m in self.modifiers if flags[m]])+key
 162                     if self.debug: print "keystroke = %s" % keys
 163                     keystrokes.append(keys)
 164                 else:
 165                     if self.debug: print "unknown key %s" % acc[j:j+chars]
 166                 i=j+chars
 167         if self.debug: print "keystrokes: %s" % keystrokes
 168         return keystrokes
 169                 
 170     ##
 171     # Create the nested dicts that point to the function to be
 172     # executed on the completion of the keystroke
 173     def define(self,acc,fcn):
 174         hotkeys = self.lookup
 175         if self.debug: print "define: acc=%s" % acc
 176         keystrokes = self.split(acc)
 177         if self.debug: print "define: keystrokes=%s" % keystrokes
 178         if keystrokes:
 179             # create the nested dicts for everything but the last keystroke
 180             for keystroke in keystrokes[:-1]:
 181                 if keystroke in hotkeys:
 182                     if self.exceptionsWhenDuplicate and not isinstance(hotkeys[keystroke], dict):
 183                         raise Exception("Some other hotkey shares a prefix with this hotkey: %s"%acc)
 184                     if not isinstance(hotkeys[keystroke],dict):
 185                         # if we're overwriting a function, we need to
 186                         # replace the function call with a dict so
 187                         # that the remaining keystrokes can be parsed.
 188                         hotkeys[keystroke] = {}
 189                 else:
 190                     hotkeys[keystroke] = {}
 191                 hotkeys = hotkeys[keystroke]
 192 
 193             # the last keystroke maps to the function to execute
 194             if self.exceptionsWhenDuplicate and keystrokes[-1] in hotkeys:
 195                 raise Exception("Some other hotkey shares a prefix with this hotkey: %s"%acc)
 196             hotkeys[keystrokes[-1]] = fcn
 197         return " ".join(keystrokes)
 198 
 199 
 200 
 201 ##
 202 # Driver class for key processing.  Takes multiple keymaps and looks
 203 # at them in order, first the minor modes, then the local, and finally
 204 # if nothing matches, the global key maps.
 205 class KeyProcessor(object):
 206     def __init__(self,status=None):
 207         self.debug=False
 208         
 209         self.keymaps=[]
 210         self.minorKeymaps=[]
 211         self.globalKeymap=KeyMap()
 212         self.localKeymap=KeyMap()
 213         
 214         self.num=0
 215         self.status=status
 216 
 217         # I'm guessing here; I don't know what the Mac should default
 218         # to.
 219         if wx.Platform == '__WXMAC__':
 220             self.remapMeta="Cmd" # or Cmd
 221         else:
 222             self.remapMeta="Alt" # or Cmd
 223 
 224         # XEmacs defaults to the Ctrl-G to abort keystroke processing
 225         self.abortKey="C-G"
 226 
 227         # Probably should create a standard way to process sticky
 228         # keys, but for now ESC corresponds to a sticky meta key just
 229         # like XEmacs
 230         self.stickyMeta="ESCAPE"
 231         self.metaNext=False
 232         self.nextStickyMetaCancel=False
 233 
 234         self.number=None
 235         self.defaultNumber=4 # for some reason, XEmacs defaults to 4
 236         self.scale=1 # scale factor, usually either 1 or -1
 237         self.universalArgument="C-U"
 238         self.processingArgument=0
 239 
 240         self.hasshown=False
 241         self.reset()
 242 
 243         # Mapping of wx keystroke numbers to keystroke names
 244         self.wxkeys={}
 245         # set up the wxkeys{} dict
 246         self.wxkeymap()
 247 
 248     def wxkeymap(self):
 249         for i in wxkeynames:
 250             self.wxkeys[getattr(wx, "WXK_"+i)] = i
 251         for i in ("SHIFT", "ALT", "CONTROL", "MENU"):
 252             if wx.Platform == '__WXMSW__':
 253                 self.wxkeys[getattr(wx, "WXK_"+i)] = ''
 254             else:
 255                 # unix doesn't create a keystroke when a modifier key
 256                 # is also modified by another modifier key, so we
 257                 # create entries here so that decode() doesn't have to
 258                 # have platform-specific code
 259                 self.wxkeys[getattr(wx, "WXK_"+i)] = i[0:1]+'-'
 260 
 261     ##
 262     # set up the search order of keymaps
 263     def fixmaps(self):
 264         self.keymaps=self.minorKeymaps+[self.localKeymap,self.globalKeymap]
 265         self.num=len(self.keymaps)
 266         self.reset()
 267 
 268     ##
 269     # Add the keymap to the list of keymaps recognized by this
 270     # processor.  Minor mode keymaps are processed in the order that
 271     # they are added.
 272     def addMinorKeyMap(self,keymap):
 273         self.minorKeymaps.append(keymap)
 274         self.fixmaps()
 275 
 276     def clearMinorKeyMaps(self):
 277         self.minorKeymaps=[]
 278         self.fixmaps()
 279 
 280     def setGlobalKeyMap(self,keymap):
 281         # Always add the ESC-ESC-ESC quit key sequence
 282         keymap.define("M-"+self.stickyMeta+" "+self.stickyMeta,None)
 283         self.globalKeymap=keymap
 284         self.fixmaps()
 285 
 286     def clearGlobalKeyMap(self):
 287         keymap=KeyMap()
 288         self.setGlobalKeyMap(keymap)
 289 
 290     def setLocalKeyMap(self,keymap):
 291         self.localKeymap=keymap
 292         self.fixmaps()
 293 
 294     def clearLocalKeyMap(self):
 295         keymap=KeyMap()
 296         self.setLocalKeyMap(keymap)
 297 
 298     ##
 299     # Raw event processor that takes the keycode and produces a string
 300     # that describes the key pressed.  The modifier keys are always
 301     # returned in the order C-, S-, A-, M-
 302     def decode(self,evt):
 303         keycode = evt.GetKeyCode()
 304         raw = evt.GetRawKeyCode()
 305         keyname = self.wxkeys.get(keycode, None)
 306         modifiers = ""
 307 
 308         # handle remapping of some modifier keys.  Should probably be
 309         # written to be more general so that all modifier keys could
 310         # be remapped.
 311         if self.remapMeta=="Alt":
 312             metadown=evt.AltDown()
 313             altdown=False
 314         else:
 315             altdown=evt.AltDown()
 316             if self.remapMeta=="Cmd":
 317                 metadown=evt.CmdDown()
 318             else:
 319                 metadown=evt.MetaDown()
 320 
 321         # Get the modifier string in order C-, S-, A-, M-
 322         for mod, ch in ((evt.ControlDown(), 'C-'),
 323                         (evt.ShiftDown(), 'S-'),
 324                         (altdown, 'A-'),
 325                         (metadown, 'M-')
 326                         ):
 327             if mod:
 328                 modifiers += ch
 329 
 330         # Check the sticky-meta
 331         if self.metaNext:
 332             if not metadown:
 333                 # if the actual meta modifier is not pressed, add it.  We don't want to end up with M-M-key
 334                 modifiers += 'M-'
 335             self.metaNext=False
 336 
 337             # if this is the second consecutive ESC, flag the next one
 338             # to cancel the keystroke input
 339             if keyname==self.stickyMeta:
 340                 self.nextStickyMetaCancel=True
 341         else:
 342             # ESC hasn't been pressed before, so flag it for next
 343             # time.
 344             if keyname==self.stickyMeta:
 345                 self.metaNext=True
 346 
 347         # check for printable character
 348         if keyname is None:
 349             if 27 < keycode < 256:
 350                 keyname = chr(keycode)
 351             else:
 352                 keyname = "(%s)unknown" % keycode
 353         if self.debug: print "keycode=%d raw=%d key=%s" % (keycode,raw,modifiers+keyname)
 354         return modifiers + keyname
 355 
 356     ##
 357     # reset the lookup table to the root in each keymap.
 358     def reset(self):
 359         if self.debug: print "reset"
 360         self.sofar = ''
 361         for keymap in self.keymaps:
 362             keymap.reset()
 363         if self.hasshown:
 364             # If we've displayed some stuff in the status area, clear
 365             # it.
 366             self.show('')
 367             self.hasshown=False
 368             
 369         self.number=None
 370         self.metaNext=False
 371         self.nextStickyMetaCancel=False
 372         self.processingArgument=0
 373         self.args=''
 374 
 375     ##
 376     # Display the current keystroke processing in the status area
 377     def show(self,text):
 378         if self.status:
 379             self.status.SetStatusText(text)
 380             self.hasshown=True
 381 
 382     ##
 383     # Attempt to add this keystroke by processing all keymaps in
 384     # parallel and stop at the first complete match.  The other way
 385     # that processing stops is if the new keystroke is unknown in all
 386     # keymaps.  Returns a tuple (skip,unknown,function), where skip is
 387     # true if the keystroke should be skipped up to the next event
 388     # handler, unknown is true if the partial keymap doesn't match
 389     # anything, and function is either None or the function to execute.
 390     def add(self, key):
 391         unknown=0
 392         processed=0
 393         function=None
 394         for keymap in self.keymaps:
 395             if keymap.add(key):
 396                 processed+=1
 397                 if keymap.function:
 398                     # once the first function is found, we stop processing
 399                     function=keymap.function
 400                     break
 401             if keymap.isUnknown():
 402                 unknown+=1
 403         if processed>0:
 404             # at least one keymap is still matching, so continue processing
 405             self.sofar += key + ' '
 406             if self.debug: print "add: sofar=%s processed=%d unknown=%d function=%s" % (self.sofar,processed,unknown,function)
 407         else:
 408             if unknown==self.num and self.sofar=='':
 409                 # if the keystroke doesn't match the first character
 410                 # in any of the keymaps, don't flag it as unknown.  It
 411                 # is a key that should be processed by the
 412                 # application, not us.
 413                 unknown=0
 414             if self.debug: print "add: sofar=%s processed=%d unknown=%d skipping %s" % (self.sofar,processed,unknown,key)
 415         return (processed==0,unknown==self.num,function)
 416 
 417     ##
 418     # This starts the emacs-style numeric arguments that are ended by
 419     # the first non-numeric keystroke
 420     def startArgument(self, key=None):
 421         self.number=None
 422         self.scale=1
 423         if key is not None:
 424             self.args=key + ' '
 425         self.processingArgument=1
 426 
 427     ##
 428     # Helper function to decode a numeric argument keystroke.  It can
 429     # be a number or, if the first keystroke, the '-' sign.  If C-U is
 430     # used to start the argumen processing, the numbers don't have to
 431     # have the Ctrl modifier pressed.
 432     def getNumber(self, key, musthavectrl=False):
 433         ctrl=False
 434         if key[0:2]=='C-':
 435             key=key[2:]
 436             ctrl=True
 437         if musthavectrl and not ctrl:
 438             return None
 439         
 440         # only allow minus sign at first character
 441         if key=='-' and self.processingArgument==1:
 442             return -1
 443         elif key>='0' and key<='9':
 444             return ord(key)-ord('0')
 445         return None
 446 
 447     ##
 448     # Process a numeric keystroke
 449     def argument(self, key):
 450         # allow control and a number to work as well
 451         num=self.getNumber(key)
 452         if num is None:
 453             # this keystroke isn't a number, so calculate the final
 454             # value of the numeric argument and flag that we're done
 455             if self.number is None:
 456                 self.number=self.defaultNumber
 457             else:
 458                 self.number=self.scale*self.number
 459             if self.debug: print "number = %d" % self.number
 460             self.processingArgument=0
 461         else:
 462             # this keystroke IS a number, so process it.
 463             if num==-1:
 464                 self.scale=-1
 465             else:
 466                 if self.number is None:
 467                     self.number=num
 468                 else:
 469                     self.number=10*self.number+num
 470             self.args+=key + ' '
 471             self.processingArgument+=1
 472 
 473     ##
 474     # The main driver routine.  Get a keystroke and run through the
 475     # processing chain.
 476     def process(self, evt):
 477         key = self.decode(evt)
 478 
 479         if key == self.abortKey:
 480             self.reset()
 481             self.show("Quit")
 482         elif self.nextStickyMetaCancel and key==self.stickyMeta:
 483             # this must be processed before the check for metaNext,
 484             # otherwise we'll never be able to process the ESC-ESC-ESC
 485             # quit sequence
 486             self.reset()
 487             self.show("Quit")
 488         elif self.metaNext:
 489             # OK, the meta sticky key is down, but it's not a quit
 490             # sequence
 491             self.show(self.args+self.sofar+" "+self.stickyMeta)
 492         elif key.endswith('-') and len(key) > 1 and not key.endswith('--'):
 493             #modifiers only, if we don't skip these events, then when people
 494             #hold down modifier keys, things get ugly
 495             evt.Skip()
 496         elif key==self.universalArgument:
 497             # signal the start of a numeric argument
 498             self.startArgument(key)
 499             self.show(self.args)
 500         elif not self.processingArgument and self.getNumber(key,musthavectrl=True) is not None:
 501             # allow Ctrl plus number keys to also start a numeric argument
 502             self.startArgument()
 503             self.argument(key)
 504         else:
 505             # OK, not one of those special cases.
 506 
 507             if self.processingArgument:
 508                 # if we're inside a numeric argument chain, show it.
 509                 # Note that processingArgument may get reset inside
 510                 # the call to argument()
 511                 self.argument(key)
 512                 self.show(self.args)
 513 
 514             # Can't use an else here because the flag
 515             # self.processingArgument may get reset inside
 516             # self.argument() if the key is not a number.  We don't
 517             # want to lose that keystroke if it isn't a number so
 518             # process it as a potential hotkey.
 519             if not self.processingArgument:
 520                 # So, we're not processing a numeric argument now.
 521                 # Check to see where we are in the processing chain.
 522                 skip,unknown,function=self.add(key)
 523                 if function:
 524                     # Found a function in one of the keymaps, so
 525                     # execute it.
 526                     save=self.number
 527                     self.reset()
 528                     if save is not None:
 529                         function(evt,save)
 530                     else:
 531                         function(evt)
 532                 elif unknown:
 533                     # This is an unknown keystroke combo
 534                     sf = "%s not defined."%(self.sofar)
 535                     self.reset()
 536                     self.show(sf)
 537                 elif skip:
 538                     # this is the first keystroke and it doesn't match
 539                     # anything.  Skip it up to the next event handler
 540                     # to get processed elsewhere.
 541                     self.reset()
 542                     evt.Skip()
 543                 else:
 544                     self.show(self.args+self.sofar)
 545 
 546 
 547 
 548 if __name__ == '__main__':
 549     #a utility function and class
 550     class StatusUpdater:
 551         def __init__(self, frame, message):
 552             self.frame = frame
 553             self.message = message
 554         def __call__(self, evt, number=None):
 555             if number is not None:
 556                 self.frame.SetStatusText("%d x %s" % (number,self.message))
 557             else:
 558                 self.frame.SetStatusText(self.message)
 559 
 560     class RemoveLocal:
 561         def __init__(self, frame, message):
 562             self.frame = frame
 563             self.message = message
 564         def __call__(self, evt, number=None):
 565             self.frame.keys.clearLocalKeyMap()
 566             self.frame.SetStatusText(self.message)
 567 
 568     class ApplyLocal:
 569         def __init__(self, frame, message):
 570             self.frame = frame
 571             self.message = message
 572         def __call__(self, evt, number=None):
 573             self.frame.keys.setLocalKeyMap(self.frame.localKeyMap)
 574             self.frame.SetStatusText(self.message)
 575 
 576     #The frame with hotkey chaining.
 577 
 578     class MainFrame(wx.Frame):
 579         def __init__(self):
 580             wx.Frame.__init__(self, None, -1, "test")
 581             self.CreateStatusBar()
 582             ctrl = self.ctrl = wx.TextCtrl(self, -1, style=wx.TE_MULTILINE|wx.WANTS_CHARS|wx.TE_RICH2)
 583             ctrl.SetFocus()
 584             ctrl.Bind(wx.EVT_KEY_DOWN, self.KeyPressed, ctrl)
 585 
 586             self.globalKeyMap=KeyMap()
 587             self.localKeyMap=KeyMap()
 588             self.keys=KeyProcessor(status=self)
 589             self.keys.setGlobalKeyMap(self.globalKeyMap)
 590             self.keys.setLocalKeyMap(self.localKeyMap)
 591 
 592             menuBar = wx.MenuBar()
 593             self.SetMenuBar(menuBar)  # Adding the MenuBar to the Frame content.
 594             self.menuBar = menuBar
 595 
 596             self.whichkeymap={}
 597             gmap = wx.Menu()
 598             self.whichkeymap[gmap]=self.globalKeyMap
 599             self.menuAddM(menuBar, gmap, "Global", "Global key map")
 600             self.menuAdd(gmap, "Open \tC-X\tC-F", "Open File", StatusUpdater(self, "open..."))
 601             self.menuAdd(gmap, "Save File\tC-X\tC-S", "Save Current File", StatusUpdater(self, "saved..."))
 602             self.menuAdd(gmap, "Sit \tC-X\tC-X\tC-S", "Sit", StatusUpdater(self, "sit..."))
 603             self.menuAdd(gmap, "Stay \tC-S\tC-X\tC-S", "Stay", StatusUpdater(self, "stay..."))
 604             self.menuAdd(gmap, "Execute \tCtrl-C Ctrl-C", "Execute Buffer", StatusUpdater(self, "execute buffer..."))
 605             self.menuAdd(gmap, "New Frame\tC-x 5 2", "New Frame", StatusUpdater(self, "open new frame"))
 606             self.menuAdd(gmap, "Help\tCtrl-H", "Help", StatusUpdater(self, "show help"))
 607             self.menuAdd(gmap, "Help\tShift+Z", "Shift Z", StatusUpdater(self, "Shift Z"))
 608             self.menuAdd(gmap, "Exit\tC-X C-C", "Exit", sys.exit)
 609 
 610             lmap = wx.Menu()
 611             self.whichkeymap[lmap]=self.localKeyMap
 612             self.menuAddM(menuBar, lmap, "Local", "Local key map")
 613             self.menuAdd(lmap, "Turn Off Local Keymap", "Turn off local keymap", RemoveLocal(self, "local keymap removed"))
 614             self.menuAdd(lmap, "Turn On Local Keymap", "Turn off local keymap", ApplyLocal(self, "local keymap added"))
 615             self.menuAdd(lmap, "Comment Region\tC-C C-C", "testdesc", StatusUpdater(self, "comment region"))
 616             self.menuAdd(lmap, "Stay \tC-S C-X C-S", "Stay", StatusUpdater(self, "stay..."))
 617             self.menuAdd(lmap, "Multi-Modifier \tC-S-a S-C-m", "Shift-Control test", StatusUpdater(self, "pressed Shift-Control-A, Shift-Control-M"))
 618             self.menuAdd(lmap, "Control a\tC-A", "lower case a", StatusUpdater(self, "pressed Control-A"))
 619             self.menuAdd(lmap, "Control b\tC-b", "upper case b", StatusUpdater(self, "pressed Control-B"))
 620             self.menuAdd(lmap, "Control Shift b\tC-S-b", "upper case b", StatusUpdater(self, "pressed Control-Shift-B"))
 621             self.menuAdd(lmap, "Control RET\tC-RET", "control-return", StatusUpdater(self, "pressed C-RET"))
 622             self.menuAdd(lmap, "Control SPC\tC-SPC", "control-space", StatusUpdater(self, "pressed C-SPC"))
 623             self.menuAdd(lmap, "Control Page Up\tC-PRIOR", "control-prior", StatusUpdater(self, "pressed C-PRIOR"))
 624             self.menuAdd(lmap, "Control F5\tC-F5", "control-f5", StatusUpdater(self, "pressed C-F5"))
 625             self.menuAdd(lmap, "Meta-X\tM-x", "meta-x", StatusUpdater(self, "pressed meta-x"))
 626             self.menuAdd(lmap, "Meta-nothing\tM-", "meta-nothing", StatusUpdater(self, "pressed meta-nothing"))
 627             self.menuAdd(lmap, "Double Meta-nothing\tM- M-", "meta-nothing", StatusUpdater(self, "pressed meta-nothing"))
 628 
 629             #print self.lookup
 630             self.Show(1)
 631 
 632 
 633         def menuAdd(self, menu, name, desc, fcn, id=-1, kind=wx.ITEM_NORMAL):
 634             if id == -1:
 635                 id = wx.NewId()
 636             a = wx.MenuItem(menu, id, 'TEMPORARYNAME', desc, kind)
 637             menu.AppendItem(a)
 638             wx.EVT_MENU(self, id, fcn)
 639 
 640             def _spl(st):
 641                 if '\t' in st:
 642                     return st.split('\t', 1)
 643                 return st, ''
 644 
 645             ns, acc = _spl(name)
 646 
 647             if acc:
 648                 if menu in self.whichkeymap:
 649                     keymap=self.whichkeymap[menu]
 650                 else:
 651                     # menu not listed in menu-to-keymap mapping.  Put in
 652                     # local
 653                     keymap=self.localKeyMap
 654                 keymap.define(acc, fcn)
 655 
 656                 acc=acc.replace('\t',' ')
 657                 #print "acc=%s" % acc
 658                 if wx.Platform == '__WXMSW__':
 659                     # If windows recognizes the accelerator (e.g. "Ctrl+A") OR
 660                     # it doesn't recognize the whole accererator text but does
 661                     # recognize the last part (e.g. "C-A" where it doesn't
 662                     # know what the "C-" is but does see the "A", it will
 663                     # automatically process the accelerator before we even see
 664                     # it.  So, append an ascii zero to the end.
 665                     menu.SetLabel(id, '%s\t%s\00'%(ns,acc))
 666                 else:
 667                     # unix doesn't allow displaying arbitrary text as the
 668                     # accelerator, so we have to just put it in the menu
 669                     # itself.  This doesn't look very nice, but that's about
 670                     # all we can do.
 671                     menu.SetLabel(id, '%s (%s)'%(ns,acc))
 672             else:
 673                 menu.SetLabel(id,ns)
 674             menu.SetHelpString(id, desc)
 675 
 676         def menuAddM(self, parent, menu, name, help=''):
 677             if isinstance(parent, wx.Menu) or isinstance(parent, wx.MenuPtr):
 678                 id = wx.NewId()
 679                 parent.AppendMenu(id, "TEMPORARYNAME", menu, help)
 680 
 681                 self.menuBar.SetLabel(id, name)
 682                 self.menuBar.SetHelpString(id, help)
 683             else:
 684                 parent.Append(menu, name)
 685 
 686         def KeyPressed(self, evt):
 687             self.keys.process(evt)
 688     
 689     app = wx.PySimpleApp()
 690     frame = MainFrame()
 691     app.MainLoop()

-- RobMcMullen

EmacsStyleKeybindings (last edited 2010-12-14 00:20:20 by 208)

NOTE: To edit pages in this wiki you must be a member of the TrustedEditorsGroup.