1""" 2Dialog for building Tkinter accelerator key bindings 3""" 4from tkinter import Toplevel, Listbox, Text, StringVar, TclError 5from tkinter.ttk import Frame, Button, Checkbutton, Entry, Label, Scrollbar 6from tkinter import messagebox 7import string 8import sys 9 10 11FUNCTION_KEYS = ('F1', 'F2' ,'F3' ,'F4' ,'F5' ,'F6', 12 'F7', 'F8' ,'F9' ,'F10' ,'F11' ,'F12') 13ALPHANUM_KEYS = tuple(string.ascii_lowercase + string.digits) 14PUNCTUATION_KEYS = tuple('~!@#%^&*()_-+={}[]|;:,.<>/?') 15WHITESPACE_KEYS = ('Tab', 'Space', 'Return') 16EDIT_KEYS = ('BackSpace', 'Delete', 'Insert') 17MOVE_KEYS = ('Home', 'End', 'Page Up', 'Page Down', 'Left Arrow', 18 'Right Arrow', 'Up Arrow', 'Down Arrow') 19AVAILABLE_KEYS = (ALPHANUM_KEYS + PUNCTUATION_KEYS + FUNCTION_KEYS + 20 WHITESPACE_KEYS + EDIT_KEYS + MOVE_KEYS) 21 22 23def translate_key(key, modifiers): 24 "Translate from keycap symbol to the Tkinter keysym." 25 mapping = {'Space':'space', 26 '~':'asciitilde', '!':'exclam', '@':'at', '#':'numbersign', 27 '%':'percent', '^':'asciicircum', '&':'ampersand', 28 '*':'asterisk', '(':'parenleft', ')':'parenright', 29 '_':'underscore', '-':'minus', '+':'plus', '=':'equal', 30 '{':'braceleft', '}':'braceright', 31 '[':'bracketleft', ']':'bracketright', '|':'bar', 32 ';':'semicolon', ':':'colon', ',':'comma', '.':'period', 33 '<':'less', '>':'greater', '/':'slash', '?':'question', 34 'Page Up':'Prior', 'Page Down':'Next', 35 'Left Arrow':'Left', 'Right Arrow':'Right', 36 'Up Arrow':'Up', 'Down Arrow': 'Down', 'Tab':'Tab'} 37 key = mapping.get(key, key) 38 if 'Shift' in modifiers and key in string.ascii_lowercase: 39 key = key.upper() 40 return f'Key-{key}' 41 42 43class GetKeysDialog(Toplevel): 44 45 # Dialog title for invalid key sequence 46 keyerror_title = 'Key Sequence Error' 47 48 def __init__(self, parent, title, action, current_key_sequences, 49 *, _htest=False, _utest=False): 50 """ 51 parent - parent of this dialog 52 title - string which is the title of the popup dialog 53 action - string, the name of the virtual event these keys will be 54 mapped to 55 current_key_sequences - list, a list of all key sequence lists 56 currently mapped to virtual events, for overlap checking 57 _htest - bool, change box location when running htest 58 _utest - bool, do not wait when running unittest 59 """ 60 Toplevel.__init__(self, parent) 61 self.withdraw() # Hide while setting geometry. 62 self.configure(borderwidth=5) 63 self.resizable(height=False, width=False) 64 self.title(title) 65 self.transient(parent) 66 self.grab_set() 67 self.protocol("WM_DELETE_WINDOW", self.cancel) 68 self.parent = parent 69 self.action = action 70 self.current_key_sequences = current_key_sequences 71 self.result = '' 72 self.key_string = StringVar(self) 73 self.key_string.set('') 74 # Set self.modifiers, self.modifier_label. 75 self.set_modifiers_for_platform() 76 self.modifier_vars = [] 77 for modifier in self.modifiers: 78 variable = StringVar(self) 79 variable.set('') 80 self.modifier_vars.append(variable) 81 self.advanced = False 82 self.create_widgets() 83 self.update_idletasks() 84 self.geometry( 85 "+%d+%d" % ( 86 parent.winfo_rootx() + 87 (parent.winfo_width()/2 - self.winfo_reqwidth()/2), 88 parent.winfo_rooty() + 89 ((parent.winfo_height()/2 - self.winfo_reqheight()/2) 90 if not _htest else 150) 91 ) ) # Center dialog over parent (or below htest box). 92 if not _utest: 93 self.deiconify() # Geometry set, unhide. 94 self.wait_window() 95 96 def showerror(self, *args, **kwargs): 97 # Make testing easier. Replace in #30751. 98 messagebox.showerror(*args, **kwargs) 99 100 def create_widgets(self): 101 self.frame = frame = Frame(self, borderwidth=2, relief='sunken') 102 frame.pack(side='top', expand=True, fill='both') 103 104 frame_buttons = Frame(self) 105 frame_buttons.pack(side='bottom', fill='x') 106 107 self.button_ok = Button(frame_buttons, text='OK', 108 width=8, command=self.ok) 109 self.button_ok.grid(row=0, column=0, padx=5, pady=5) 110 self.button_cancel = Button(frame_buttons, text='Cancel', 111 width=8, command=self.cancel) 112 self.button_cancel.grid(row=0, column=1, padx=5, pady=5) 113 114 # Basic entry key sequence. 115 self.frame_keyseq_basic = Frame(frame, name='keyseq_basic') 116 self.frame_keyseq_basic.grid(row=0, column=0, sticky='nsew', 117 padx=5, pady=5) 118 basic_title = Label(self.frame_keyseq_basic, 119 text=f"New keys for '{self.action}' :") 120 basic_title.pack(anchor='w') 121 122 basic_keys = Label(self.frame_keyseq_basic, justify='left', 123 textvariable=self.key_string, relief='groove', 124 borderwidth=2) 125 basic_keys.pack(ipadx=5, ipady=5, fill='x') 126 127 # Basic entry controls. 128 self.frame_controls_basic = Frame(frame) 129 self.frame_controls_basic.grid(row=1, column=0, sticky='nsew', padx=5) 130 131 # Basic entry modifiers. 132 self.modifier_checkbuttons = {} 133 column = 0 134 for modifier, variable in zip(self.modifiers, self.modifier_vars): 135 label = self.modifier_label.get(modifier, modifier) 136 check = Checkbutton(self.frame_controls_basic, 137 command=self.build_key_string, text=label, 138 variable=variable, onvalue=modifier, offvalue='') 139 check.grid(row=0, column=column, padx=2, sticky='w') 140 self.modifier_checkbuttons[modifier] = check 141 column += 1 142 143 # Basic entry help text. 144 help_basic = Label(self.frame_controls_basic, justify='left', 145 text="Select the desired modifier keys\n"+ 146 "above, and the final key from the\n"+ 147 "list on the right.\n\n" + 148 "Use upper case Symbols when using\n" + 149 "the Shift modifier. (Letters will be\n" + 150 "converted automatically.)") 151 help_basic.grid(row=1, column=0, columnspan=4, padx=2, sticky='w') 152 153 # Basic entry key list. 154 self.list_keys_final = Listbox(self.frame_controls_basic, width=15, 155 height=10, selectmode='single') 156 self.list_keys_final.insert('end', *AVAILABLE_KEYS) 157 self.list_keys_final.bind('<ButtonRelease-1>', self.final_key_selected) 158 self.list_keys_final.grid(row=0, column=4, rowspan=4, sticky='ns') 159 scroll_keys_final = Scrollbar(self.frame_controls_basic, 160 orient='vertical', 161 command=self.list_keys_final.yview) 162 self.list_keys_final.config(yscrollcommand=scroll_keys_final.set) 163 scroll_keys_final.grid(row=0, column=5, rowspan=4, sticky='ns') 164 self.button_clear = Button(self.frame_controls_basic, 165 text='Clear Keys', 166 command=self.clear_key_seq) 167 self.button_clear.grid(row=2, column=0, columnspan=4) 168 169 # Advanced entry key sequence. 170 self.frame_keyseq_advanced = Frame(frame, name='keyseq_advanced') 171 self.frame_keyseq_advanced.grid(row=0, column=0, sticky='nsew', 172 padx=5, pady=5) 173 advanced_title = Label(self.frame_keyseq_advanced, justify='left', 174 text=f"Enter new binding(s) for '{self.action}' :\n" + 175 "(These bindings will not be checked for validity!)") 176 advanced_title.pack(anchor='w') 177 self.advanced_keys = Entry(self.frame_keyseq_advanced, 178 textvariable=self.key_string) 179 self.advanced_keys.pack(fill='x') 180 181 # Advanced entry help text. 182 self.frame_help_advanced = Frame(frame) 183 self.frame_help_advanced.grid(row=1, column=0, sticky='nsew', padx=5) 184 help_advanced = Label(self.frame_help_advanced, justify='left', 185 text="Key bindings are specified using Tkinter keysyms as\n"+ 186 "in these samples: <Control-f>, <Shift-F2>, <F12>,\n" 187 "<Control-space>, <Meta-less>, <Control-Alt-Shift-X>.\n" 188 "Upper case is used when the Shift modifier is present!\n\n" + 189 "'Emacs style' multi-keystroke bindings are specified as\n" + 190 "follows: <Control-x><Control-y>, where the first key\n" + 191 "is the 'do-nothing' keybinding.\n\n" + 192 "Multiple separate bindings for one action should be\n"+ 193 "separated by a space, eg., <Alt-v> <Meta-v>." ) 194 help_advanced.grid(row=0, column=0, sticky='nsew') 195 196 # Switch between basic and advanced. 197 self.button_level = Button(frame, command=self.toggle_level, 198 text='<< Basic Key Binding Entry') 199 self.button_level.grid(row=2, column=0, stick='ew', padx=5, pady=5) 200 self.toggle_level() 201 202 def set_modifiers_for_platform(self): 203 """Determine list of names of key modifiers for this platform. 204 205 The names are used to build Tk bindings -- it doesn't matter if the 206 keyboard has these keys; it matters if Tk understands them. The 207 order is also important: key binding equality depends on it, so 208 config-keys.def must use the same ordering. 209 """ 210 if sys.platform == "darwin": 211 self.modifiers = ['Shift', 'Control', 'Option', 'Command'] 212 else: 213 self.modifiers = ['Control', 'Alt', 'Shift'] 214 self.modifier_label = {'Control': 'Ctrl'} # Short name. 215 216 def toggle_level(self): 217 "Toggle between basic and advanced keys." 218 if self.button_level.cget('text').startswith('Advanced'): 219 self.clear_key_seq() 220 self.button_level.config(text='<< Basic Key Binding Entry') 221 self.frame_keyseq_advanced.lift() 222 self.frame_help_advanced.lift() 223 self.advanced_keys.focus_set() 224 self.advanced = True 225 else: 226 self.clear_key_seq() 227 self.button_level.config(text='Advanced Key Binding Entry >>') 228 self.frame_keyseq_basic.lift() 229 self.frame_controls_basic.lift() 230 self.advanced = False 231 232 def final_key_selected(self, event=None): 233 "Handler for clicking on key in basic settings list." 234 self.build_key_string() 235 236 def build_key_string(self): 237 "Create formatted string of modifiers plus the key." 238 keylist = modifiers = self.get_modifiers() 239 final_key = self.list_keys_final.get('anchor') 240 if final_key: 241 final_key = translate_key(final_key, modifiers) 242 keylist.append(final_key) 243 self.key_string.set(f"<{'-'.join(keylist)}>") 244 245 def get_modifiers(self): 246 "Return ordered list of modifiers that have been selected." 247 mod_list = [variable.get() for variable in self.modifier_vars] 248 return [mod for mod in mod_list if mod] 249 250 def clear_key_seq(self): 251 "Clear modifiers and keys selection." 252 self.list_keys_final.select_clear(0, 'end') 253 self.list_keys_final.yview('moveto', '0.0') 254 for variable in self.modifier_vars: 255 variable.set('') 256 self.key_string.set('') 257 258 def ok(self, event=None): 259 keys = self.key_string.get().strip() 260 if not keys: 261 self.showerror(title=self.keyerror_title, parent=self, 262 message="No key specified.") 263 return 264 if (self.advanced or self.keys_ok(keys)) and self.bind_ok(keys): 265 self.result = keys 266 self.grab_release() 267 self.destroy() 268 269 def cancel(self, event=None): 270 self.result = '' 271 self.grab_release() 272 self.destroy() 273 274 def keys_ok(self, keys): 275 """Validity check on user's 'basic' keybinding selection. 276 277 Doesn't check the string produced by the advanced dialog because 278 'modifiers' isn't set. 279 """ 280 final_key = self.list_keys_final.get('anchor') 281 modifiers = self.get_modifiers() 282 title = self.keyerror_title 283 key_sequences = [key for keylist in self.current_key_sequences 284 for key in keylist] 285 if not keys.endswith('>'): 286 self.showerror(title, parent=self, 287 message='Missing the final Key') 288 elif (not modifiers 289 and final_key not in FUNCTION_KEYS + MOVE_KEYS): 290 self.showerror(title=title, parent=self, 291 message='No modifier key(s) specified.') 292 elif (modifiers == ['Shift']) \ 293 and (final_key not in 294 FUNCTION_KEYS + MOVE_KEYS + ('Tab', 'Space')): 295 msg = 'The shift modifier by itself may not be used with'\ 296 ' this key symbol.' 297 self.showerror(title=title, parent=self, message=msg) 298 elif keys in key_sequences: 299 msg = 'This key combination is already in use.' 300 self.showerror(title=title, parent=self, message=msg) 301 else: 302 return True 303 return False 304 305 def bind_ok(self, keys): 306 "Return True if Tcl accepts the new keys else show message." 307 try: 308 binding = self.bind(keys, lambda: None) 309 except TclError as err: 310 self.showerror( 311 title=self.keyerror_title, parent=self, 312 message=(f'The entered key sequence is not accepted.\n\n' 313 f'Error: {err}')) 314 return False 315 else: 316 self.unbind(keys, binding) 317 return True 318 319 320if __name__ == '__main__': 321 from unittest import main 322 main('idlelib.idle_test.test_config_key', verbosity=2, exit=False) 323 324 from idlelib.idle_test.htest import run 325 run(GetKeysDialog) 326