Coverage for configdialog.py: 11%

1029 statements  

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

1"""IDLE Configuration Dialog: support user customization of IDLE by GUI 

2 

3Customize font faces, sizes, and colorization attributes. Set indentation 

4defaults. Customize keybindings. Colorization and keybindings can be 

5saved as user defined sets. Select startup options including shell/editor 

6and default window size. Define additional help sources. 

7 

8Note that tab width in IDLE is currently fixed at eight due to Tk issues. 

9Refer to comments in EditorWindow autoindent code for details. 

10 

11""" 

12import re 

13 

14from tkinter import (Toplevel, Listbox, Canvas, 

15 StringVar, BooleanVar, IntVar, TRUE, FALSE, 

16 TOP, BOTTOM, RIGHT, LEFT, SOLID, GROOVE, 

17 NONE, BOTH, X, Y, W, E, EW, NS, NSEW, NW, 

18 HORIZONTAL, VERTICAL, ANCHOR, ACTIVE, END, TclError) 

19from tkinter.ttk import (Frame, LabelFrame, Button, Checkbutton, Entry, Label, 

20 OptionMenu, Notebook, Radiobutton, Scrollbar, Style, 

21 Spinbox, Combobox) 

22from tkinter import colorchooser 

23import tkinter.font as tkfont 

24from tkinter import messagebox 

25 

26from idlelib.config import idleConf, ConfigChanges 

27from idlelib.config_key import GetKeysWindow 

28from idlelib.dynoption import DynOptionMenu 

29from idlelib import macosx 

30from idlelib.query import SectionName, HelpSource 

31from idlelib.textview import view_text 

32from idlelib.autocomplete import AutoComplete 

33from idlelib.codecontext import CodeContext 

34from idlelib.parenmatch import ParenMatch 

35from idlelib.format import FormatParagraph 

36from idlelib.squeezer import Squeezer 

37from idlelib.textview import ScrollableTextFrame 

38 

39changes = ConfigChanges() 

40# Reload changed options in the following classes. 

41reloadables = (AutoComplete, CodeContext, ParenMatch, FormatParagraph, 

42 Squeezer) 

43 

44 

45class ConfigDialog(Toplevel): 

46 """Config dialog for IDLE. 

47 """ 

48 

49 def __init__(self, parent, title='', *, _htest=False, _utest=False): 

50 """Show the tabbed dialog for user configuration. 

51 

52 Args: 

53 parent - parent of this dialog 

54 title - string which is the title of this popup dialog 

55 _htest - bool, change box location when running htest 

56 _utest - bool, don't wait_window when running unittest 

57 

58 Note: Focus set on font page fontlist. 

59 

60 Methods: 

61 create_widgets 

62 cancel: Bound to DELETE_WINDOW protocol. 

63 """ 

64 Toplevel.__init__(self, parent) 

65 self.parent = parent 

66 if _htest: 

67 parent.instance_dict = {} 

68 if not _utest: 

69 self.withdraw() 

70 

71 self.title(title or 'IDLE Preferences') 

72 x = parent.winfo_rootx() + 20 

73 y = parent.winfo_rooty() + (30 if not _htest else 150) 

74 self.geometry(f'+{x}+{y}') 

75 # Each theme element key is its display name. 

76 # The first value of the tuple is the sample area tag name. 

77 # The second value is the display name list sort index. 

78 self.create_widgets() 

79 self.resizable(height=FALSE, width=FALSE) 

80 self.transient(parent) 

81 self.protocol("WM_DELETE_WINDOW", self.cancel) 

82 self.fontpage.fontlist.focus_set() 

83 # XXX Decide whether to keep or delete these key bindings. 

84 # Key bindings for this dialog. 

85 # self.bind('<Escape>', self.Cancel) #dismiss dialog, no save 

86 # self.bind('<Alt-a>', self.Apply) #apply changes, save 

87 # self.bind('<F1>', self.Help) #context help 

88 # Attach callbacks after loading config to avoid calling them. 

89 tracers.attach() 

90 

91 if not _utest: 

92 self.grab_set() 

93 self.wm_deiconify() 

94 self.wait_window() 

95 

96 def create_widgets(self): 

97 """Create and place widgets for tabbed dialog. 

98 

99 Widgets Bound to self: 

100 frame: encloses all other widgets 

101 note: Notebook 

102 highpage: HighPage 

103 fontpage: FontPage 

104 keyspage: KeysPage 

105 winpage: WinPage 

106 shedpage: ShedPage 

107 extpage: ExtPage 

108 

109 Methods: 

110 create_action_buttons 

111 load_configs: Load pages except for extensions. 

112 activate_config_changes: Tell editors to reload. 

113 """ 

114 self.frame = frame = Frame(self, padding="5px") 

115 self.frame.grid(sticky="nwes") 

116 self.note = note = Notebook(frame) 

117 self.extpage = ExtPage(note) 

118 self.highpage = HighPage(note, self.extpage) 

119 self.fontpage = FontPage(note, self.highpage) 

120 self.keyspage = KeysPage(note, self.extpage) 

121 self.winpage = WinPage(note) 

122 self.shedpage = ShedPage(note) 

123 

124 note.add(self.fontpage, text=' Fonts ') 

125 note.add(self.highpage, text='Highlights') 

126 note.add(self.keyspage, text=' Keys ') 

127 note.add(self.winpage, text=' Windows ') 

128 note.add(self.shedpage, text=' Shell/Ed ') 

129 note.add(self.extpage, text='Extensions') 

130 note.enable_traversal() 

131 note.pack(side=TOP, expand=TRUE, fill=BOTH) 

132 self.create_action_buttons().pack(side=BOTTOM) 

133 

134 def create_action_buttons(self): 

135 """Return frame of action buttons for dialog. 

136 

137 Methods: 

138 ok 

139 apply 

140 cancel 

141 help 

142 

143 Widget Structure: 

144 outer: Frame 

145 buttons: Frame 

146 (no assignment): Button (ok) 

147 (no assignment): Button (apply) 

148 (no assignment): Button (cancel) 

149 (no assignment): Button (help) 

150 (no assignment): Frame 

151 """ 

152 if macosx.isAquaTk(): 

153 # Changing the default padding on OSX results in unreadable 

154 # text in the buttons. 

155 padding_args = {} 

156 else: 

157 padding_args = {'padding': (6, 3)} 

158 outer = Frame(self.frame, padding=2) 

159 buttons_frame = Frame(outer, padding=2) 

160 self.buttons = {} 

161 for txt, cmd in ( 

162 ('Ok', self.ok), 

163 ('Apply', self.apply), 

164 ('Cancel', self.cancel), 

165 ('Help', self.help)): 

166 self.buttons[txt] = Button(buttons_frame, text=txt, command=cmd, 

167 takefocus=FALSE, **padding_args) 

168 self.buttons[txt].pack(side=LEFT, padx=5) 

169 # Add space above buttons. 

170 Frame(outer, height=2, borderwidth=0).pack(side=TOP) 

171 buttons_frame.pack(side=BOTTOM) 

172 return outer 

173 

174 def ok(self): 

175 """Apply config changes, then dismiss dialog.""" 

176 self.apply() 

177 self.destroy() 

178 

179 def apply(self): 

180 """Apply config changes and leave dialog open.""" 

181 self.deactivate_current_config() 

182 changes.save_all() 

183 self.extpage.save_all_changed_extensions() 

184 self.activate_config_changes() 

185 

186 def cancel(self): 

187 """Dismiss config dialog. 

188 

189 Methods: 

190 destroy: inherited 

191 """ 

192 changes.clear() 

193 self.destroy() 

194 

195 def destroy(self): 

196 global font_sample_text 

197 font_sample_text = self.fontpage.font_sample.get('1.0', 'end') 

198 self.grab_release() 

199 super().destroy() 

200 

201 def help(self): 

202 """Create textview for config dialog help. 

203 

204 Attributes accessed: 

205 note 

206 Methods: 

207 view_text: Method from textview module. 

208 """ 

209 page = self.note.tab(self.note.select(), option='text').strip() 

210 view_text(self, title='Help for IDLE preferences', 

211 contents=help_common+help_pages.get(page, '')) 

212 

213 def deactivate_current_config(self): 

214 """Remove current key bindings. 

215 Iterate over window instances defined in parent and remove 

216 the keybindings. 

217 """ 

218 # Before a config is saved, some cleanup of current 

219 # config must be done - remove the previous keybindings. 

220 win_instances = self.parent.instance_dict.keys() 

221 for instance in win_instances: 

222 instance.RemoveKeybindings() 

223 

224 def activate_config_changes(self): 

225 """Apply configuration changes to current windows. 

226 

227 Dynamically update the current parent window instances 

228 with some of the configuration changes. 

229 """ 

230 win_instances = self.parent.instance_dict.keys() 

231 for instance in win_instances: 

232 instance.ResetColorizer() 

233 instance.ResetFont() 

234 instance.set_notabs_indentwidth() 

235 instance.ApplyKeybindings() 

236 instance.reset_help_menu_entries() 

237 instance.update_cursor_blink() 

238 for klass in reloadables: 

239 klass.reload() 

240 

241 

242# class TabPage(Frame): # A template for Page classes. 

243# def __init__(self, master): 

244# super().__init__(master) 

245# self.create_page_tab() 

246# self.load_tab_cfg() 

247# def create_page_tab(self): 

248# # Define tk vars and register var and callback with tracers. 

249# # Create subframes and widgets. 

250# # Pack widgets. 

251# def load_tab_cfg(self): 

252# # Initialize widgets with data from idleConf. 

253# def var_changed_var_name(): 

254# # For each tk var that needs other than default callback. 

255# def other_methods(): 

256# # Define tab-specific behavior. 

257 

258font_sample_text = ( 

259 '<ASCII/Latin1>\n' 

260 'AaBbCcDdEeFfGgHhIiJj\n1234567890#:+=(){}[]\n' 

261 '\u00a2\u00a3\u00a5\u00a7\u00a9\u00ab\u00ae\u00b6\u00bd\u011e' 

262 '\u00c0\u00c1\u00c2\u00c3\u00c4\u00c5\u00c7\u00d0\u00d8\u00df\n' 

263 '\n<IPA,Greek,Cyrillic>\n' 

264 '\u0250\u0255\u0258\u025e\u025f\u0264\u026b\u026e\u0270\u0277' 

265 '\u027b\u0281\u0283\u0286\u028e\u029e\u02a2\u02ab\u02ad\u02af\n' 

266 '\u0391\u03b1\u0392\u03b2\u0393\u03b3\u0394\u03b4\u0395\u03b5' 

267 '\u0396\u03b6\u0397\u03b7\u0398\u03b8\u0399\u03b9\u039a\u03ba\n' 

268 '\u0411\u0431\u0414\u0434\u0416\u0436\u041f\u043f\u0424\u0444' 

269 '\u0427\u0447\u042a\u044a\u042d\u044d\u0460\u0464\u046c\u04dc\n' 

270 '\n<Hebrew, Arabic>\n' 

271 '\u05d0\u05d1\u05d2\u05d3\u05d4\u05d5\u05d6\u05d7\u05d8\u05d9' 

272 '\u05da\u05db\u05dc\u05dd\u05de\u05df\u05e0\u05e1\u05e2\u05e3\n' 

273 '\u0627\u0628\u062c\u062f\u0647\u0648\u0632\u062d\u0637\u064a' 

274 '\u0660\u0661\u0662\u0663\u0664\u0665\u0666\u0667\u0668\u0669\n' 

275 '\n<Devanagari, Tamil>\n' 

276 '\u0966\u0967\u0968\u0969\u096a\u096b\u096c\u096d\u096e\u096f' 

277 '\u0905\u0906\u0907\u0908\u0909\u090a\u090f\u0910\u0913\u0914\n' 

278 '\u0be6\u0be7\u0be8\u0be9\u0bea\u0beb\u0bec\u0bed\u0bee\u0bef' 

279 '\u0b85\u0b87\u0b89\u0b8e\n' 

280 '\n<East Asian>\n' 

281 '\u3007\u4e00\u4e8c\u4e09\u56db\u4e94\u516d\u4e03\u516b\u4e5d\n' 

282 '\u6c49\u5b57\u6f22\u5b57\u4eba\u6728\u706b\u571f\u91d1\u6c34\n' 

283 '\uac00\ub0d0\ub354\ub824\ubaa8\ubd64\uc218\uc720\uc988\uce58\n' 

284 '\u3042\u3044\u3046\u3048\u304a\u30a2\u30a4\u30a6\u30a8\u30aa\n' 

285 ) 

286 

287 

288class FontPage(Frame): 

289 

290 def __init__(self, master, highpage): 

291 super().__init__(master) 

292 self.highlight_sample = highpage.highlight_sample 

293 self.create_page_font() 

294 self.load_font_cfg() 

295 

296 def create_page_font(self): 

297 """Return frame of widgets for Font tab. 

298 

299 Fonts: Enable users to provisionally change font face, size, or 

300 boldness and to see the consequence of proposed choices. Each 

301 action set 3 options in changes structuree and changes the 

302 corresponding aspect of the font sample on this page and 

303 highlight sample on highlight page. 

304 

305 Function load_font_cfg initializes font vars and widgets from 

306 idleConf entries and tk. 

307 

308 Fontlist: mouse button 1 click or up or down key invoke 

309 on_fontlist_select(), which sets var font_name. 

310 

311 Sizelist: clicking the menubutton opens the dropdown menu. A 

312 mouse button 1 click or return key sets var font_size. 

313 

314 Bold_toggle: clicking the box toggles var font_bold. 

315 

316 Changing any of the font vars invokes var_changed_font, which 

317 adds all 3 font options to changes and calls set_samples. 

318 Set_samples applies a new font constructed from the font vars to 

319 font_sample and to highlight_sample on the highlight page. 

320 

321 Widgets for FontPage(Frame): (*) widgets bound to self 

322 frame_font: LabelFrame 

323 frame_font_name: Frame 

324 font_name_title: Label 

325 (*)fontlist: ListBox - font_name 

326 scroll_font: Scrollbar 

327 frame_font_param: Frame 

328 font_size_title: Label 

329 (*)sizelist: DynOptionMenu - font_size 

330 (*)bold_toggle: Checkbutton - font_bold 

331 frame_sample: LabelFrame 

332 (*)font_sample: Label 

333 """ 

