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