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.
The best answer is "It depends." The various Emacsen that I've used on the Mac use the Apple key (or Cmd key) the same as the Alt key on PCs. In other words it is used as the Meta key in Emacs. In wx.KeyEvent the MetaDown() method will be true when the Cmd key is pressed.
However, if you want to be compliant with other Mac apps, then the Cmd key is usually used the same way that Ctrl would be used in apps on the PC. For example, standard accelerator keys like Ctrl-C, Ctrl-V, Ctrl-Z, etc. in PC apps would be Cmd-C, Cmd-V, Cmd-Z, etc. in Mac apps. In wx any accelerators defined in menu items with Ctrl are automatically converted to be Cmd accelerators. Also, in wx.KeyEvent you can use CmdDown() and it will be equivallent to either ControlDown or MetaDown depending on the platform you are running on.
-- RobinDunn
Update: I've recently updated my Emacs config on Mac to use the Option (Alt) key as the Emacs Alt/Meta key, and to leave the Cmd key alone so it can be bound to standard functions that will make it act like the Cmd key in all other Apple HIG compliant apps. This makes it feel much more natural and a better fit on the platform, but it sacrifices the default functionality of the Option key on Macs, which is like the AltGr key on non-U.S. keyboards on a PC and is how special accented characters and etc. are entered. But since I'm a English speaker and Emacs for me is almost exclusivly for editing source code this was not a great loss for me.
-- RobinDunn
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:
- Ctrl-C
- Alt-A
- C-X C-S
- C-X 5 2
- Meta-Alt-Shift-Q
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:
so that the keybinding is defined by:
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