334 self.font_name = tracers.add(StringVar(self), self.var_changed_font) 

335 self.font_size = tracers.add(StringVar(self), self.var_changed_font) 

336 self.font_bold = tracers.add(BooleanVar(self), self.var_changed_font) 

337 

338 # Define frames and widgets. 

339 frame_font = LabelFrame(self, borderwidth=2, relief=GROOVE, 

340 text=' Shell/Editor Font ') 

341 frame_sample = LabelFrame(self, borderwidth=2, relief=GROOVE, 

342 text=' Font Sample (Editable) ') 

343 # frame_font. 

344 frame_font_name = Frame(frame_font) 

345 frame_font_param = Frame(frame_font) 

346 font_name_title = Label( 

347 frame_font_name, justify=LEFT, text='Font Face :') 

348 self.fontlist = Listbox(frame_font_name, height=15, 

349 takefocus=True, exportselection=FALSE) 

350 self.fontlist.bind('<ButtonRelease-1>', self.on_fontlist_select) 

351 self.fontlist.bind('<KeyRelease-Up>', self.on_fontlist_select) 

352 self.fontlist.bind('<KeyRelease-Down>', self.on_fontlist_select) 

353 scroll_font = Scrollbar(frame_font_name) 

354 scroll_font.config(command=self.fontlist.yview) 

355 self.fontlist.config(yscrollcommand=scroll_font.set) 

356 font_size_title = Label(frame_font_param, text='Size :') 

357 self.sizelist = DynOptionMenu(frame_font_param, self.font_size, None) 

358 self.bold_toggle = Checkbutton( 

359 frame_font_param, variable=self.font_bold, 

360 onvalue=1, offvalue=0, text='Bold') 

361 # frame_sample. 

362 font_sample_frame = ScrollableTextFrame(frame_sample) 

363 self.font_sample = font_sample_frame.text 

364 self.font_sample.config(wrap=NONE, width=1, height=1) 

365 self.font_sample.insert(END, font_sample_text) 

366 

367 # Grid and pack widgets: 

368 self.columnconfigure(1, weight=1) 

369 self.rowconfigure(2, weight=1) 

370 frame_font.grid(row=0, column=0, padx=5, pady=5) 

371 frame_sample.grid(row=0, column=1, rowspan=3, padx=5, pady=5, 

372 sticky='nsew') 

373 # frame_font. 

374 frame_font_name.pack(side=TOP, padx=5, pady=5, fill=X) 

375 frame_font_param.pack(side=TOP, padx=5, pady=5, fill=X) 

376 font_name_title.pack(side=TOP, anchor=W) 

377 self.fontlist.pack(side=LEFT, expand=TRUE, fill=X) 

378 scroll_font.pack(side=LEFT, fill=Y) 

379 font_size_title.pack(side=LEFT, anchor=W) 

380 self.sizelist.pack(side=LEFT, anchor=W) 

381 self.bold_toggle.pack(side=LEFT, anchor=W, padx=20) 

382 # frame_sample. 

383 font_sample_frame.pack(expand=TRUE, fill=BOTH) 

384 

385 def load_font_cfg(self): 

386 """Load current configuration settings for the font options. 

387 

388 Retrieve current font with idleConf.GetFont and font families 

389 from tk. Setup fontlist and set font_name. Setup sizelist, 

390 which sets font_size. Set font_bold. Call set_samples. 

391 """ 

392 configured_font = idleConf.GetFont(self, 'main', 'EditorWindow') 

393 font_name = configured_font[0].lower() 

394 font_size = configured_font[1] 

395 font_bold = configured_font[2]=='bold' 

396 

397 # Set sorted no-duplicate editor font selection list and font_name. 

398 fonts = sorted(set(tkfont.families(self))) 

399 for font in fonts: 

400 self.fontlist.insert(END, font) 

401 self.font_name.set(font_name) 

402 lc_fonts = [s.lower() for s in fonts] 

403 try: 

404 current_font_index = lc_fonts.index(font_name) 

405 self.fontlist.see(current_font_index) 

406 self.fontlist.select_set(current_font_index) 

407 self.fontlist.select_anchor(current_font_index) 

408 self.fontlist.activate(current_font_index) 

409 except ValueError: 

410 pass 

411 # Set font size dropdown. 

412 self.sizelist.SetMenu(('7', '8', '9', '10', '11', '12', '13', '14', 

413 '16', '18', '20', '22', '25', '29', '34', '40'), 

414 font_size) 

415 # Set font weight. 

416 self.font_bold.set(font_bold) 

417 self.set_samples() 

418 

419 def var_changed_font(self, *params): 

420 """Store changes to font attributes. 

421 

422 When one font attribute changes, save them all, as they are 

423 not independent from each other. In particular, when we are 

424 overriding the default font, we need to write out everything. 

425 """ 

426 value = self.font_name.get() 

427 changes.add_option('main', 'EditorWindow', 'font', value) 

428 value = self.font_size.get() 

429 changes.add_option('main', 'EditorWindow', 'font-size', value) 

430 value = self.font_bold.get() 

431 changes.add_option('main', 'EditorWindow', 'font-bold', value) 

432 self.set_samples() 

433 

434 def on_fontlist_select(self, event): 

435 """Handle selecting a font from the list. 

436 

437 Event can result from either mouse click or Up or Down key. 

438 Set font_name and example displays to selection. 

439 """ 

440 font = self.fontlist.get( 

441 ACTIVE if event.type.name == 'KeyRelease' else ANCHOR) 

442 self.font_name.set(font.lower()) 

443 

444 def set_samples(self, event=None): 

445 """Update update both screen samples with the font settings. 

446 

447 Called on font initialization and change events. 

448 Accesses font_name, font_size, and font_bold Variables. 

449 Updates font_sample and highlight page highlight_sample. 

450 """ 

451 font_name = self.font_name.get() 

452 font_weight = tkfont.BOLD if self.font_bold.get() else tkfont.NORMAL 

453 new_font = (font_name, self.font_size.get(), font_weight) 

454 self.font_sample['font'] = new_font 

455 self.highlight_sample['font'] = new_font 

456 

457 

458class HighPage(Frame): 

459 

460 def __init__(self, master, extpage): 

461 super().__init__(master) 

462 self.extpage = extpage 

463 self.cd = master.winfo_toplevel() 

464 self.style = Style(master) 

465 self.create_page_highlight() 

466 self.load_theme_cfg() 

467 

468 def create_page_highlight(self): 

469 """Return frame of widgets for Highlights tab. 

470 

471 Enable users to provisionally change foreground and background 

472 colors applied to textual tags. Color mappings are stored in 

473 complete listings called themes. Built-in themes in 

474 idlelib/config-highlight.def are fixed as far as the dialog is 

475 concerned. Any theme can be used as the base for a new custom 

476 theme, stored in .idlerc/config-highlight.cfg. 

477 

478 Function load_theme_cfg() initializes tk variables and theme 

479 lists and calls paint_theme_sample() and set_highlight_target() 

480 for the current theme. Radiobuttons builtin_theme_on and 

481 custom_theme_on toggle var theme_source, which controls if the 

482 current set of colors are from a builtin or custom theme. 

483 DynOptionMenus builtinlist and customlist contain lists of the 

484 builtin and custom themes, respectively, and the current item 

485 from each list is stored in vars builtin_name and custom_name. 

486 

487 Function paint_theme_sample() applies the colors from the theme 

488 to the tags in text widget highlight_sample and then invokes 

489 set_color_sample(). Function set_highlight_target() sets the state 

490 of the radiobuttons fg_on and bg_on based on the tag and it also 

491 invokes set_color_sample(). 

492 

493 Function set_color_sample() sets the background color for the frame 

494 holding the color selector. This provides a larger visual of the 

495 color for the current tag and plane (foreground/background). 

496 

497 Note: set_color_sample() is called from many places and is often 

498 called more than once when a change is made. It is invoked when 

499 foreground or background is selected (radiobuttons), from 

500 paint_theme_sample() (theme is changed or load_cfg is called), and 

501 from set_highlight_target() (target tag is changed or load_cfg called). 

502 

503 Button delete_custom invokes delete_custom() to delete 

504 a custom theme from idleConf.userCfg['highlight'] and changes. 

505 Button save_custom invokes save_as_new_theme() which calls 

506 get_new_theme_name() and create_new() to save a custom theme 

507 and its colors to idleConf.userCfg['highlight']. 

508 

509 Radiobuttons fg_on and bg_on toggle var fg_bg_toggle to control 

510 if the current selected color for a tag is for the foreground or 

511 background. 

512 

513 DynOptionMenu targetlist contains a readable description of the 

514 tags applied to Python source within IDLE. Selecting one of the 

515 tags from this list populates highlight_target, which has a callback 

516 function set_highlight_target(). 

517 

518 Text widget highlight_sample displays a block of text (which is 

519 mock Python code) in which is embedded the defined tags and reflects 

520 the color attributes of the current theme and changes for those tags. 

521 Mouse button 1 allows for selection of a tag and updates 

522 highlight_target with that tag value. 

523 

524 Note: The font in highlight_sample is set through the config in 

525 the fonts tab. 

526 

527 In other words, a tag can be selected either from targetlist or 

528 by clicking on the sample text within highlight_sample. The 

529 plane (foreground/background) is selected via the radiobutton. 

530 Together, these two (tag and plane) control what color is 

531 shown in set_color_sample() for the current theme. Button set_color 

532 invokes get_color() which displays a ColorChooser to change the 

533 color for the selected tag/plane. If a new color is picked, 

534 it will be saved to changes and the highlight_sample and 

535 frame background will be updated. 

536 

537 Tk Variables: 

538 color: Color of selected target. 

539 builtin_name: Menu variable for built-in theme. 

540 custom_name: Menu variable for custom theme. 

541 fg_bg_toggle: Toggle for foreground/background color. 

542 Note: this has no callback. 

543 theme_source: Selector for built-in or custom theme. 

544 highlight_target: Menu variable for the highlight tag target. 

545 

546 Instance Data Attributes: 

547 theme_elements: Dictionary of tags for text highlighting. 

548 The key is the display name and the value is a tuple of 

549 (tag name, display sort order). 

550 

551 Methods [attachment]: 

552 load_theme_cfg: Load current highlight colors. 

553 get_color: Invoke colorchooser [button_set_color]. 

554 set_color_sample_binding: Call set_color_sample [fg_bg_toggle]. 

555 set_highlight_target: set fg_bg_toggle, set_color_sample(). 

556 set_color_sample: Set frame background to target. 

557 on_new_color_set: Set new color and add option. 

558 paint_theme_sample: Recolor sample. 

559 get_new_theme_name: Get from popup. 

560 create_new: Combine theme with changes and save. 

561 save_as_new_theme: Save [button_save_custom]. 

562 set_theme_type: Command for [theme_source]. 

563 delete_custom: Activate default [button_delete_custom]. 

564 save_new: Save to userCfg['theme'] (is function). 

565 

566 Widgets of highlights page frame: (*) widgets bound to self 

567 frame_custom: LabelFrame 

568 (*)highlight_sample: Text 

569 (*)frame_color_set: Frame 

570 (*)button_set_color: Button 

571 (*)targetlist: DynOptionMenu - highlight_target 

572 frame_fg_bg_toggle: Frame 

573 (*)fg_on: Radiobutton - fg_bg_toggle 

574 (*)bg_on: Radiobutton - fg_bg_toggle 

575 (*)button_save_custom: Button 

576 frame_theme: LabelFrame 

577 theme_type_title: Label 

578 (*)builtin_theme_on: Radiobutton - theme_source 

579 (*)custom_theme_on: Radiobutton - theme_source 

580 (*)builtinlist: DynOptionMenu - builtin_name 

581 (*)customlist: DynOptionMenu - custom_name 

582 (*)button_delete_custom: Button 

583 (*)theme_message: Label 

584 """ 

585 self.theme_elements = { 

586 'Normal Code or Text': ('normal', '00'), 

587 'Code Context': ('context', '01'), 

588 'Python Keywords': ('keyword', '02'), 

589 'Python Definitions': ('definition', '03'), 

590 'Python Builtins': ('builtin', '04'), 

591 'Python Comments': ('comment', '05'), 

592 'Python Strings': ('string', '06'), 

593 'Selected Text': ('hilite', '07'), 

594 'Found Text': ('hit', '08'), 

595 'Cursor': ('cursor', '09'), 

596 'Editor Breakpoint': ('break', '10'), 

597 'Shell Prompt': ('console', '11'), 

598 'Error Text': ('error', '12'), 

599 'Shell User Output': ('stdout', '13'), 

600 'Shell User Exception': ('stderr', '14'), 

601 'Line Number': ('linenumber', '16'), 

602 } 

603 self.builtin_name = tracers.add( 

604 StringVar(self), self.var_changed_builtin_name) 

605 self.custom_name = tracers.add( 

606 StringVar(self), self.var_changed_custom_name) 

607 self.fg_bg_toggle = BooleanVar(self) 

608 self.color = tracers.add( 

609 StringVar(self), self.var_changed_color) 

610 self.theme_source = tracers.add( 

611 BooleanVar(self), self.var_changed_theme_source) 

612 self.highlight_target = tracers.add( 

613 StringVar(self), self.var_changed_highlight_target) 

614 

615 # Create widgets: 

616 # body frame and section frames. 

617 frame_custom = LabelFrame(self, borderwidth=2, relief=GROOVE, 

618 text=' Custom Highlighting ') 

