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