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, Scale, 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) 19from tkinter.ttk import (Frame, LabelFrame, Button, Checkbutton, Entry, Label, 20 OptionMenu, Notebook, Radiobutton, Scrollbar, Style) 21import tkinter.colorchooser as tkColorChooser 22import tkinter.font as tkFont 23from tkinter import messagebox 24 25from idlelib.config import idleConf, ConfigChanges 26from idlelib.config_key import GetKeysDialog 27from idlelib.dynoption import DynOptionMenu 28from idlelib import macosx 29from idlelib.query import SectionName, HelpSource 30from idlelib.textview import view_text 31from idlelib.autocomplete import AutoComplete 32from idlelib.codecontext import CodeContext 33from idlelib.parenmatch import ParenMatch 34from idlelib.format import FormatParagraph 35from idlelib.squeezer import Squeezer 36from idlelib.textview import ScrollableTextFrame 37 38changes = ConfigChanges() 39# Reload changed options in the following classes. 40reloadables = (AutoComplete, CodeContext, ParenMatch, FormatParagraph, 41 Squeezer) 42 43 44class ConfigDialog(Toplevel): 45 """Config dialog for IDLE. 46 """ 47 48 def __init__(self, parent, title='', *, _htest=False, _utest=False): 49 """Show the tabbed dialog for user configuration. 50 51 Args: 52 parent - parent of this dialog 53 title - string which is the title of this popup dialog 54 _htest - bool, change box location when running htest 55 _utest - bool, don't wait_window when running unittest 56 57 Note: Focus set on font page fontlist. 58 59 Methods: 60 create_widgets 61 cancel: Bound to DELETE_WINDOW protocol. 62 """ 63 Toplevel.__init__(self, parent) 64 self.parent = parent 65 if _htest: 66 parent.instance_dict = {} 67 if not _utest: 68 self.withdraw() 69 70 self.configure(borderwidth=5) 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 note: Notebook 101 highpage: HighPage 102 fontpage: FontPage 103 keyspage: KeysPage 104 genpage: GenPage 105 extpage: self.create_page_extensions 106 107 Methods: 108 create_action_buttons 109 load_configs: Load pages except for extensions. 110 activate_config_changes: Tell editors to reload. 111 """ 112 self.note = note = Notebook(self) 113 self.highpage = HighPage(note) 114 self.fontpage = FontPage(note, self.highpage) 115 self.keyspage = KeysPage(note) 116 self.genpage = GenPage(note) 117 self.extpage = self.create_page_extensions() 118 note.add(self.fontpage, text='Fonts/Tabs') 119 note.add(self.highpage, text='Highlights') 120 note.add(self.keyspage, text=' Keys ') 121 note.add(self.genpage, text=' General ') 122 note.add(self.extpage, text='Extensions') 123 note.enable_traversal() 124 note.pack(side=TOP, expand=TRUE, fill=BOTH) 125 self.create_action_buttons().pack(side=BOTTOM) 126 127 def create_action_buttons(self): 128 """Return frame of action buttons for dialog. 129 130 Methods: 131 ok 132 apply 133 cancel 134 help 135 136 Widget Structure: 137 outer: Frame 138 buttons: Frame 139 (no assignment): Button (ok) 140 (no assignment): Button (apply) 141 (no assignment): Button (cancel) 142 (no assignment): Button (help) 143 (no assignment): Frame 144 """ 145 if macosx.isAquaTk(): 146 # Changing the default padding on OSX results in unreadable 147 # text in the buttons. 148 padding_args = {} 149 else: 150 padding_args = {'padding': (6, 3)} 151 outer = Frame(self, padding=2) 152 buttons_frame = Frame(outer, padding=2) 153 self.buttons = {} 154 for txt, cmd in ( 155 ('Ok', self.ok), 156 ('Apply', self.apply), 157 ('Cancel', self.cancel), 158 ('Help', self.help)): 159 self.buttons[txt] = Button(buttons_frame, text=txt, command=cmd, 160 takefocus=FALSE, **padding_args) 161 self.buttons[txt].pack(side=LEFT, padx=5) 162 # Add space above buttons. 163 Frame(outer, height=2, borderwidth=0).pack(side=TOP) 164 buttons_frame.pack(side=BOTTOM) 165 return outer 166 167 def ok(self): 168 """Apply config changes, then dismiss dialog. 169 170 Methods: 171 apply 172 destroy: inherited 173 """ 174 self.apply() 175 self.destroy() 176 177 def apply(self): 178 """Apply config changes and leave dialog open. 179 180 Methods: 181 deactivate_current_config 182 save_all_changed_extensions 183 activate_config_changes 184 """ 185 self.deactivate_current_config() 186 changes.save_all() 187 self.save_all_changed_extensions() 188 self.activate_config_changes() 189 190 def cancel(self): 191 """Dismiss config dialog. 192 193 Methods: 194 destroy: inherited 195 """ 196 changes.clear() 197 self.destroy() 198 199 def destroy(self): 200 global font_sample_text 201 font_sample_text = self.fontpage.font_sample.get('1.0', 'end') 202 self.grab_release() 203 super().destroy() 204 205 def help(self): 206 """Create textview for config dialog help. 207 208 Attributes accessed: 209 note 210 Methods: 211 view_text: Method from textview module. 212 """ 213 page = self.note.tab(self.note.select(), option='text').strip() 214 view_text(self, title='Help for IDLE preferences', 215 contents=help_common+help_pages.get(page, '')) 216 217 def deactivate_current_config(self): 218 """Remove current key bindings. 219 Iterate over window instances defined in parent and remove 220 the keybindings. 221 """ 222 # Before a config is saved, some cleanup of current 223 # config must be done - remove the previous keybindings. 224 win_instances = self.parent.instance_dict.keys() 225 for instance in win_instances: 226 instance.RemoveKeybindings() 227 228 def activate_config_changes(self): 229 """Apply configuration changes to current windows. 230 231 Dynamically update the current parent window instances 232 with some of the configuration changes. 233 """ 234 win_instances = self.parent.instance_dict.keys() 235 for instance in win_instances: 236 instance.ResetColorizer() 237 instance.ResetFont() 238 instance.set_notabs_indentwidth() 239 instance.ApplyKeybindings() 240 instance.reset_help_menu_entries() 241 instance.update_cursor_blink() 242 for klass in reloadables: 243 klass.reload() 244 245 def create_page_extensions(self): 246 """Part of the config dialog used for configuring IDLE extensions. 247 248 This code is generic - it works for any and all IDLE extensions. 249 250 IDLE extensions save their configuration options using idleConf. 251 This code reads the current configuration using idleConf, supplies a 252 GUI interface to change the configuration values, and saves the 253 changes using idleConf. 254 255 Not all changes take effect immediately - some may require restarting IDLE. 256 This depends on each extension's implementation. 257 258 All values are treated as text, and it is up to the user to supply 259 reasonable values. The only exception to this are the 'enable*' options, 260 which are boolean, and can be toggled with a True/False button. 261 262 Methods: 263 load_extensions: 264 extension_selected: Handle selection from list. 265 create_extension_frame: Hold widgets for one extension. 266 set_extension_value: Set in userCfg['extensions']. 267 save_all_changed_extensions: Call extension page Save(). 268 """ 269 parent = self.parent 270 frame = Frame(self.note) 271 self.ext_defaultCfg = idleConf.defaultCfg['extensions'] 272 self.ext_userCfg = idleConf.userCfg['extensions'] 273 self.is_int = self.register(is_int) 274 self.load_extensions() 275 # Create widgets - a listbox shows all available extensions, with the 276 # controls for the extension selected in the listbox to the right. 277 self.extension_names = StringVar(self) 278 frame.rowconfigure(0, weight=1) 279 frame.columnconfigure(2, weight=1) 280 self.extension_list = Listbox(frame, listvariable=self.extension_names, 281 selectmode='browse') 282 self.extension_list.bind('<<ListboxSelect>>', self.extension_selected) 283 scroll = Scrollbar(frame, command=self.extension_list.yview) 284 self.extension_list.yscrollcommand=scroll.set 285 self.details_frame = LabelFrame(frame, width=250, height=250) 286 self.extension_list.grid(column=0, row=0, sticky='nws') 287 scroll.grid(column=1, row=0, sticky='ns') 288 self.details_frame.grid(column=2, row=0, sticky='nsew', padx=[10, 0]) 289 frame.configure(padding=10) 290 self.config_frame = {} 291 self.current_extension = None 292 293 self.outerframe = self # TEMPORARY 294 self.tabbed_page_set = self.extension_list # TEMPORARY 295 296 # Create the frame holding controls for each extension. 297 ext_names = '' 298 for ext_name in sorted(self.extensions): 299 self.create_extension_frame(ext_name) 300 ext_names = ext_names + '{' + ext_name + '} ' 301 self.extension_names.set(ext_names) 302 self.extension_list.selection_set(0) 303 self.extension_selected(None) 304 305 return frame 306 307 def load_extensions(self): 308 "Fill self.extensions with data from the default and user configs." 309 self.extensions = {} 310 for ext_name in idleConf.GetExtensions(active_only=False): 311 # Former built-in extensions are already filtered out. 312 self.extensions[ext_name] = [] 313 314 for ext_name in self.extensions: 315 opt_list = sorted(self.ext_defaultCfg.GetOptionList(ext_name)) 316 317 # Bring 'enable' options to the beginning of the list. 318 enables = [opt_name for opt_name in opt_list 319 if opt_name.startswith('enable')] 320 for opt_name in enables: 321 opt_list.remove(opt_name) 322 opt_list = enables + opt_list 323 324 for opt_name in opt_list: 325 def_str = self.ext_defaultCfg.Get( 326 ext_name, opt_name, raw=True) 327 try: 328 def_obj = {'True':True, 'False':False}[def_str] 329 opt_type = 'bool' 330 except KeyError: 331 try: 332 def_obj = int(def_str) 333 opt_type = 'int' 334 except ValueError: 335 def_obj = def_str 336 opt_type = None 337 try: 338 value = self.ext_userCfg.Get( 339 ext_name, opt_name, type=opt_type, raw=True, 340 default=def_obj) 341 except ValueError: # Need this until .Get fixed. 342 value = def_obj # Bad values overwritten by entry. 343 var = StringVar(self) 344 var.set(str(value)) 345 346 self.extensions[ext_name].append({'name': opt_name, 347 'type': opt_type, 348 'default': def_str, 349 'value': value, 350 'var': var, 351 }) 352 353 def extension_selected(self, event): 354 "Handle selection of an extension from the list." 355 newsel = self.extension_list.curselection() 356 if newsel: 357 newsel = self.extension_list.get(newsel) 358 if newsel is None or newsel != self.current_extension: 359 if self.current_extension: 360 self.details_frame.config(text='') 361 self.config_frame[self.current_extension].grid_forget() 362 self.current_extension = None 363 if newsel: 364 self.details_frame.config(text=newsel) 365 self.config_frame[newsel].grid(column=0, row=0, sticky='nsew') 366 self.current_extension = newsel 367 368 def create_extension_frame(self, ext_name): 369 """Create a frame holding the widgets to configure one extension""" 370 f = VerticalScrolledFrame(self.details_frame, height=250, width=250) 371 self.config_frame[ext_name] = f 372 entry_area = f.interior 373 # Create an entry for each configuration option. 374 for row, opt in enumerate(self.extensions[ext_name]): 375 # Create a row with a label and entry/checkbutton. 376 label = Label(entry_area, text=opt['name']) 377 label.grid(row=row, column=0, sticky=NW) 378 var = opt['var'] 379 if opt['type'] == 'bool': 380 Checkbutton(entry_area, variable=var, 381 onvalue='True', offvalue='False', width=8 382 ).grid(row=row, column=1, sticky=W, padx=7) 383 elif opt['type'] == 'int': 384 Entry(entry_area, textvariable=var, validate='key', 385 validatecommand=(self.is_int, '%P'), width=10 386 ).grid(row=row, column=1, sticky=NSEW, padx=7) 387 388 else: # type == 'str' 389 # Limit size to fit non-expanding space with larger font. 390 Entry(entry_area, textvariable=var, width=15 391 ).grid(row=row, column=1, sticky=NSEW, padx=7) 392 return 393 394 def set_extension_value(self, section, opt): 395 """Return True if the configuration was added or changed. 396 397 If the value is the same as the default, then remove it 398 from user config file. 399 """ 400 name = opt['name'] 401 default = opt['default'] 402 value = opt['var'].get().strip() or default 403 opt['var'].set(value) 404 # if self.defaultCfg.has_section(section): 405 # Currently, always true; if not, indent to return. 406 if (value == default): 407 return self.ext_userCfg.RemoveOption(section, name) 408 # Set the option. 409 return self.ext_userCfg.SetOption(section, name, value) 410 411 def save_all_changed_extensions(self): 412 """Save configuration changes to the user config file. 413 414 Attributes accessed: 415 extensions 416 417 Methods: 418 set_extension_value 419 """ 420 has_changes = False 421 for ext_name in self.extensions: 422 options = self.extensions[ext_name] 423 for opt in options: 424 if self.set_extension_value(ext_name, opt): 425 has_changes = True 426 if has_changes: 427 self.ext_userCfg.Save() 428 429 430# class TabPage(Frame): # A template for Page classes. 431# def __init__(self, master): 432# super().__init__(master) 433# self.create_page_tab() 434# self.load_tab_cfg() 435# def create_page_tab(self): 436# # Define tk vars and register var and callback with tracers. 437# # Create subframes and widgets. 438# # Pack widgets. 439# def load_tab_cfg(self): 440# # Initialize widgets with data from idleConf. 441# def var_changed_var_name(): 442# # For each tk var that needs other than default callback. 443# def other_methods(): 444# # Define tab-specific behavior. 445 446font_sample_text = ( 447 '<ASCII/Latin1>\n' 448 'AaBbCcDdEeFfGgHhIiJj\n1234567890#:+=(){}[]\n' 449 '\u00a2\u00a3\u00a5\u00a7\u00a9\u00ab\u00ae\u00b6\u00bd\u011e' 450 '\u00c0\u00c1\u00c2\u00c3\u00c4\u00c5\u00c7\u00d0\u00d8\u00df\n' 451 '\n<IPA,Greek,Cyrillic>\n' 452 '\u0250\u0255\u0258\u025e\u025f\u0264\u026b\u026e\u0270\u0277' 453 '\u027b\u0281\u0283\u0286\u028e\u029e\u02a2\u02ab\u02ad\u02af\n' 454 '\u0391\u03b1\u0392\u03b2\u0393\u03b3\u0394\u03b4\u0395\u03b5' 455 '\u0396\u03b6\u0397\u03b7\u0398\u03b8\u0399\u03b9\u039a\u03ba\n' 456 '\u0411\u0431\u0414\u0434\u0416\u0436\u041f\u043f\u0424\u0444' 457 '\u0427\u0447\u042a\u044a\u042d\u044d\u0460\u0464\u046c\u04dc\n' 458 '\n<Hebrew, Arabic>\n' 459 '\u05d0\u05d1\u05d2\u05d3\u05d4\u05d5\u05d6\u05d7\u05d8\u05d9' 460 '\u05da\u05db\u05dc\u05dd\u05de\u05df\u05e0\u05e1\u05e2\u05e3\n' 461 '\u0627\u0628\u062c\u062f\u0647\u0648\u0632\u062d\u0637\u064a' 462 '\u0660\u0661\u0662\u0663\u0664\u0665\u0666\u0667\u0668\u0669\n' 463 '\n<Devanagari, Tamil>\n' 464 '\u0966\u0967\u0968\u0969\u096a\u096b\u096c\u096d\u096e\u096f' 465 '\u0905\u0906\u0907\u0908\u0909\u090a\u090f\u0910\u0913\u0914\n' 466 '\u0be6\u0be7\u0be8\u0be9\u0bea\u0beb\u0bec\u0bed\u0bee\u0bef' 467 '\u0b85\u0b87\u0b89\u0b8e\n' 468 '\n<East Asian>\n' 469 '\u3007\u4e00\u4e8c\u4e09\u56db\u4e94\u516d\u4e03\u516b\u4e5d\n' 470 '\u6c49\u5b57\u6f22\u5b57\u4eba\u6728\u706b\u571f\u91d1\u6c34\n' 471 '\uac00\ub0d0\ub354\ub824\ubaa8\ubd64\uc218\uc720\uc988\uce58\n' 472 '\u3042\u3044\u3046\u3048\u304a\u30a2\u30a4\u30a6\u30a8\u30aa\n' 473 ) 474 475 476class FontPage(Frame): 477 478 def __init__(self, master, highpage): 479 super().__init__(master) 480 self.highlight_sample = highpage.highlight_sample 481 self.create_page_font_tab() 482 self.load_font_cfg() 483 self.load_tab_cfg() 484 485 def create_page_font_tab(self): 486 """Return frame of widgets for Font/Tabs tab. 487 488 Fonts: Enable users to provisionally change font face, size, or 489 boldness and to see the consequence of proposed choices. Each 490 action set 3 options in changes structuree and changes the 491 corresponding aspect of the font sample on this page and 492 highlight sample on highlight page. 493 494 Function load_font_cfg initializes font vars and widgets from 495 idleConf entries and tk. 496 497 Fontlist: mouse button 1 click or up or down key invoke 498 on_fontlist_select(), which sets var font_name. 499 500 Sizelist: clicking the menubutton opens the dropdown menu. A 501 mouse button 1 click or return key sets var font_size. 502 503 Bold_toggle: clicking the box toggles var font_bold. 504 505 Changing any of the font vars invokes var_changed_font, which 506 adds all 3 font options to changes and calls set_samples. 507 Set_samples applies a new font constructed from the font vars to 508 font_sample and to highlight_sample on the highlight page. 509 510 Tabs: Enable users to change spaces entered for indent tabs. 511 Changing indent_scale value with the mouse sets Var space_num, 512 which invokes the default callback to add an entry to 513 changes. Load_tab_cfg initializes space_num to default. 514 515 Widgets for FontPage(Frame): (*) widgets bound to self 516 frame_font: LabelFrame 517 frame_font_name: Frame 518 font_name_title: Label 519 (*)fontlist: ListBox - font_name 520 scroll_font: Scrollbar 521 frame_font_param: Frame 522 font_size_title: Label 523 (*)sizelist: DynOptionMenu - font_size 524 (*)bold_toggle: Checkbutton - font_bold 525 frame_sample: LabelFrame 526 (*)font_sample: Label 527 frame_indent: LabelFrame 528 indent_title: Label 529 (*)indent_scale: Scale - space_num 530 """ 531 self.font_name = tracers.add(StringVar(self), self.var_changed_font) 532 self.font_size = tracers.add(StringVar(self), self.var_changed_font) 533 self.font_bold = tracers.add(BooleanVar(self), self.var_changed_font) 534 self.space_num = tracers.add(IntVar(self), ('main', 'Indent', 'num-spaces')) 535 536 # Define frames and widgets. 537 frame_font = LabelFrame( 538 self, borderwidth=2, relief=GROOVE, text=' Shell/Editor Font ') 539 frame_sample = LabelFrame( 540 self, borderwidth=2, relief=GROOVE, 541 text=' Font Sample (Editable) ') 542 frame_indent = LabelFrame( 543 self, borderwidth=2, relief=GROOVE, text=' Indentation Width ') 544 # frame_font. 545 frame_font_name = Frame(frame_font) 546 frame_font_param = Frame(frame_font) 547 font_name_title = Label( 548 frame_font_name, justify=LEFT, text='Font Face :') 549 self.fontlist = Listbox(frame_font_name, height=15, 550 takefocus=True, exportselection=FALSE) 551 self.fontlist.bind('<ButtonRelease-1>', self.on_fontlist_select) 552 self.fontlist.bind('<KeyRelease-Up>', self.on_fontlist_select) 553 self.fontlist.bind('<KeyRelease-Down>', self.on_fontlist_select) 554 scroll_font = Scrollbar(frame_font_name) 555 scroll_font.config(command=self.fontlist.yview) 556 self.fontlist.config(yscrollcommand=scroll_font.set) 557 font_size_title = Label(frame_font_param, text='Size :') 558 self.sizelist = DynOptionMenu(frame_font_param, self.font_size, None) 559 self.bold_toggle = Checkbutton( 560 frame_font_param, variable=self.font_bold, 561 onvalue=1, offvalue=0, text='Bold') 562 # frame_sample. 563 font_sample_frame = ScrollableTextFrame(frame_sample) 564 self.font_sample = font_sample_frame.text 565 self.font_sample.config(wrap=NONE, width=1, height=1) 566 self.font_sample.insert(END, font_sample_text) 567 # frame_indent. 568 indent_title = Label( 569 frame_indent, justify=LEFT, 570 text='Python Standard: 4 Spaces!') 571 self.indent_scale = Scale( 572 frame_indent, variable=self.space_num, 573 orient='horizontal', tickinterval=2, from_=2, to=16) 574 575 # Grid and pack widgets: 576 self.columnconfigure(1, weight=1) 577 self.rowconfigure(2, weight=1) 578 frame_font.grid(row=0, column=0, padx=5, pady=5) 579 frame_sample.grid(row=0, column=1, rowspan=3, padx=5, pady=5, 580 sticky='nsew') 581 frame_indent.grid(row=1, column=0, padx=5, pady=5, sticky='ew') 582 # frame_font. 583 frame_font_name.pack(side=TOP, padx=5, pady=5, fill=X) 584 frame_font_param.pack(side=TOP, padx=5, pady=5, fill=X) 585 font_name_title.pack(side=TOP, anchor=W) 586 self.fontlist.pack(side=LEFT, expand=TRUE, fill=X) 587 scroll_font.pack(side=LEFT, fill=Y) 588 font_size_title.pack(side=LEFT, anchor=W) 589 self.sizelist.pack(side=LEFT, anchor=W) 590 self.bold_toggle.pack(side=LEFT, anchor=W, padx=20) 591 # frame_sample. 592 font_sample_frame.pack(expand=TRUE, fill=BOTH) 593 # frame_indent. 594 indent_title.pack(side=TOP, anchor=W, padx=5) 595 self.indent_scale.pack(side=TOP, padx=5, fill=X) 596 597 def load_font_cfg(self): 598 """Load current configuration settings for the font options. 599 600 Retrieve current font with idleConf.GetFont and font families 601 from tk. Setup fontlist and set font_name. Setup sizelist, 602 which sets font_size. Set font_bold. Call set_samples. 603 """ 604 configured_font = idleConf.GetFont(self, 'main', 'EditorWindow') 605 font_name = configured_font[0].lower() 606 font_size = configured_font[1] 607 font_bold = configured_font[2]=='bold' 608 609 # Set sorted no-duplicate editor font selection list and font_name. 610 fonts = sorted(set(tkFont.families(self))) 611 for font in fonts: 612 self.fontlist.insert(END, font) 613 self.font_name.set(font_name) 614 lc_fonts = [s.lower() for s in fonts] 615 try: 616 current_font_index = lc_fonts.index(font_name) 617 self.fontlist.see(current_font_index) 618 self.fontlist.select_set(current_font_index) 619 self.fontlist.select_anchor(current_font_index) 620 self.fontlist.activate(current_font_index) 621 except ValueError: 622 pass 623 # Set font size dropdown. 624 self.sizelist.SetMenu(('7', '8', '9', '10', '11', '12', '13', '14', 625 '16', '18', '20', '22', '25', '29', '34', '40'), 626 font_size) 627 # Set font weight. 628 self.font_bold.set(font_bold) 629 self.set_samples() 630 631 def var_changed_font(self, *params): 632 """Store changes to font attributes. 633 634 When one font attribute changes, save them all, as they are 635 not independent from each other. In particular, when we are 636 overriding the default font, we need to write out everything. 637 """ 638 value = self.font_name.get() 639 changes.add_option('main', 'EditorWindow', 'font', value) 640 value = self.font_size.get() 641 changes.add_option('main', 'EditorWindow', 'font-size', value) 642 value = self.font_bold.get() 643 changes.add_option('main', 'EditorWindow', 'font-bold', value) 644 self.set_samples() 645 646 def on_fontlist_select(self, event): 647 """Handle selecting a font from the list. 648 649 Event can result from either mouse click or Up or Down key. 650 Set font_name and example displays to selection. 651 """ 652 font = self.fontlist.get( 653 ACTIVE if event.type.name == 'KeyRelease' else ANCHOR) 654 self.font_name.set(font.lower()) 655 656 def set_samples(self, event=None): 657 """Update update both screen samples with the font settings. 658 659 Called on font initialization and change events. 660 Accesses font_name, font_size, and font_bold Variables. 661 Updates font_sample and highlight page highlight_sample. 662 """ 663 font_name = self.font_name.get() 664 font_weight = tkFont.BOLD if self.font_bold.get() else tkFont.NORMAL 665 new_font = (font_name, self.font_size.get(), font_weight) 666 self.font_sample['font'] = new_font 667 self.highlight_sample['font'] = new_font 668 669 def load_tab_cfg(self): 670 """Load current configuration settings for the tab options. 671 672 Attributes updated: 673 space_num: Set to value from idleConf. 674 """ 675 # Set indent sizes. 676 space_num = idleConf.GetOption( 677 'main', 'Indent', 'num-spaces', default=4, type='int') 678 self.space_num.set(space_num) 679 680 def var_changed_space_num(self, *params): 681 "Store change to indentation size." 682 value = self.space_num.get() 683 changes.add_option('main', 'Indent', 'num-spaces', value) 684 685 686class HighPage(Frame): 687 688 def __init__(self, master): 689 super().__init__(master) 690 self.cd = master.master 691 self.style = Style(master) 692 self.create_page_highlight() 693 self.load_theme_cfg() 694 695 def create_page_highlight(self): 696 """Return frame of widgets for Highlighting tab. 697 698 Enable users to provisionally change foreground and background 699 colors applied to textual tags. Color mappings are stored in 700 complete listings called themes. Built-in themes in 701 idlelib/config-highlight.def are fixed as far as the dialog is 702 concerned. Any theme can be used as the base for a new custom 703 theme, stored in .idlerc/config-highlight.cfg. 704 705 Function load_theme_cfg() initializes tk variables and theme 706 lists and calls paint_theme_sample() and set_highlight_target() 707 for the current theme. Radiobuttons builtin_theme_on and 708 custom_theme_on toggle var theme_source, which controls if the 709 current set of colors are from a builtin or custom theme. 710 DynOptionMenus builtinlist and customlist contain lists of the 711 builtin and custom themes, respectively, and the current item 712 from each list is stored in vars builtin_name and custom_name. 713 714 Function paint_theme_sample() applies the colors from the theme 715 to the tags in text widget highlight_sample and then invokes 716 set_color_sample(). Function set_highlight_target() sets the state 717 of the radiobuttons fg_on and bg_on based on the tag and it also 718 invokes set_color_sample(). 719 720 Function set_color_sample() sets the background color for the frame 721 holding the color selector. This provides a larger visual of the 722 color for the current tag and plane (foreground/background). 723 724 Note: set_color_sample() is called from many places and is often 725 called more than once when a change is made. It is invoked when 726 foreground or background is selected (radiobuttons), from 727 paint_theme_sample() (theme is changed or load_cfg is called), and 728 from set_highlight_target() (target tag is changed or load_cfg called). 729 730 Button delete_custom invokes delete_custom() to delete 731 a custom theme from idleConf.userCfg['highlight'] and changes. 732 Button save_custom invokes save_as_new_theme() which calls 733 get_new_theme_name() and create_new() to save a custom theme 734 and its colors to idleConf.userCfg['highlight']. 735 736 Radiobuttons fg_on and bg_on toggle var fg_bg_toggle to control 737 if the current selected color for a tag is for the foreground or 738 background. 739 740 DynOptionMenu targetlist contains a readable description of the 741 tags applied to Python source within IDLE. Selecting one of the 742 tags from this list populates highlight_target, which has a callback 743 function set_highlight_target(). 744 745 Text widget highlight_sample displays a block of text (which is 746 mock Python code) in which is embedded the defined tags and reflects 747 the color attributes of the current theme and changes for those tags. 748 Mouse button 1 allows for selection of a tag and updates 749 highlight_target with that tag value. 750 751 Note: The font in highlight_sample is set through the config in 752 the fonts tab. 753 754 In other words, a tag can be selected either from targetlist or 755 by clicking on the sample text within highlight_sample. The 756 plane (foreground/background) is selected via the radiobutton. 757 Together, these two (tag and plane) control what color is 758 shown in set_color_sample() for the current theme. Button set_color 759 invokes get_color() which displays a ColorChooser to change the 760 color for the selected tag/plane. If a new color is picked, 761 it will be saved to changes and the highlight_sample and 762 frame background will be updated. 763 764 Tk Variables: 765 color: Color of selected target. 766 builtin_name: Menu variable for built-in theme. 767 custom_name: Menu variable for custom theme. 768 fg_bg_toggle: Toggle for foreground/background color. 769 Note: this has no callback. 770 theme_source: Selector for built-in or custom theme. 771 highlight_target: Menu variable for the highlight tag target. 772 773 Instance Data Attributes: 774 theme_elements: Dictionary of tags for text highlighting. 775 The key is the display name and the value is a tuple of 776 (tag name, display sort order). 777 778 Methods [attachment]: 779 load_theme_cfg: Load current highlight colors. 780 get_color: Invoke colorchooser [button_set_color]. 781 set_color_sample_binding: Call set_color_sample [fg_bg_toggle]. 782 set_highlight_target: set fg_bg_toggle, set_color_sample(). 783 set_color_sample: Set frame background to target. 784 on_new_color_set: Set new color and add option. 785 paint_theme_sample: Recolor sample. 786 get_new_theme_name: Get from popup. 787 create_new: Combine theme with changes and save. 788 save_as_new_theme: Save [button_save_custom]. 789 set_theme_type: Command for [theme_source]. 790 delete_custom: Activate default [button_delete_custom]. 791 save_new: Save to userCfg['theme'] (is function). 792 793 Widgets of highlights page frame: (*) widgets bound to self 794 frame_custom: LabelFrame 795 (*)highlight_sample: Text 796 (*)frame_color_set: Frame 797 (*)button_set_color: Button 798 (*)targetlist: DynOptionMenu - highlight_target 799 frame_fg_bg_toggle: Frame 800 (*)fg_on: Radiobutton - fg_bg_toggle 801 (*)bg_on: Radiobutton - fg_bg_toggle 802 (*)button_save_custom: Button 803 frame_theme: LabelFrame 804 theme_type_title: Label 805 (*)builtin_theme_on: Radiobutton - theme_source 806 (*)custom_theme_on: Radiobutton - theme_source 807 (*)builtinlist: DynOptionMenu - builtin_name 808 (*)customlist: DynOptionMenu - custom_name 809 (*)button_delete_custom: Button 810 (*)theme_message: Label 811 """ 812 self.theme_elements = { 813 'Normal Code or Text': ('normal', '00'), 814 'Code Context': ('context', '01'), 815 'Python Keywords': ('keyword', '02'), 816 'Python Definitions': ('definition', '03'), 817 'Python Builtins': ('builtin', '04'), 818 'Python Comments': ('comment', '05'), 819 'Python Strings': ('string', '06'), 820 'Selected Text': ('hilite', '07'), 821 'Found Text': ('hit', '08'), 822 'Cursor': ('cursor', '09'), 823 'Editor Breakpoint': ('break', '10'), 824 'Shell Prompt': ('console', '11'), 825 'Error Text': ('error', '12'), 826 'Shell User Output': ('stdout', '13'), 827 'Shell User Exception': ('stderr', '14'), 828 'Line Number': ('linenumber', '16'), 829 } 830 self.builtin_name = tracers.add( 831 StringVar(self), self.var_changed_builtin_name) 832 self.custom_name = tracers.add( 833 StringVar(self), self.var_changed_custom_name) 834 self.fg_bg_toggle = BooleanVar(self) 835 self.color = tracers.add( 836 StringVar(self), self.var_changed_color) 837 self.theme_source = tracers.add( 838 BooleanVar(self), self.var_changed_theme_source) 839 self.highlight_target = tracers.add( 840 StringVar(self), self.var_changed_highlight_target) 841 842 # Create widgets: 843 # body frame and section frames. 844 frame_custom = LabelFrame(self, borderwidth=2, relief=GROOVE, 845 text=' Custom Highlighting ') 846 frame_theme = LabelFrame(self, borderwidth=2, relief=GROOVE, 847 text=' Highlighting Theme ') 848 # frame_custom. 849 sample_frame = ScrollableTextFrame( 850 frame_custom, relief=SOLID, borderwidth=1) 851 text = self.highlight_sample = sample_frame.text 852 text.configure( 853 font=('courier', 12, ''), cursor='hand2', width=1, height=1, 854 takefocus=FALSE, highlightthickness=0, wrap=NONE) 855 # Prevent perhaps invisible selection of word or slice. 856 text.bind('<Double-Button-1>', lambda e: 'break') 857 text.bind('<B1-Motion>', lambda e: 'break') 858 string_tags=( 859 ('# Click selects item.', 'comment'), ('\n', 'normal'), 860 ('code context section', 'context'), ('\n', 'normal'), 861 ('| cursor', 'cursor'), ('\n', 'normal'), 862 ('def', 'keyword'), (' ', 'normal'), 863 ('func', 'definition'), ('(param):\n ', 'normal'), 864 ('"Return None."', 'string'), ('\n var0 = ', 'normal'), 865 ("'string'", 'string'), ('\n var1 = ', 'normal'), 866 ("'selected'", 'hilite'), ('\n var2 = ', 'normal'), 867 ("'found'", 'hit'), ('\n var3 = ', 'normal'), 868 ('list', 'builtin'), ('(', 'normal'), 869 ('None', 'keyword'), (')\n', 'normal'), 870 (' breakpoint("line")', 'break'), ('\n\n', 'normal'), 871 ('>>>', 'console'), (' 3.14**2\n', 'normal'), 872 ('9.8596', 'stdout'), ('\n', 'normal'), 873 ('>>>', 'console'), (' pri ', 'normal'), 874 ('n', 'error'), ('t(\n', 'normal'), 875 ('SyntaxError', 'stderr'), ('\n', 'normal')) 876 for string, tag in string_tags: 877 text.insert(END, string, tag) 878 n_lines = len(text.get('1.0', END).splitlines()) 879 for lineno in range(1, n_lines): 880 text.insert(f'{lineno}.0', 881 f'{lineno:{len(str(n_lines))}d} ', 882 'linenumber') 883 for element in self.theme_elements: 884 def tem(event, elem=element): 885 # event.widget.winfo_top_level().highlight_target.set(elem) 886 self.highlight_target.set(elem) 887 text.tag_bind( 888 self.theme_elements[element][0], '<ButtonPress-1>', tem) 889 text['state'] = 'disabled' 890 self.style.configure('frame_color_set.TFrame', borderwidth=1, 891 relief='solid') 892 self.frame_color_set = Frame(frame_custom, style='frame_color_set.TFrame') 893 frame_fg_bg_toggle = Frame(frame_custom) 894 self.button_set_color = Button( 895 self.frame_color_set, text='Choose Color for :', 896 command=self.get_color) 897 self.targetlist = DynOptionMenu( 898 self.frame_color_set, self.highlight_target, None, 899 highlightthickness=0) #, command=self.set_highlight_targetBinding 900 self.fg_on = Radiobutton( 901 frame_fg_bg_toggle, variable=self.fg_bg_toggle, value=1, 902 text='Foreground', command=self.set_color_sample_binding) 903 self.bg_on = Radiobutton( 904 frame_fg_bg_toggle, variable=self.fg_bg_toggle, value=0, 905 text='Background', command=self.set_color_sample_binding) 906 self.fg_bg_toggle.set(1) 907 self.button_save_custom = Button( 908 frame_custom, text='Save as New Custom Theme', 909 command=self.save_as_new_theme) 910 # frame_theme. 911 theme_type_title = Label(frame_theme, text='Select : ') 912 self.builtin_theme_on = Radiobutton( 913 frame_theme, variable=self.theme_source, value=1, 914 command=self.set_theme_type, text='a Built-in Theme') 915 self.custom_theme_on = Radiobutton( 916 frame_theme, variable=self.theme_source, value=0, 917 command=self.set_theme_type, text='a Custom Theme') 918 self.builtinlist = DynOptionMenu( 919 frame_theme, self.builtin_name, None, command=None) 920 self.customlist = DynOptionMenu( 921 frame_theme, self.custom_name, None, command=None) 922 self.button_delete_custom = Button( 923 frame_theme, text='Delete Custom Theme', 924 command=self.delete_custom) 925 self.theme_message = Label(frame_theme, borderwidth=2) 926 # Pack widgets: 927 # body. 928 frame_custom.pack(side=LEFT, padx=5, pady=5, expand=TRUE, fill=BOTH) 929 frame_theme.pack(side=TOP, padx=5, pady=5, fill=X) 930 # frame_custom. 931 self.frame_color_set.pack(side=TOP, padx=5, pady=5, fill=X) 932 frame_fg_bg_toggle.pack(side=TOP, padx=5, pady=0) 933 sample_frame.pack( 934 side=TOP, padx=5, pady=5, expand=TRUE, fill=BOTH) 935 self.button_set_color.pack(side=TOP, expand=TRUE, fill=X, padx=8, pady=4) 936 self.targetlist.pack(side=TOP, expand=TRUE, fill=X, padx=8, pady=3) 937 self.fg_on.pack(side=LEFT, anchor=E) 938 self.bg_on.pack(side=RIGHT, anchor=W) 939 self.button_save_custom.pack(side=BOTTOM, fill=X, padx=5, pady=5) 940 # frame_theme. 941 theme_type_title.pack(side=TOP, anchor=W, padx=5, pady=5) 942 self.builtin_theme_on.pack(side=TOP, anchor=W, padx=5) 943 self.custom_theme_on.pack(side=TOP, anchor=W, padx=5, pady=2) 944 self.builtinlist.pack(side=TOP, fill=X, padx=5, pady=5) 945 self.customlist.pack(side=TOP, fill=X, anchor=W, padx=5, pady=5) 946 self.button_delete_custom.pack(side=TOP, fill=X, padx=5, pady=5) 947 self.theme_message.pack(side=TOP, fill=X, pady=5) 948 949 def load_theme_cfg(self): 950 """Load current configuration settings for the theme options. 951 952 Based on the theme_source toggle, the theme is set as 953 either builtin or custom and the initial widget values 954 reflect the current settings from idleConf. 955 956 Attributes updated: 957 theme_source: Set from idleConf. 958 builtinlist: List of default themes from idleConf. 959 customlist: List of custom themes from idleConf. 960 custom_theme_on: Disabled if there are no custom themes. 961 custom_theme: Message with additional information. 962 targetlist: Create menu from self.theme_elements. 963 964 Methods: 965 set_theme_type 966 paint_theme_sample 967 set_highlight_target 968 """ 969 # Set current theme type radiobutton. 970 self.theme_source.set(idleConf.GetOption( 971 'main', 'Theme', 'default', type='bool', default=1)) 972 # Set current theme. 973 current_option = idleConf.CurrentTheme() 974 # Load available theme option menus. 975 if self.theme_source.get(): # Default theme selected. 976 item_list = idleConf.GetSectionList('default', 'highlight') 977 item_list.sort() 978 self.builtinlist.SetMenu(item_list, current_option) 979 item_list = idleConf.GetSectionList('user', 'highlight') 980 item_list.sort() 981 if not item_list: 982 self.custom_theme_on.state(('disabled',)) 983 self.custom_name.set('- no custom themes -') 984 else: 985 self.customlist.SetMenu(item_list, item_list[0]) 986 else: # User theme selected. 987 item_list = idleConf.GetSectionList('user', 'highlight') 988 item_list.sort() 989 self.customlist.SetMenu(item_list, current_option) 990 item_list = idleConf.GetSectionList('default', 'highlight') 991 item_list.sort() 992 self.builtinlist.SetMenu(item_list, item_list[0]) 993 self.set_theme_type() 994 # Load theme element option menu. 995 theme_names = list(self.theme_elements.keys()) 996 theme_names.sort(key=lambda x: self.theme_elements[x][1]) 997 self.targetlist.SetMenu(theme_names, theme_names[0]) 998 self.paint_theme_sample() 999 self.set_highlight_target() 1000 1001 def var_changed_builtin_name(self, *params): 1002 """Process new builtin theme selection. 1003 1004 Add the changed theme's name to the changed_items and recreate 1005 the sample with the values from the selected theme. 1006 """ 1007 old_themes = ('IDLE Classic', 'IDLE New') 1008 value = self.builtin_name.get() 1009 if value not in old_themes: 1010 if idleConf.GetOption('main', 'Theme', 'name') not in old_themes: 1011 changes.add_option('main', 'Theme', 'name', old_themes[0]) 1012 changes.add_option('main', 'Theme', 'name2', value) 1013 self.theme_message['text'] = 'New theme, see Help' 1014 else: 1015 changes.add_option('main', 'Theme', 'name', value) 1016 changes.add_option('main', 'Theme', 'name2', '') 1017 self.theme_message['text'] = '' 1018 self.paint_theme_sample() 1019 1020 def var_changed_custom_name(self, *params): 1021 """Process new custom theme selection. 1022 1023 If a new custom theme is selected, add the name to the 1024 changed_items and apply the theme to the sample. 1025 """ 1026 value = self.custom_name.get() 1027 if value != '- no custom themes -': 1028 changes.add_option('main', 'Theme', 'name', value) 1029 self.paint_theme_sample() 1030 1031 def var_changed_theme_source(self, *params): 1032 """Process toggle between builtin and custom theme. 1033 1034 Update the default toggle value and apply the newly 1035 selected theme type. 1036 """ 1037 value = self.theme_source.get() 1038 changes.add_option('main', 'Theme', 'default', value) 1039 if value: 1040 self.var_changed_builtin_name() 1041 else: 1042 self.var_changed_custom_name() 1043 1044 def var_changed_color(self, *params): 1045 "Process change to color choice." 1046 self.on_new_color_set() 1047 1048 def var_changed_highlight_target(self, *params): 1049 "Process selection of new target tag for highlighting." 1050 self.set_highlight_target() 1051 1052 def set_theme_type(self): 1053 """Set available screen options based on builtin or custom theme. 1054 1055 Attributes accessed: 1056 theme_source 1057 1058 Attributes updated: 1059 builtinlist 1060 customlist 1061 button_delete_custom 1062 custom_theme_on 1063 1064 Called from: 1065 handler for builtin_theme_on and custom_theme_on 1066 delete_custom 1067 create_new 1068 load_theme_cfg 1069 """ 1070 if self.theme_source.get(): 1071 self.builtinlist['state'] = 'normal' 1072 self.customlist['state'] = 'disabled' 1073 self.button_delete_custom.state(('disabled',)) 1074 else: 1075 self.builtinlist['state'] = 'disabled' 1076 self.custom_theme_on.state(('!disabled',)) 1077 self.customlist['state'] = 'normal' 1078 self.button_delete_custom.state(('!disabled',)) 1079 1080 def get_color(self): 1081 """Handle button to select a new color for the target tag. 1082 1083 If a new color is selected while using a builtin theme, a 1084 name must be supplied to create a custom theme. 1085 1086 Attributes accessed: 1087 highlight_target 1088 frame_color_set 1089 theme_source 1090 1091 Attributes updated: 1092 color 1093 1094 Methods: 1095 get_new_theme_name 1096 create_new 1097 """ 1098 target = self.highlight_target.get() 1099 prev_color = self.style.lookup(self.frame_color_set['style'], 1100 'background') 1101 rgbTuplet, color_string = tkColorChooser.askcolor( 1102 parent=self, title='Pick new color for : '+target, 1103 initialcolor=prev_color) 1104 if color_string and (color_string != prev_color): 1105 # User didn't cancel and they chose a new color. 1106 if self.theme_source.get(): # Current theme is a built-in. 1107 message = ('Your changes will be saved as a new Custom Theme. ' 1108 'Enter a name for your new Custom Theme below.') 1109 new_theme = self.get_new_theme_name(message) 1110 if not new_theme: # User cancelled custom theme creation. 1111 return 1112 else: # Create new custom theme based on previously active theme. 1113 self.create_new(new_theme) 1114 self.color.set(color_string) 1115 else: # Current theme is user defined. 1116 self.color.set(color_string) 1117 1118 def on_new_color_set(self): 1119 "Display sample of new color selection on the dialog." 1120 new_color = self.color.get() 1121 self.style.configure('frame_color_set.TFrame', background=new_color) 1122 plane = 'foreground' if self.fg_bg_toggle.get() else 'background' 1123 sample_element = self.theme_elements[self.highlight_target.get()][0] 1124 self.highlight_sample.tag_config(sample_element, **{plane: new_color}) 1125 theme = self.custom_name.get() 1126 theme_element = sample_element + '-' + plane 1127 changes.add_option('highlight', theme, theme_element, new_color) 1128 1129 def get_new_theme_name(self, message): 1130 "Return name of new theme from query popup." 1131 used_names = (idleConf.GetSectionList('user', 'highlight') + 1132 idleConf.GetSectionList('default', 'highlight')) 1133 new_theme = SectionName( 1134 self, 'New Custom Theme', message, used_names).result 1135 return new_theme 1136 1137 def save_as_new_theme(self): 1138 """Prompt for new theme name and create the theme. 1139 1140 Methods: 1141 get_new_theme_name 1142 create_new 1143 """ 1144 new_theme_name = self.get_new_theme_name('New Theme Name:') 1145 if new_theme_name: 1146 self.create_new(new_theme_name) 1147 1148 def create_new(self, new_theme_name): 1149 """Create a new custom theme with the given name. 1150 1151 Create the new theme based on the previously active theme 1152 with the current changes applied. Once it is saved, then 1153 activate the new theme. 1154 1155 Attributes accessed: 1156 builtin_name 1157 custom_name 1158 1159 Attributes updated: 1160 customlist 1161 theme_source 1162 1163 Method: 1164 save_new 1165 set_theme_type 1166 """ 1167 if self.theme_source.get(): 1168 theme_type = 'default' 1169 theme_name = self.builtin_name.get() 1170 else: 1171 theme_type = 'user' 1172 theme_name = self.custom_name.get() 1173 new_theme = idleConf.GetThemeDict(theme_type, theme_name) 1174 # Apply any of the old theme's unsaved changes to the new theme. 1175 if theme_name in changes['highlight']: 1176 theme_changes = changes['highlight'][theme_name] 1177 for element in theme_changes: 1178 new_theme[element] = theme_changes[element] 1179 # Save the new theme. 1180 self.save_new(new_theme_name, new_theme) 1181 # Change GUI over to the new theme. 1182 custom_theme_list = idleConf.GetSectionList('user', 'highlight') 1183 custom_theme_list.sort() 1184 self.customlist.SetMenu(custom_theme_list, new_theme_name) 1185 self.theme_source.set(0) 1186 self.set_theme_type() 1187 1188 def set_highlight_target(self): 1189 """Set fg/bg toggle and color based on highlight tag target. 1190 1191 Instance variables accessed: 1192 highlight_target 1193 1194 Attributes updated: 1195 fg_on 1196 bg_on 1197 fg_bg_toggle 1198 1199 Methods: 1200 set_color_sample 1201 1202 Called from: 1203 var_changed_highlight_target 1204 load_theme_cfg 1205 """ 1206 if self.highlight_target.get() == 'Cursor': # bg not possible 1207 self.fg_on.state(('disabled',)) 1208 self.bg_on.state(('disabled',)) 1209 self.fg_bg_toggle.set(1) 1210 else: # Both fg and bg can be set. 1211 self.fg_on.state(('!disabled',)) 1212 self.bg_on.state(('!disabled',)) 1213 self.fg_bg_toggle.set(1) 1214 self.set_color_sample() 1215 1216 def set_color_sample_binding(self, *args): 1217 """Change color sample based on foreground/background toggle. 1218 1219 Methods: 1220 set_color_sample 1221 """ 1222 self.set_color_sample() 1223 1224 def set_color_sample(self): 1225 """Set the color of the frame background to reflect the selected target. 1226 1227 Instance variables accessed: 1228 theme_elements 1229 highlight_target 1230 fg_bg_toggle 1231 highlight_sample 1232 1233 Attributes updated: 1234 frame_color_set 1235 """ 1236 # Set the color sample area. 1237 tag = self.theme_elements[self.highlight_target.get()][0] 1238 plane = 'foreground' if self.fg_bg_toggle.get() else 'background' 1239 color = self.highlight_sample.tag_cget(tag, plane) 1240 self.style.configure('frame_color_set.TFrame', background=color) 1241 1242 def paint_theme_sample(self): 1243 """Apply the theme colors to each element tag in the sample text. 1244 1245 Instance attributes accessed: 1246 theme_elements 1247 theme_source 1248 builtin_name 1249 custom_name 1250 1251 Attributes updated: 1252 highlight_sample: Set the tag elements to the theme. 1253 1254 Methods: 1255 set_color_sample 1256 1257 Called from: 1258 var_changed_builtin_name 1259 var_changed_custom_name 1260 load_theme_cfg 1261 """ 1262 if self.theme_source.get(): # Default theme 1263 theme = self.builtin_name.get() 1264 else: # User theme 1265 theme = self.custom_name.get() 1266 for element_title in self.theme_elements: 1267 element = self.theme_elements[element_title][0] 1268 colors = idleConf.GetHighlight(theme, element) 1269 if element == 'cursor': # Cursor sample needs special painting. 1270 colors['background'] = idleConf.GetHighlight( 1271 theme, 'normal')['background'] 1272 # Handle any unsaved changes to this theme. 1273 if theme in changes['highlight']: 1274 theme_dict = changes['highlight'][theme] 1275 if element + '-foreground' in theme_dict: 1276 colors['foreground'] = theme_dict[element + '-foreground'] 1277 if element + '-background' in theme_dict: 1278 colors['background'] = theme_dict[element + '-background'] 1279 self.highlight_sample.tag_config(element, **colors) 1280 self.set_color_sample() 1281 1282 def save_new(self, theme_name, theme): 1283 """Save a newly created theme to idleConf. 1284 1285 theme_name - string, the name of the new theme 1286 theme - dictionary containing the new theme 1287 """ 1288 idleConf.userCfg['highlight'].AddSection(theme_name) 1289 for element in theme: 1290 value = theme[element] 1291 idleConf.userCfg['highlight'].SetOption(theme_name, element, value) 1292 1293 def askyesno(self, *args, **kwargs): 1294 # Make testing easier. Could change implementation. 1295 return messagebox.askyesno(*args, **kwargs) 1296 1297 def delete_custom(self): 1298 """Handle event to delete custom theme. 1299 1300 The current theme is deactivated and the default theme is 1301 activated. The custom theme is permanently removed from 1302 the config file. 1303 1304 Attributes accessed: 1305 custom_name 1306 1307 Attributes updated: 1308 custom_theme_on 1309 customlist 1310 theme_source 1311 builtin_name 1312 1313 Methods: 1314 deactivate_current_config 1315 save_all_changed_extensions 1316 activate_config_changes 1317 set_theme_type 1318 """ 1319 theme_name = self.custom_name.get() 1320 delmsg = 'Are you sure you wish to delete the theme %r ?' 1321 if not self.askyesno( 1322 'Delete Theme', delmsg % theme_name, parent=self): 1323 return 1324 self.cd.deactivate_current_config() 1325 # Remove theme from changes, config, and file. 1326 changes.delete_section('highlight', theme_name) 1327 # Reload user theme list. 1328 item_list = idleConf.GetSectionList('user', 'highlight') 1329 item_list.sort() 1330 if not item_list: 1331 self.custom_theme_on.state(('disabled',)) 1332 self.customlist.SetMenu(item_list, '- no custom themes -') 1333 else: 1334 self.customlist.SetMenu(item_list, item_list[0]) 1335 # Revert to default theme. 1336 self.theme_source.set(idleConf.defaultCfg['main'].Get('Theme', 'default')) 1337 self.builtin_name.set(idleConf.defaultCfg['main'].Get('Theme', 'name')) 1338 # User can't back out of these changes, they must be applied now. 1339 changes.save_all() 1340 self.cd.save_all_changed_extensions() 1341 self.cd.activate_config_changes() 1342 self.set_theme_type() 1343 1344 1345class KeysPage(Frame): 1346 1347 def __init__(self, master): 1348 super().__init__(master) 1349 self.cd = master.master 1350 self.create_page_keys() 1351 self.load_key_cfg() 1352 1353 def create_page_keys(self): 1354 """Return frame of widgets for Keys tab. 1355 1356 Enable users to provisionally change both individual and sets of 1357 keybindings (shortcut keys). Except for features implemented as 1358 extensions, keybindings are stored in complete sets called 1359 keysets. Built-in keysets in idlelib/config-keys.def are fixed 1360 as far as the dialog is concerned. Any keyset can be used as the 1361 base for a new custom keyset, stored in .idlerc/config-keys.cfg. 1362 1363 Function load_key_cfg() initializes tk variables and keyset 1364 lists and calls load_keys_list for the current keyset. 1365 Radiobuttons builtin_keyset_on and custom_keyset_on toggle var 1366 keyset_source, which controls if the current set of keybindings 1367 are from a builtin or custom keyset. DynOptionMenus builtinlist 1368 and customlist contain lists of the builtin and custom keysets, 1369 respectively, and the current item from each list is stored in 1370 vars builtin_name and custom_name. 1371 1372 Button delete_custom_keys invokes delete_custom_keys() to delete 1373 a custom keyset from idleConf.userCfg['keys'] and changes. Button 1374 save_custom_keys invokes save_as_new_key_set() which calls 1375 get_new_keys_name() and create_new_key_set() to save a custom keyset 1376 and its keybindings to idleConf.userCfg['keys']. 1377 1378 Listbox bindingslist contains all of the keybindings for the 1379 selected keyset. The keybindings are loaded in load_keys_list() 1380 and are pairs of (event, [keys]) where keys can be a list 1381 of one or more key combinations to bind to the same event. 1382 Mouse button 1 click invokes on_bindingslist_select(), which 1383 allows button_new_keys to be clicked. 1384 1385 So, an item is selected in listbindings, which activates 1386 button_new_keys, and clicking button_new_keys calls function 1387 get_new_keys(). Function get_new_keys() gets the key mappings from the 1388 current keyset for the binding event item that was selected. The 1389 function then displays another dialog, GetKeysDialog, with the 1390 selected binding event and current keys and allows new key sequences 1391 to be entered for that binding event. If the keys aren't 1392 changed, nothing happens. If the keys are changed and the keyset 1393 is a builtin, function get_new_keys_name() will be called 1394 for input of a custom keyset name. If no name is given, then the 1395 change to the keybinding will abort and no updates will be made. If 1396 a custom name is entered in the prompt or if the current keyset was 1397 already custom (and thus didn't require a prompt), then 1398 idleConf.userCfg['keys'] is updated in function create_new_key_set() 1399 with the change to the event binding. The item listing in bindingslist 1400 is updated with the new keys. Var keybinding is also set which invokes 1401 the callback function, var_changed_keybinding, to add the change to 1402 the 'keys' or 'extensions' changes tracker based on the binding type. 1403 1404 Tk Variables: 1405 keybinding: Action/key bindings. 1406 1407 Methods: 1408 load_keys_list: Reload active set. 1409 create_new_key_set: Combine active keyset and changes. 1410 set_keys_type: Command for keyset_source. 1411 save_new_key_set: Save to idleConf.userCfg['keys'] (is function). 1412 deactivate_current_config: Remove keys bindings in editors. 1413 1414 Widgets for KeysPage(frame): (*) widgets bound to self 1415 frame_key_sets: LabelFrame 1416 frames[0]: Frame 1417 (*)builtin_keyset_on: Radiobutton - var keyset_source 1418 (*)custom_keyset_on: Radiobutton - var keyset_source 1419 (*)builtinlist: DynOptionMenu - var builtin_name, 1420 func keybinding_selected 1421 (*)customlist: DynOptionMenu - var custom_name, 1422 func keybinding_selected 1423 (*)keys_message: Label 1424 frames[1]: Frame 1425 (*)button_delete_custom_keys: Button - delete_custom_keys 1426 (*)button_save_custom_keys: Button - save_as_new_key_set 1427 frame_custom: LabelFrame 1428 frame_target: Frame 1429 target_title: Label 1430 scroll_target_y: Scrollbar 1431 scroll_target_x: Scrollbar 1432 (*)bindingslist: ListBox - on_bindingslist_select 1433 (*)button_new_keys: Button - get_new_keys & ..._name 1434 """ 1435 self.builtin_name = tracers.add( 1436 StringVar(self), self.var_changed_builtin_name) 1437 self.custom_name = tracers.add( 1438 StringVar(self), self.var_changed_custom_name) 1439 self.keyset_source = tracers.add( 1440 BooleanVar(self), self.var_changed_keyset_source) 1441 self.keybinding = tracers.add( 1442 StringVar(self), self.var_changed_keybinding) 1443 1444 # Create widgets: 1445 # body and section frames. 1446 frame_custom = LabelFrame( 1447 self, borderwidth=2, relief=GROOVE, 1448 text=' Custom Key Bindings ') 1449 frame_key_sets = LabelFrame( 1450 self, borderwidth=2, relief=GROOVE, text=' Key Set ') 1451 # frame_custom. 1452 frame_target = Frame(frame_custom) 1453 target_title = Label(frame_target, text='Action - Key(s)') 1454 scroll_target_y = Scrollbar(frame_target) 1455 scroll_target_x = Scrollbar(frame_target, orient=HORIZONTAL) 1456 self.bindingslist = Listbox( 1457 frame_target, takefocus=FALSE, exportselection=FALSE) 1458 self.bindingslist.bind('<ButtonRelease-1>', 1459 self.on_bindingslist_select) 1460 scroll_target_y['command'] = self.bindingslist.yview 1461 scroll_target_x['command'] = self.bindingslist.xview 1462 self.bindingslist['yscrollcommand'] = scroll_target_y.set 1463 self.bindingslist['xscrollcommand'] = scroll_target_x.set 1464 self.button_new_keys = Button( 1465 frame_custom, text='Get New Keys for Selection', 1466 command=self.get_new_keys, state='disabled') 1467 # frame_key_sets. 1468 frames = [Frame(frame_key_sets, padding=2, borderwidth=0) 1469 for i in range(2)] 1470 self.builtin_keyset_on = Radiobutton( 1471 frames[0], variable=self.keyset_source, value=1, 1472 command=self.set_keys_type, text='Use a Built-in Key Set') 1473 self.custom_keyset_on = Radiobutton( 1474 frames[0], variable=self.keyset_source, value=0, 1475 command=self.set_keys_type, text='Use a Custom Key Set') 1476 self.builtinlist = DynOptionMenu( 1477 frames[0], self.builtin_name, None, command=None) 1478 self.customlist = DynOptionMenu( 1479 frames[0], self.custom_name, None, command=None) 1480 self.button_delete_custom_keys = Button( 1481 frames[1], text='Delete Custom Key Set', 1482 command=self.delete_custom_keys) 1483 self.button_save_custom_keys = Button( 1484 frames[1], text='Save as New Custom Key Set', 1485 command=self.save_as_new_key_set) 1486 self.keys_message = Label(frames[0], borderwidth=2) 1487 1488 # Pack widgets: 1489 # body. 1490 frame_custom.pack(side=BOTTOM, padx=5, pady=5, expand=TRUE, fill=BOTH) 1491 frame_key_sets.pack(side=BOTTOM, padx=5, pady=5, fill=BOTH) 1492 # frame_custom. 1493 self.button_new_keys.pack(side=BOTTOM, fill=X, padx=5, pady=5) 1494 frame_target.pack(side=LEFT, padx=5, pady=5, expand=TRUE, fill=BOTH) 1495 # frame_target. 1496 frame_target.columnconfigure(0, weight=1) 1497 frame_target.rowconfigure(1, weight=1) 1498 target_title.grid(row=0, column=0, columnspan=2, sticky=W) 1499 self.bindingslist.grid(row=1, column=0, sticky=NSEW) 1500 scroll_target_y.grid(row=1, column=1, sticky=NS) 1501 scroll_target_x.grid(row=2, column=0, sticky=EW) 1502 # frame_key_sets. 1503 self.builtin_keyset_on.grid(row=0, column=0, sticky=W+NS) 1504 self.custom_keyset_on.grid(row=1, column=0, sticky=W+NS) 1505 self.builtinlist.grid(row=0, column=1, sticky=NSEW) 1506 self.customlist.grid(row=1, column=1, sticky=NSEW) 1507 self.keys_message.grid(row=0, column=2, sticky=NSEW, padx=5, pady=5) 1508 self.button_delete_custom_keys.pack(side=LEFT, fill=X, expand=True, padx=2) 1509 self.button_save_custom_keys.pack(side=LEFT, fill=X, expand=True, padx=2) 1510 frames[0].pack(side=TOP, fill=BOTH, expand=True) 1511 frames[1].pack(side=TOP, fill=X, expand=True, pady=2) 1512 1513 def load_key_cfg(self): 1514 "Load current configuration settings for the keybinding options." 1515 # Set current keys type radiobutton. 1516 self.keyset_source.set(idleConf.GetOption( 1517 'main', 'Keys', 'default', type='bool', default=1)) 1518 # Set current keys. 1519 current_option = idleConf.CurrentKeys() 1520 # Load available keyset option menus. 1521 if self.keyset_source.get(): # Default theme selected. 1522 item_list = idleConf.GetSectionList('default', 'keys') 1523 item_list.sort() 1524 self.builtinlist.SetMenu(item_list, current_option) 1525 item_list = idleConf.GetSectionList('user', 'keys') 1526 item_list.sort() 1527 if not item_list: 1528 self.custom_keyset_on.state(('disabled',)) 1529 self.custom_name.set('- no custom keys -') 1530 else: 1531 self.customlist.SetMenu(item_list, item_list[0]) 1532 else: # User key set selected. 1533 item_list = idleConf.GetSectionList('user', 'keys') 1534 item_list.sort() 1535 self.customlist.SetMenu(item_list, current_option) 1536 item_list = idleConf.GetSectionList('default', 'keys') 1537 item_list.sort() 1538 self.builtinlist.SetMenu(item_list, idleConf.default_keys()) 1539 self.set_keys_type() 1540 # Load keyset element list. 1541 keyset_name = idleConf.CurrentKeys() 1542 self.load_keys_list(keyset_name) 1543 1544 def var_changed_builtin_name(self, *params): 1545 "Process selection of builtin key set." 1546 old_keys = ( 1547 'IDLE Classic Windows', 1548 'IDLE Classic Unix', 1549 'IDLE Classic Mac', 1550 'IDLE Classic OSX', 1551 ) 1552 value = self.builtin_name.get() 1553 if value not in old_keys: 1554 if idleConf.GetOption('main', 'Keys', 'name') not in old_keys: 1555 changes.add_option('main', 'Keys', 'name', old_keys[0]) 1556 changes.add_option('main', 'Keys', 'name2', value) 1557 self.keys_message['text'] = 'New key set, see Help' 1558 else: 1559 changes.add_option('main', 'Keys', 'name', value) 1560 changes.add_option('main', 'Keys', 'name2', '') 1561 self.keys_message['text'] = '' 1562 self.load_keys_list(value) 1563 1564 def var_changed_custom_name(self, *params): 1565 "Process selection of custom key set." 1566 value = self.custom_name.get() 1567 if value != '- no custom keys -': 1568 changes.add_option('main', 'Keys', 'name', value) 1569 self.load_keys_list(value) 1570 1571 def var_changed_keyset_source(self, *params): 1572 "Process toggle between builtin key set and custom key set." 1573 value = self.keyset_source.get() 1574 changes.add_option('main', 'Keys', 'default', value) 1575 if value: 1576 self.var_changed_builtin_name() 1577 else: 1578 self.var_changed_custom_name() 1579 1580 def var_changed_keybinding(self, *params): 1581 "Store change to a keybinding." 1582 value = self.keybinding.get() 1583 key_set = self.custom_name.get() 1584 event = self.bindingslist.get(ANCHOR).split()[0] 1585 if idleConf.IsCoreBinding(event): 1586 changes.add_option('keys', key_set, event, value) 1587 else: # Event is an extension binding. 1588 ext_name = idleConf.GetExtnNameForEvent(event) 1589 ext_keybind_section = ext_name + '_cfgBindings' 1590 changes.add_option('extensions', ext_keybind_section, event, value) 1591 1592 def set_keys_type(self): 1593 "Set available screen options based on builtin or custom key set." 1594 if self.keyset_source.get(): 1595 self.builtinlist['state'] = 'normal' 1596 self.customlist['state'] = 'disabled' 1597 self.button_delete_custom_keys.state(('disabled',)) 1598 else: 1599 self.builtinlist['state'] = 'disabled' 1600 self.custom_keyset_on.state(('!disabled',)) 1601 self.customlist['state'] = 'normal' 1602 self.button_delete_custom_keys.state(('!disabled',)) 1603 1604 def get_new_keys(self): 1605 """Handle event to change key binding for selected line. 1606 1607 A selection of a key/binding in the list of current 1608 bindings pops up a dialog to enter a new binding. If 1609 the current key set is builtin and a binding has 1610 changed, then a name for a custom key set needs to be 1611 entered for the change to be applied. 1612 """ 1613 list_index = self.bindingslist.index(ANCHOR) 1614 binding = self.bindingslist.get(list_index) 1615 bind_name = binding.split()[0] 1616 if self.keyset_source.get(): 1617 current_key_set_name = self.builtin_name.get() 1618 else: 1619 current_key_set_name = self.custom_name.get() 1620 current_bindings = idleConf.GetCurrentKeySet() 1621 if current_key_set_name in changes['keys']: # unsaved changes 1622 key_set_changes = changes['keys'][current_key_set_name] 1623 for event in key_set_changes: 1624 current_bindings[event] = key_set_changes[event].split() 1625 current_key_sequences = list(current_bindings.values()) 1626 new_keys = GetKeysDialog(self, 'Get New Keys', bind_name, 1627 current_key_sequences).result 1628 if new_keys: 1629 if self.keyset_source.get(): # Current key set is a built-in. 1630 message = ('Your changes will be saved as a new Custom Key Set.' 1631 ' Enter a name for your new Custom Key Set below.') 1632 new_keyset = self.get_new_keys_name(message) 1633 if not new_keyset: # User cancelled custom key set creation. 1634 self.bindingslist.select_set(list_index) 1635 self.bindingslist.select_anchor(list_index) 1636 return 1637 else: # Create new custom key set based on previously active key set. 1638 self.create_new_key_set(new_keyset) 1639 self.bindingslist.delete(list_index) 1640 self.bindingslist.insert(list_index, bind_name+' - '+new_keys) 1641 self.bindingslist.select_set(list_index) 1642 self.bindingslist.select_anchor(list_index) 1643 self.keybinding.set(new_keys) 1644 else: 1645 self.bindingslist.select_set(list_index) 1646 self.bindingslist.select_anchor(list_index) 1647 1648 def get_new_keys_name(self, message): 1649 "Return new key set name from query popup." 1650 used_names = (idleConf.GetSectionList('user', 'keys') + 1651 idleConf.GetSectionList('default', 'keys')) 1652 new_keyset = SectionName( 1653 self, 'New Custom Key Set', message, used_names).result 1654 return new_keyset 1655 1656 def save_as_new_key_set(self): 1657 "Prompt for name of new key set and save changes using that name." 1658 new_keys_name = self.get_new_keys_name('New Key Set Name:') 1659 if new_keys_name: 1660 self.create_new_key_set(new_keys_name) 1661 1662 def on_bindingslist_select(self, event): 1663 "Activate button to assign new keys to selected action." 1664 self.button_new_keys.state(('!disabled',)) 1665 1666 def create_new_key_set(self, new_key_set_name): 1667 """Create a new custom key set with the given name. 1668 1669 Copy the bindings/keys from the previously active keyset 1670 to the new keyset and activate the new custom keyset. 1671 """ 1672 if self.keyset_source.get(): 1673 prev_key_set_name = self.builtin_name.get() 1674 else: 1675 prev_key_set_name = self.custom_name.get() 1676 prev_keys = idleConf.GetCoreKeys(prev_key_set_name) 1677 new_keys = {} 1678 for event in prev_keys: # Add key set to changed items. 1679 event_name = event[2:-2] # Trim off the angle brackets. 1680 binding = ' '.join(prev_keys[event]) 1681 new_keys[event_name] = binding 1682 # Handle any unsaved changes to prev key set. 1683 if prev_key_set_name in changes['keys']: 1684 key_set_changes = changes['keys'][prev_key_set_name] 1685 for event in key_set_changes: 1686 new_keys[event] = key_set_changes[event] 1687 # Save the new key set. 1688 self.save_new_key_set(new_key_set_name, new_keys) 1689 # Change GUI over to the new key set. 1690 custom_key_list = idleConf.GetSectionList('user', 'keys') 1691 custom_key_list.sort() 1692 self.customlist.SetMenu(custom_key_list, new_key_set_name) 1693 self.keyset_source.set(0) 1694 self.set_keys_type() 1695 1696 def load_keys_list(self, keyset_name): 1697 """Reload the list of action/key binding pairs for the active key set. 1698 1699 An action/key binding can be selected to change the key binding. 1700 """ 1701 reselect = False 1702 if self.bindingslist.curselection(): 1703 reselect = True 1704 list_index = self.bindingslist.index(ANCHOR) 1705 keyset = idleConf.GetKeySet(keyset_name) 1706 bind_names = list(keyset.keys()) 1707 bind_names.sort() 1708 self.bindingslist.delete(0, END) 1709 for bind_name in bind_names: 1710 key = ' '.join(keyset[bind_name]) 1711 bind_name = bind_name[2:-2] # Trim off the angle brackets. 1712 if keyset_name in changes['keys']: 1713 # Handle any unsaved changes to this key set. 1714 if bind_name in changes['keys'][keyset_name]: 1715 key = changes['keys'][keyset_name][bind_name] 1716 self.bindingslist.insert(END, bind_name+' - '+key) 1717 if reselect: 1718 self.bindingslist.see(list_index) 1719 self.bindingslist.select_set(list_index) 1720 self.bindingslist.select_anchor(list_index) 1721 1722 @staticmethod 1723 def save_new_key_set(keyset_name, keyset): 1724 """Save a newly created core key set. 1725 1726 Add keyset to idleConf.userCfg['keys'], not to disk. 1727 If the keyset doesn't exist, it is created. The 1728 binding/keys are taken from the keyset argument. 1729 1730 keyset_name - string, the name of the new key set 1731 keyset - dictionary containing the new keybindings 1732 """ 1733 idleConf.userCfg['keys'].AddSection(keyset_name) 1734 for event in keyset: 1735 value = keyset[event] 1736 idleConf.userCfg['keys'].SetOption(keyset_name, event, value) 1737 1738 def askyesno(self, *args, **kwargs): 1739 # Make testing easier. Could change implementation. 1740 return messagebox.askyesno(*args, **kwargs) 1741 1742 def delete_custom_keys(self): 1743 """Handle event to delete a custom key set. 1744 1745 Applying the delete deactivates the current configuration and 1746 reverts to the default. The custom key set is permanently 1747 deleted from the config file. 1748 """ 1749 keyset_name = self.custom_name.get() 1750 delmsg = 'Are you sure you wish to delete the key set %r ?' 1751 if not self.askyesno( 1752 'Delete Key Set', delmsg % keyset_name, parent=self): 1753 return 1754 self.cd.deactivate_current_config() 1755 # Remove key set from changes, config, and file. 1756 changes.delete_section('keys', keyset_name) 1757 # Reload user key set list. 1758 item_list = idleConf.GetSectionList('user', 'keys') 1759 item_list.sort() 1760 if not item_list: 1761 self.custom_keyset_on.state(('disabled',)) 1762 self.customlist.SetMenu(item_list, '- no custom keys -') 1763 else: 1764 self.customlist.SetMenu(item_list, item_list[0]) 1765 # Revert to default key set. 1766 self.keyset_source.set(idleConf.defaultCfg['main'] 1767 .Get('Keys', 'default')) 1768 self.builtin_name.set(idleConf.defaultCfg['main'].Get('Keys', 'name') 1769 or idleConf.default_keys()) 1770 # User can't back out of these changes, they must be applied now. 1771 changes.save_all() 1772 self.cd.save_all_changed_extensions() 1773 self.cd.activate_config_changes() 1774 self.set_keys_type() 1775 1776 1777class GenPage(Frame): 1778 1779 def __init__(self, master): 1780 super().__init__(master) 1781 1782 self.init_validators() 1783 self.create_page_general() 1784 self.load_general_cfg() 1785 1786 def init_validators(self): 1787 digits_or_empty_re = re.compile(r'[0-9]*') 1788 def is_digits_or_empty(s): 1789 "Return 's is blank or contains only digits'" 1790 return digits_or_empty_re.fullmatch(s) is not None 1791 self.digits_only = (self.register(is_digits_or_empty), '%P',) 1792 1793 def create_page_general(self): 1794 """Return frame of widgets for General tab. 1795 1796 Enable users to provisionally change general options. Function 1797 load_general_cfg initializes tk variables and helplist using 1798 idleConf. Radiobuttons startup_shell_on and startup_editor_on 1799 set var startup_edit. Radiobuttons save_ask_on and save_auto_on 1800 set var autosave. Entry boxes win_width_int and win_height_int 1801 set var win_width and win_height. Setting var_name invokes the 1802 default callback that adds option to changes. 1803 1804 Helplist: load_general_cfg loads list user_helplist with 1805 name, position pairs and copies names to listbox helplist. 1806 Clicking a name invokes help_source selected. Clicking 1807 button_helplist_name invokes helplist_item_name, which also 1808 changes user_helplist. These functions all call 1809 set_add_delete_state. All but load call update_help_changes to 1810 rewrite changes['main']['HelpFiles']. 1811 1812 Widgets for GenPage(Frame): (*) widgets bound to self 1813 frame_window: LabelFrame 1814 frame_run: Frame 1815 startup_title: Label 1816 (*)startup_editor_on: Radiobutton - startup_edit 1817 (*)startup_shell_on: Radiobutton - startup_edit 1818 frame_win_size: Frame 1819 win_size_title: Label 1820 win_width_title: Label 1821 (*)win_width_int: Entry - win_width 1822 win_height_title: Label 1823 (*)win_height_int: Entry - win_height 1824 frame_cursor_blink: Frame 1825 cursor_blink_title: Label 1826 (*)cursor_blink_bool: Checkbutton - cursor_blink 1827 frame_autocomplete: Frame 1828 auto_wait_title: Label 1829 (*)auto_wait_int: Entry - autocomplete_wait 1830 frame_paren1: Frame 1831 paren_style_title: Label 1832 (*)paren_style_type: OptionMenu - paren_style 1833 frame_paren2: Frame 1834 paren_time_title: Label 1835 (*)paren_flash_time: Entry - flash_delay 1836 (*)bell_on: Checkbutton - paren_bell 1837 frame_editor: LabelFrame 1838 frame_save: Frame 1839 run_save_title: Label 1840 (*)save_ask_on: Radiobutton - autosave 1841 (*)save_auto_on: Radiobutton - autosave 1842 frame_format: Frame 1843 format_width_title: Label 1844 (*)format_width_int: Entry - format_width 1845 frame_line_numbers_default: Frame 1846 line_numbers_default_title: Label 1847 (*)line_numbers_default_bool: Checkbutton - line_numbers_default 1848 frame_context: Frame 1849 context_title: Label 1850 (*)context_int: Entry - context_lines 1851 frame_shell: LabelFrame 1852 frame_auto_squeeze_min_lines: Frame 1853 auto_squeeze_min_lines_title: Label 1854 (*)auto_squeeze_min_lines_int: Entry - auto_squeeze_min_lines 1855 frame_help: LabelFrame 1856 frame_helplist: Frame 1857 frame_helplist_buttons: Frame 1858 (*)button_helplist_edit 1859 (*)button_helplist_add 1860 (*)button_helplist_remove 1861 (*)helplist: ListBox 1862 scroll_helplist: Scrollbar 1863 """ 1864 # Integer values need StringVar because int('') raises. 1865 self.startup_edit = tracers.add( 1866 IntVar(self), ('main', 'General', 'editor-on-startup')) 1867 self.win_width = tracers.add( 1868 StringVar(self), ('main', 'EditorWindow', 'width')) 1869 self.win_height = tracers.add( 1870 StringVar(self), ('main', 'EditorWindow', 'height')) 1871 self.cursor_blink = tracers.add( 1872 BooleanVar(self), ('main', 'EditorWindow', 'cursor-blink')) 1873 self.autocomplete_wait = tracers.add( 1874 StringVar(self), ('extensions', 'AutoComplete', 'popupwait')) 1875 self.paren_style = tracers.add( 1876 StringVar(self), ('extensions', 'ParenMatch', 'style')) 1877 self.flash_delay = tracers.add( 1878 StringVar(self), ('extensions', 'ParenMatch', 'flash-delay')) 1879 self.paren_bell = tracers.add( 1880 BooleanVar(self), ('extensions', 'ParenMatch', 'bell')) 1881 1882 self.auto_squeeze_min_lines = tracers.add( 1883 StringVar(self), ('main', 'PyShell', 'auto-squeeze-min-lines')) 1884 1885 self.autosave = tracers.add( 1886 IntVar(self), ('main', 'General', 'autosave')) 1887 self.format_width = tracers.add( 1888 StringVar(self), ('extensions', 'FormatParagraph', 'max-width')) 1889 self.line_numbers_default = tracers.add( 1890 BooleanVar(self), 1891 ('main', 'EditorWindow', 'line-numbers-default')) 1892 self.context_lines = tracers.add( 1893 StringVar(self), ('extensions', 'CodeContext', 'maxlines')) 1894 1895 # Create widgets: 1896 # Section frames. 1897 frame_window = LabelFrame(self, borderwidth=2, relief=GROOVE, 1898 text=' Window Preferences') 1899 frame_editor = LabelFrame(self, borderwidth=2, relief=GROOVE, 1900 text=' Editor Preferences') 1901 frame_shell = LabelFrame(self, borderwidth=2, relief=GROOVE, 1902 text=' Shell Preferences') 1903 frame_help = LabelFrame(self, borderwidth=2, relief=GROOVE, 1904 text=' Additional Help Sources ') 1905 # Frame_window. 1906 frame_run = Frame(frame_window, borderwidth=0) 1907 startup_title = Label(frame_run, text='At Startup') 1908 self.startup_editor_on = Radiobutton( 1909 frame_run, variable=self.startup_edit, value=1, 1910 text="Open Edit Window") 1911 self.startup_shell_on = Radiobutton( 1912 frame_run, variable=self.startup_edit, value=0, 1913 text='Open Shell Window') 1914 1915 frame_win_size = Frame(frame_window, borderwidth=0) 1916 win_size_title = Label( 1917 frame_win_size, text='Initial Window Size (in characters)') 1918 win_width_title = Label(frame_win_size, text='Width') 1919 self.win_width_int = Entry( 1920 frame_win_size, textvariable=self.win_width, width=3, 1921 validatecommand=self.digits_only, validate='key', 1922 ) 1923 win_height_title = Label(frame_win_size, text='Height') 1924 self.win_height_int = Entry( 1925 frame_win_size, textvariable=self.win_height, width=3, 1926 validatecommand=self.digits_only, validate='key', 1927 ) 1928 1929 frame_cursor_blink = Frame(frame_window, borderwidth=0) 1930 cursor_blink_title = Label(frame_cursor_blink, text='Cursor Blink') 1931 self.cursor_blink_bool = Checkbutton(frame_cursor_blink, 1932 variable=self.cursor_blink, width=1) 1933 1934 frame_autocomplete = Frame(frame_window, borderwidth=0,) 1935 auto_wait_title = Label(frame_autocomplete, 1936 text='Completions Popup Wait (milliseconds)') 1937 self.auto_wait_int = Entry(frame_autocomplete, width=6, 1938 textvariable=self.autocomplete_wait, 1939 validatecommand=self.digits_only, 1940 validate='key', 1941 ) 1942 1943 frame_paren1 = Frame(frame_window, borderwidth=0) 1944 paren_style_title = Label(frame_paren1, text='Paren Match Style') 1945 self.paren_style_type = OptionMenu( 1946 frame_paren1, self.paren_style, 'expression', 1947 "opener","parens","expression") 1948 frame_paren2 = Frame(frame_window, borderwidth=0) 1949 paren_time_title = Label( 1950 frame_paren2, text='Time Match Displayed (milliseconds)\n' 1951 '(0 is until next input)') 1952 self.paren_flash_time = Entry( 1953 frame_paren2, textvariable=self.flash_delay, width=6) 1954 self.bell_on = Checkbutton( 1955 frame_paren2, text="Bell on Mismatch", variable=self.paren_bell) 1956 1957 # Frame_editor. 1958 frame_save = Frame(frame_editor, borderwidth=0) 1959 run_save_title = Label(frame_save, text='At Start of Run (F5) ') 1960 self.save_ask_on = Radiobutton( 1961 frame_save, variable=self.autosave, value=0, 1962 text="Prompt to Save") 1963 self.save_auto_on = Radiobutton( 1964 frame_save, variable=self.autosave, value=1, 1965 text='No Prompt') 1966 1967 frame_format = Frame(frame_editor, borderwidth=0) 1968 format_width_title = Label(frame_format, 1969 text='Format Paragraph Max Width') 1970 self.format_width_int = Entry( 1971 frame_format, textvariable=self.format_width, width=4, 1972 validatecommand=self.digits_only, validate='key', 1973 ) 1974 1975 frame_line_numbers_default = Frame(frame_editor, borderwidth=0) 1976 line_numbers_default_title = Label( 1977 frame_line_numbers_default, text='Show line numbers in new windows') 1978 self.line_numbers_default_bool = Checkbutton( 1979 frame_line_numbers_default, 1980 variable=self.line_numbers_default, 1981 width=1) 1982 1983 frame_context = Frame(frame_editor, borderwidth=0) 1984 context_title = Label(frame_context, text='Max Context Lines :') 1985 self.context_int = Entry( 1986 frame_context, textvariable=self.context_lines, width=3, 1987 validatecommand=self.digits_only, validate='key', 1988 ) 1989 1990 # Frame_shell. 1991 frame_auto_squeeze_min_lines = Frame(frame_shell, borderwidth=0) 1992 auto_squeeze_min_lines_title = Label(frame_auto_squeeze_min_lines, 1993 text='Auto-Squeeze Min. Lines:') 1994 self.auto_squeeze_min_lines_int = Entry( 1995 frame_auto_squeeze_min_lines, width=4, 1996 textvariable=self.auto_squeeze_min_lines, 1997 validatecommand=self.digits_only, validate='key', 1998 ) 1999 2000 # frame_help. 2001 frame_helplist = Frame(frame_help) 2002 frame_helplist_buttons = Frame(frame_helplist) 2003 self.helplist = Listbox( 2004 frame_helplist, height=5, takefocus=True, 2005 exportselection=FALSE) 2006 scroll_helplist = Scrollbar(frame_helplist) 2007 scroll_helplist['command'] = self.helplist.yview 2008 self.helplist['yscrollcommand'] = scroll_helplist.set 2009 self.helplist.bind('<ButtonRelease-1>', self.help_source_selected) 2010 self.button_helplist_edit = Button( 2011 frame_helplist_buttons, text='Edit', state='disabled', 2012 width=8, command=self.helplist_item_edit) 2013 self.button_helplist_add = Button( 2014 frame_helplist_buttons, text='Add', 2015 width=8, command=self.helplist_item_add) 2016 self.button_helplist_remove = Button( 2017 frame_helplist_buttons, text='Remove', state='disabled', 2018 width=8, command=self.helplist_item_remove) 2019 2020 # Pack widgets: 2021 # Body. 2022 frame_window.pack(side=TOP, padx=5, pady=5, expand=TRUE, fill=BOTH) 2023 frame_editor.pack(side=TOP, padx=5, pady=5, expand=TRUE, fill=BOTH) 2024 frame_shell.pack(side=TOP, padx=5, pady=5, expand=TRUE, fill=BOTH) 2025 frame_help.pack(side=TOP, padx=5, pady=5, expand=TRUE, fill=BOTH) 2026 # frame_run. 2027 frame_run.pack(side=TOP, padx=5, pady=0, fill=X) 2028 startup_title.pack(side=LEFT, anchor=W, padx=5, pady=5) 2029 self.startup_shell_on.pack(side=RIGHT, anchor=W, padx=5, pady=5) 2030 self.startup_editor_on.pack(side=RIGHT, anchor=W, padx=5, pady=5) 2031 # frame_win_size. 2032 frame_win_size.pack(side=TOP, padx=5, pady=0, fill=X) 2033 win_size_title.pack(side=LEFT, anchor=W, padx=5, pady=5) 2034 self.win_height_int.pack(side=RIGHT, anchor=E, padx=10, pady=5) 2035 win_height_title.pack(side=RIGHT, anchor=E, pady=5) 2036 self.win_width_int.pack(side=RIGHT, anchor=E, padx=10, pady=5) 2037 win_width_title.pack(side=RIGHT, anchor=E, pady=5) 2038 # frame_cursor_blink. 2039 frame_cursor_blink.pack(side=TOP, padx=5, pady=0, fill=X) 2040 cursor_blink_title.pack(side=LEFT, anchor=W, padx=5, pady=5) 2041 self.cursor_blink_bool.pack(side=LEFT, padx=5, pady=5) 2042 # frame_autocomplete. 2043 frame_autocomplete.pack(side=TOP, padx=5, pady=0, fill=X) 2044 auto_wait_title.pack(side=LEFT, anchor=W, padx=5, pady=5) 2045 self.auto_wait_int.pack(side=TOP, padx=10, pady=5) 2046 # frame_paren. 2047 frame_paren1.pack(side=TOP, padx=5, pady=0, fill=X) 2048 paren_style_title.pack(side=LEFT, anchor=W, padx=5, pady=5) 2049 self.paren_style_type.pack(side=TOP, padx=10, pady=5) 2050 frame_paren2.pack(side=TOP, padx=5, pady=0, fill=X) 2051 paren_time_title.pack(side=LEFT, anchor=W, padx=5) 2052 self.bell_on.pack(side=RIGHT, anchor=E, padx=15, pady=5) 2053 self.paren_flash_time.pack(side=TOP, anchor=W, padx=15, pady=5) 2054 2055 # frame_save. 2056 frame_save.pack(side=TOP, padx=5, pady=0, fill=X) 2057 run_save_title.pack(side=LEFT, anchor=W, padx=5, pady=5) 2058 self.save_auto_on.pack(side=RIGHT, anchor=W, padx=5, pady=5) 2059 self.save_ask_on.pack(side=RIGHT, anchor=W, padx=5, pady=5) 2060 # frame_format. 2061 frame_format.pack(side=TOP, padx=5, pady=0, fill=X) 2062 format_width_title.pack(side=LEFT, anchor=W, padx=5, pady=5) 2063 self.format_width_int.pack(side=TOP, padx=10, pady=5) 2064 # frame_line_numbers_default. 2065 frame_line_numbers_default.pack(side=TOP, padx=5, pady=0, fill=X) 2066 line_numbers_default_title.pack(side=LEFT, anchor=W, padx=5, pady=5) 2067 self.line_numbers_default_bool.pack(side=LEFT, padx=5, pady=5) 2068 # frame_context. 2069 frame_context.pack(side=TOP, padx=5, pady=0, fill=X) 2070 context_title.pack(side=LEFT, anchor=W, padx=5, pady=5) 2071 self.context_int.pack(side=TOP, padx=5, pady=5) 2072 2073 # frame_auto_squeeze_min_lines 2074 frame_auto_squeeze_min_lines.pack(side=TOP, padx=5, pady=0, fill=X) 2075 auto_squeeze_min_lines_title.pack(side=LEFT, anchor=W, padx=5, pady=5) 2076 self.auto_squeeze_min_lines_int.pack(side=TOP, padx=5, pady=5) 2077 2078 # frame_help. 2079 frame_helplist_buttons.pack(side=RIGHT, padx=5, pady=5, fill=Y) 2080 frame_helplist.pack(side=TOP, padx=5, pady=5, expand=TRUE, fill=BOTH) 2081 scroll_helplist.pack(side=RIGHT, anchor=W, fill=Y) 2082 self.helplist.pack(side=LEFT, anchor=E, expand=TRUE, fill=BOTH) 2083 self.button_helplist_edit.pack(side=TOP, anchor=W, pady=5) 2084 self.button_helplist_add.pack(side=TOP, anchor=W) 2085 self.button_helplist_remove.pack(side=TOP, anchor=W, pady=5) 2086 2087 def load_general_cfg(self): 2088 "Load current configuration settings for the general options." 2089 # Set variables for all windows. 2090 self.startup_edit.set(idleConf.GetOption( 2091 'main', 'General', 'editor-on-startup', type='bool')) 2092 self.win_width.set(idleConf.GetOption( 2093 'main', 'EditorWindow', 'width', type='int')) 2094 self.win_height.set(idleConf.GetOption( 2095 'main', 'EditorWindow', 'height', type='int')) 2096 self.cursor_blink.set(idleConf.GetOption( 2097 'main', 'EditorWindow', 'cursor-blink', type='bool')) 2098 self.autocomplete_wait.set(idleConf.GetOption( 2099 'extensions', 'AutoComplete', 'popupwait', type='int')) 2100 self.paren_style.set(idleConf.GetOption( 2101 'extensions', 'ParenMatch', 'style')) 2102 self.flash_delay.set(idleConf.GetOption( 2103 'extensions', 'ParenMatch', 'flash-delay', type='int')) 2104 self.paren_bell.set(idleConf.GetOption( 2105 'extensions', 'ParenMatch', 'bell')) 2106 2107 # Set variables for editor windows. 2108 self.autosave.set(idleConf.GetOption( 2109 'main', 'General', 'autosave', default=0, type='bool')) 2110 self.format_width.set(idleConf.GetOption( 2111 'extensions', 'FormatParagraph', 'max-width', type='int')) 2112 self.line_numbers_default.set(idleConf.GetOption( 2113 'main', 'EditorWindow', 'line-numbers-default', type='bool')) 2114 self.context_lines.set(idleConf.GetOption( 2115 'extensions', 'CodeContext', 'maxlines', type='int')) 2116 2117 # Set variables for shell windows. 2118 self.auto_squeeze_min_lines.set(idleConf.GetOption( 2119 'main', 'PyShell', 'auto-squeeze-min-lines', type='int')) 2120 2121 # Set additional help sources. 2122 self.user_helplist = idleConf.GetAllExtraHelpSourcesList() 2123 self.helplist.delete(0, 'end') 2124 for help_item in self.user_helplist: 2125 self.helplist.insert(END, help_item[0]) 2126 self.set_add_delete_state() 2127 2128 def help_source_selected(self, event): 2129 "Handle event for selecting additional help." 2130 self.set_add_delete_state() 2131 2132 def set_add_delete_state(self): 2133 "Toggle the state for the help list buttons based on list entries." 2134 if self.helplist.size() < 1: # No entries in list. 2135 self.button_helplist_edit.state(('disabled',)) 2136 self.button_helplist_remove.state(('disabled',)) 2137 else: # Some entries. 2138 if self.helplist.curselection(): # There currently is a selection. 2139 self.button_helplist_edit.state(('!disabled',)) 2140 self.button_helplist_remove.state(('!disabled',)) 2141 else: # There currently is not a selection. 2142 self.button_helplist_edit.state(('disabled',)) 2143 self.button_helplist_remove.state(('disabled',)) 2144 2145 def helplist_item_add(self): 2146 """Handle add button for the help list. 2147 2148 Query for name and location of new help sources and add 2149 them to the list. 2150 """ 2151 help_source = HelpSource(self, 'New Help Source').result 2152 if help_source: 2153 self.user_helplist.append(help_source) 2154 self.helplist.insert(END, help_source[0]) 2155 self.update_help_changes() 2156 2157 def helplist_item_edit(self): 2158 """Handle edit button for the help list. 2159 2160 Query with existing help source information and update 2161 config if the values are changed. 2162 """ 2163 item_index = self.helplist.index(ANCHOR) 2164 help_source = self.user_helplist[item_index] 2165 new_help_source = HelpSource( 2166 self, 'Edit Help Source', 2167 menuitem=help_source[0], 2168 filepath=help_source[1], 2169 ).result 2170 if new_help_source and new_help_source != help_source: 2171 self.user_helplist[item_index] = new_help_source 2172 self.helplist.delete(item_index) 2173 self.helplist.insert(item_index, new_help_source[0]) 2174 self.update_help_changes() 2175 self.set_add_delete_state() # Selected will be un-selected 2176 2177 def helplist_item_remove(self): 2178 """Handle remove button for the help list. 2179 2180 Delete the help list item from config. 2181 """ 2182 item_index = self.helplist.index(ANCHOR) 2183 del(self.user_helplist[item_index]) 2184 self.helplist.delete(item_index) 2185 self.update_help_changes() 2186 self.set_add_delete_state() 2187 2188 def update_help_changes(self): 2189 "Clear and rebuild the HelpFiles section in changes" 2190 changes['main']['HelpFiles'] = {} 2191 for num in range(1, len(self.user_helplist) + 1): 2192 changes.add_option( 2193 'main', 'HelpFiles', str(num), 2194 ';'.join(self.user_helplist[num-1][:2])) 2195 2196 2197class VarTrace: 2198 """Maintain Tk variables trace state.""" 2199 2200 def __init__(self): 2201 """Store Tk variables and callbacks. 2202 2203 untraced: List of tuples (var, callback) 2204 that do not have the callback attached 2205 to the Tk var. 2206 traced: List of tuples (var, callback) where 2207 that callback has been attached to the var. 2208 """ 2209 self.untraced = [] 2210 self.traced = [] 2211 2212 def clear(self): 2213 "Clear lists (for tests)." 2214 # Call after all tests in a module to avoid memory leaks. 2215 self.untraced.clear() 2216 self.traced.clear() 2217 2218 def add(self, var, callback): 2219 """Add (var, callback) tuple to untraced list. 2220 2221 Args: 2222 var: Tk variable instance. 2223 callback: Either function name to be used as a callback 2224 or a tuple with IdleConf config-type, section, and 2225 option names used in the default callback. 2226 2227 Return: 2228 Tk variable instance. 2229 """ 2230 if isinstance(callback, tuple): 2231 callback = self.make_callback(var, callback) 2232 self.untraced.append((var, callback)) 2233 return var 2234 2235 @staticmethod 2236 def make_callback(var, config): 2237 "Return default callback function to add values to changes instance." 2238 def default_callback(*params): 2239 "Add config values to changes instance." 2240 changes.add_option(*config, var.get()) 2241 return default_callback 2242 2243 def attach(self): 2244 "Attach callback to all vars that are not traced." 2245 while self.untraced: 2246 var, callback = self.untraced.pop() 2247 var.trace_add('write', callback) 2248 self.traced.append((var, callback)) 2249 2250 def detach(self): 2251 "Remove callback from traced vars." 2252 while self.traced: 2253 var, callback = self.traced.pop() 2254 var.trace_remove('write', var.trace_info()[0][1]) 2255 self.untraced.append((var, callback)) 2256 2257 2258tracers = VarTrace() 2259 2260help_common = '''\ 2261When you click either the Apply or Ok buttons, settings in this 2262dialog that are different from IDLE's default are saved in 2263a .idlerc directory in your home directory. Except as noted, 2264these changes apply to all versions of IDLE installed on this 2265machine. [Cancel] only cancels changes made since the last save. 2266''' 2267help_pages = { 2268 'Fonts/Tabs':''' 2269Font sample: This shows what a selection of Basic Multilingual Plane 2270unicode characters look like for the current font selection. If the 2271selected font does not define a character, Tk attempts to find another 2272font that does. Substitute glyphs depend on what is available on a 2273particular system and will not necessarily have the same size as the 2274font selected. Line contains 20 characters up to Devanagari, 14 for 2275Tamil, and 10 for East Asia. 2276 2277Hebrew and Arabic letters should display right to left, starting with 2278alef, \u05d0 and \u0627. Arabic digits display left to right. The 2279Devanagari and Tamil lines start with digits. The East Asian lines 2280are Chinese digits, Chinese Hanzi, Korean Hangul, and Japanese 2281Hiragana and Katakana. 2282 2283You can edit the font sample. Changes remain until IDLE is closed. 2284''', 2285 'Highlights': ''' 2286Highlighting: 2287The IDLE Dark color theme is new in October 2015. It can only 2288be used with older IDLE releases if it is saved as a custom 2289theme, with a different name. 2290''', 2291 'Keys': ''' 2292Keys: 2293The IDLE Modern Unix key set is new in June 2016. It can only 2294be used with older IDLE releases if it is saved as a custom 2295key set, with a different name. 2296''', 2297 'General': ''' 2298General: 2299 2300AutoComplete: Popupwait is milliseconds to wait after key char, without 2301cursor movement, before popping up completion box. Key char is '.' after 2302identifier or a '/' (or '\\' on Windows) within a string. 2303 2304FormatParagraph: Max-width is max chars in lines after re-formatting. 2305Use with paragraphs in both strings and comment blocks. 2306 2307ParenMatch: Style indicates what is highlighted when closer is entered: 2308'opener' - opener '({[' corresponding to closer; 'parens' - both chars; 2309'expression' (default) - also everything in between. Flash-delay is how 2310long to highlight if cursor is not moved (0 means forever). 2311 2312CodeContext: Maxlines is the maximum number of code context lines to 2313display when Code Context is turned on for an editor window. 2314 2315Shell Preferences: Auto-Squeeze Min. Lines is the minimum number of lines 2316of output to automatically "squeeze". 2317''' 2318} 2319 2320 2321def is_int(s): 2322 "Return 's is blank or represents an int'" 2323 if not s: 2324 return True 2325 try: 2326 int(s) 2327 return True 2328 except ValueError: 2329 return False 2330 2331 2332class VerticalScrolledFrame(Frame): 2333 """A pure Tkinter vertically scrollable frame. 2334 2335 * Use the 'interior' attribute to place widgets inside the scrollable frame 2336 * Construct and pack/place/grid normally 2337 * This frame only allows vertical scrolling 2338 """ 2339 def __init__(self, parent, *args, **kw): 2340 Frame.__init__(self, parent, *args, **kw) 2341 2342 # Create a canvas object and a vertical scrollbar for scrolling it. 2343 vscrollbar = Scrollbar(self, orient=VERTICAL) 2344 vscrollbar.pack(fill=Y, side=RIGHT, expand=FALSE) 2345 canvas = Canvas(self, borderwidth=0, highlightthickness=0, 2346 yscrollcommand=vscrollbar.set, width=240) 2347 canvas.pack(side=LEFT, fill=BOTH, expand=TRUE) 2348 vscrollbar.config(command=canvas.yview) 2349 2350 # Reset the view. 2351 canvas.xview_moveto(0) 2352 canvas.yview_moveto(0) 2353 2354 # Create a frame inside the canvas which will be scrolled with it. 2355 self.interior = interior = Frame(canvas) 2356 interior_id = canvas.create_window(0, 0, window=interior, anchor=NW) 2357 2358 # Track changes to the canvas and frame width and sync them, 2359 # also updating the scrollbar. 2360 def _configure_interior(event): 2361 # Update the scrollbars to match the size of the inner frame. 2362 size = (interior.winfo_reqwidth(), interior.winfo_reqheight()) 2363 canvas.config(scrollregion="0 0 %s %s" % size) 2364 interior.bind('<Configure>', _configure_interior) 2365 2366 def _configure_canvas(event): 2367 if interior.winfo_reqwidth() != canvas.winfo_width(): 2368 # Update the inner frame's width to fill the canvas. 2369 canvas.itemconfigure(interior_id, width=canvas.winfo_width()) 2370 canvas.bind('<Configure>', _configure_canvas) 2371 2372 return 2373 2374 2375if __name__ == '__main__': 2376 from unittest import main 2377 main('idlelib.idle_test.test_configdialog', verbosity=2, exit=False) 2378 2379 from idlelib.idle_test.htest import run 2380 run(ConfigDialog) 2381