619 frame_theme = LabelFrame(self, borderwidth=2, relief=GROOVE, 

620 text=' Highlighting Theme ') 

621 # frame_custom. 

622 sample_frame = ScrollableTextFrame( 

623 frame_custom, relief=SOLID, borderwidth=1) 

624 text = self.highlight_sample = sample_frame.text 

625 text.configure( 

626 font=('courier', 12, ''), cursor='hand2', width=1, height=1, 

627 takefocus=FALSE, highlightthickness=0, wrap=NONE) 

628 # Prevent perhaps invisible selection of word or slice. 

629 text.bind('<Double-Button-1>', lambda e: 'break') 

630 text.bind('<B1-Motion>', lambda e: 'break') 

631 string_tags=( 

632 ('# Click selects item.', 'comment'), ('\n', 'normal'), 

633 ('code context section', 'context'), ('\n', 'normal'), 

634 ('| cursor', 'cursor'), ('\n', 'normal'), 

635 ('def', 'keyword'), (' ', 'normal'), 

636 ('func', 'definition'), ('(param):\n ', 'normal'), 

637 ('"Return None."', 'string'), ('\n var0 = ', 'normal'), 

638 ("'string'", 'string'), ('\n var1 = ', 'normal'), 

639 ("'selected'", 'hilite'), ('\n var2 = ', 'normal'), 

640 ("'found'", 'hit'), ('\n var3 = ', 'normal'), 

641 ('list', 'builtin'), ('(', 'normal'), 

642 ('None', 'keyword'), (')\n', 'normal'), 

643 (' breakpoint("line")', 'break'), ('\n\n', 'normal'), 

644 ('>>>', 'console'), (' 3.14**2\n', 'normal'), 

645 ('9.8596', 'stdout'), ('\n', 'normal'), 

646 ('>>>', 'console'), (' pri ', 'normal'), 

647 ('n', 'error'), ('t(\n', 'normal'), 

648 ('SyntaxError', 'stderr'), ('\n', 'normal')) 

649 for string, tag in string_tags: 

650 text.insert(END, string, tag) 

651 n_lines = len(text.get('1.0', END).splitlines()) 

652 for lineno in range(1, n_lines): 

653 text.insert(f'{lineno}.0', 

654 f'{lineno:{len(str(n_lines))}d} ', 

655 'linenumber') 

656 for element in self.theme_elements: 

657 def tem(event, elem=element): 

658 # event.widget.winfo_top_level().highlight_target.set(elem) 

659 self.highlight_target.set(elem) 

660 text.tag_bind( 

661 self.theme_elements[element][0], '<ButtonPress-1>', tem) 

662 text['state'] = 'disabled' 

663 self.style.configure('frame_color_set.TFrame', borderwidth=1, 

664 relief='solid') 

665 self.frame_color_set = Frame(frame_custom, style='frame_color_set.TFrame') 

666 frame_fg_bg_toggle = Frame(frame_custom) 

667 self.button_set_color = Button( 

668 self.frame_color_set, text='Choose Color for :', 

669 command=self.get_color) 

670 self.targetlist = DynOptionMenu( 

671 self.frame_color_set, self.highlight_target, None, 

672 highlightthickness=0) #, command=self.set_highlight_targetBinding 

673 self.fg_on = Radiobutton( 

674 frame_fg_bg_toggle, variable=self.fg_bg_toggle, value=1, 

675 text='Foreground', command=self.set_color_sample_binding) 

676 self.bg_on = Radiobutton( 

677 frame_fg_bg_toggle, variable=self.fg_bg_toggle, value=0, 

678 text='Background', command=self.set_color_sample_binding) 

679 self.fg_bg_toggle.set(1) 

680 self.button_save_custom = Button( 

681 frame_custom, text='Save as New Custom Theme', 

682 command=self.save_as_new_theme) 

683 # frame_theme. 

684 theme_type_title = Label(frame_theme, text='Select : ') 

685 self.builtin_theme_on = Radiobutton( 

686 frame_theme, variable=self.theme_source, value=1, 

687 command=self.set_theme_type, text='a Built-in Theme') 

688 self.custom_theme_on = Radiobutton( 

689 frame_theme, variable=self.theme_source, value=0, 

690 command=self.set_theme_type, text='a Custom Theme') 

691 self.builtinlist = DynOptionMenu( 

692 frame_theme, self.builtin_name, None, command=None) 

693 self.customlist = DynOptionMenu( 

694 frame_theme, self.custom_name, None, command=None) 

695 self.button_delete_custom = Button( 

696 frame_theme, text='Delete Custom Theme', 

697 command=self.delete_custom) 

698 self.theme_message = Label(frame_theme, borderwidth=2) 

699 # Pack widgets: 

700 # body. 

701 frame_custom.pack(side=LEFT, padx=5, pady=5, expand=TRUE, fill=BOTH) 

702 frame_theme.pack(side=TOP, padx=5, pady=5, fill=X) 

703 # frame_custom. 

704 self.frame_color_set.pack(side=TOP, padx=5, pady=5, fill=X) 

705 frame_fg_bg_toggle.pack(side=TOP, padx=5, pady=0) 

706 sample_frame.pack( 

707 side=TOP, padx=5, pady=5, expand=TRUE, fill=BOTH) 

708 self.button_set_color.pack(side=TOP, expand=TRUE, fill=X, padx=8, pady=4) 

709 self.targetlist.pack(side=TOP, expand=TRUE, fill=X, padx=8, pady=3) 

710 self.fg_on.pack(side=LEFT, anchor=E) 

711 self.bg_on.pack(side=RIGHT, anchor=W) 

712 self.button_save_custom.pack(side=BOTTOM, fill=X, padx=5, pady=5) 

713 # frame_theme. 

714 theme_type_title.pack(side=TOP, anchor=W, padx=5, pady=5) 

715 self.builtin_theme_on.pack(side=TOP, anchor=W, padx=5) 

716 self.custom_theme_on.pack(side=TOP, anchor=W, padx=5, pady=2) 

717 self.builtinlist.pack(side=TOP, fill=X, padx=5, pady=5) 

718 self.customlist.pack(side=TOP, fill=X, anchor=W, padx=5, pady=5) 

719 self.button_delete_custom.pack(side=TOP, fill=X, padx=5, pady=5) 

720 self.theme_message.pack(side=TOP, fill=X, pady=5) 

721 

722 def load_theme_cfg(self): 

723 """Load current configuration settings for the theme options. 

724 

725 Based on the theme_source toggle, the theme is set as 

726 either builtin or custom and the initial widget values 

727 reflect the current settings from idleConf. 

728 

729 Attributes updated: 

730 theme_source: Set from idleConf. 

731 builtinlist: List of default themes from idleConf. 

732 customlist: List of custom themes from idleConf. 

733 custom_theme_on: Disabled if there are no custom themes. 

734 custom_theme: Message with additional information. 

735 targetlist: Create menu from self.theme_elements. 

736 

737 Methods: 

738 set_theme_type 

739 paint_theme_sample 

740 set_highlight_target 

741 """ 

742 # Set current theme type radiobutton. 

743 self.theme_source.set(idleConf.GetOption( 

744 'main', 'Theme', 'default', type='bool', default=1)) 

745 # Set current theme. 

746 current_option = idleConf.CurrentTheme() 

747 # Load available theme option menus. 

748 if self.theme_source.get(): # Default theme selected. 

749 item_list = idleConf.GetSectionList('default', 'highlight') 

750 item_list.sort() 

751 self.builtinlist.SetMenu(item_list, current_option) 

752 item_list = idleConf.GetSectionList('user', 'highlight') 

753 item_list.sort() 

754 if not item_list: 

755 self.custom_theme_on.state(('disabled',)) 

756 self.custom_name.set('- no custom themes -') 

757 else: 

758 self.customlist.SetMenu(item_list, item_list[0]) 

759 else: # User theme selected. 

760 item_list = idleConf.GetSectionList('user', 'highlight') 

761 item_list.sort() 

762 self.customlist.SetMenu(item_list, current_option) 

763 item_list = idleConf.GetSectionList('default', 'highlight') 

764 item_list.sort() 

765 self.builtinlist.SetMenu(item_list, item_list[0]) 

766 self.set_theme_type() 

767 # Load theme element option menu. 

768 theme_names = list(self.theme_elements.keys()) 

769 theme_names.sort(key=lambda x: self.theme_elements[x][1]) 

770 self.targetlist.SetMenu(theme_names, theme_names[0]) 

771 self.paint_theme_sample() 

772 self.set_highlight_target() 

773 

774 def var_changed_builtin_name(self, *params): 

775 """Process new builtin theme selection. 

776 

777 Add the changed theme's name to the changed_items and recreate 

778 the sample with the values from the selected theme. 

779 """ 

780 old_themes = ('IDLE Classic', 'IDLE New') 

781 value = self.builtin_name.get() 

782 if value not in old_themes: 

783 if idleConf.GetOption('main', 'Theme', 'name') not in old_themes: 

784 changes.add_option('main', 'Theme', 'name', old_themes[0]) 

785 changes.add_option('main', 'Theme', 'name2', value) 

786 self.theme_message['text'] = 'New theme, see Help' 

787 else: 

788 changes.add_option('main', 'Theme', 'name', value) 

789 changes.add_option('main', 'Theme', 'name2', '') 

790 self.theme_message['text'] = '' 

791 self.paint_theme_sample() 

792 

793 def var_changed_custom_name(self, *params): 

794 """Process new custom theme selection. 

795 

796 If a new custom theme is selected, add the name to the 

797 changed_items and apply the theme to the sample. 

798 """ 

799 value = self.custom_name.get() 

800 if value != '- no custom themes -': 

801 changes.add_option('main', 'Theme', 'name', value) 

802 self.paint_theme_sample() 

803 

804 def var_changed_theme_source(self, *params): 

805 """Process toggle between builtin and custom theme. 

806 

807 Update the default toggle value and apply the newly 

808 selected theme type. 

809 """ 

810 value = self.theme_source.get() 

811 changes.add_option('main', 'Theme', 'default', value) 

812 if value: 

813 self.var_changed_builtin_name() 

814 else: 

815 self.var_changed_custom_name() 

816 

817 def var_changed_color(self, *params): 

818 "Process change to color choice." 

819 self.on_new_color_set() 

820 

821 def var_changed_highlight_target(self, *params): 

822 "Process selection of new target tag for highlighting." 

823 self.set_highlight_target() 

824 

825 def set_theme_type(self): 

826 """Set available screen options based on builtin or custom theme. 

827 

828 Attributes accessed: 

829 theme_source 

830 

831 Attributes updated: 

832 builtinlist 

833 customlist 

834 button_delete_custom 

835 custom_theme_on 

836 

837 Called from: 

838 handler for builtin_theme_on and custom_theme_on 

839 delete_custom 

840 create_new 

841 load_theme_cfg 

842 """ 

843 if self.theme_source.get(): 

844 self.builtinlist['state'] = 'normal' 

845 self.customlist['state'] = 'disabled' 

846 self.button_delete_custom.state(('disabled',)) 

847 else: 

848 self.builtinlist['state'] = 'disabled' 

849 self.custom_theme_on.state(('!disabled',)) 

850 self.customlist['state'] = 'normal' 

851 self.button_delete_custom.state(('!disabled',)) 

852 

853 def get_color(self): 

854 """Handle button to select a new color for the target tag. 

855 

856 If a new color is selected while using a builtin theme, a 

857 name must be supplied to create a custom theme. 

858 

859 Attributes accessed: 

860 highlight_target 

861 frame_color_set 

862 theme_source 

863 

864 Attributes updated: 

865 color 

866 

867 Methods: 

868 get_new_theme_name 

869 create_new 

870 """ 

871 target = self.highlight_target.get() 

872 prev_color = self.style.lookup(self.frame_color_set['style'], 

873 'background') 

874 rgbTuplet, color_string = colorchooser.askcolor( 

875 parent=self, title='Pick new color for : '+target, 

876 initialcolor=prev_color) 

877 if color_string and (color_string != prev_color): 

878 # User didn't cancel and they chose a new color. 

879 if self.theme_source.get(): # Current theme is a built-in. 

880 message = ('Your changes will be saved as a new Custom Theme. ' 

881 'Enter a name for your new Custom Theme below.') 

882 new_theme = self.get_new_theme_name(message) 

883 if not new_theme: # User cancelled custom theme creation. 

884 return 

885 else: # Create new custom theme based on previously active theme. 

886 self.create_new(new_theme) 

887 self.color.set(color_string) 

888 else: # Current theme is user defined. 

889 self.color.set(color_string) 

890 

891 def on_new_color_set(self): 

892 "Display sample of new color selection on the dialog." 

893 new_color = self.color.get() 

894 self.style.configure('frame_color_set.TFrame', background=new_color) 

895 plane = 'foreground' if self.fg_bg_toggle.get() else 'background' 

896 sample_element = self.theme_elements[self.highlight_target.get()][0] 

897 self.highlight_sample.tag_config(sample_element, **{plane: new_color}) 

898 theme = self.custom_name.get() 

899 theme_element = sample_element + '-' + plane 

900 changes.add_option('highlight', theme, theme_element, new_color) 

901 

902 def get_new_theme_name(self, message): 

903 "Return name of new theme from query popup." 

904 used_names = (idleConf.GetSectionList('user', 'highlight') + 

905 idleConf.GetSectionList('default', 'highlight')) 

906 new_theme = SectionName( 

907 self, 'New Custom Theme', message, used_names).result 

908 return new_theme 

909 

910 def save_as_new_theme(self): 

911 """Prompt for new theme name and create the theme. 

912 

913 Methods: 

914 get_new_theme_name 

915 create_new 

916 """ 

917 new_theme_name = self.get_new_theme_name('New Theme Name:') 

918 if new_theme_name: 

919 self.create_new(new_theme_name) 

920 

921 def create_new(self, new_theme_name): 

