• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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