config_key.py 13.1 KB
Newer Older
1
"""
2
Dialog for building Tkinter accelerator key bindings
3
"""
4
from tkinter import *
5
from tkinter.ttk import Scrollbar
6
from tkinter import messagebox
7
import string
8
import sys
9

10

11
class GetKeysDialog(Toplevel):
12 13 14 15

    # Dialog title for invalid key sequence
    keyerror_title = 'Key Sequence Error'

16
    def __init__(self, parent, title, action, currentKeySequences,
17
                 *, _htest=False, _utest=False):
18 19 20 21
        """
        action - string, the name of the virtual event these keys will be
                 mapped to
        currentKeys - list, a list of all key sequence lists currently mapped
22
                 to virtual events, for overlap checking
23
        _utest - bool, do not wait when running unittest
24
        _htest - bool, change box location when running htest
25
        """
26
        Toplevel.__init__(self, parent)
27
        self.withdraw() #hide while setting geometry
28
        self.configure(borderwidth=5)
29
        self.resizable(height=FALSE, width=FALSE)
30 31 32 33 34 35
        self.title(title)
        self.transient(parent)
        self.grab_set()
        self.protocol("WM_DELETE_WINDOW", self.Cancel)
        self.parent = parent
        self.action=action
36 37 38
        self.currentKeySequences = currentKeySequences
        self.result = ''
        self.keyString = StringVar(self)
39
        self.keyString.set('')
40
        self.SetModifiersForPlatform() # set self.modifiers, self.modifier_label
41 42 43 44 45
        self.modifier_vars = []
        for modifier in self.modifiers:
            variable = StringVar(self)
            variable.set('')
            self.modifier_vars.append(variable)
46
        self.advanced = False
47 48 49
        self.CreateWidgets()
        self.LoadFinalKeyList()
        self.update_idletasks()
50 51 52 53 54 55 56 57
        self.geometry(
                "+%d+%d" % (
                    parent.winfo_rootx() +
                    (parent.winfo_width()/2 - self.winfo_reqwidth()/2),
                    parent.winfo_rooty() +
                    ((parent.winfo_height()/2 - self.winfo_reqheight()/2)
                    if not _htest else 150)
                ) )  #centre dialog over parent (or below htest box)
58 59 60
        if not _utest:
            self.deiconify() #geometry set, unhide
            self.wait_window()
61

62 63 64 65
    def showerror(self, *args, **kwargs):
        # Make testing easier.  Replace in #30751.
        messagebox.showerror(*args, **kwargs)

66 67 68 69 70
    def CreateWidgets(self):
        frameMain = Frame(self,borderwidth=2,relief=SUNKEN)
        frameMain.pack(side=TOP,expand=TRUE,fill=BOTH)
        frameButtons=Frame(self)
        frameButtons.pack(side=BOTTOM,fill=X)
71 72 73
        self.buttonOK = Button(frameButtons,text='OK',
                width=8,command=self.OK)
        self.buttonOK.grid(row=0,column=0,padx=5,pady=5)
74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95
        self.buttonCancel = Button(frameButtons,text='Cancel',
                width=8,command=self.Cancel)
        self.buttonCancel.grid(row=0,column=1,padx=5,pady=5)
        self.frameKeySeqBasic = Frame(frameMain)
        self.frameKeySeqAdvanced = Frame(frameMain)
        self.frameControlsBasic = Frame(frameMain)
        self.frameHelpAdvanced = Frame(frameMain)
        self.frameKeySeqAdvanced.grid(row=0,column=0,sticky=NSEW,padx=5,pady=5)
        self.frameKeySeqBasic.grid(row=0,column=0,sticky=NSEW,padx=5,pady=5)
        self.frameKeySeqBasic.lift()
        self.frameHelpAdvanced.grid(row=1,column=0,sticky=NSEW,padx=5)
        self.frameControlsBasic.grid(row=1,column=0,sticky=NSEW,padx=5)
        self.frameControlsBasic.lift()
        self.buttonLevel = Button(frameMain,command=self.ToggleLevel,
                text='Advanced Key Binding Entry >>')
        self.buttonLevel.grid(row=2,column=0,stick=EW,padx=5,pady=5)
        labelTitleBasic = Label(self.frameKeySeqBasic,
                text="New keys for  '"+self.action+"' :")
        labelTitleBasic.pack(anchor=W)
        labelKeysBasic = Label(self.frameKeySeqBasic,justify=LEFT,
                textvariable=self.keyString,relief=GROOVE,borderwidth=2)
        labelKeysBasic.pack(ipadx=5,ipady=5,fill=X)
96 97 98 99 100
        self.modifier_checkbuttons = {}
        column = 0
        for modifier, variable in zip(self.modifiers, self.modifier_vars):
            label = self.modifier_label.get(modifier, modifier)
            check=Checkbutton(self.frameControlsBasic,
101
                command=self.BuildKeyString,
102 103 104 105
                text=label,variable=variable,onvalue=modifier,offvalue='')
            check.grid(row=0,column=column,padx=2,sticky=W)
            self.modifier_checkbuttons[modifier] = check
            column += 1