922 """Create a new custom theme with the given name. 

923 

924 Create the new theme based on the previously active theme 

925 with the current changes applied. Once it is saved, then 

926 activate the new theme. 

927 

928 Attributes accessed: 

929 builtin_name 

930 custom_name 

931 

932 Attributes updated: 

933 customlist 

934 theme_source 

935 

936 Method: 

937 save_new 

938 set_theme_type 

939 """ 

940 if self.theme_source.get(): 

941 theme_type = 'default' 

942 theme_name = self.builtin_name.get() 

943 else: 

944 theme_type = 'user' 

945 theme_name = self.custom_name.get() 

946 new_theme = idleConf.GetThemeDict(theme_type, theme_name) 

947 # Apply any of the old theme's unsaved changes to the new theme. 

948 if theme_name in changes['highlight']: 

949 theme_changes = changes['highlight'][theme_name] 

950 for element in theme_changes: 

951 new_theme[element] = theme_changes[element] 

952 # Save the new theme. 

953 self.save_new(new_theme_name, new_theme) 

954 # Change GUI over to the new theme. 

955 custom_theme_list = idleConf.GetSectionList('user', 'highlight') 

956 custom_theme_list.sort() 

957 self.customlist.SetMenu(custom_theme_list, new_theme_name) 

958 self.theme_source.set(0) 

959 self.set_theme_type() 

960 

961 def set_highlight_target(self): 

962 """Set fg/bg toggle and color based on highlight tag target. 

963 

964 Instance variables accessed: 

965 highlight_target 

966 

967 Attributes updated: 

968 fg_on 

969 bg_on 

970 fg_bg_toggle 

971 

972 Methods: 

973 set_color_sample 

974 

975 Called from: 

976 var_changed_highlight_target 

977 load_theme_cfg 

978 """ 

979 if self.highlight_target.get() == 'Cursor': # bg not possible 

980 self.fg_on.state(('disabled',)) 

981 self.bg_on.state(('disabled',)) 

982 self.fg_bg_toggle.set(1) 

983 else: # Both fg and bg can be set. 

984 self.fg_on.state(('!disabled',)) 

985 self.bg_on.state(('!disabled',)) 

986 self.fg_bg_toggle.set(1) 

987 self.set_color_sample() 

988 

989 def set_color_sample_binding(self, *args): 

990 """Change color sample based on foreground/background toggle. 

991 

992 Methods: 

993 set_color_sample 

994 """ 

995 self.set_color_sample() 

996 

997 def set_color_sample(self): 

998 """Set the color of the frame background to reflect the selected target. 

999 

1000 Instance variables accessed: 

1001 theme_elements 

1002 highlight_target 

1003 fg_bg_toggle 

1004 highlight_sample 

1005 

1006 Attributes updated: 

1007 frame_color_set 

1008 """ 

1009 # Set the color sample area. 

1010 tag = self.theme_elements[self.highlight_target.get()][0] 

1011 plane = 'foreground' if self.fg_bg_toggle.get() else 'background' 

1012 color = self.highlight_sample.tag_cget(tag, plane) 

1013 self.style.configure('frame_color_set.TFrame', background=color) 

1014 

1015 def paint_theme_sample(self): 

1016 """Apply the theme colors to each element tag in the sample text. 

1017 

1018 Instance attributes accessed: 

1019 theme_elements 

1020 theme_source 

1021 builtin_name 

1022 custom_name 

1023 

1024 Attributes updated: 

1025 highlight_sample: Set the tag elements to the theme. 

1026 

1027 Methods: 

1028 set_color_sample 

1029 

1030 Called from: 

1031 var_changed_builtin_name 

1032 var_changed_custom_name 

1033 load_theme_cfg 

1034 """ 

1035 if self.theme_source.get(): # Default theme 

1036 theme = self.builtin_name.get() 

1037 else: # User theme 

1038 theme = self.custom_name.get() 

1039 for element_title in self.theme_elements: 

1040 element = self.theme_elements[element_title][0] 

1041 colors = idleConf.GetHighlight(theme, element) 

1042 if element == 'cursor': # Cursor sample needs special painting. 

1043 colors['background'] = idleConf.GetHighlight( 

1044 theme, 'normal')['background'] 

1045 # Handle any unsaved changes to this theme. 

1046 if theme in changes['highlight']: 

1047 theme_dict = changes['highlight'][theme] 

1048 if element + '-foreground' in theme_dict: 

1049 colors['foreground'] = theme_dict[element + '-foreground'] 

1050 if element + '-background' in theme_dict: 

1051 colors['background'] = theme_dict[element + '-background'] 

1052 self.highlight_sample.tag_config(element, **colors) 

1053 self.set_color_sample() 

1054 

1055 def save_new(self, theme_name, theme): 

1056 """Save a newly created theme to idleConf. 

1057 

1058 theme_name - string, the name of the new theme 

1059 theme - dictionary containing the new theme 

1060 """ 

1061 idleConf.userCfg['highlight'].AddSection(theme_name) 

1062 for element in theme: 

1063 value = theme[element] 

1064 idleConf.userCfg['highlight'].SetOption(theme_name, element, value) 

1065 

1066 def askyesno(self, *args, **kwargs): 

1067 # Make testing easier. Could change implementation. 

1068 return messagebox.askyesno(*args, **kwargs) 

1069 

1070 def delete_custom(self): 

1071 """Handle event to delete custom theme. 

1072 

1073 The current theme is deactivated and the default theme is 

1074 activated. The custom theme is permanently removed from 

1075 the config file. 

1076 

1077 Attributes accessed: 

1078 custom_name 

1079 

1080 Attributes updated: 

1081 custom_theme_on 

1082 customlist 

1083 theme_source 

1084 builtin_name 

1085 

1086 Methods: 

1087 deactivate_current_config 

1088 save_all_changed_extensions 

1089 activate_config_changes 

1090 set_theme_type 

1091 """ 

1092 theme_name = self.custom_name.get() 

1093 delmsg = 'Are you sure you wish to delete the theme %r ?' 

1094 if not self.askyesno( 

1095 'Delete Theme', delmsg % theme_name, parent=self): 

1096 return 

1097 self.cd.deactivate_current_config() 

1098 # Remove theme from changes, config, and file. 

1099 changes.delete_section('highlight', theme_name) 

1100 # Reload user theme list. 

1101 item_list = idleConf.GetSectionList('user', 'highlight') 

1102 item_list.sort() 

1103 if not item_list: 

1104 self.custom_theme_on.state(('disabled',)) 

1105 self.customlist.SetMenu(item_list, '- no custom themes -') 

1106 else: 

1107 self.customlist.SetMenu(item_list, item_list[0]) 

1108 # Revert to default theme. 

1109 self.theme_source.set(idleConf.defaultCfg['main'].Get('Theme', 'default')) 

1110 self.builtin_name.set(idleConf.defaultCfg['main'].Get('Theme', 'name')) 

1111 # User can't back out of these changes, they must be applied now. 

1112 changes.save_all() 

1113 self.extpage.save_all_changed_extensions() 

1114 self.cd.activate_config_changes() 

1115 self.set_theme_type() 

1116 

1117 

1118class KeysPage(Frame): 

1119 

1120 def __init__(self, master, extpage): 

1121 super().__init__(master) 

1122 self.extpage = extpage 

1123 self.cd = master.winfo_toplevel() 

1124 self.create_page_keys() 

1125 self.load_key_cfg() 

1126 

1127 def create_page_keys(self): 

1128 """Return frame of widgets for Keys tab. 

1129 

1130 Enable users to provisionally change both individual and sets of 

1131 keybindings (shortcut keys). Except for features implemented as 

1132 extensions, keybindings are stored in complete sets called 

1133 keysets. Built-in keysets in idlelib/config-keys.def are fixed 

1134 as far as the dialog is concerned. Any keyset can be used as the 

1135 base for a new custom keyset, stored in .idlerc/config-keys.cfg. 

1136 

1137 Function load_key_cfg() initializes tk variables and keyset 

1138 lists and calls load_keys_list for the current keyset. 

1139 Radiobuttons builtin_keyset_on and custom_keyset_on toggle var 

1140 keyset_source, which controls if the current set of keybindings 

1141 are from a builtin or custom keyset. DynOptionMenus builtinlist 

1142 and customlist contain lists of the builtin and custom keysets, 

1143 respectively, and the current item from each list is stored in 

1144 vars builtin_name and custom_name. 

1145 

1146 Button delete_custom_keys invokes delete_custom_keys() to delete 

1147 a custom keyset from idleConf.userCfg['keys'] and changes. Button 

1148 save_custom_keys invokes save_as_new_key_set() which calls 

1149 get_new_keys_name() and create_new_key_set() to save a custom keyset 

1150 and its keybindings to idleConf.userCfg['keys']. 

1151 

1152 Listbox bindingslist contains all of the keybindings for the 

1153 selected keyset. The keybindings are loaded in load_keys_list() 

1154 and are pairs of (event, [keys]) where keys can be a list 

1155 of one or more key combinations to bind to the same event. 

1156 Mouse button 1 click invokes on_bindingslist_select(), which 

1157 allows button_new_keys to be clicked. 

1158 

1159 So, an item is selected in listbindings, which activates 

1160 button_new_keys, and clicking button_new_keys calls function 

1161 get_new_keys(). Function get_new_keys() gets the key mappings from the 

1162 current keyset for the binding event item that was selected. The 

1163 function then displays another dialog, GetKeysDialog, with the 

1164 selected binding event and current keys and allows new key sequences 

1165 to be entered for that binding event. If the keys aren't 

1166 changed, nothing happens. If the keys are changed and the keyset 

1167 is a builtin, function get_new_keys_name() will be called 

1168 for input of a custom keyset name. If no name is given, then the 

1169 change to the keybinding will abort and no updates will be made. If 

1170 a custom name is entered in the prompt or if the current keyset was 

1171 already custom (and thus didn't require a prompt), then 

1172 idleConf.userCfg['keys'] is updated in function create_new_key_set() 

1173 with the change to the event binding. The item listing in bindingslist 

1174 is updated with the new keys. Var keybinding is also set which invokes 

1175 the callback function, var_changed_keybinding, to add the change to 

1176 the 'keys' or 'extensions' changes tracker based on the binding type. 

1177 

1178 Tk Variables: 

1179 keybinding: Action/key bindings. 

1180 

1181 Methods: 

1182 load_keys_list: Reload active set. 

1183 create_new_key_set: Combine active keyset and changes. 

1184 set_keys_type: Command for keyset_source. 

1185 save_new_key_set: Save to idleConf.userCfg['keys'] (is function). 

1186 deactivate_current_config: Remove keys bindings in editors. 

1187 

1188 Widgets for KeysPage(frame): (*) widgets bound to self 

1189 frame_key_sets: LabelFrame 

1190 frames[0]: Frame 

1191 (*)builtin_keyset_on: Radiobutton - var keyset_source 

1192 (*)custom_keyset_on: Radiobutton - var keyset_source 

1193 (*)builtinlist: DynOptionMenu - var builtin_name, 

1194 func keybinding_selected 

1195 (*)customlist: DynOptionMenu - var custom_name, 

1196 func keybinding_selected 

1197 (*)keys_message: Label 

1198 frames[1]: Frame 

1199 (*)button_delete_custom_keys: Button - delete_custom_keys 

1200 (*)button_save_custom_keys: Button - save_as_new_key_set 

1201 frame_custom: LabelFrame 

1202 frame_target: Frame 

1203 target_title: Label 

1204 scroll_target_y: Scrollbar 

1205 scroll_target_x: Scrollbar 

1206 (*)bindingslist: ListBox - on_bindingslist_select 

1207 (*)button_new_keys: Button - get_new_keys & ..._name 

1208 """ 

1209 self.builtin_name = tracers.add( 

1210 StringVar(self), self.var_changed_builtin_name) 

1211 self.custom_name = tracers.add( 

1212 StringVar(self), self.var_changed_custom_name) 

1213 self.keyset_source = tracers.add( 

1214 BooleanVar(self), self.var_changed_keyset_source) 

1215 self.keybinding = tracers.add( 

1216 StringVar(self), self.var_changed_keybinding) 

1217 

1218 # Create widgets: 

1219 # body and section frames. 

1220 frame_custom = LabelFrame( 

1221 self, borderwidth=2, relief=GROOVE, 

1222 text=' Custom Key Bindings ') 

1223 frame_key_sets = LabelFrame( 

1224 self, borderwidth=2, relief=GROOVE, text=' Key Set ') 

1225 # frame_custom. 

1226 frame_target = Frame(frame_custom) 

1227 target_title = Label(frame_target, text='Action - Key(s)') 

1228 scroll_target_y = Scrollbar(frame_target) 

1229 scroll_target_x = Scrollbar(frame_target, orient=HORIZONTAL) 

1230 self.bindingslist = Listbox( 

1231 frame_target, takefocus=FALSE, exportselection=FALSE) 

1232 self.bindingslist.bind('<ButtonRelease-1>', 

1233 self.on_bindingslist_select) 

1234 scroll_target_y['command'] = self.bindingslist.yview 

1235 scroll_target_x['command'] = self.bindingslist.xview 

1236 self.bindingslist['yscrollcommand'] = scroll_target_y.set 

1237 self.bindingslist['xscrollcommand'] = scroll_target_x.set 

1238 self.button_new_keys = Button( 

1239 frame_custom, text='Get New Keys for Selection', 

1240 command=self.get_new_keys, state='disabled') 

1241 # frame_key_sets. 

1242 frames = [Frame(frame_key_sets, padding=2, borderwidth=0) 

1243 for i in range(2)] 

1244 self.builtin_keyset_on = Radiobutton( 

1245 frames[0], variable=self.keyset_source, value=1, 

1246 command=self.set_keys_type, text='Use a Built-in Key Set') 

