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