Coverage for config_key.py: 21%

197 statements  

« prev     ^ index     » next       coverage.py v7.2.5, created at 2023-05-11 13:22 -0700

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', 1b

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) 1b

39 if 'Shift' in modifiers and key in string.ascii_lowercase: 1b

40 key = key.upper() 1b

41 return f'Key-{key}' 1b

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__': 349 ↛ 350line 349 didn't jump to line 350, because the condition on line 349 was never true

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)