1247 self.custom_keyset_on = Radiobutton( 

1248 frames[0], variable=self.keyset_source, value=0, 

1249 command=self.set_keys_type, text='Use a Custom Key Set') 

1250 self.builtinlist = DynOptionMenu( 

1251 frames[0], self.builtin_name, None, command=None) 

1252 self.customlist = DynOptionMenu( 

1253 frames[0], self.custom_name, None, command=None) 

1254 self.button_delete_custom_keys = Button( 

1255 frames[1], text='Delete Custom Key Set', 

1256 command=self.delete_custom_keys) 

1257 self.button_save_custom_keys = Button( 

1258 frames[1], text='Save as New Custom Key Set', 

1259 command=self.save_as_new_key_set) 

1260 self.keys_message = Label(frames[0], borderwidth=2) 

1261 

1262 # Pack widgets: 

1263 # body. 

1264 frame_custom.pack(side=BOTTOM, padx=5, pady=5, expand=TRUE, fill=BOTH) 

1265 frame_key_sets.pack(side=BOTTOM, padx=5, pady=5, fill=BOTH) 

1266 # frame_custom. 

1267 self.button_new_keys.pack(side=BOTTOM, fill=X, padx=5, pady=5) 

1268 frame_target.pack(side=LEFT, padx=5, pady=5, expand=TRUE, fill=BOTH) 

1269 # frame_target. 

1270 frame_target.columnconfigure(0, weight=1) 

1271 frame_target.rowconfigure(1, weight=1) 

1272 target_title.grid(row=0, column=0, columnspan=2, sticky=W) 

1273 self.bindingslist.grid(row=1, column=0, sticky=NSEW) 

1274 scroll_target_y.grid(row=1, column=1, sticky=NS) 

1275 scroll_target_x.grid(row=2, column=0, sticky=EW) 

1276 # frame_key_sets. 

1277 self.builtin_keyset_on.grid(row=0, column=0, sticky=W+NS) 

1278 self.custom_keyset_on.grid(row=1, column=0, sticky=W+NS) 

1279 self.builtinlist.grid(row=0, column=1, sticky=NSEW) 

1280 self.customlist.grid(row=1, column=1, sticky=NSEW) 

1281 self.keys_message.grid(row=0, column=2, sticky=NSEW, padx=5, pady=5) 

1282 self.button_delete_custom_keys.pack(side=LEFT, fill=X, expand=True, padx=2) 

1283 self.button_save_custom_keys.pack(side=LEFT, fill=X, expand=True, padx=2) 

1284 frames[0].pack(side=TOP, fill=BOTH, expand=True) 

1285 frames[1].pack(side=TOP, fill=X, expand=True, pady=2) 

1286 

1287 def load_key_cfg(self): 

1288 "Load current configuration settings for the keybinding options." 

1289 # Set current keys type radiobutton. 

1290 self.keyset_source.set(idleConf.GetOption( 

1291 'main', 'Keys', 'default', type='bool', default=1)) 

1292 # Set current keys. 

1293 current_option = idleConf.CurrentKeys() 

1294 # Load available keyset option menus. 

1295 if self.keyset_source.get(): # Default theme selected. 

1296 item_list = idleConf.GetSectionList('default', 'keys') 

1297 item_list.sort() 

1298 self.builtinlist.SetMenu(item_list, current_option) 

1299 item_list = idleConf.GetSectionList('user', 'keys') 

1300 item_list.sort() 

1301 if not item_list: 

1302 self.custom_keyset_on.state(('disabled',)) 

1303 self.custom_name.set('- no custom keys -') 

1304 else: 

1305 self.customlist.SetMenu(item_list, item_list[0]) 

1306 else: # User key set selected. 

1307 item_list = idleConf.GetSectionList('user', 'keys') 

1308 item_list.sort() 

1309 self.customlist.SetMenu(item_list, current_option) 

1310 item_list = idleConf.GetSectionList('default', 'keys') 

1311 item_list.sort() 

1312 self.builtinlist.SetMenu(item_list, idleConf.default_keys()) 

1313 self.set_keys_type() 

1314 # Load keyset element list. 

1315 keyset_name = idleConf.CurrentKeys() 

1316 self.load_keys_list(keyset_name) 

1317 

1318 def var_changed_builtin_name(self, *params): 

1319 "Process selection of builtin key set." 

1320 old_keys = ( 

1321 'IDLE Classic Windows', 

1322 'IDLE Classic Unix', 

1323 'IDLE Classic Mac', 

1324 'IDLE Classic OSX', 

1325 ) 

1326 value = self.builtin_name.get() 

1327 if value not in old_keys: 

1328 if idleConf.GetOption('main', 'Keys', 'name') not in old_keys: 

1329 changes.add_option('main', 'Keys', 'name', old_keys[0]) 

1330 changes.add_option('main', 'Keys', 'name2', value) 

1331 self.keys_message['text'] = 'New key set, see Help' 

1332 else: 

1333 changes.add_option('main', 'Keys', 'name', value) 

1334 changes.add_option('main', 'Keys', 'name2', '') 

1335 self.keys_message['text'] = '' 

1336 self.load_keys_list(value) 

1337 

1338 def var_changed_custom_name(self, *params): 

1339 "Process selection of custom key set." 

1340 value = self.custom_name.get() 

1341 if value != '- no custom keys -': 

1342 changes.add_option('main', 'Keys', 'name', value) 

1343 self.load_keys_list(value) 

1344 

1345 def var_changed_keyset_source(self, *params): 

1346 "Process toggle between builtin key set and custom key set." 

1347 value = self.keyset_source.get() 

1348 changes.add_option('main', 'Keys', 'default', value) 

1349 if value: 

1350 self.var_changed_builtin_name() 

1351 else: 

1352 self.var_changed_custom_name() 

1353 

1354 def var_changed_keybinding(self, *params): 

1355 "Store change to a keybinding." 

1356 value = self.keybinding.get() 

1357 key_set = self.custom_name.get() 

1358 event = self.bindingslist.get(ANCHOR).split()[0] 

1359 if idleConf.IsCoreBinding(event): 

1360 changes.add_option('keys', key_set, event, value) 

1361 else: # Event is an extension binding. 

1362 ext_name = idleConf.GetExtnNameForEvent(event) 

1363 ext_keybind_section = ext_name + '_cfgBindings' 

1364 changes.add_option('extensions', ext_keybind_section, event, value) 

1365 

1366 def set_keys_type(self): 

1367 "Set available screen options based on builtin or custom key set." 

1368 if self.keyset_source.get(): 

1369 self.builtinlist['state'] = 'normal' 

1370 self.customlist['state'] = 'disabled' 

1371 self.button_delete_custom_keys.state(('disabled',)) 

1372 else: 

1373 self.builtinlist['state'] = 'disabled' 

1374 self.custom_keyset_on.state(('!disabled',)) 

1375 self.customlist['state'] = 'normal' 

1376 self.button_delete_custom_keys.state(('!disabled',)) 

1377 

1378 def get_new_keys(self): 

1379 """Handle event to change key binding for selected line. 

1380 

1381 A selection of a key/binding in the list of current 

1382 bindings pops up a dialog to enter a new binding. If 

1383 the current key set is builtin and a binding has 

1384 changed, then a name for a custom key set needs to be 

1385 entered for the change to be applied. 

1386 """ 

1387 list_index = self.bindingslist.index(ANCHOR) 

1388 binding = self.bindingslist.get(list_index) 

1389 bind_name = binding.split()[0] 

1390 if self.keyset_source.get(): 

1391 current_key_set_name = self.builtin_name.get() 

1392 else: 

1393 current_key_set_name = self.custom_name.get() 

1394 current_bindings = idleConf.GetCurrentKeySet() 

1395 if current_key_set_name in changes['keys']: # unsaved changes 

1396 key_set_changes = changes['keys'][current_key_set_name] 

1397 for event in key_set_changes: 

1398 current_bindings[event] = key_set_changes[event].split() 

1399 current_key_sequences = list(current_bindings.values()) 

1400 new_keys = GetKeysWindow(self, 'Get New Keys', bind_name, 

1401 current_key_sequences).result 

1402 if new_keys: 

1403 if self.keyset_source.get(): # Current key set is a built-in. 

1404 message = ('Your changes will be saved as a new Custom Key Set.' 

1405 ' Enter a name for your new Custom Key Set below.') 

1406 new_keyset = self.get_new_keys_name(message) 

1407 if not new_keyset: # User cancelled custom key set creation. 

1408 self.bindingslist.select_set(list_index) 

1409 self.bindingslist.select_anchor(list_index) 

1410 return 

1411 else: # Create new custom key set based on previously active key set. 

1412 self.create_new_key_set(new_keyset) 

1413 self.bindingslist.delete(list_index) 

1414 self.bindingslist.insert(list_index, bind_name+' - '+new_keys) 

1415 self.bindingslist.select_set(list_index) 

1416 self.bindingslist.select_anchor(list_index) 

1417 self.keybinding.set(new_keys) 

1418 else: 

1419 self.bindingslist.select_set(list_index) 

1420 self.bindingslist.select_anchor(list_index) 

1421 

1422 def get_new_keys_name(self, message): 

1423 "Return new key set name from query popup." 

1424 used_names = (idleConf.GetSectionList('user', 'keys') + 

1425 idleConf.GetSectionList('default', 'keys')) 

1426 new_keyset = SectionName( 

1427 self, 'New Custom Key Set', message, used_names).result 

1428 return new_keyset 

1429 

1430 def save_as_new_key_set(self): 

1431 "Prompt for name of new key set and save changes using that name." 

1432 new_keys_name = self.get_new_keys_name('New Key Set Name:') 

1433 if new_keys_name: 

1434 self.create_new_key_set(new_keys_name) 

1435 

1436 def on_bindingslist_select(self, event): 

1437 "Activate button to assign new keys to selected action." 

1438 self.button_new_keys.state(('!disabled',)) 

1439 

1440 def create_new_key_set(self, new_key_set_name): 

1441 """Create a new custom key set with the given name. 

1442 

1443 Copy the bindings/keys from the previously active keyset 

1444 to the new keyset and activate the new custom keyset. 

1445 """ 

1446 if self.keyset_source.get(): 

1447 prev_key_set_name = self.builtin_name.get() 

1448 else: 

1449 prev_key_set_name = self.custom_name.get() 

1450 prev_keys = idleConf.GetCoreKeys(prev_key_set_name) 

1451 new_keys = {} 

1452 for event in prev_keys: # Add key set to changed items. 

1453 event_name = event[2:-2] # Trim off the angle brackets. 

1454 binding = ' '.join(prev_keys[event]) 

1455 new_keys[event_name] = binding 

1456 # Handle any unsaved changes to prev key set. 

1457 if prev_key_set_name in changes['keys']: 

1458 key_set_changes = changes['keys'][prev_key_set_name] 

1459 for event in key_set_changes: 

1460 new_keys[event] = key_set_changes[event] 

1461 # Save the new key set. 

1462 self.save_new_key_set(new_key_set_name, new_keys) 

1463 # Change GUI over to the new key set. 

1464 custom_key_list = idleConf.GetSectionList('user', 'keys') 

1465 custom_key_list.sort() 

1466 self.customlist.SetMenu(custom_key_list, new_key_set_name) 

1467 self.keyset_source.set(0) 

1468 self.set_keys_type() 

1469 

1470 def load_keys_list(self, keyset_name): 

1471 """Reload the list of action/key binding pairs for the active key set. 

1472 

1473 An action/key binding can be selected to change the key binding. 

1474 """ 

1475 reselect = False 

1476 if self.bindingslist.curselection(): 

1477 reselect = True 

1478 list_index = self.bindingslist.index(ANCHOR) 

1479 keyset = idleConf.GetKeySet(keyset_name) 

1480 bind_names = list(keyset.keys()) 

1481 bind_names.sort() 

1482 self.bindingslist.delete(0, END) 

1483 for bind_name in bind_names: 

1484 key = ' '.join(keyset[bind_name]) 

1485 bind_name = bind_name[2:-2] # Trim off the angle brackets. 

1486 if keyset_name in changes['keys']: 

1487 # Handle any unsaved changes to this key set. 

1488 if bind_name in changes['keys'][keyset_name]: 

1489 key = changes['keys'][keyset_name][bind_name] 

1490 self.bindingslist.insert(END, bind_name+' - '+key) 

1491 if reselect: 

1492 self.bindingslist.see(list_index) 

1493 self.bindingslist.select_set(list_index) 

1494 self.bindingslist.select_anchor(list_index) 

1495 

1496 @staticmethod 

1497 def save_new_key_set(keyset_name, keyset): 

1498 """Save a newly created core key set. 

1499 

1500 Add keyset to idleConf.userCfg['keys'], not to disk. 

1501 If the keyset doesn't exist, it is created. The 

1502 binding/keys are taken from the keyset argument. 

1503 

1504 keyset_name - string, the name of the new key set 

1505 keyset - dictionary containing the new keybindings 

1506 """ 

1507 idleConf.userCfg['keys'].AddSection(keyset_name) 

1508 for event in keyset: 

1509 value = keyset[event] 

1510 idleConf.userCfg['keys'].SetOption(keyset_name, event, value) 

1511 

1512 def askyesno(self, *args, **kwargs): 

1513 # Make testing easier. Could change implementation. 

1514 return messagebox.askyesno(*args, **kwargs) 

1515 

1516 def delete_custom_keys(self): 

1517 """Handle event to delete a custom key set. 

1518 

1519 Applying the delete deactivates the current configuration and 

1520 reverts to the default. The custom key set is permanently 

1521 deleted from the config file. 

1522 """ 

1523 keyset_name = self.custom_name.get() 

1524 delmsg = 'Are you sure you wish to delete the key set %r ?' 

1525 if not self.askyesno( 

1526 'Delete Key Set', delmsg % keyset_name, parent=self): 

1527 return 

1528 self.cd.deactivate_current_config() 

1529 # Remove key set from changes, config, and file. 

