1 """
2 Dialog for building Tkinter accelerator key bindings
3 """
4 from tkinter import Toplevel, Listbox, StringVar, TclError
5 from tkinter.ttk import Frame, Button, Checkbutton, Entry, Label, Scrollbar
6 from tkinter import messagebox
7 from tkinter.simpledialog import _setup_dialog
8 import string
9 import sys
10
11
12 FUNCTION_KEYS = ('F1', 'F2' ,'F3' ,'F4' ,'F5' ,'F6',
13 'F7', 'F8' ,'F9' ,'F10' ,'F11' ,'F12')
14 ALPHANUM_KEYS = tuple(string.ascii_lowercase + string.digits)
15 PUNCTUATION_KEYS = tuple('~!@#%^&*()_-+={}[]|;:,.<>/?')
16 WHITESPACE_KEYS = ('Tab', 'Space', 'Return')
17 EDIT_KEYS = ('BackSpace', 'Delete', 'Insert')
18 MOVE_KEYS = ('Home', 'End', 'Page Up', 'Page Down', 'Left Arrow',
19 'Right Arrow', 'Up Arrow', 'Down Arrow')
20 AVAILABLE_KEYS = (ALPHANUM_KEYS + PUNCTUATION_KEYS + FUNCTION_KEYS +
21 WHITESPACE_KEYS + EDIT_KEYS + MOVE_KEYS)
22
23
24 def 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
44 class ESC[4;38;5;81mGetKeysFrame(ESC[4;38;5;149mFrame):
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
282 class ESC[4;38;5;81mGetKeysWindow(ESC[4;38;5;149mToplevel):
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
349 if __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(GetKeysWindow)