106
        labelFnAdvice=Label(self.frameControlsBasic,justify=LEFT,
107 108 109 110 111 112 113
                            text=\
                            "Select the desired modifier keys\n"+
                            "above, and the final key from the\n"+
                            "list on the right.\n\n" +
                            "Use upper case Symbols when using\n" +
                            "the Shift modifier.  (Letters will be\n" +
                            "converted automatically.)")
114 115 116 117 118 119 120 121 122
        labelFnAdvice.grid(row=1,column=0,columnspan=4,padx=2,sticky=W)
        self.listKeysFinal=Listbox(self.frameControlsBasic,width=15,height=10,
                selectmode=SINGLE)
        self.listKeysFinal.bind('<ButtonRelease-1>',self.FinalKeySelected)
        self.listKeysFinal.grid(row=0,column=4,rowspan=4,sticky=NS)
        scrollKeysFinal=Scrollbar(self.frameControlsBasic,orient=VERTICAL,
                command=self.listKeysFinal.yview)
        self.listKeysFinal.config(yscrollcommand=scrollKeysFinal.set)
        scrollKeysFinal.grid(row=0,column=5,rowspan=4,sticky=NS)
123 124 125
        self.buttonClear=Button(self.frameControlsBasic,
                text='Clear Keys',command=self.ClearKeySeq)
        self.buttonClear.grid(row=2,column=0,columnspan=4)
126 127
        labelTitleAdvanced = Label(self.frameKeySeqAdvanced,justify=LEFT,
                text="Enter new binding(s) for  '"+self.action+"' :\n"+
128
                "(These bindings will not be checked for validity!)")
129 130 131 132 133
        labelTitleAdvanced.pack(anchor=W)
        self.entryKeysAdvanced=Entry(self.frameKeySeqAdvanced,
                textvariable=self.keyString)
        self.entryKeysAdvanced.pack(fill=X)
        labelHelpAdvanced=Label(self.frameHelpAdvanced,justify=LEFT,
134
            text="Key bindings are specified using Tkinter keysyms as\n"+
135
                 "in these samples: <Control-f>, <Shift-F2>, <F12>,\n"
136 137 138 139 140
                 "<Control-space>, <Meta-less>, <Control-Alt-Shift-X>.\n"
                 "Upper case is used when the Shift modifier is present!\n\n" +
                 "'Emacs style' multi-keystroke bindings are specified as\n" +
                 "follows: <Control-x><Control-y>, where the first key\n" +
                 "is the 'do-nothing' keybinding.\n\n" +
141 142 143 144
                 "Multiple separate bindings for one action should be\n"+
                 "separated by a space, eg., <Alt-v> <Meta-v>." )
        labelHelpAdvanced.grid(row=0,column=0,sticky=NSEW)

145 146 147 148 149 150 151 152
    def SetModifiersForPlatform(self):
        """Determine list of names of key modifiers for this platform.

        The names are used to build Tk bindings -- it doesn't matter if the
        keyboard has these keys, it matters if Tk understands them. The
        order is also important: key binding equality depends on it, so
        config-keys.def must use the same ordering.
        """
153
        if sys.platform == "darwin":
154 155 156
            self.modifiers = ['Shift', 'Control', 'Option', 'Command']
        else:
            self.modifiers = ['Control', 'Alt', 'Shift']
157
        self.modifier_label = {'Control': 'Ctrl'} # short name
158

159 160
    def ToggleLevel(self):
        if  self.buttonLevel.cget('text')[:8]=='Advanced':
161
            self.ClearKeySeq()
162 163 164 165
            self.buttonLevel.config(text='<< Basic Key Binding Entry')
            self.frameKeySeqAdvanced.lift()
            self.frameHelpAdvanced.lift()
            self.entryKeysAdvanced.focus_set()
166
            self.advanced = True
167
        else:
168
            self.ClearKeySeq()
169 170
            self.buttonLevel.config(text='Advanced Key Binding Entry >>')
            self.frameKeySeqBasic.lift()
171
            self.frameControlsBasic.lift()
172
            self.advanced = False
173

174 175
    def FinalKeySelected(self,event):
        self.BuildKeyString()
176

177
    def BuildKeyString(self):
178 179
        keyList = modifiers = self.GetModifiers()
        finalKey = self.listKeysFinal.get(ANCHOR)
180
        if finalKey:
181 182
            finalKey = self.TranslateKey(finalKey, modifiers)
            keyList.append(finalKey)
183
        self.keyString.set('<' + '-'.join(keyList) + '>')
184

185
    def GetModifiers(self):
186
        modList = [variable.get() for variable in self.modifier_vars]
187
        return [mod for mod in modList if mod]
188