1530 changes.delete_section('keys', keyset_name) 

1531 # Reload user key set list. 

1532 item_list = idleConf.GetSectionList('user', 'keys') 

1533 item_list.sort() 

1534 if not item_list: 

1535 self.custom_keyset_on.state(('disabled',)) 

1536 self.customlist.SetMenu(item_list, '- no custom keys -') 

1537 else: 

1538 self.customlist.SetMenu(item_list, item_list[0]) 

1539 # Revert to default key set. 

1540 self.keyset_source.set(idleConf.defaultCfg['main'] 

1541 .Get('Keys', 'default')) 

1542 self.builtin_name.set(idleConf.defaultCfg['main'].Get('Keys', 'name') 

1543 or idleConf.default_keys()) 

1544 # User can't back out of these changes, they must be applied now. 

1545 changes.save_all() 

1546 self.extpage.save_all_changed_extensions() 

1547 self.cd.activate_config_changes() 

1548 self.set_keys_type() 

1549 

1550 

1551class WinPage(Frame): 

1552 

1553 def __init__(self, master): 

1554 super().__init__(master) 

1555 

1556 self.init_validators() 

1557 self.create_page_windows() 

1558 self.load_windows_cfg() 

1559 

1560 def init_validators(self): 

1561 digits_or_empty_re = re.compile(r'[0-9]*') 

1562 def is_digits_or_empty(s): 

1563 "Return 's is blank or contains only digits'" 

1564 return digits_or_empty_re.fullmatch(s) is not None 

1565 self.digits_only = (self.register(is_digits_or_empty), '%P',) 

1566 

1567 def create_page_windows(self): 

1568 """Return frame of widgets for Windows tab. 

1569 

1570 Enable users to provisionally change general window options. 

1571 Function load_windows_cfg initializes tk variable idleConf. 

1572 Radiobuttons startup_shell_on and startup_editor_on set var 

1573 startup_edit. Entry boxes win_width_int and win_height_int set var 

1574 win_width and win_height. Setting var_name invokes the default 

1575 callback that adds option to changes. 

1576 

1577 Widgets for WinPage(Frame): > vars, bound to self 

1578 frame_window: LabelFrame 

1579 frame_run: Frame 

1580 startup_title: Label 

1581 startup_editor_on: Radiobutton > startup_edit 

1582 startup_shell_on: Radiobutton > startup_edit 

1583 frame_win_size: Frame 

1584 win_size_title: Label 

1585 win_width_title: Label 

1586 win_width_int: Entry > win_width 

1587 win_height_title: Label 

1588 win_height_int: Entry > win_height 

1589 frame_cursor: Frame 

1590 indent_title: Label 

1591 indent_chooser: Spinbox > indent_spaces 

1592 blink_on: Checkbutton > cursor_blink 

1593 frame_autocomplete: Frame 

1594 auto_wait_title: Label 

1595 auto_wait_int: Entry > autocomplete_wait 

1596 frame_paren1: Frame 

1597 paren_style_title: Label 

1598 paren_style_type: OptionMenu > paren_style 

1599 frame_paren2: Frame 

1600 paren_time_title: Label 

1601 paren_flash_time: Entry > flash_delay 

1602 bell_on: Checkbutton > paren_bell 

1603 frame_format: Frame 

1604 format_width_title: Label 

1605 format_width_int: Entry > format_width 

1606 """ 

1607 # Integer values need StringVar because int('') raises. 

1608 self.startup_edit = tracers.add( 

1609 IntVar(self), ('main', 'General', 'editor-on-startup')) 

1610 self.win_width = tracers.add( 

1611 StringVar(self), ('main', 'EditorWindow', 'width')) 

1612 self.win_height = tracers.add( 

1613 StringVar(self), ('main', 'EditorWindow', 'height')) 

1614 self.indent_spaces = tracers.add( 

1615 StringVar(self), ('main', 'Indent', 'num-spaces')) 

1616 self.cursor_blink = tracers.add( 

1617 BooleanVar(self), ('main', 'EditorWindow', 'cursor-blink')) 

1618 self.autocomplete_wait = tracers.add( 

1619 StringVar(self), ('extensions', 'AutoComplete', 'popupwait')) 

1620 self.paren_style = tracers.add( 

1621 StringVar(self), ('extensions', 'ParenMatch', 'style')) 

1622 self.flash_delay = tracers.add( 

1623 StringVar(self), ('extensions', 'ParenMatch', 'flash-delay')) 

1624 self.paren_bell = tracers.add( 

1625 BooleanVar(self), ('extensions', 'ParenMatch', 'bell')) 

1626 self.format_width = tracers.add( 

1627 StringVar(self), ('extensions', 'FormatParagraph', 'max-width')) 

1628 

1629 # Create widgets: 

1630 frame_window = LabelFrame(self, borderwidth=2, relief=GROOVE, 

1631 text=' Window Preferences') 

1632 

1633 frame_run = Frame(frame_window, borderwidth=0) 

1634 startup_title = Label(frame_run, text='At Startup') 

1635 self.startup_editor_on = Radiobutton( 

1636 frame_run, variable=self.startup_edit, value=1, 

1637 text="Open Edit Window") 

1638 self.startup_shell_on = Radiobutton( 

1639 frame_run, variable=self.startup_edit, value=0, 

1640 text='Open Shell Window') 

1641 

1642 frame_win_size = Frame(frame_window, borderwidth=0) 

1643 win_size_title = Label( 

1644 frame_win_size, text='Initial Window Size (in characters)') 

1645 win_width_title = Label(frame_win_size, text='Width') 

1646 self.win_width_int = Entry( 

1647 frame_win_size, textvariable=self.win_width, width=3, 

1648 validatecommand=self.digits_only, validate='key', 

1649 ) 

1650 win_height_title = Label(frame_win_size, text='Height') 

1651 self.win_height_int = Entry( 

1652 frame_win_size, textvariable=self.win_height, width=3, 

1653 validatecommand=self.digits_only, validate='key', 

1654 ) 

1655 

1656 frame_cursor = Frame(frame_window, borderwidth=0) 

1657 indent_title = Label(frame_cursor, 

1658 text='Indent spaces (4 is standard)') 

1659 try: 

1660 self.indent_chooser = Spinbox( 

1661 frame_cursor, textvariable=self.indent_spaces, 

1662 from_=1, to=10, width=2, 

1663 validatecommand=self.digits_only, validate='key') 

1664 except TclError: 

1665 self.indent_chooser = Combobox( 

1666 frame_cursor, textvariable=self.indent_spaces, 

1667 state="readonly", values=list(range(1,11)), width=3) 

1668 cursor_blink_title = Label(frame_cursor, text='Cursor Blink') 

1669 self.cursor_blink_bool = Checkbutton(frame_cursor, text="Cursor blink", 

1670 variable=self.cursor_blink) 

1671 

1672 frame_autocomplete = Frame(frame_window, borderwidth=0,) 

1673 auto_wait_title = Label(frame_autocomplete, 

1674 text='Completions Popup Wait (milliseconds)') 

1675 self.auto_wait_int = Entry( 

1676 frame_autocomplete, textvariable=self.autocomplete_wait, 

1677 width=6, validatecommand=self.digits_only, validate='key') 

1678 

1679 frame_paren1 = Frame(frame_window, borderwidth=0) 

1680 paren_style_title = Label(frame_paren1, text='Paren Match Style') 

1681 self.paren_style_type = OptionMenu( 

1682 frame_paren1, self.paren_style, 'expression', 

1683 "opener","parens","expression") 

1684 frame_paren2 = Frame(frame_window, borderwidth=0) 

1685 paren_time_title = Label( 

1686 frame_paren2, text='Time Match Displayed (milliseconds)\n' 

1687 '(0 is until next input)') 

1688 self.paren_flash_time = Entry( 

1689 frame_paren2, textvariable=self.flash_delay, width=6, 

1690 validatecommand=self.digits_only, validate='key') 

1691 self.bell_on = Checkbutton( 

1692 frame_paren2, text="Bell on Mismatch", variable=self.paren_bell) 

1693 frame_format = Frame(frame_window, borderwidth=0) 

1694 format_width_title = Label(frame_format, 

1695 text='Format Paragraph Max Width') 

1696 self.format_width_int = Entry( 

1697 frame_format, textvariable=self.format_width, width=4, 

1698 validatecommand=self.digits_only, validate='key', 

1699 ) 

1700 

1701 # Pack widgets: 

1702 frame_window.pack(side=TOP, padx=5, pady=5, expand=TRUE, fill=BOTH) 

1703 # frame_run. 

1704 frame_run.pack(side=TOP, padx=5, pady=0, fill=X) 

1705 startup_title.pack(side=LEFT, anchor=W, padx=5, pady=5) 

1706 self.startup_shell_on.pack(side=RIGHT, anchor=W, padx=5, pady=5) 

1707 self.startup_editor_on.pack(side=RIGHT, anchor=W, padx=5, pady=5) 

1708 # frame_win_size. 

1709 frame_win_size.pack(side=TOP, padx=5, pady=0, fill=X) 

1710 win_size_title.pack(side=LEFT, anchor=W, padx=5, pady=5) 

1711 self.win_height_int.pack(side=RIGHT, anchor=E, padx=10, pady=5) 

1712 win_height_title.pack(side=RIGHT, anchor=E, pady=5) 

1713 self.win_width_int.pack(side=RIGHT, anchor=E, padx=10, pady=5) 

1714 win_width_title.pack(side=RIGHT, anchor=E, pady=5) 

1715 # frame_cursor. 

1716 frame_cursor.pack(side=TOP, padx=5, pady=0, fill=X) 

1717 indent_title.pack(side=LEFT, anchor=W, padx=5) 

1718 self.indent_chooser.pack(side=LEFT, anchor=W, padx=10) 

1719 self.cursor_blink_bool.pack(side=RIGHT, anchor=E, padx=15, pady=5) 

1720 # frame_autocomplete. 

1721 frame_autocomplete.pack(side=TOP, padx=5, pady=0, fill=X) 

1722 auto_wait_title.pack(side=LEFT, anchor=W, padx=5, pady=5) 

1723 self.auto_wait_int.pack(side=TOP, padx=10, pady=5) 

1724 # frame_paren. 

1725 frame_paren1.pack(side=TOP, padx=5, pady=0, fill=X) 

1726 paren_style_title.pack(side=LEFT, anchor=W, padx=5, pady=5) 

1727 self.paren_style_type.pack(side=TOP, padx=10, pady=5) 

1728 frame_paren2.pack(side=TOP, padx=5, pady=0, fill=X) 

1729 paren_time_title.pack(side=LEFT, anchor=W, padx=5) 

1730 self.bell_on.pack(side=RIGHT, anchor=E, padx=15, pady=5) 

1731 self.paren_flash_time.pack(side=TOP, anchor=W, padx=15, pady=5) 

1732 # frame_format. 

1733 frame_format.pack(side=TOP, padx=5, pady=0, fill=X) 

1734 format_width_title.pack(side=LEFT, anchor=W, padx=5, pady=5) 

1735 self.format_width_int.pack(side=TOP, padx=10, pady=5) 

1736 

1737 def load_windows_cfg(self): 

1738 # Set variables for all windows. 

1739 self.startup_edit.set(idleConf.GetOption( 

1740 'main', 'General', 'editor-on-startup', type='bool')) 

1741 self.win_width.set(idleConf.GetOption( 

1742 'main', 'EditorWindow', 'width', type='int')) 

1743 self.win_height.set(idleConf.GetOption( 

1744 'main', 'EditorWindow', 'height', type='int')) 

1745 self.indent_spaces.set(idleConf.GetOption( 

1746 'main', 'Indent', 'num-spaces', type='int')) 

1747 self.cursor_blink.set(idleConf.GetOption( 

1748 'main', 'EditorWindow', 'cursor-blink', type='bool')) 

1749 self.autocomplete_wait.set(idleConf.GetOption( 

1750 'extensions', 'AutoComplete', 'popupwait', type='int')) 

1751 self.paren_style.set(idleConf.GetOption( 

1752 'extensions', 'ParenMatch', 'style')) 

1753 self.flash_delay.set(idleConf.GetOption( 

1754 'extensions', 'ParenMatch', 'flash-delay', type='int')) 

1755 self.paren_bell.set(idleConf.GetOption( 

1756 'extensions', 'ParenMatch', 'bell')) 

1757 self.format_width.set(idleConf.GetOption( 

1758 'extensions', 'FormatParagraph', 'max-width', type='int')) 

1759 

1760 

1761class ShedPage(Frame): 

1762 

1763 def __init__(self, master): 

1764 super().__init__(master) 

1765 

1766 self.init_validators() 

1767 self.create_page_shed() 

1768 self.load_shelled_cfg() 

1769 

1770 def init_validators(self): 

1771 digits_or_empty_re = re.compile(r'[0-9]*') 

1772 def is_digits_or_empty(s): 

1773 "Return 's is blank or contains only digits'" 

1774 return digits_or_empty_re.fullmatch(s) is not None 

1775 self.digits_only = (self.register(is_digits_or_empty), '%P',) 

1776 

1777 def create_page_shed(self): 

1778 """Return frame of widgets for Shell/Ed tab. 

1779 

1780 Enable users to provisionally change shell and editor options. 

1781 Function load_shed_cfg initializes tk variables using idleConf. 

1782 Entry box auto_squeeze_min_lines_int sets 

1783 auto_squeeze_min_lines_int. Setting var_name invokes the 

1784 default callback that adds option to changes. 

1785 

1786 Widgets for ShedPage(Frame): (*) widgets bound to self 

1787 frame_shell: LabelFrame 

1788 frame_auto_squeeze_min_lines: Frame 

1789 auto_squeeze_min_lines_title: Label 

1790 (*)auto_squeeze_min_lines_int: Entry - 

1791 auto_squeeze_min_lines 

1792 frame_editor: LabelFrame 

1793 frame_save: Frame 

1794 run_save_title: Label 

1795 (*)save_ask_on: Radiobutton - autosave 

1796 (*)save_auto_on: Radiobutton - autosave 

1797 frame_format: Frame 

1798 format_width_title: Label 

1799 (*)format_width_int: Entry - format_width 

1800 frame_line_numbers_default: Frame 

1801 line_numbers_default_title: Label 

1802 (*)line_numbers_default_bool: Checkbutton - line_numbers_default 

1803 frame_context: Frame 

1804 context_title: Label 

1805 (*)context_int: Entry - context_lines 

1806 """ 

