Commit | Line | Data |
---|---|---|
920dae64 AT |
1 | """ |
2 | Dialog for building Tkinter accelerator key bindings | |
3 | """ | |
4 | from Tkinter import * | |
5 | import tkMessageBox | |
6 | import string, os | |
7 | ||
8 | class GetKeysDialog(Toplevel): | |
9 | def __init__(self,parent,title,action,currentKeySequences): | |
10 | """ | |
11 | action - string, the name of the virtual event these keys will be | |
12 | mapped to | |
13 | currentKeys - list, a list of all key sequence lists currently mapped | |
14 | to virtual events, for overlap checking | |
15 | """ | |
16 | Toplevel.__init__(self, parent) | |
17 | self.configure(borderwidth=5) | |
18 | self.resizable(height=FALSE,width=FALSE) | |
19 | self.title(title) | |
20 | self.transient(parent) | |
21 | self.grab_set() | |
22 | self.protocol("WM_DELETE_WINDOW", self.Cancel) | |
23 | self.parent = parent | |
24 | self.action=action | |
25 | self.currentKeySequences=currentKeySequences | |
26 | self.result='' | |
27 | self.keyString=StringVar(self) | |
28 | self.keyString.set('') | |
29 | self.SetModifiersForPlatform() | |
30 | self.modifier_vars = [] | |
31 | for modifier in self.modifiers: | |
32 | variable = StringVar(self) | |
33 | variable.set('') | |
34 | self.modifier_vars.append(variable) | |
35 | self.CreateWidgets() | |
36 | self.LoadFinalKeyList() | |
37 | self.withdraw() #hide while setting geometry | |
38 | self.update_idletasks() | |
39 | self.geometry("+%d+%d" % | |
40 | ((parent.winfo_rootx()+((parent.winfo_width()/2) | |
41 | -(self.winfo_reqwidth()/2)), | |
42 | parent.winfo_rooty()+((parent.winfo_height()/2) | |
43 | -(self.winfo_reqheight()/2)) )) ) #centre dialog over parent | |
44 | self.deiconify() #geometry set, unhide | |
45 | self.wait_window() | |
46 | ||
47 | def CreateWidgets(self): | |
48 | frameMain = Frame(self,borderwidth=2,relief=SUNKEN) | |
49 | frameMain.pack(side=TOP,expand=TRUE,fill=BOTH) | |
50 | frameButtons=Frame(self) | |
51 | frameButtons.pack(side=BOTTOM,fill=X) | |
52 | self.buttonOK = Button(frameButtons,text='OK', | |
53 | width=8,command=self.OK) | |
54 | self.buttonOK.grid(row=0,column=0,padx=5,pady=5) | |
55 | self.buttonCancel = Button(frameButtons,text='Cancel', | |
56 | width=8,command=self.Cancel) | |
57 | self.buttonCancel.grid(row=0,column=1,padx=5,pady=5) | |
58 | self.frameKeySeqBasic = Frame(frameMain) | |
59 | self.frameKeySeqAdvanced = Frame(frameMain) | |
60 | self.frameControlsBasic = Frame(frameMain) | |
61 | self.frameHelpAdvanced = Frame(frameMain) | |
62 | self.frameKeySeqAdvanced.grid(row=0,column=0,sticky=NSEW,padx=5,pady=5) | |
63 | self.frameKeySeqBasic.grid(row=0,column=0,sticky=NSEW,padx=5,pady=5) | |
64 | self.frameKeySeqBasic.lift() | |
65 | self.frameHelpAdvanced.grid(row=1,column=0,sticky=NSEW,padx=5) | |
66 | self.frameControlsBasic.grid(row=1,column=0,sticky=NSEW,padx=5) | |
67 | self.frameControlsBasic.lift() | |
68 | self.buttonLevel = Button(frameMain,command=self.ToggleLevel, | |
69 | text='Advanced Key Binding Entry >>') | |
70 | self.buttonLevel.grid(row=2,column=0,stick=EW,padx=5,pady=5) | |
71 | labelTitleBasic = Label(self.frameKeySeqBasic, | |
72 | text="New keys for '"+self.action+"' :") | |
73 | labelTitleBasic.pack(anchor=W) | |
74 | labelKeysBasic = Label(self.frameKeySeqBasic,justify=LEFT, | |
75 | textvariable=self.keyString,relief=GROOVE,borderwidth=2) | |
76 | labelKeysBasic.pack(ipadx=5,ipady=5,fill=X) | |
77 | self.modifier_checkbuttons = {} | |
78 | column = 0 | |
79 | for modifier, variable in zip(self.modifiers, self.modifier_vars): | |
80 | label = self.modifier_label.get(modifier, modifier) | |
81 | check=Checkbutton(self.frameControlsBasic, | |
82 | command=self.BuildKeyString, | |
83 | text=label,variable=variable,onvalue=modifier,offvalue='') | |
84 | check.grid(row=0,column=column,padx=2,sticky=W) | |
85 | self.modifier_checkbuttons[modifier] = check | |
86 | column += 1 | |
87 | labelFnAdvice=Label(self.frameControlsBasic,justify=LEFT, | |
88 | text=\ | |
89 | "Select the desired modifier keys\n"+ | |
90 | "above, and the final key from the\n"+ | |
91 | "list on the right.\n\n" + | |
92 | "Use upper case Symbols when using\n" + | |
93 | "the Shift modifier. (Letters will be\n" + | |
94 | "converted automatically.)") | |
95 | labelFnAdvice.grid(row=1,column=0,columnspan=4,padx=2,sticky=W) | |
96 | self.listKeysFinal=Listbox(self.frameControlsBasic,width=15,height=10, | |
97 | selectmode=SINGLE) | |
98 | self.listKeysFinal.bind('<ButtonRelease-1>',self.FinalKeySelected) | |
99 | self.listKeysFinal.grid(row=0,column=4,rowspan=4,sticky=NS) | |
100 | scrollKeysFinal=Scrollbar(self.frameControlsBasic,orient=VERTICAL, | |
101 | command=self.listKeysFinal.yview) | |
102 | self.listKeysFinal.config(yscrollcommand=scrollKeysFinal.set) | |
103 | scrollKeysFinal.grid(row=0,column=5,rowspan=4,sticky=NS) | |
104 | self.buttonClear=Button(self.frameControlsBasic, | |
105 | text='Clear Keys',command=self.ClearKeySeq) | |
106 | self.buttonClear.grid(row=2,column=0,columnspan=4) | |
107 | labelTitleAdvanced = Label(self.frameKeySeqAdvanced,justify=LEFT, | |
108 | text="Enter new binding(s) for '"+self.action+"' :\n"+ | |
109 | "(These bindings will not be checked for validity!)") | |
110 | labelTitleAdvanced.pack(anchor=W) | |
111 | self.entryKeysAdvanced=Entry(self.frameKeySeqAdvanced, | |
112 | textvariable=self.keyString) | |
113 | self.entryKeysAdvanced.pack(fill=X) | |
114 | labelHelpAdvanced=Label(self.frameHelpAdvanced,justify=LEFT, | |
115 | text="Key bindings are specified using Tkinter keysyms as\n"+ | |
116 | "in these samples: <Control-f>, <Shift-F2>, <F12>,\n" | |
117 | "<Control-space>, <Meta-less>, <Control-Alt-Shift-X>.\n" | |
118 | "Upper case is used when the Shift modifier is present!\n\n" + | |
119 | "'Emacs style' multi-keystroke bindings are specified as\n" + | |
120 | "follows: <Control-x><Control-y>, where the first key\n" + | |
121 | "is the 'do-nothing' keybinding.\n\n" + | |
122 | "Multiple separate bindings for one action should be\n"+ | |
123 | "separated by a space, eg., <Alt-v> <Meta-v>." ) | |
124 | labelHelpAdvanced.grid(row=0,column=0,sticky=NSEW) | |
125 | ||
126 | def SetModifiersForPlatform(self): | |
127 | """Determine list of names of key modifiers for this platform. | |
128 | ||
129 | The names are used to build Tk bindings -- it doesn't matter if the | |
130 | keyboard has these keys, it matters if Tk understands them. The | |
131 | order is also important: key binding equality depends on it, so | |
132 | config-keys.def must use the same ordering. | |
133 | """ | |
134 | import sys | |
135 | if sys.platform == 'darwin' and sys.executable.count('.app'): | |
136 | self.modifiers = ['Shift', 'Control', 'Option', 'Command'] | |
137 | else: | |
138 | self.modifiers = ['Control', 'Alt', 'Shift'] | |
139 | self.modifier_label = {'Control': 'Ctrl'} | |
140 | ||
141 | def ToggleLevel(self): | |
142 | if self.buttonLevel.cget('text')[:8]=='Advanced': | |
143 | self.ClearKeySeq() | |
144 | self.buttonLevel.config(text='<< Basic Key Binding Entry') | |
145 | self.frameKeySeqAdvanced.lift() | |
146 | self.frameHelpAdvanced.lift() | |
147 | self.entryKeysAdvanced.focus_set() | |
148 | else: | |
149 | self.ClearKeySeq() | |
150 | self.buttonLevel.config(text='Advanced Key Binding Entry >>') | |
151 | self.frameKeySeqBasic.lift() | |
152 | self.frameControlsBasic.lift() | |
153 | ||
154 | def FinalKeySelected(self,event): | |
155 | self.BuildKeyString() | |
156 | ||
157 | def BuildKeyString(self): | |
158 | keyList = modifiers = self.GetModifiers() | |
159 | finalKey = self.listKeysFinal.get(ANCHOR) | |
160 | if finalKey: | |
161 | finalKey = self.TranslateKey(finalKey, modifiers) | |
162 | keyList.append(finalKey) | |
163 | self.keyString.set('<' + string.join(keyList,'-') + '>') | |
164 | ||
165 | def GetModifiers(self): | |
166 | modList = [variable.get() for variable in self.modifier_vars] | |
167 | return filter(None, modList) | |
168 | ||
169 | def ClearKeySeq(self): | |
170 | self.listKeysFinal.select_clear(0,END) | |
171 | self.listKeysFinal.yview(MOVETO, '0.0') | |
172 | for variable in self.modifier_vars: | |
173 | variable.set('') | |
174 | self.keyString.set('') | |
175 | ||
176 | def LoadFinalKeyList(self): | |
177 | #these tuples are also available for use in validity checks | |
178 | self.functionKeys=('F1','F2','F2','F4','F5','F6','F7','F8','F9', | |
179 | 'F10','F11','F12') | |
180 | self.alphanumKeys=tuple(string.ascii_lowercase+string.digits) | |
181 | self.punctuationKeys=tuple('~!@#%^&*()_-+={}[]|;:,.<>/?') | |
182 | self.whitespaceKeys=('Tab','Space','Return') | |
183 | self.editKeys=('BackSpace','Delete','Insert') | |
184 | self.moveKeys=('Home','End','Page Up','Page Down','Left Arrow', | |
185 | 'Right Arrow','Up Arrow','Down Arrow') | |
186 | #make a tuple of most of the useful common 'final' keys | |
187 | keys=(self.alphanumKeys+self.punctuationKeys+self.functionKeys+ | |
188 | self.whitespaceKeys+self.editKeys+self.moveKeys) | |
189 | self.listKeysFinal.insert(END, *keys) | |
190 | ||
191 | def TranslateKey(self, key, modifiers): | |
192 | "Translate from keycap symbol to the Tkinter keysym" | |
193 | translateDict = {'Space':'space', | |
194 | '~':'asciitilde','!':'exclam','@':'at','#':'numbersign', | |
195 | '%':'percent','^':'asciicircum','&':'ampersand','*':'asterisk', | |
196 | '(':'parenleft',')':'parenright','_':'underscore','-':'minus', | |
197 | '+':'plus','=':'equal','{':'braceleft','}':'braceright', | |
198 | '[':'bracketleft',']':'bracketright','|':'bar',';':'semicolon', | |
199 | ':':'colon',',':'comma','.':'period','<':'less','>':'greater', | |
200 | '/':'slash','?':'question','Page Up':'Prior','Page Down':'Next', | |
201 | 'Left Arrow':'Left','Right Arrow':'Right','Up Arrow':'Up', | |
202 | 'Down Arrow': 'Down', 'Tab':'tab'} | |
203 | if key in translateDict.keys(): | |
204 | key = translateDict[key] | |
205 | if 'Shift' in modifiers and key in string.ascii_lowercase: | |
206 | key = key.upper() | |
207 | key = 'Key-' + key | |
208 | return key | |
209 | ||
210 | def OK(self, event=None): | |
211 | if self.KeysOK(): | |
212 | self.result=self.keyString.get() | |
213 | self.destroy() | |
214 | ||
215 | def Cancel(self, event=None): | |
216 | self.result='' | |
217 | self.destroy() | |
218 | ||
219 | def KeysOK(self): | |
220 | "Validity check on user's keybinding selection" | |
221 | keys = self.keyString.get() | |
222 | keys.strip() | |
223 | finalKey = self.listKeysFinal.get(ANCHOR) | |
224 | modifiers = self.GetModifiers() | |
225 | # create a key sequence list for overlap check: | |
226 | keySequence = keys.split() | |
227 | keysOK = False | |
228 | title = 'Key Sequence Error' | |
229 | if not keys: | |
230 | tkMessageBox.showerror(title=title, parent=self, | |
231 | message='No keys specified.') | |
232 | elif not keys.endswith('>'): | |
233 | tkMessageBox.showerror(title=title, parent=self, | |
234 | message='Missing the final Key') | |
235 | elif not modifiers and finalKey not in self.functionKeys: | |
236 | tkMessageBox.showerror(title=title, parent=self, | |
237 | message='No modifier key(s) specified.') | |
238 | elif (modifiers == ['Shift']) \ | |
239 | and (finalKey not in | |
240 | self.functionKeys + ('Tab', 'Space')): | |
241 | msg = 'The shift modifier by itself may not be used with' \ | |
242 | ' this key symbol; only with F1-F12, Tab, or Space' | |
243 | tkMessageBox.showerror(title=title, parent=self, | |
244 | message=msg) | |
245 | elif keySequence in self.currentKeySequences: | |
246 | msg = 'This key combination is already in use.' | |
247 | tkMessageBox.showerror(title=title, parent=self, | |
248 | message=msg) | |
249 | else: | |
250 | keysOK = True | |
251 | return keysOK | |
252 | ||
253 | if __name__ == '__main__': | |
254 | #test the dialog | |
255 | root=Tk() | |
256 | def run(): | |
257 | keySeq='' | |
258 | dlg=GetKeysDialog(root,'Get Keys','find-again',[]) | |
259 | print dlg.result | |
260 | Button(root,text='Dialog',command=run).pack() | |
261 | root.mainloop() |