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