1807 # Integer values need StringVar because int('') raises. 

1808 self.auto_squeeze_min_lines = tracers.add( 

1809 StringVar(self), ('main', 'PyShell', 'auto-squeeze-min-lines')) 

1810 

1811 self.autosave = tracers.add( 

1812 IntVar(self), ('main', 'General', 'autosave')) 

1813 self.line_numbers_default = tracers.add( 

1814 BooleanVar(self), 

1815 ('main', 'EditorWindow', 'line-numbers-default')) 

1816 self.context_lines = tracers.add( 

1817 StringVar(self), ('extensions', 'CodeContext', 'maxlines')) 

1818 

1819 # Create widgets: 

1820 frame_shell = LabelFrame(self, borderwidth=2, relief=GROOVE, 

1821 text=' Shell Preferences') 

1822 frame_editor = LabelFrame(self, borderwidth=2, relief=GROOVE, 

1823 text=' Editor Preferences') 

1824 # Frame_shell. 

1825 frame_auto_squeeze_min_lines = Frame(frame_shell, borderwidth=0) 

1826 auto_squeeze_min_lines_title = Label(frame_auto_squeeze_min_lines, 

1827 text='Auto-Squeeze Min. Lines:') 

1828 self.auto_squeeze_min_lines_int = Entry( 

1829 frame_auto_squeeze_min_lines, width=4, 

1830 textvariable=self.auto_squeeze_min_lines, 

1831 validatecommand=self.digits_only, validate='key', 

1832 ) 

1833 # Frame_editor. 

1834 frame_save = Frame(frame_editor, borderwidth=0) 

1835 run_save_title = Label(frame_save, text='At Start of Run (F5) ') 

1836 

1837 self.save_ask_on = Radiobutton( 

1838 frame_save, variable=self.autosave, value=0, 

1839 text="Prompt to Save") 

1840 self.save_auto_on = Radiobutton( 

1841 frame_save, variable=self.autosave, value=1, 

1842 text='No Prompt') 

1843 

1844 frame_line_numbers_default = Frame(frame_editor, borderwidth=0) 

1845 line_numbers_default_title = Label( 

1846 frame_line_numbers_default, text='Show line numbers in new windows') 

1847 self.line_numbers_default_bool = Checkbutton( 

1848 frame_line_numbers_default, 

1849 variable=self.line_numbers_default, 

1850 width=1) 

1851 

1852 frame_context = Frame(frame_editor, borderwidth=0) 

1853 context_title = Label(frame_context, text='Max Context Lines :') 

1854 self.context_int = Entry( 

1855 frame_context, textvariable=self.context_lines, width=3, 

1856 validatecommand=self.digits_only, validate='key', 

1857 ) 

1858 

1859 # Pack widgets: 

1860 frame_shell.pack(side=TOP, padx=5, pady=5, fill=BOTH) 

1861 Label(self).pack() # Spacer -- better solution? 

1862 frame_editor.pack(side=TOP, padx=5, pady=5, fill=BOTH) 

1863 # frame_auto_squeeze_min_lines 

1864 frame_auto_squeeze_min_lines.pack(side=TOP, padx=5, pady=0, fill=X) 

1865 auto_squeeze_min_lines_title.pack(side=LEFT, anchor=W, padx=5, pady=5) 

1866 self.auto_squeeze_min_lines_int.pack(side=TOP, padx=5, pady=5) 

1867 # frame_save. 

1868 frame_save.pack(side=TOP, padx=5, pady=0, fill=X) 

1869 run_save_title.pack(side=LEFT, anchor=W, padx=5, pady=5) 

1870 self.save_auto_on.pack(side=RIGHT, anchor=W, padx=5, pady=5) 

1871 self.save_ask_on.pack(side=RIGHT, anchor=W, padx=5, pady=5) 

1872 # frame_line_numbers_default. 

1873 frame_line_numbers_default.pack(side=TOP, padx=5, pady=0, fill=X) 

1874 line_numbers_default_title.pack(side=LEFT, anchor=W, padx=5, pady=5) 

1875 self.line_numbers_default_bool.pack(side=LEFT, padx=5, pady=5) 

1876 # frame_context. 

1877 frame_context.pack(side=TOP, padx=5, pady=0, fill=X) 

1878 context_title.pack(side=LEFT, anchor=W, padx=5, pady=5) 

1879 self.context_int.pack(side=TOP, padx=5, pady=5) 

1880 

1881 def load_shelled_cfg(self): 

1882 # Set variables for shell windows. 

1883 self.auto_squeeze_min_lines.set(idleConf.GetOption( 

1884 'main', 'PyShell', 'auto-squeeze-min-lines', type='int')) 

1885 # Set variables for editor windows. 

1886 self.autosave.set(idleConf.GetOption( 

1887 'main', 'General', 'autosave', default=0, type='bool')) 

1888 self.line_numbers_default.set(idleConf.GetOption( 

1889 'main', 'EditorWindow', 'line-numbers-default', type='bool')) 

1890 self.context_lines.set(idleConf.GetOption( 

1891 'extensions', 'CodeContext', 'maxlines', type='int')) 

1892 

1893 

1894class ExtPage(Frame): 

1895 def __init__(self, master): 

1896 super().__init__(master) 

1897 self.ext_defaultCfg = idleConf.defaultCfg['extensions'] 

1898 self.ext_userCfg = idleConf.userCfg['extensions'] 

1899 self.is_int = self.register(is_int) 

1900 self.load_extensions() 

1901 self.create_page_extensions() # Requires extension names. 

1902 

1903 def create_page_extensions(self): 

1904 """Configure IDLE feature extensions and help menu extensions. 

1905 

1906 List the feature extensions and a configuration box for the 

1907 selected extension. Help menu extensions are in a HelpFrame. 

1908 

1909 This code reads the current configuration using idleConf, 

1910 supplies a GUI interface to change the configuration values, 

1911 and saves the changes using idleConf. 

1912 

1913 Some changes may require restarting IDLE. This depends on each 

1914 extension's implementation. 

1915 

1916 All values are treated as text, and it is up to the user to 

1917 supply reasonable values. The only exception to this are the 

1918 'enable*' options, which are boolean, and can be toggled with a 

1919 True/False button. 

1920 

1921 Methods: 

1922 extension_selected: Handle selection from list. 

1923 create_extension_frame: Hold widgets for one extension. 

1924 set_extension_value: Set in userCfg['extensions']. 

1925 save_all_changed_extensions: Call extension page Save(). 

1926 """ 

1927 self.extension_names = StringVar(self) 

1928 

1929 frame_ext = LabelFrame(self, borderwidth=2, relief=GROOVE, 

1930 text=' Feature Extensions ') 

1931 self.frame_help = HelpFrame(self, borderwidth=2, relief=GROOVE, 

1932 text=' Help Menu Extensions ') 

1933 

1934 frame_ext.rowconfigure(0, weight=1) 

1935 frame_ext.columnconfigure(2, weight=1) 

1936 self.extension_list = Listbox(frame_ext, listvariable=self.extension_names, 

1937 selectmode='browse') 

1938 self.extension_list.bind('<<ListboxSelect>>', self.extension_selected) 

1939 scroll = Scrollbar(frame_ext, command=self.extension_list.yview) 

1940 self.extension_list.yscrollcommand=scroll.set 

1941 self.details_frame = LabelFrame(frame_ext, width=250, height=250) 

1942 self.extension_list.grid(column=0, row=0, sticky='nws') 

1943 scroll.grid(column=1, row=0, sticky='ns') 

1944 self.details_frame.grid(column=2, row=0, sticky='nsew', padx=[10, 0]) 

1945 frame_ext.configure(padding=10) 

1946 self.config_frame = {} 

1947 self.current_extension = None 

1948 

1949 self.outerframe = self # TEMPORARY 

1950 self.tabbed_page_set = self.extension_list # TEMPORARY 

1951 

1952 # Create the frame holding controls for each extension. 

1953 ext_names = '' 

1954 for ext_name in sorted(self.extensions): 

1955 self.create_extension_frame(ext_name) 

1956 ext_names = ext_names + '{' + ext_name + '} ' 

1957 self.extension_names.set(ext_names) 

1958 self.extension_list.selection_set(0) 

1959 self.extension_selected(None) 

1960 

1961 

1962 frame_ext.grid(row=0, column=0, sticky='nsew') 

1963 Label(self).grid(row=1, column=0) # Spacer. Replace with config? 

1964 self.frame_help.grid(row=2, column=0, sticky='sew') 

1965 

1966 def load_extensions(self): 

1967 "Fill self.extensions with data from the default and user configs." 

1968 self.extensions = {} 

1969 for ext_name in idleConf.GetExtensions(active_only=False): 

1970 # Former built-in extensions are already filtered out. 

1971 self.extensions[ext_name] = [] 

1972 

1973 for ext_name in self.extensions: 

1974 opt_list = sorted(self.ext_defaultCfg.GetOptionList(ext_name)) 

1975 

1976 # Bring 'enable' options to the beginning of the list. 

1977 enables = [opt_name for opt_name in opt_list 

1978 if opt_name.startswith('enable')] 

1979 for opt_name in enables: 

1980 opt_list.remove(opt_name) 

1981 opt_list = enables + opt_list 

1982 

1983 for opt_name in opt_list: 

1984 def_str = self.ext_defaultCfg.Get( 

1985 ext_name, opt_name, raw=True) 

1986 try: 

1987 def_obj = {'True':True, 'False':False}[def_str] 

1988 opt_type = 'bool' 

1989 except KeyError: 

1990 try: 

1991 def_obj = int(def_str) 

1992 opt_type = 'int' 

1993 except ValueError: 

1994 def_obj = def_str 

1995 opt_type = None 

1996 try: 

1997 value = self.ext_userCfg.Get( 

1998 ext_name, opt_name, type=opt_type, raw=True, 

1999 default=def_obj) 

2000 except ValueError: # Need this until .Get fixed. 

2001 value = def_obj # Bad values overwritten by entry. 

2002 var = StringVar(self) 

2003 var.set(str(value)) 

2004 

2005 self.extensions[ext_name].append({'name': opt_name, 

2006 'type': opt_type, 

2007 'default': def_str, 

2008 'value': value, 

2009 'var': var, 

2010 }) 

2011 

2012 def extension_selected(self, event): 

2013 "Handle selection of an extension from the list." 

2014 newsel = self.extension_list.curselection() 

2015 if newsel: 

2016 newsel = self.extension_list.get(newsel) 

2017 if newsel is None or newsel != self.current_extension: 

2018 if self.current_extension: 

2019 self.details_frame.config(text='') 

2020 self.config_frame[self.current_extension].grid_forget() 

2021 self.current_extension = None 

2022 if newsel: 

2023 self.details_frame.config(text=newsel) 

2024 self.config_frame[newsel].grid(column=0, row=0, sticky='nsew') 

2025 self.current_extension = newsel 

2026 

2027 def create_extension_frame(self, ext_name): 

2028 """Create a frame holding the widgets to configure one extension""" 

2029 f = VerticalScrolledFrame(self.details_frame, height=250, width=250) 

2030 self.config_frame[ext_name] = f 

2031 entry_area = f.interior 

2032 # Create an entry for each configuration option. 

2033 for row, opt in enumerate(self.extensions[ext_name]): 

2034 # Create a row with a label and entry/checkbutton. 

2035 label = Label(entry_area, text=opt['name']) 

2036 label.grid(row=row, column=0, sticky=NW) 

2037 var = opt['var'] 

2038 if opt['type'] == 'bool': 

2039 Checkbutton(entry_area, variable=var, 

2040 onvalue='True', offvalue='False', width=8 

2041 ).grid(row=row, column=1, sticky=W, padx=7) 

2042 elif opt['type'] == 'int': 

2043 Entry(entry_area, textvariable=var, validate='key', 

2044 validatecommand=(self.is_int, '%P'), width=10 

2045 ).grid(row=row, column=1, sticky=NSEW, padx=7) 

2046 

2047 else: # type == 'str' 

2048 # Limit size to fit non-expanding space with larger font. 

2049 Entry(entry_area, textvariable=var, width=15 

2050 ).grid(row=row, column=1, sticky=NSEW, padx=7) 

2051 return 

2052 

2053 def set_extension_value(self, section, opt): 

2054 """Return True if the configuration was added or changed. 

2055 

2056 If the value is the same as the default, then remove it 

2057 from user config file. 

2058 """ 

2059 name = opt['name'] 

2060 default = opt['default'] 

2061 value = opt['var'].get().strip() or default 

2062 opt['var'].set(value) 

2063 # if self.defaultCfg.has_section(section): 

2064 # Currently, always true; if not, indent to return. 

2065 if (value == default): 

2066 return self.ext_userCfg.RemoveOption(section, name) 

2067 # Set the option. 

2068 return self.ext_userCfg.SetOption(section, name, value) 

2069 

2070 def save_all_changed_extensions(self): 

2071 """Save configuration changes to the user config file. 

2072 

2073 Attributes accessed: 

2074 extensions 

2075 

2076 Methods: 

2077 set_extension_value 

2078 """ 

2079 has_changes = False 

2080 for ext_name in self.extensions: 

2081 options = self.extensions[ext_name] 

2082 for opt in options: 

2083 if self.set_extension_value(ext_name, opt): 

2084 has_changes = True 