189 190 191
    def ClearKeySeq(self):
        self.listKeysFinal.select_clear(0,END)
        self.listKeysFinal.yview(MOVETO, '0.0')
192 193 194
        for variable in self.modifier_vars:
            variable.set('')
        self.keyString.set('')
195

196
    def LoadFinalKeyList(self):
197
        #these tuples are also available for use in validity checks
198
        self.functionKeys=('F1','F2','F3','F4','F5','F6','F7','F8','F9',
199
                'F10','F11','F12')
200
        self.alphanumKeys=tuple(string.ascii_lowercase+string.digits)
201 202 203 204 205
        self.punctuationKeys=tuple('~!@#%^&*()_-+={}[]|;:,.<>/?')
        self.whitespaceKeys=('Tab','Space','Return')
        self.editKeys=('BackSpace','Delete','Insert')
        self.moveKeys=('Home','End','Page Up','Page Down','Left Arrow',
                'Right Arrow','Up Arrow','Down Arrow')
206
        #make a tuple of most of the useful common 'final' keys
207 208
        keys=(self.alphanumKeys+self.punctuationKeys+self.functionKeys+
                self.whitespaceKeys+self.editKeys+self.moveKeys)
209
        self.listKeysFinal.insert(END, *keys)
210

211 212 213 214
    def TranslateKey(self, key, modifiers):
        "Translate from keycap symbol to the Tkinter keysym"
        translateDict = {'Space':'space',
                '~':'asciitilde','!':'exclam','@':'at','#':'numbersign',
215 216 217 218 219 220 221
                '%':'percent','^':'asciicircum','&':'ampersand','*':'asterisk',
                '(':'parenleft',')':'parenright','_':'underscore','-':'minus',
                '+':'plus','=':'equal','{':'braceleft','}':'braceright',
                '[':'bracketleft',']':'bracketright','|':'bar',';':'semicolon',
                ':':'colon',',':'comma','.':'period','<':'less','>':'greater',
                '/':'slash','?':'question','Page Up':'Prior','Page Down':'Next',
                'Left Arrow':'Left','Right Arrow':'Right','Up Arrow':'Up',
222
                'Down Arrow': 'Down', 'Tab':'Tab'}
223
        if key in translateDict:
224 225 226 227
            key = translateDict[key]
        if 'Shift' in modifiers and key in string.ascii_lowercase:
            key = key.upper()
        key = 'Key-' + key
228
        return key
229

230
    def OK(self, event=None):
231 232 233 234 235 236 237
        keys = self.keyString.get().strip()
        if not keys:
            self.showerror(title=self.keyerror_title, parent=self,
                           message="No key specified.")
            return
        if (self.advanced or self.KeysOK(keys)) and self.bind_ok(keys):
            self.result = keys
238
        self.grab_release()
239
        self.destroy()
240

241 242
    def Cancel(self, event=None):
        self.result=''
243
        self.grab_release()
244
        self.destroy()
245

246
    def KeysOK(self, keys):
247 248 249 250 251 252
        '''Validity check on user's 'basic' keybinding selection.

        Doesn't check the string produced by the advanced dialog because
        'modifiers' isn't set.

        '''
253 254 255
        finalKey = self.listKeysFinal.get(ANCHOR)
        modifiers = self.GetModifiers()
        keysOK = False
256
        title = self.keyerror_title
257 258
        key_sequences = [key for keylist in self.currentKeySequences
                             for key in keylist]
259 260 261
        if not keys.endswith('>'):
            self.showerror(title, parent=self,
                           message='Missing the final Key')
262 263
        elif (not modifiers
              and finalKey not in self.functionKeys + self.moveKeys):
264 265
            self.showerror(title=title, parent=self,
                           message='No modifier key(s) specified.')
266 267
        elif (modifiers == ['Shift']) \
                 and (finalKey not in
268 269 270
                      self.functionKeys + self.moveKeys + ('Tab', 'Space')):
            msg = 'The shift modifier by itself may not be used with'\
                  ' this key symbol.'
271
            self.showerror(title=title, parent=self, message=msg)
272
        elif keys in key_sequences:
273
            msg = 'This key combination is already in use.'
274
            self.showerror(title=title, parent=self, message=msg)
275 276 277
        else:
            keysOK = True
        return keysOK
278

279 280 281 282 283 284 285 286 287 288 289 290 291 292 293
    def bind_ok(self, keys):
        "Return True if Tcl accepts the new keys else show message."

        try:
            binding = self.bind(keys, lambda: None)
        except TclError as err:
            self.showerror(
                    title=self.keyerror_title, parent=self,
                    message=(f'The entered key sequence is not accepted.\n\n'
                             f'Error: {err}'))
            return False
        else:
            self.unbind(keys, binding)
            return True

294

295
if __name__ == '__main__':
296 297
    from unittest import main
    main('idlelib.idle_test.test_config_key', verbosity=2, exit=False)
298

299 300
    from idlelib.idle_test.htest import run
    run(GetKeysDialog)