• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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