2085 if has_changes: 

2086 self.ext_userCfg.Save() 

2087 

2088 

2089class HelpFrame(LabelFrame): 

2090 

2091 def __init__(self, master, **cfg): 

2092 super().__init__(master, **cfg) 

2093 self.create_frame_help() 

2094 self.load_helplist() 

2095 

2096 def create_frame_help(self): 

2097 """Create LabelFrame for additional help menu sources. 

2098 

2099 load_helplist loads list user_helplist with 

2100 name, position pairs and copies names to listbox helplist. 

2101 Clicking a name invokes help_source selected. Clicking 

2102 button_helplist_name invokes helplist_item_name, which also 

2103 changes user_helplist. These functions all call 

2104 set_add_delete_state. All but load call update_help_changes to 

2105 rewrite changes['main']['HelpFiles']. 

2106 

2107 Widgets for HelpFrame(LabelFrame): (*) widgets bound to self 

2108 frame_helplist: Frame 

2109 (*)helplist: ListBox 

2110 scroll_helplist: Scrollbar 

2111 frame_buttons: Frame 

2112 (*)button_helplist_edit 

2113 (*)button_helplist_add 

2114 (*)button_helplist_remove 

2115 """ 

2116 # self = frame_help in dialog (until ExtPage class). 

2117 frame_helplist = Frame(self) 

2118 self.helplist = Listbox( 

2119 frame_helplist, height=5, takefocus=True, 

2120 exportselection=FALSE) 

2121 scroll_helplist = Scrollbar(frame_helplist) 

2122 scroll_helplist['command'] = self.helplist.yview 

2123 self.helplist['yscrollcommand'] = scroll_helplist.set 

2124 self.helplist.bind('<ButtonRelease-1>', self.help_source_selected) 

2125 

2126 frame_buttons = Frame(self) 

2127 self.button_helplist_edit = Button( 

2128 frame_buttons, text='Edit', state='disabled', 

2129 width=8, command=self.helplist_item_edit) 

2130 self.button_helplist_add = Button( 

2131 frame_buttons, text='Add', 

2132 width=8, command=self.helplist_item_add) 

2133 self.button_helplist_remove = Button( 

2134 frame_buttons, text='Remove', state='disabled', 

2135 width=8, command=self.helplist_item_remove) 

2136 

2137 # Pack frame_help. 

2138 frame_helplist.pack(side=LEFT, padx=5, pady=5, expand=TRUE, fill=BOTH) 

2139 self.helplist.pack(side=LEFT, anchor=E, expand=TRUE, fill=BOTH) 

2140 scroll_helplist.pack(side=RIGHT, anchor=W, fill=Y) 

2141 frame_buttons.pack(side=RIGHT, padx=5, pady=5, fill=Y) 

2142 self.button_helplist_edit.pack(side=TOP, anchor=W, pady=5) 

2143 self.button_helplist_add.pack(side=TOP, anchor=W) 

2144 self.button_helplist_remove.pack(side=TOP, anchor=W, pady=5) 

2145 

2146 def help_source_selected(self, event): 

2147 "Handle event for selecting additional help." 

2148 self.set_add_delete_state() 

2149 

2150 def set_add_delete_state(self): 

2151 "Toggle the state for the help list buttons based on list entries." 

2152 if self.helplist.size() < 1: # No entries in list. 

2153 self.button_helplist_edit.state(('disabled',)) 

2154 self.button_helplist_remove.state(('disabled',)) 

2155 else: # Some entries. 

2156 if self.helplist.curselection(): # There currently is a selection. 

2157 self.button_helplist_edit.state(('!disabled',)) 

2158 self.button_helplist_remove.state(('!disabled',)) 

2159 else: # There currently is not a selection. 

2160 self.button_helplist_edit.state(('disabled',)) 

2161 self.button_helplist_remove.state(('disabled',)) 

2162 

2163 def helplist_item_add(self): 

2164 """Handle add button for the help list. 

2165 

2166 Query for name and location of new help sources and add 

2167 them to the list. 

2168 """ 

2169 help_source = HelpSource(self, 'New Help Source').result 

2170 if help_source: 

2171 self.user_helplist.append(help_source) 

2172 self.helplist.insert(END, help_source[0]) 

2173 self.update_help_changes() 

2174 

2175 def helplist_item_edit(self): 

2176 """Handle edit button for the help list. 

2177 

2178 Query with existing help source information and update 

2179 config if the values are changed. 

2180 """ 

2181 item_index = self.helplist.index(ANCHOR) 

2182 help_source = self.user_helplist[item_index] 

2183 new_help_source = HelpSource( 

2184 self, 'Edit Help Source', 

2185 menuitem=help_source[0], 

2186 filepath=help_source[1], 

2187 ).result 

2188 if new_help_source and new_help_source != help_source: 

2189 self.user_helplist[item_index] = new_help_source 

2190 self.helplist.delete(item_index) 

2191 self.helplist.insert(item_index, new_help_source[0]) 

2192 self.update_help_changes() 

2193 self.set_add_delete_state() # Selected will be un-selected 

2194 

2195 def helplist_item_remove(self): 

2196 """Handle remove button for the help list. 

2197 

2198 Delete the help list item from config. 

2199 """ 

2200 item_index = self.helplist.index(ANCHOR) 

2201 del(self.user_helplist[item_index]) 

2202 self.helplist.delete(item_index) 

2203 self.update_help_changes() 

2204 self.set_add_delete_state() 

2205 

2206 def update_help_changes(self): 

2207 "Clear and rebuild the HelpFiles section in changes" 

2208 changes['main']['HelpFiles'] = {} 

2209 for num in range(1, len(self.user_helplist) + 1): 

2210 changes.add_option( 

2211 'main', 'HelpFiles', str(num), 

2212 ';'.join(self.user_helplist[num-1][:2])) 

2213 

2214 def load_helplist(self): 

2215 # Set additional help sources. 

2216 self.user_helplist = idleConf.GetAllExtraHelpSourcesList() 

2217 self.helplist.delete(0, 'end') 

2218 for help_item in self.user_helplist: 

2219 self.helplist.insert(END, help_item[0]) 

2220 self.set_add_delete_state() 

2221 

2222 

2223class VarTrace: 

2224 """Maintain Tk variables trace state.""" 

2225 

2226 def __init__(self): 

2227 """Store Tk variables and callbacks. 

2228 

2229 untraced: List of tuples (var, callback) 

2230 that do not have the callback attached 

2231 to the Tk var. 

2232 traced: List of tuples (var, callback) where 

2233 that callback has been attached to the var. 

2234 """ 

2235 self.untraced = [] 

2236 self.traced = [] 

2237 

2238 def clear(self): 

2239 "Clear lists (for tests)." 

2240 # Call after all tests in a module to avoid memory leaks. 

2241 self.untraced.clear() 

2242 self.traced.clear() 

2243 

2244 def add(self, var, callback): 

2245 """Add (var, callback) tuple to untraced list. 

2246 

2247 Args: 

2248 var: Tk variable instance. 

2249 callback: Either function name to be used as a callback 

2250 or a tuple with IdleConf config-type, section, and 

2251 option names used in the default callback. 

2252 

2253 Return: 

2254 Tk variable instance. 

2255 """ 

2256 if isinstance(callback, tuple): 

2257 callback = self.make_callback(var, callback) 

2258 self.untraced.append((var, callback)) 

2259 return var 

2260 

2261 @staticmethod 

2262 def make_callback(var, config): 

2263 "Return default callback function to add values to changes instance." 

2264 def default_callback(*params): 

2265 "Add config values to changes instance." 

2266 changes.add_option(*config, var.get()) 

2267 return default_callback 

2268 

2269 def attach(self): 

2270 "Attach callback to all vars that are not traced." 

2271 while self.untraced: 

2272 var, callback = self.untraced.pop() 

2273 var.trace_add('write', callback) 

2274 self.traced.append((var, callback)) 

2275 

2276 def detach(self): 

2277 "Remove callback from traced vars." 

2278 while self.traced: 

2279 var, callback = self.traced.pop() 

2280 var.trace_remove('write', var.trace_info()[0][1]) 

2281 self.untraced.append((var, callback)) 

2282 

2283 

2284tracers = VarTrace() 

2285 

2286help_common = '''\ 

2287When you click either the Apply or Ok buttons, settings in this 

2288dialog that are different from IDLE's default are saved in 

2289a .idlerc directory in your home directory. Except as noted, 

2290these changes apply to all versions of IDLE installed on this 

2291machine. [Cancel] only cancels changes made since the last save. 

2292''' 

2293help_pages = { 

2294 'Fonts/Tabs':''' 

2295Font sample: This shows what a selection of Basic Multilingual Plane 

2296unicode characters look like for the current font selection. If the 

2297selected font does not define a character, Tk attempts to find another 

2298font that does. Substitute glyphs depend on what is available on a 

2299particular system and will not necessarily have the same size as the 

2300font selected. Line contains 20 characters up to Devanagari, 14 for 

2301Tamil, and 10 for East Asia. 

2302 

2303Hebrew and Arabic letters should display right to left, starting with 

2304alef, \u05d0 and \u0627. Arabic digits display left to right. The 

2305Devanagari and Tamil lines start with digits. The East Asian lines 

2306are Chinese digits, Chinese Hanzi, Korean Hangul, and Japanese 

2307Hiragana and Katakana. 

2308 

2309You can edit the font sample. Changes remain until IDLE is closed. 

2310''', 

2311 'Highlights': ''' 

2312Highlighting: 

2313The IDLE Dark color theme is new in October 2015. It can only 

2314be used with older IDLE releases if it is saved as a custom 

2315theme, with a different name. 

2316''', 

2317 'Keys': ''' 

2318Keys: 

2319The IDLE Modern Unix key set is new in June 2016. It can only 

2320be used with older IDLE releases if it is saved as a custom 

2321key set, with a different name. 

2322''', 

2323 'General': ''' 

2324General: 

2325 

2326AutoComplete: Popupwait is milliseconds to wait after key char, without 

2327cursor movement, before popping up completion box. Key char is '.' after 

2328identifier or a '/' (or '\\' on Windows) within a string. 

2329 

2330FormatParagraph: Max-width is max chars in lines after re-formatting. 

2331Use with paragraphs in both strings and comment blocks. 

2332 

2333ParenMatch: Style indicates what is highlighted when closer is entered: 

2334'opener' - opener '({[' corresponding to closer; 'parens' - both chars; 

2335'expression' (default) - also everything in between. Flash-delay is how 

2336long to highlight if cursor is not moved (0 means forever). 

2337 

2338CodeContext: Maxlines is the maximum number of code context lines to 

2339display when Code Context is turned on for an editor window. 

2340 

2341Shell Preferences: Auto-Squeeze Min. Lines is the minimum number of lines 

2342of output to automatically "squeeze". 

2343''', 

2344 'Extensions': ''' 

2345ZzDummy: This extension is provided as an example for how to create and 

2346use an extension. Enable indicates whether the extension is active or 

2347not; likewise enable_editor and enable_shell indicate which windows it 

2348will be active on. For this extension, z-text is the text that will be 

2349inserted at or removed from the beginning of the lines of selected text, 

2350or the current line if no selection. 

2351''', 

2352} 

2353 

2354 

2355def is_int(s): 

2356 "Return 's is blank or represents an int'" 

2357 if not s: 

2358 return True 

2359 try: 

2360 int(s) 

2361 return True 

2362 except ValueError: 

2363 return False 

2364 

2365 

2366class VerticalScrolledFrame(Frame): 

2367 """A pure Tkinter vertically scrollable frame. 

2368 

2369 * Use the 'interior' attribute to place widgets inside the scrollable frame 

2370 * Construct and pack/place/grid normally 

2371 * This frame only allows vertical scrolling 

2372 """ 

2373 def __init__(self, parent, *args, **kw): 

2374 Frame.__init__(self, parent, *args, **kw) 

2375 

2376 # Create a canvas object and a vertical scrollbar for scrolling it. 

2377 vscrollbar = Scrollbar(self, orient=VERTICAL) 

2378 vscrollbar.pack(fill=Y, side=RIGHT, expand=FALSE) 

2379 canvas = Canvas(self, borderwidth=0, highlightthickness=0, 

2380 yscrollcommand=vscrollbar.set, width=240) 

2381 canvas.pack(side=LEFT, fill=BOTH, expand=TRUE) 

2382 vscrollbar.config(command=canvas.yview) 

2383 

2384 # Reset the view. 

2385 canvas.xview_moveto(0) 

2386 canvas.yview_moveto(0) 

2387 

2388 # Create a frame inside the canvas which will be scrolled with it. 

2389 self.interior = interior = Frame(canvas) 

2390 interior_id = canvas.create_window(0, 0, window=interior, anchor=NW) 

2391 

2392 # Track changes to the canvas and frame width and sync them, 

2393 # also updating the scrollbar. 

2394 def _configure_interior(event): 

2395 # Update the scrollbars to match the size of the inner frame. 

2396 size = (interior.winfo_reqwidth(), interior.winfo_reqheight()) 

2397 canvas.config(scrollregion="0 0 %s %s" % size) 

2398 interior.bind('<Configure>', _configure_interior) 

2399 

2400 def _configure_canvas(event): 

2401 if interior.winfo_reqwidth() != canvas.winfo_width(): 

2402 # Update the inner frame's width to fill the canvas. 

2403 canvas.itemconfigure(interior_id, width=canvas.winfo_width()) 

2404 canvas.bind('<Configure>', _configure_canvas) 

2405 

2406 return 

2407 

2408 

2409if __name__ == '__main__': 2409 ↛ 2410line 2409 didn't jump to line 2410, because the condition on line 2409 was never true

2410 from unittest import main 

2411 main('idlelib.idle_test.test_configdialog', verbosity=2, exit=False) 

2412 

2413 from idlelib.idle_test.htest import run 

2414 run(ConfigDialog)