diff CSP2/CSP2_env/env-d9b9114564458d9d-741b3de822f2aaca6c6caa4325c4afce/lib/python3.8/idlelib/configdialog.py @ 68:5028fdace37b

planemo upload commit 2e9511a184a1ca667c7be0c6321a36dc4e3d116d
author jpayne
date Tue, 18 Mar 2025 16:23:26 -0400
parents
children
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/CSP2/CSP2_env/env-d9b9114564458d9d-741b3de822f2aaca6c6caa4325c4afce/lib/python3.8/idlelib/configdialog.py	Tue Mar 18 16:23:26 2025 -0400
@@ -0,0 +1,2380 @@
+"""IDLE Configuration Dialog: support user customization of IDLE by GUI
+
+Customize font faces, sizes, and colorization attributes.  Set indentation
+defaults.  Customize keybindings.  Colorization and keybindings can be
+saved as user defined sets.  Select startup options including shell/editor
+and default window size.  Define additional help sources.
+
+Note that tab width in IDLE is currently fixed at eight due to Tk issues.
+Refer to comments in EditorWindow autoindent code for details.
+
+"""
+import re
+
+from tkinter import (Toplevel, Listbox, Text, Scale, Canvas,
+                     StringVar, BooleanVar, IntVar, TRUE, FALSE,
+                     TOP, BOTTOM, RIGHT, LEFT, SOLID, GROOVE,
+                     NONE, BOTH, X, Y, W, E, EW, NS, NSEW, NW,
+                     HORIZONTAL, VERTICAL, ANCHOR, ACTIVE, END)
+from tkinter.ttk import (Frame, LabelFrame, Button, Checkbutton, Entry, Label,
+                         OptionMenu, Notebook, Radiobutton, Scrollbar, Style)
+import tkinter.colorchooser as tkColorChooser
+import tkinter.font as tkFont
+from tkinter import messagebox
+
+from idlelib.config import idleConf, ConfigChanges
+from idlelib.config_key import GetKeysDialog
+from idlelib.dynoption import DynOptionMenu
+from idlelib import macosx
+from idlelib.query import SectionName, HelpSource
+from idlelib.textview import view_text
+from idlelib.autocomplete import AutoComplete
+from idlelib.codecontext import CodeContext
+from idlelib.parenmatch import ParenMatch
+from idlelib.format import FormatParagraph
+from idlelib.squeezer import Squeezer
+from idlelib.textview import ScrollableTextFrame
+
+changes = ConfigChanges()
+# Reload changed options in the following classes.
+reloadables = (AutoComplete, CodeContext, ParenMatch, FormatParagraph,
+               Squeezer)
+
+
+class ConfigDialog(Toplevel):
+    """Config dialog for IDLE.
+    """
+
+    def __init__(self, parent, title='', *, _htest=False, _utest=False):
+        """Show the tabbed dialog for user configuration.
+
+        Args:
+            parent - parent of this dialog
+            title - string which is the title of this popup dialog
+            _htest - bool, change box location when running htest
+            _utest - bool, don't wait_window when running unittest
+
+        Note: Focus set on font page fontlist.
+
+        Methods:
+            create_widgets
+            cancel: Bound to DELETE_WINDOW protocol.
+        """
+        Toplevel.__init__(self, parent)
+        self.parent = parent
+        if _htest:
+            parent.instance_dict = {}
+        if not _utest:
+            self.withdraw()
+
+        self.configure(borderwidth=5)
+        self.title(title or 'IDLE Preferences')
+        x = parent.winfo_rootx() + 20
+        y = parent.winfo_rooty() + (30 if not _htest else 150)
+        self.geometry(f'+{x}+{y}')
+        # Each theme element key is its display name.
+        # The first value of the tuple is the sample area tag name.
+        # The second value is the display name list sort index.
+        self.create_widgets()
+        self.resizable(height=FALSE, width=FALSE)
+        self.transient(parent)
+        self.protocol("WM_DELETE_WINDOW", self.cancel)
+        self.fontpage.fontlist.focus_set()
+        # XXX Decide whether to keep or delete these key bindings.
+        # Key bindings for this dialog.
+        # self.bind('<Escape>', self.Cancel) #dismiss dialog, no save
+        # self.bind('<Alt-a>', self.Apply) #apply changes, save
+        # self.bind('<F1>', self.Help) #context help
+        # Attach callbacks after loading config to avoid calling them.
+        tracers.attach()
+
+        if not _utest:
+            self.grab_set()
+            self.wm_deiconify()
+            self.wait_window()
+
+    def create_widgets(self):
+        """Create and place widgets for tabbed dialog.
+
+        Widgets Bound to self:
+            note: Notebook
+            highpage: HighPage
+            fontpage: FontPage
+            keyspage: KeysPage
+            genpage: GenPage
+            extpage: self.create_page_extensions
+
+        Methods:
+            create_action_buttons
+            load_configs: Load pages except for extensions.
+            activate_config_changes: Tell editors to reload.
+        """
+        self.note = note = Notebook(self)
+        self.highpage = HighPage(note)
+        self.fontpage = FontPage(note, self.highpage)
+        self.keyspage = KeysPage(note)
+        self.genpage = GenPage(note)
+        self.extpage = self.create_page_extensions()
+        note.add(self.fontpage, text='Fonts/Tabs')
+        note.add(self.highpage, text='Highlights')
+        note.add(self.keyspage, text=' Keys ')
+        note.add(self.genpage, text=' General ')
+        note.add(self.extpage, text='Extensions')
+        note.enable_traversal()
+        note.pack(side=TOP, expand=TRUE, fill=BOTH)
+        self.create_action_buttons().pack(side=BOTTOM)
+
+    def create_action_buttons(self):
+        """Return frame of action buttons for dialog.
+
+        Methods:
+            ok
+            apply
+            cancel
+            help
+
+        Widget Structure:
+            outer: Frame
+                buttons: Frame
+                    (no assignment): Button (ok)
+                    (no assignment): Button (apply)
+                    (no assignment): Button (cancel)
+                    (no assignment): Button (help)
+                (no assignment): Frame
+        """
+        if macosx.isAquaTk():
+            # Changing the default padding on OSX results in unreadable
+            # text in the buttons.
+            padding_args = {}
+        else:
+            padding_args = {'padding': (6, 3)}
+        outer = Frame(self, padding=2)
+        buttons = Frame(outer, padding=2)
+        for txt, cmd in (
+            ('Ok', self.ok),
+            ('Apply', self.apply),
+            ('Cancel', self.cancel),
+            ('Help', self.help)):
+            Button(buttons, text=txt, command=cmd, takefocus=FALSE,
+                   **padding_args).pack(side=LEFT, padx=5)
+        # Add space above buttons.
+        Frame(outer, height=2, borderwidth=0).pack(side=TOP)
+        buttons.pack(side=BOTTOM)
+        return outer
+
+    def ok(self):
+        """Apply config changes, then dismiss dialog.
+
+        Methods:
+            apply
+            destroy: inherited
+        """
+        self.apply()
+        self.destroy()
+
+    def apply(self):
+        """Apply config changes and leave dialog open.
+
+        Methods:
+            deactivate_current_config
+            save_all_changed_extensions
+            activate_config_changes
+        """
+        self.deactivate_current_config()
+        changes.save_all()
+        self.save_all_changed_extensions()
+        self.activate_config_changes()
+
+    def cancel(self):
+        """Dismiss config dialog.
+
+        Methods:
+            destroy: inherited
+        """
+        self.destroy()
+
+    def destroy(self):
+        global font_sample_text
+        font_sample_text = self.fontpage.font_sample.get('1.0', 'end')
+        self.grab_release()
+        super().destroy()
+
+    def help(self):
+        """Create textview for config dialog help.
+
+        Attributes accessed:
+            note
+
+        Methods:
+            view_text: Method from textview module.
+        """
+        page = self.note.tab(self.note.select(), option='text').strip()
+        view_text(self, title='Help for IDLE preferences',
+                 text=help_common+help_pages.get(page, ''))
+
+    def deactivate_current_config(self):
+        """Remove current key bindings.
+        Iterate over window instances defined in parent and remove
+        the keybindings.
+        """
+        # Before a config is saved, some cleanup of current
+        # config must be done - remove the previous keybindings.
+        win_instances = self.parent.instance_dict.keys()
+        for instance in win_instances:
+            instance.RemoveKeybindings()
+
+    def activate_config_changes(self):
+        """Apply configuration changes to current windows.
+
+        Dynamically update the current parent window instances
+        with some of the configuration changes.
+        """
+        win_instances = self.parent.instance_dict.keys()
+        for instance in win_instances:
+            instance.ResetColorizer()
+            instance.ResetFont()
+            instance.set_notabs_indentwidth()
+            instance.ApplyKeybindings()
+            instance.reset_help_menu_entries()
+            instance.update_cursor_blink()
+        for klass in reloadables:
+            klass.reload()
+
+    def create_page_extensions(self):
+        """Part of the config dialog used for configuring IDLE extensions.
+
+        This code is generic - it works for any and all IDLE extensions.
+
+        IDLE extensions save their configuration options using idleConf.
+        This code reads the current configuration using idleConf, supplies a
+        GUI interface to change the configuration values, and saves the
+        changes using idleConf.
+
+        Not all changes take effect immediately - some may require restarting IDLE.
+        This depends on each extension's implementation.
+
+        All values are treated as text, and it is up to the user to supply
+        reasonable values. The only exception to this are the 'enable*' options,
+        which are boolean, and can be toggled with a True/False button.
+
+        Methods:
+            load_extensions:
+            extension_selected: Handle selection from list.
+            create_extension_frame: Hold widgets for one extension.
+            set_extension_value: Set in userCfg['extensions'].
+            save_all_changed_extensions: Call extension page Save().
+        """
+        parent = self.parent
+        frame = Frame(self.note)
+        self.ext_defaultCfg = idleConf.defaultCfg['extensions']
+        self.ext_userCfg = idleConf.userCfg['extensions']
+        self.is_int = self.register(is_int)
+        self.load_extensions()
+        # Create widgets - a listbox shows all available extensions, with the
+        # controls for the extension selected in the listbox to the right.
+        self.extension_names = StringVar(self)
+        frame.rowconfigure(0, weight=1)
+        frame.columnconfigure(2, weight=1)
+        self.extension_list = Listbox(frame, listvariable=self.extension_names,
+                                      selectmode='browse')
+        self.extension_list.bind('<<ListboxSelect>>', self.extension_selected)
+        scroll = Scrollbar(frame, command=self.extension_list.yview)
+        self.extension_list.yscrollcommand=scroll.set
+        self.details_frame = LabelFrame(frame, width=250, height=250)
+        self.extension_list.grid(column=0, row=0, sticky='nws')
+        scroll.grid(column=1, row=0, sticky='ns')
+        self.details_frame.grid(column=2, row=0, sticky='nsew', padx=[10, 0])
+        frame.configure(padding=10)
+        self.config_frame = {}
+        self.current_extension = None
+
+        self.outerframe = self                      # TEMPORARY
+        self.tabbed_page_set = self.extension_list  # TEMPORARY
+
+        # Create the frame holding controls for each extension.
+        ext_names = ''
+        for ext_name in sorted(self.extensions):
+            self.create_extension_frame(ext_name)
+            ext_names = ext_names + '{' + ext_name + '} '
+        self.extension_names.set(ext_names)
+        self.extension_list.selection_set(0)
+        self.extension_selected(None)
+
+        return frame
+
+    def load_extensions(self):
+        "Fill self.extensions with data from the default and user configs."
+        self.extensions = {}
+        for ext_name in idleConf.GetExtensions(active_only=False):
+            # Former built-in extensions are already filtered out.
+            self.extensions[ext_name] = []
+
+        for ext_name in self.extensions:
+            opt_list = sorted(self.ext_defaultCfg.GetOptionList(ext_name))
+
+            # Bring 'enable' options to the beginning of the list.
+            enables = [opt_name for opt_name in opt_list
+                       if opt_name.startswith('enable')]
+            for opt_name in enables:
+                opt_list.remove(opt_name)
+            opt_list = enables + opt_list
+
+            for opt_name in opt_list:
+                def_str = self.ext_defaultCfg.Get(
+                        ext_name, opt_name, raw=True)
+                try:
+                    def_obj = {'True':True, 'False':False}[def_str]
+                    opt_type = 'bool'
+                except KeyError:
+                    try:
+                        def_obj = int(def_str)
+                        opt_type = 'int'
+                    except ValueError:
+                        def_obj = def_str
+                        opt_type = None
+                try:
+                    value = self.ext_userCfg.Get(
+                            ext_name, opt_name, type=opt_type, raw=True,
+                            default=def_obj)
+                except ValueError:  # Need this until .Get fixed.
+                    value = def_obj  # Bad values overwritten by entry.
+                var = StringVar(self)
+                var.set(str(value))
+
+                self.extensions[ext_name].append({'name': opt_name,
+                                                  'type': opt_type,
+                                                  'default': def_str,
+                                                  'value': value,
+                                                  'var': var,
+                                                 })
+
+    def extension_selected(self, event):
+        "Handle selection of an extension from the list."
+        newsel = self.extension_list.curselection()
+        if newsel:
+            newsel = self.extension_list.get(newsel)
+        if newsel is None or newsel != self.current_extension:
+            if self.current_extension:
+                self.details_frame.config(text='')
+                self.config_frame[self.current_extension].grid_forget()
+                self.current_extension = None
+        if newsel:
+            self.details_frame.config(text=newsel)
+            self.config_frame[newsel].grid(column=0, row=0, sticky='nsew')
+            self.current_extension = newsel
+
+    def create_extension_frame(self, ext_name):
+        """Create a frame holding the widgets to configure one extension"""
+        f = VerticalScrolledFrame(self.details_frame, height=250, width=250)
+        self.config_frame[ext_name] = f
+        entry_area = f.interior
+        # Create an entry for each configuration option.
+        for row, opt in enumerate(self.extensions[ext_name]):
+            # Create a row with a label and entry/checkbutton.
+            label = Label(entry_area, text=opt['name'])
+            label.grid(row=row, column=0, sticky=NW)
+            var = opt['var']
+            if opt['type'] == 'bool':
+                Checkbutton(entry_area, variable=var,
+                            onvalue='True', offvalue='False', width=8
+                            ).grid(row=row, column=1, sticky=W, padx=7)
+            elif opt['type'] == 'int':
+                Entry(entry_area, textvariable=var, validate='key',
+                      validatecommand=(self.is_int, '%P'), width=10
+                      ).grid(row=row, column=1, sticky=NSEW, padx=7)
+
+            else:  # type == 'str'
+                # Limit size to fit non-expanding space with larger font.
+                Entry(entry_area, textvariable=var, width=15
+                      ).grid(row=row, column=1, sticky=NSEW, padx=7)
+        return
+
+    def set_extension_value(self, section, opt):
+        """Return True if the configuration was added or changed.
+
+        If the value is the same as the default, then remove it
+        from user config file.
+        """
+        name = opt['name']
+        default = opt['default']
+        value = opt['var'].get().strip() or default
+        opt['var'].set(value)
+        # if self.defaultCfg.has_section(section):
+        # Currently, always true; if not, indent to return.
+        if (value == default):
+            return self.ext_userCfg.RemoveOption(section, name)
+        # Set the option.
+        return self.ext_userCfg.SetOption(section, name, value)
+
+    def save_all_changed_extensions(self):
+        """Save configuration changes to the user config file.
+
+        Attributes accessed:
+            extensions
+
+        Methods:
+            set_extension_value
+        """
+        has_changes = False
+        for ext_name in self.extensions:
+            options = self.extensions[ext_name]
+            for opt in options:
+                if self.set_extension_value(ext_name, opt):
+                    has_changes = True
+        if has_changes:
+            self.ext_userCfg.Save()
+
+
+# class TabPage(Frame):  # A template for Page classes.
+#     def __init__(self, master):
+#         super().__init__(master)
+#         self.create_page_tab()
+#         self.load_tab_cfg()
+#     def create_page_tab(self):
+#         # Define tk vars and register var and callback with tracers.
+#         # Create subframes and widgets.
+#         # Pack widgets.
+#     def load_tab_cfg(self):
+#         # Initialize widgets with data from idleConf.
+#     def var_changed_var_name():
+#         # For each tk var that needs other than default callback.
+#     def other_methods():
+#         # Define tab-specific behavior.
+
+font_sample_text = (
+    '<ASCII/Latin1>\n'
+    'AaBbCcDdEeFfGgHhIiJj\n1234567890#:+=(){}[]\n'
+    '\u00a2\u00a3\u00a5\u00a7\u00a9\u00ab\u00ae\u00b6\u00bd\u011e'
+    '\u00c0\u00c1\u00c2\u00c3\u00c4\u00c5\u00c7\u00d0\u00d8\u00df\n'
+    '\n<IPA,Greek,Cyrillic>\n'
+    '\u0250\u0255\u0258\u025e\u025f\u0264\u026b\u026e\u0270\u0277'
+    '\u027b\u0281\u0283\u0286\u028e\u029e\u02a2\u02ab\u02ad\u02af\n'
+    '\u0391\u03b1\u0392\u03b2\u0393\u03b3\u0394\u03b4\u0395\u03b5'
+    '\u0396\u03b6\u0397\u03b7\u0398\u03b8\u0399\u03b9\u039a\u03ba\n'
+    '\u0411\u0431\u0414\u0434\u0416\u0436\u041f\u043f\u0424\u0444'
+    '\u0427\u0447\u042a\u044a\u042d\u044d\u0460\u0464\u046c\u04dc\n'
+    '\n<Hebrew, Arabic>\n'
+    '\u05d0\u05d1\u05d2\u05d3\u05d4\u05d5\u05d6\u05d7\u05d8\u05d9'
+    '\u05da\u05db\u05dc\u05dd\u05de\u05df\u05e0\u05e1\u05e2\u05e3\n'
+    '\u0627\u0628\u062c\u062f\u0647\u0648\u0632\u062d\u0637\u064a'
+    '\u0660\u0661\u0662\u0663\u0664\u0665\u0666\u0667\u0668\u0669\n'
+    '\n<Devanagari, Tamil>\n'
+    '\u0966\u0967\u0968\u0969\u096a\u096b\u096c\u096d\u096e\u096f'
+    '\u0905\u0906\u0907\u0908\u0909\u090a\u090f\u0910\u0913\u0914\n'
+    '\u0be6\u0be7\u0be8\u0be9\u0bea\u0beb\u0bec\u0bed\u0bee\u0bef'
+    '\u0b85\u0b87\u0b89\u0b8e\n'
+    '\n<East Asian>\n'
+    '\u3007\u4e00\u4e8c\u4e09\u56db\u4e94\u516d\u4e03\u516b\u4e5d\n'
+    '\u6c49\u5b57\u6f22\u5b57\u4eba\u6728\u706b\u571f\u91d1\u6c34\n'
+    '\uac00\ub0d0\ub354\ub824\ubaa8\ubd64\uc218\uc720\uc988\uce58\n'
+    '\u3042\u3044\u3046\u3048\u304a\u30a2\u30a4\u30a6\u30a8\u30aa\n'
+    )
+
+
+class FontPage(Frame):
+
+    def __init__(self, master, highpage):
+        super().__init__(master)
+        self.highlight_sample = highpage.highlight_sample
+        self.create_page_font_tab()
+        self.load_font_cfg()
+        self.load_tab_cfg()
+
+    def create_page_font_tab(self):
+        """Return frame of widgets for Font/Tabs tab.
+
+        Fonts: Enable users to provisionally change font face, size, or
+        boldness and to see the consequence of proposed choices.  Each
+        action set 3 options in changes structuree and changes the
+        corresponding aspect of the font sample on this page and
+        highlight sample on highlight page.
+
+        Function load_font_cfg initializes font vars and widgets from
+        idleConf entries and tk.
+
+        Fontlist: mouse button 1 click or up or down key invoke
+        on_fontlist_select(), which sets var font_name.
+
+        Sizelist: clicking the menubutton opens the dropdown menu. A
+        mouse button 1 click or return key sets var font_size.
+
+        Bold_toggle: clicking the box toggles var font_bold.
+
+        Changing any of the font vars invokes var_changed_font, which
+        adds all 3 font options to changes and calls set_samples.
+        Set_samples applies a new font constructed from the font vars to
+        font_sample and to highlight_sample on the highlight page.
+
+        Tabs: Enable users to change spaces entered for indent tabs.
+        Changing indent_scale value with the mouse sets Var space_num,
+        which invokes the default callback to add an entry to
+        changes.  Load_tab_cfg initializes space_num to default.
+
+        Widgets for FontPage(Frame):  (*) widgets bound to self
+            frame_font: LabelFrame
+                frame_font_name: Frame
+                    font_name_title: Label
+                    (*)fontlist: ListBox - font_name
+                    scroll_font: Scrollbar
+                frame_font_param: Frame
+                    font_size_title: Label
+                    (*)sizelist: DynOptionMenu - font_size
+                    (*)bold_toggle: Checkbutton - font_bold
+            frame_sample: LabelFrame
+                (*)font_sample: Label
+            frame_indent: LabelFrame
+                    indent_title: Label
+                    (*)indent_scale: Scale - space_num
+        """
+        self.font_name = tracers.add(StringVar(self), self.var_changed_font)
+        self.font_size = tracers.add(StringVar(self), self.var_changed_font)
+        self.font_bold = tracers.add(BooleanVar(self), self.var_changed_font)
+        self.space_num = tracers.add(IntVar(self), ('main', 'Indent', 'num-spaces'))
+
+        # Define frames and widgets.
+        frame_font = LabelFrame(
+                self, borderwidth=2, relief=GROOVE, text=' Shell/Editor Font ')
+        frame_sample = LabelFrame(
+                self, borderwidth=2, relief=GROOVE,
+                text=' Font Sample (Editable) ')
+        frame_indent = LabelFrame(
+                self, borderwidth=2, relief=GROOVE, text=' Indentation Width ')
+        # frame_font.
+        frame_font_name = Frame(frame_font)
+        frame_font_param = Frame(frame_font)
+        font_name_title = Label(
+                frame_font_name, justify=LEFT, text='Font Face :')
+        self.fontlist = Listbox(frame_font_name, height=15,
+                                takefocus=True, exportselection=FALSE)
+        self.fontlist.bind('<ButtonRelease-1>', self.on_fontlist_select)
+        self.fontlist.bind('<KeyRelease-Up>', self.on_fontlist_select)
+        self.fontlist.bind('<KeyRelease-Down>', self.on_fontlist_select)
+        scroll_font = Scrollbar(frame_font_name)
+        scroll_font.config(command=self.fontlist.yview)
+        self.fontlist.config(yscrollcommand=scroll_font.set)
+        font_size_title = Label(frame_font_param, text='Size :')
+        self.sizelist = DynOptionMenu(frame_font_param, self.font_size, None)
+        self.bold_toggle = Checkbutton(
+                frame_font_param, variable=self.font_bold,
+                onvalue=1, offvalue=0, text='Bold')
+        # frame_sample.
+        font_sample_frame = ScrollableTextFrame(frame_sample)
+        self.font_sample = font_sample_frame.text
+        self.font_sample.config(wrap=NONE, width=1, height=1)
+        self.font_sample.insert(END, font_sample_text)
+        # frame_indent.
+        indent_title = Label(
+                frame_indent, justify=LEFT,
+                text='Python Standard: 4 Spaces!')
+        self.indent_scale = Scale(
+                frame_indent, variable=self.space_num,
+                orient='horizontal', tickinterval=2, from_=2, to=16)
+
+        # Grid and pack widgets:
+        self.columnconfigure(1, weight=1)
+        self.rowconfigure(2, weight=1)
+        frame_font.grid(row=0, column=0, padx=5, pady=5)
+        frame_sample.grid(row=0, column=1, rowspan=3, padx=5, pady=5,
+                          sticky='nsew')
+        frame_indent.grid(row=1, column=0, padx=5, pady=5, sticky='ew')
+        # frame_font.
+        frame_font_name.pack(side=TOP, padx=5, pady=5, fill=X)
+        frame_font_param.pack(side=TOP, padx=5, pady=5, fill=X)
+        font_name_title.pack(side=TOP, anchor=W)
+        self.fontlist.pack(side=LEFT, expand=TRUE, fill=X)
+        scroll_font.pack(side=LEFT, fill=Y)
+        font_size_title.pack(side=LEFT, anchor=W)
+        self.sizelist.pack(side=LEFT, anchor=W)
+        self.bold_toggle.pack(side=LEFT, anchor=W, padx=20)
+        # frame_sample.
+        font_sample_frame.pack(expand=TRUE, fill=BOTH)
+        # frame_indent.
+        indent_title.pack(side=TOP, anchor=W, padx=5)
+        self.indent_scale.pack(side=TOP, padx=5, fill=X)
+
+    def load_font_cfg(self):
+        """Load current configuration settings for the font options.
+
+        Retrieve current font with idleConf.GetFont and font families
+        from tk. Setup fontlist and set font_name.  Setup sizelist,
+        which sets font_size.  Set font_bold.  Call set_samples.
+        """
+        configured_font = idleConf.GetFont(self, 'main', 'EditorWindow')
+        font_name = configured_font[0].lower()
+        font_size = configured_font[1]
+        font_bold  = configured_font[2]=='bold'
+
+        # Set editor font selection list and font_name.
+        fonts = list(tkFont.families(self))
+        fonts.sort()
+        for font in fonts:
+            self.fontlist.insert(END, font)
+        self.font_name.set(font_name)
+        lc_fonts = [s.lower() for s in fonts]
+        try:
+            current_font_index = lc_fonts.index(font_name)
+            self.fontlist.see(current_font_index)
+            self.fontlist.select_set(current_font_index)
+            self.fontlist.select_anchor(current_font_index)
+            self.fontlist.activate(current_font_index)
+        except ValueError:
+            pass
+        # Set font size dropdown.
+        self.sizelist.SetMenu(('7', '8', '9', '10', '11', '12', '13', '14',
+                               '16', '18', '20', '22', '25', '29', '34', '40'),
+                              font_size)
+        # Set font weight.
+        self.font_bold.set(font_bold)
+        self.set_samples()
+
+    def var_changed_font(self, *params):
+        """Store changes to font attributes.
+
+        When one font attribute changes, save them all, as they are
+        not independent from each other. In particular, when we are
+        overriding the default font, we need to write out everything.
+        """
+        value = self.font_name.get()
+        changes.add_option('main', 'EditorWindow', 'font', value)
+        value = self.font_size.get()
+        changes.add_option('main', 'EditorWindow', 'font-size', value)
+        value = self.font_bold.get()
+        changes.add_option('main', 'EditorWindow', 'font-bold', value)
+        self.set_samples()
+
+    def on_fontlist_select(self, event):
+        """Handle selecting a font from the list.
+
+        Event can result from either mouse click or Up or Down key.
+        Set font_name and example displays to selection.
+        """
+        font = self.fontlist.get(
+                ACTIVE if event.type.name == 'KeyRelease' else ANCHOR)
+        self.font_name.set(font.lower())
+
+    def set_samples(self, event=None):
+        """Update update both screen samples with the font settings.
+
+        Called on font initialization and change events.
+        Accesses font_name, font_size, and font_bold Variables.
+        Updates font_sample and highlight page highlight_sample.
+        """
+        font_name = self.font_name.get()
+        font_weight = tkFont.BOLD if self.font_bold.get() else tkFont.NORMAL
+        new_font = (font_name, self.font_size.get(), font_weight)
+        self.font_sample['font'] = new_font
+        self.highlight_sample['font'] = new_font
+
+    def load_tab_cfg(self):
+        """Load current configuration settings for the tab options.
+
+        Attributes updated:
+            space_num: Set to value from idleConf.
+        """
+        # Set indent sizes.
+        space_num = idleConf.GetOption(
+            'main', 'Indent', 'num-spaces', default=4, type='int')
+        self.space_num.set(space_num)
+
+    def var_changed_space_num(self, *params):
+        "Store change to indentation size."
+        value = self.space_num.get()
+        changes.add_option('main', 'Indent', 'num-spaces', value)
+
+
+class HighPage(Frame):
+
+    def __init__(self, master):
+        super().__init__(master)
+        self.cd = master.master
+        self.style = Style(master)
+        self.create_page_highlight()
+        self.load_theme_cfg()
+
+    def create_page_highlight(self):
+        """Return frame of widgets for Highlighting tab.
+
+        Enable users to provisionally change foreground and background
+        colors applied to textual tags.  Color mappings are stored in
+        complete listings called themes.  Built-in themes in
+        idlelib/config-highlight.def are fixed as far as the dialog is
+        concerned. Any theme can be used as the base for a new custom
+        theme, stored in .idlerc/config-highlight.cfg.
+
+        Function load_theme_cfg() initializes tk variables and theme
+        lists and calls paint_theme_sample() and set_highlight_target()
+        for the current theme.  Radiobuttons builtin_theme_on and
+        custom_theme_on toggle var theme_source, which controls if the
+        current set of colors are from a builtin or custom theme.
+        DynOptionMenus builtinlist and customlist contain lists of the
+        builtin and custom themes, respectively, and the current item
+        from each list is stored in vars builtin_name and custom_name.
+
+        Function paint_theme_sample() applies the colors from the theme
+        to the tags in text widget highlight_sample and then invokes
+        set_color_sample().  Function set_highlight_target() sets the state
+        of the radiobuttons fg_on and bg_on based on the tag and it also
+        invokes set_color_sample().
+
+        Function set_color_sample() sets the background color for the frame
+        holding the color selector.  This provides a larger visual of the
+        color for the current tag and plane (foreground/background).
+
+        Note: set_color_sample() is called from many places and is often
+        called more than once when a change is made.  It is invoked when
+        foreground or background is selected (radiobuttons), from
+        paint_theme_sample() (theme is changed or load_cfg is called), and
+        from set_highlight_target() (target tag is changed or load_cfg called).
+
+        Button delete_custom invokes delete_custom() to delete
+        a custom theme from idleConf.userCfg['highlight'] and changes.
+        Button save_custom invokes save_as_new_theme() which calls
+        get_new_theme_name() and create_new() to save a custom theme
+        and its colors to idleConf.userCfg['highlight'].
+
+        Radiobuttons fg_on and bg_on toggle var fg_bg_toggle to control
+        if the current selected color for a tag is for the foreground or
+        background.
+
+        DynOptionMenu targetlist contains a readable description of the
+        tags applied to Python source within IDLE.  Selecting one of the
+        tags from this list populates highlight_target, which has a callback
+        function set_highlight_target().
+
+        Text widget highlight_sample displays a block of text (which is
+        mock Python code) in which is embedded the defined tags and reflects
+        the color attributes of the current theme and changes for those tags.
+        Mouse button 1 allows for selection of a tag and updates
+        highlight_target with that tag value.
+
+        Note: The font in highlight_sample is set through the config in
+        the fonts tab.
+
+        In other words, a tag can be selected either from targetlist or
+        by clicking on the sample text within highlight_sample.  The
+        plane (foreground/background) is selected via the radiobutton.
+        Together, these two (tag and plane) control what color is
+        shown in set_color_sample() for the current theme.  Button set_color
+        invokes get_color() which displays a ColorChooser to change the
+        color for the selected tag/plane.  If a new color is picked,
+        it will be saved to changes and the highlight_sample and
+        frame background will be updated.
+
+        Tk Variables:
+            color: Color of selected target.
+            builtin_name: Menu variable for built-in theme.
+            custom_name: Menu variable for custom theme.
+            fg_bg_toggle: Toggle for foreground/background color.
+                Note: this has no callback.
+            theme_source: Selector for built-in or custom theme.
+            highlight_target: Menu variable for the highlight tag target.
+
+        Instance Data Attributes:
+            theme_elements: Dictionary of tags for text highlighting.
+                The key is the display name and the value is a tuple of
+                (tag name, display sort order).
+
+        Methods [attachment]:
+            load_theme_cfg: Load current highlight colors.
+            get_color: Invoke colorchooser [button_set_color].
+            set_color_sample_binding: Call set_color_sample [fg_bg_toggle].
+            set_highlight_target: set fg_bg_toggle, set_color_sample().
+            set_color_sample: Set frame background to target.
+            on_new_color_set: Set new color and add option.
+            paint_theme_sample: Recolor sample.
+            get_new_theme_name: Get from popup.
+            create_new: Combine theme with changes and save.
+            save_as_new_theme: Save [button_save_custom].
+            set_theme_type: Command for [theme_source].
+            delete_custom: Activate default [button_delete_custom].
+            save_new: Save to userCfg['theme'] (is function).
+
+        Widgets of highlights page frame:  (*) widgets bound to self
+            frame_custom: LabelFrame
+                (*)highlight_sample: Text
+                (*)frame_color_set: Frame
+                    (*)button_set_color: Button
+                    (*)targetlist: DynOptionMenu - highlight_target
+                frame_fg_bg_toggle: Frame
+                    (*)fg_on: Radiobutton - fg_bg_toggle
+                    (*)bg_on: Radiobutton - fg_bg_toggle
+                (*)button_save_custom: Button
+            frame_theme: LabelFrame
+                theme_type_title: Label
+                (*)builtin_theme_on: Radiobutton - theme_source
+                (*)custom_theme_on: Radiobutton - theme_source
+                (*)builtinlist: DynOptionMenu - builtin_name
+                (*)customlist: DynOptionMenu - custom_name
+                (*)button_delete_custom: Button
+                (*)theme_message: Label
+        """
+        self.theme_elements = {
+            'Normal Code or Text': ('normal', '00'),
+            'Code Context': ('context', '01'),
+            'Python Keywords': ('keyword', '02'),
+            'Python Definitions': ('definition', '03'),
+            'Python Builtins': ('builtin', '04'),
+            'Python Comments': ('comment', '05'),
+            'Python Strings': ('string', '06'),
+            'Selected Text': ('hilite', '07'),
+            'Found Text': ('hit', '08'),
+            'Cursor': ('cursor', '09'),
+            'Editor Breakpoint': ('break', '10'),
+            'Shell Prompt': ('console', '11'),
+            'Error Text': ('error', '12'),
+            'Shell User Output': ('stdout', '13'),
+            'Shell User Exception': ('stderr', '14'),
+            'Line Number': ('linenumber', '16'),
+            }
+        self.builtin_name = tracers.add(
+                StringVar(self), self.var_changed_builtin_name)
+        self.custom_name = tracers.add(
+                StringVar(self), self.var_changed_custom_name)
+        self.fg_bg_toggle = BooleanVar(self)
+        self.color = tracers.add(
+                StringVar(self), self.var_changed_color)
+        self.theme_source = tracers.add(
+                BooleanVar(self), self.var_changed_theme_source)
+        self.highlight_target = tracers.add(
+                StringVar(self), self.var_changed_highlight_target)
+
+        # Create widgets:
+        # body frame and section frames.
+        frame_custom = LabelFrame(self, borderwidth=2, relief=GROOVE,
+                                  text=' Custom Highlighting ')
+        frame_theme = LabelFrame(self, borderwidth=2, relief=GROOVE,
+                                 text=' Highlighting Theme ')
+        # frame_custom.
+        sample_frame = ScrollableTextFrame(
+                frame_custom, relief=SOLID, borderwidth=1)
+        text = self.highlight_sample = sample_frame.text
+        text.configure(
+                font=('courier', 12, ''), cursor='hand2', width=1, height=1,
+                takefocus=FALSE, highlightthickness=0, wrap=NONE)
+        text.bind('<Double-Button-1>', lambda e: 'break')
+        text.bind('<B1-Motion>', lambda e: 'break')
+        string_tags=(
+            ('# Click selects item.', 'comment'), ('\n', 'normal'),
+            ('code context section', 'context'), ('\n', 'normal'),
+            ('| cursor', 'cursor'), ('\n', 'normal'),
+            ('def', 'keyword'), (' ', 'normal'),
+            ('func', 'definition'), ('(param):\n  ', 'normal'),
+            ('"Return None."', 'string'), ('\n  var0 = ', 'normal'),
+            ("'string'", 'string'), ('\n  var1 = ', 'normal'),
+            ("'selected'", 'hilite'), ('\n  var2 = ', 'normal'),
+            ("'found'", 'hit'), ('\n  var3 = ', 'normal'),
+            ('list', 'builtin'), ('(', 'normal'),
+            ('None', 'keyword'), (')\n', 'normal'),
+            ('  breakpoint("line")', 'break'), ('\n\n', 'normal'),
+            ('>>>', 'console'), (' 3.14**2\n', 'normal'),
+            ('9.8596', 'stdout'), ('\n', 'normal'),
+            ('>>>', 'console'), (' pri ', 'normal'),
+            ('n', 'error'), ('t(\n', 'normal'),
+            ('SyntaxError', 'stderr'), ('\n', 'normal'))
+        for string, tag in string_tags:
+            text.insert(END, string, tag)
+        n_lines = len(text.get('1.0', END).splitlines())
+        for lineno in range(1, n_lines):
+            text.insert(f'{lineno}.0',
+                        f'{lineno:{len(str(n_lines))}d} ',
+                        'linenumber')
+        for element in self.theme_elements:
+            def tem(event, elem=element):
+                # event.widget.winfo_top_level().highlight_target.set(elem)
+                self.highlight_target.set(elem)
+            text.tag_bind(
+                    self.theme_elements[element][0], '<ButtonPress-1>', tem)
+        text['state'] = 'disabled'
+        self.style.configure('frame_color_set.TFrame', borderwidth=1,
+                             relief='solid')
+        self.frame_color_set = Frame(frame_custom, style='frame_color_set.TFrame')
+        frame_fg_bg_toggle = Frame(frame_custom)
+        self.button_set_color = Button(
+                self.frame_color_set, text='Choose Color for :',
+                command=self.get_color)
+        self.targetlist = DynOptionMenu(
+                self.frame_color_set, self.highlight_target, None,
+                highlightthickness=0) #, command=self.set_highlight_targetBinding
+        self.fg_on = Radiobutton(
+                frame_fg_bg_toggle, variable=self.fg_bg_toggle, value=1,
+                text='Foreground', command=self.set_color_sample_binding)
+        self.bg_on = Radiobutton(
+                frame_fg_bg_toggle, variable=self.fg_bg_toggle, value=0,
+                text='Background', command=self.set_color_sample_binding)
+        self.fg_bg_toggle.set(1)
+        self.button_save_custom = Button(
+                frame_custom, text='Save as New Custom Theme',
+                command=self.save_as_new_theme)
+        # frame_theme.
+        theme_type_title = Label(frame_theme, text='Select : ')
+        self.builtin_theme_on = Radiobutton(
+                frame_theme, variable=self.theme_source, value=1,
+                command=self.set_theme_type, text='a Built-in Theme')
+        self.custom_theme_on = Radiobutton(
+                frame_theme, variable=self.theme_source, value=0,
+                command=self.set_theme_type, text='a Custom Theme')
+        self.builtinlist = DynOptionMenu(
+                frame_theme, self.builtin_name, None, command=None)
+        self.customlist = DynOptionMenu(
+                frame_theme, self.custom_name, None, command=None)
+        self.button_delete_custom = Button(
+                frame_theme, text='Delete Custom Theme',
+                command=self.delete_custom)
+        self.theme_message = Label(frame_theme, borderwidth=2)
+        # Pack widgets:
+        # body.
+        frame_custom.pack(side=LEFT, padx=5, pady=5, expand=TRUE, fill=BOTH)
+        frame_theme.pack(side=TOP, padx=5, pady=5, fill=X)
+        # frame_custom.
+        self.frame_color_set.pack(side=TOP, padx=5, pady=5, fill=X)
+        frame_fg_bg_toggle.pack(side=TOP, padx=5, pady=0)
+        sample_frame.pack(
+                side=TOP, padx=5, pady=5, expand=TRUE, fill=BOTH)
+        self.button_set_color.pack(side=TOP, expand=TRUE, fill=X, padx=8, pady=4)
+        self.targetlist.pack(side=TOP, expand=TRUE, fill=X, padx=8, pady=3)
+        self.fg_on.pack(side=LEFT, anchor=E)
+        self.bg_on.pack(side=RIGHT, anchor=W)
+        self.button_save_custom.pack(side=BOTTOM, fill=X, padx=5, pady=5)
+        # frame_theme.
+        theme_type_title.pack(side=TOP, anchor=W, padx=5, pady=5)
+        self.builtin_theme_on.pack(side=TOP, anchor=W, padx=5)
+        self.custom_theme_on.pack(side=TOP, anchor=W, padx=5, pady=2)
+        self.builtinlist.pack(side=TOP, fill=X, padx=5, pady=5)
+        self.customlist.pack(side=TOP, fill=X, anchor=W, padx=5, pady=5)
+        self.button_delete_custom.pack(side=TOP, fill=X, padx=5, pady=5)
+        self.theme_message.pack(side=TOP, fill=X, pady=5)
+
+    def load_theme_cfg(self):
+        """Load current configuration settings for the theme options.
+
+        Based on the theme_source toggle, the theme is set as
+        either builtin or custom and the initial widget values
+        reflect the current settings from idleConf.
+
+        Attributes updated:
+            theme_source: Set from idleConf.
+            builtinlist: List of default themes from idleConf.
+            customlist: List of custom themes from idleConf.
+            custom_theme_on: Disabled if there are no custom themes.
+            custom_theme: Message with additional information.
+            targetlist: Create menu from self.theme_elements.
+
+        Methods:
+            set_theme_type
+            paint_theme_sample
+            set_highlight_target
+        """
+        # Set current theme type radiobutton.
+        self.theme_source.set(idleConf.GetOption(
+                'main', 'Theme', 'default', type='bool', default=1))
+        # Set current theme.
+        current_option = idleConf.CurrentTheme()
+        # Load available theme option menus.
+        if self.theme_source.get():  # Default theme selected.
+            item_list = idleConf.GetSectionList('default', 'highlight')
+            item_list.sort()
+            self.builtinlist.SetMenu(item_list, current_option)
+            item_list = idleConf.GetSectionList('user', 'highlight')
+            item_list.sort()
+            if not item_list:
+                self.custom_theme_on.state(('disabled',))
+                self.custom_name.set('- no custom themes -')
+            else:
+                self.customlist.SetMenu(item_list, item_list[0])
+        else:  # User theme selected.
+            item_list = idleConf.GetSectionList('user', 'highlight')
+            item_list.sort()
+            self.customlist.SetMenu(item_list, current_option)
+            item_list = idleConf.GetSectionList('default', 'highlight')
+            item_list.sort()
+            self.builtinlist.SetMenu(item_list, item_list[0])
+        self.set_theme_type()
+        # Load theme element option menu.
+        theme_names = list(self.theme_elements.keys())
+        theme_names.sort(key=lambda x: self.theme_elements[x][1])
+        self.targetlist.SetMenu(theme_names, theme_names[0])
+        self.paint_theme_sample()
+        self.set_highlight_target()
+
+    def var_changed_builtin_name(self, *params):
+        """Process new builtin theme selection.
+
+        Add the changed theme's name to the changed_items and recreate
+        the sample with the values from the selected theme.
+        """
+        old_themes = ('IDLE Classic', 'IDLE New')
+        value = self.builtin_name.get()
+        if value not in old_themes:
+            if idleConf.GetOption('main', 'Theme', 'name') not in old_themes:
+                changes.add_option('main', 'Theme', 'name', old_themes[0])
+            changes.add_option('main', 'Theme', 'name2', value)
+            self.theme_message['text'] = 'New theme, see Help'
+        else:
+            changes.add_option('main', 'Theme', 'name', value)
+            changes.add_option('main', 'Theme', 'name2', '')
+            self.theme_message['text'] = ''
+        self.paint_theme_sample()
+
+    def var_changed_custom_name(self, *params):
+        """Process new custom theme selection.
+
+        If a new custom theme is selected, add the name to the
+        changed_items and apply the theme to the sample.
+        """
+        value = self.custom_name.get()
+        if value != '- no custom themes -':
+            changes.add_option('main', 'Theme', 'name', value)
+            self.paint_theme_sample()
+
+    def var_changed_theme_source(self, *params):
+        """Process toggle between builtin and custom theme.
+
+        Update the default toggle value and apply the newly
+        selected theme type.
+        """
+        value = self.theme_source.get()
+        changes.add_option('main', 'Theme', 'default', value)
+        if value:
+            self.var_changed_builtin_name()
+        else:
+            self.var_changed_custom_name()
+
+    def var_changed_color(self, *params):
+        "Process change to color choice."
+        self.on_new_color_set()
+
+    def var_changed_highlight_target(self, *params):
+        "Process selection of new target tag for highlighting."
+        self.set_highlight_target()
+
+    def set_theme_type(self):
+        """Set available screen options based on builtin or custom theme.
+
+        Attributes accessed:
+            theme_source
+
+        Attributes updated:
+            builtinlist
+            customlist
+            button_delete_custom
+            custom_theme_on
+
+        Called from:
+            handler for builtin_theme_on and custom_theme_on
+            delete_custom
+            create_new
+            load_theme_cfg
+        """
+        if self.theme_source.get():
+            self.builtinlist['state'] = 'normal'
+            self.customlist['state'] = 'disabled'
+            self.button_delete_custom.state(('disabled',))
+        else:
+            self.builtinlist['state'] = 'disabled'
+            self.custom_theme_on.state(('!disabled',))
+            self.customlist['state'] = 'normal'
+            self.button_delete_custom.state(('!disabled',))
+
+    def get_color(self):
+        """Handle button to select a new color for the target tag.
+
+        If a new color is selected while using a builtin theme, a
+        name must be supplied to create a custom theme.
+
+        Attributes accessed:
+            highlight_target
+            frame_color_set
+            theme_source
+
+        Attributes updated:
+            color
+
+        Methods:
+            get_new_theme_name
+            create_new
+        """
+        target = self.highlight_target.get()
+        prev_color = self.style.lookup(self.frame_color_set['style'],
+                                       'background')
+        rgbTuplet, color_string = tkColorChooser.askcolor(
+                parent=self, title='Pick new color for : '+target,
+                initialcolor=prev_color)
+        if color_string and (color_string != prev_color):
+            # User didn't cancel and they chose a new color.
+            if self.theme_source.get():  # Current theme is a built-in.
+                message = ('Your changes will be saved as a new Custom Theme. '
+                           'Enter a name for your new Custom Theme below.')
+                new_theme = self.get_new_theme_name(message)
+                if not new_theme:  # User cancelled custom theme creation.
+                    return
+                else:  # Create new custom theme based on previously active theme.
+                    self.create_new(new_theme)
+                    self.color.set(color_string)
+            else:  # Current theme is user defined.
+                self.color.set(color_string)
+
+    def on_new_color_set(self):
+        "Display sample of new color selection on the dialog."
+        new_color = self.color.get()
+        self.style.configure('frame_color_set.TFrame', background=new_color)
+        plane = 'foreground' if self.fg_bg_toggle.get() else 'background'
+        sample_element = self.theme_elements[self.highlight_target.get()][0]
+        self.highlight_sample.tag_config(sample_element, **{plane: new_color})
+        theme = self.custom_name.get()
+        theme_element = sample_element + '-' + plane
+        changes.add_option('highlight', theme, theme_element, new_color)
+
+    def get_new_theme_name(self, message):
+        "Return name of new theme from query popup."
+        used_names = (idleConf.GetSectionList('user', 'highlight') +
+                idleConf.GetSectionList('default', 'highlight'))
+        new_theme = SectionName(
+                self, 'New Custom Theme', message, used_names).result
+        return new_theme
+
+    def save_as_new_theme(self):
+        """Prompt for new theme name and create the theme.
+
+        Methods:
+            get_new_theme_name
+            create_new
+        """
+        new_theme_name = self.get_new_theme_name('New Theme Name:')
+        if new_theme_name:
+            self.create_new(new_theme_name)
+
+    def create_new(self, new_theme_name):
+        """Create a new custom theme with the given name.
+
+        Create the new theme based on the previously active theme
+        with the current changes applied.  Once it is saved, then
+        activate the new theme.
+
+        Attributes accessed:
+            builtin_name
+            custom_name
+
+        Attributes updated:
+            customlist
+            theme_source
+
+        Method:
+            save_new
+            set_theme_type
+        """
+        if self.theme_source.get():
+            theme_type = 'default'
+            theme_name = self.builtin_name.get()
+        else:
+            theme_type = 'user'
+            theme_name = self.custom_name.get()
+        new_theme = idleConf.GetThemeDict(theme_type, theme_name)
+        # Apply any of the old theme's unsaved changes to the new theme.
+        if theme_name in changes['highlight']:
+            theme_changes = changes['highlight'][theme_name]
+            for element in theme_changes:
+                new_theme[element] = theme_changes[element]
+        # Save the new theme.
+        self.save_new(new_theme_name, new_theme)
+        # Change GUI over to the new theme.
+        custom_theme_list = idleConf.GetSectionList('user', 'highlight')
+        custom_theme_list.sort()
+        self.customlist.SetMenu(custom_theme_list, new_theme_name)
+        self.theme_source.set(0)
+        self.set_theme_type()
+
+    def set_highlight_target(self):
+        """Set fg/bg toggle and color based on highlight tag target.
+
+        Instance variables accessed:
+            highlight_target
+
+        Attributes updated:
+            fg_on
+            bg_on
+            fg_bg_toggle
+
+        Methods:
+            set_color_sample
+
+        Called from:
+            var_changed_highlight_target
+            load_theme_cfg
+        """
+        if self.highlight_target.get() == 'Cursor':  # bg not possible
+            self.fg_on.state(('disabled',))
+            self.bg_on.state(('disabled',))
+            self.fg_bg_toggle.set(1)
+        else:  # Both fg and bg can be set.
+            self.fg_on.state(('!disabled',))
+            self.bg_on.state(('!disabled',))
+            self.fg_bg_toggle.set(1)
+        self.set_color_sample()
+
+    def set_color_sample_binding(self, *args):
+        """Change color sample based on foreground/background toggle.
+
+        Methods:
+            set_color_sample
+        """
+        self.set_color_sample()
+
+    def set_color_sample(self):
+        """Set the color of the frame background to reflect the selected target.
+
+        Instance variables accessed:
+            theme_elements
+            highlight_target
+            fg_bg_toggle
+            highlight_sample
+
+        Attributes updated:
+            frame_color_set
+        """
+        # Set the color sample area.
+        tag = self.theme_elements[self.highlight_target.get()][0]
+        plane = 'foreground' if self.fg_bg_toggle.get() else 'background'
+        color = self.highlight_sample.tag_cget(tag, plane)
+        self.style.configure('frame_color_set.TFrame', background=color)
+
+    def paint_theme_sample(self):
+        """Apply the theme colors to each element tag in the sample text.
+
+        Instance attributes accessed:
+            theme_elements
+            theme_source
+            builtin_name
+            custom_name
+
+        Attributes updated:
+            highlight_sample: Set the tag elements to the theme.
+
+        Methods:
+            set_color_sample
+
+        Called from:
+            var_changed_builtin_name
+            var_changed_custom_name
+            load_theme_cfg
+        """
+        if self.theme_source.get():  # Default theme
+            theme = self.builtin_name.get()
+        else:  # User theme
+            theme = self.custom_name.get()
+        for element_title in self.theme_elements:
+            element = self.theme_elements[element_title][0]
+            colors = idleConf.GetHighlight(theme, element)
+            if element == 'cursor':  # Cursor sample needs special painting.
+                colors['background'] = idleConf.GetHighlight(
+                        theme, 'normal')['background']
+            # Handle any unsaved changes to this theme.
+            if theme in changes['highlight']:
+                theme_dict = changes['highlight'][theme]
+                if element + '-foreground' in theme_dict:
+                    colors['foreground'] = theme_dict[element + '-foreground']
+                if element + '-background' in theme_dict:
+                    colors['background'] = theme_dict[element + '-background']
+            self.highlight_sample.tag_config(element, **colors)
+        self.set_color_sample()
+
+    def save_new(self, theme_name, theme):
+        """Save a newly created theme to idleConf.
+
+        theme_name - string, the name of the new theme
+        theme - dictionary containing the new theme
+        """
+        if not idleConf.userCfg['highlight'].has_section(theme_name):
+            idleConf.userCfg['highlight'].add_section(theme_name)
+        for element in theme:
+            value = theme[element]
+            idleConf.userCfg['highlight'].SetOption(theme_name, element, value)
+
+    def askyesno(self, *args, **kwargs):
+        # Make testing easier.  Could change implementation.
+        return messagebox.askyesno(*args, **kwargs)
+
+    def delete_custom(self):
+        """Handle event to delete custom theme.
+
+        The current theme is deactivated and the default theme is
+        activated.  The custom theme is permanently removed from
+        the config file.
+
+        Attributes accessed:
+            custom_name
+
+        Attributes updated:
+            custom_theme_on
+            customlist
+            theme_source
+            builtin_name
+
+        Methods:
+            deactivate_current_config
+            save_all_changed_extensions
+            activate_config_changes
+            set_theme_type
+        """
+        theme_name = self.custom_name.get()
+        delmsg = 'Are you sure you wish to delete the theme %r ?'
+        if not self.askyesno(
+                'Delete Theme',  delmsg % theme_name, parent=self):
+            return
+        self.cd.deactivate_current_config()
+        # Remove theme from changes, config, and file.
+        changes.delete_section('highlight', theme_name)
+        # Reload user theme list.
+        item_list = idleConf.GetSectionList('user', 'highlight')
+        item_list.sort()
+        if not item_list:
+            self.custom_theme_on.state(('disabled',))
+            self.customlist.SetMenu(item_list, '- no custom themes -')
+        else:
+            self.customlist.SetMenu(item_list, item_list[0])
+        # Revert to default theme.
+        self.theme_source.set(idleConf.defaultCfg['main'].Get('Theme', 'default'))
+        self.builtin_name.set(idleConf.defaultCfg['main'].Get('Theme', 'name'))
+        # User can't back out of these changes, they must be applied now.
+        changes.save_all()
+        self.cd.save_all_changed_extensions()
+        self.cd.activate_config_changes()
+        self.set_theme_type()
+
+
+class KeysPage(Frame):
+
+    def __init__(self, master):
+        super().__init__(master)
+        self.cd = master.master
+        self.create_page_keys()
+        self.load_key_cfg()
+
+    def create_page_keys(self):
+        """Return frame of widgets for Keys tab.
+
+        Enable users to provisionally change both individual and sets of
+        keybindings (shortcut keys). Except for features implemented as
+        extensions, keybindings are stored in complete sets called
+        keysets. Built-in keysets in idlelib/config-keys.def are fixed
+        as far as the dialog is concerned. Any keyset can be used as the
+        base for a new custom keyset, stored in .idlerc/config-keys.cfg.
+
+        Function load_key_cfg() initializes tk variables and keyset
+        lists and calls load_keys_list for the current keyset.
+        Radiobuttons builtin_keyset_on and custom_keyset_on toggle var
+        keyset_source, which controls if the current set of keybindings
+        are from a builtin or custom keyset. DynOptionMenus builtinlist
+        and customlist contain lists of the builtin and custom keysets,
+        respectively, and the current item from each list is stored in
+        vars builtin_name and custom_name.
+
+        Button delete_custom_keys invokes delete_custom_keys() to delete
+        a custom keyset from idleConf.userCfg['keys'] and changes.  Button
+        save_custom_keys invokes save_as_new_key_set() which calls
+        get_new_keys_name() and create_new_key_set() to save a custom keyset
+        and its keybindings to idleConf.userCfg['keys'].
+
+        Listbox bindingslist contains all of the keybindings for the
+        selected keyset.  The keybindings are loaded in load_keys_list()
+        and are pairs of (event, [keys]) where keys can be a list
+        of one or more key combinations to bind to the same event.
+        Mouse button 1 click invokes on_bindingslist_select(), which
+        allows button_new_keys to be clicked.
+
+        So, an item is selected in listbindings, which activates
+        button_new_keys, and clicking button_new_keys calls function
+        get_new_keys().  Function get_new_keys() gets the key mappings from the
+        current keyset for the binding event item that was selected.  The
+        function then displays another dialog, GetKeysDialog, with the
+        selected binding event and current keys and allows new key sequences
+        to be entered for that binding event.  If the keys aren't
+        changed, nothing happens.  If the keys are changed and the keyset
+        is a builtin, function get_new_keys_name() will be called
+        for input of a custom keyset name.  If no name is given, then the
+        change to the keybinding will abort and no updates will be made.  If
+        a custom name is entered in the prompt or if the current keyset was
+        already custom (and thus didn't require a prompt), then
+        idleConf.userCfg['keys'] is updated in function create_new_key_set()
+        with the change to the event binding.  The item listing in bindingslist
+        is updated with the new keys.  Var keybinding is also set which invokes
+        the callback function, var_changed_keybinding, to add the change to
+        the 'keys' or 'extensions' changes tracker based on the binding type.
+
+        Tk Variables:
+            keybinding: Action/key bindings.
+
+        Methods:
+            load_keys_list: Reload active set.
+            create_new_key_set: Combine active keyset and changes.
+            set_keys_type: Command for keyset_source.
+            save_new_key_set: Save to idleConf.userCfg['keys'] (is function).
+            deactivate_current_config: Remove keys bindings in editors.
+
+        Widgets for KeysPage(frame):  (*) widgets bound to self
+            frame_key_sets: LabelFrame
+                frames[0]: Frame
+                    (*)builtin_keyset_on: Radiobutton - var keyset_source
+                    (*)custom_keyset_on: Radiobutton - var keyset_source
+                    (*)builtinlist: DynOptionMenu - var builtin_name,
+                            func keybinding_selected
+                    (*)customlist: DynOptionMenu - var custom_name,
+                            func keybinding_selected
+                    (*)keys_message: Label
+                frames[1]: Frame
+                    (*)button_delete_custom_keys: Button - delete_custom_keys
+                    (*)button_save_custom_keys: Button -  save_as_new_key_set
+            frame_custom: LabelFrame
+                frame_target: Frame
+                    target_title: Label
+                    scroll_target_y: Scrollbar
+                    scroll_target_x: Scrollbar
+                    (*)bindingslist: ListBox - on_bindingslist_select
+                    (*)button_new_keys: Button - get_new_keys & ..._name
+        """
+        self.builtin_name = tracers.add(
+                StringVar(self), self.var_changed_builtin_name)
+        self.custom_name = tracers.add(
+                StringVar(self), self.var_changed_custom_name)
+        self.keyset_source = tracers.add(
+                BooleanVar(self), self.var_changed_keyset_source)
+        self.keybinding = tracers.add(
+                StringVar(self), self.var_changed_keybinding)
+
+        # Create widgets:
+        # body and section frames.
+        frame_custom = LabelFrame(
+                self, borderwidth=2, relief=GROOVE,
+                text=' Custom Key Bindings ')
+        frame_key_sets = LabelFrame(
+                self, borderwidth=2, relief=GROOVE, text=' Key Set ')
+        # frame_custom.
+        frame_target = Frame(frame_custom)
+        target_title = Label(frame_target, text='Action - Key(s)')
+        scroll_target_y = Scrollbar(frame_target)
+        scroll_target_x = Scrollbar(frame_target, orient=HORIZONTAL)
+        self.bindingslist = Listbox(
+                frame_target, takefocus=FALSE, exportselection=FALSE)
+        self.bindingslist.bind('<ButtonRelease-1>',
+                               self.on_bindingslist_select)
+        scroll_target_y['command'] = self.bindingslist.yview
+        scroll_target_x['command'] = self.bindingslist.xview
+        self.bindingslist['yscrollcommand'] = scroll_target_y.set
+        self.bindingslist['xscrollcommand'] = scroll_target_x.set
+        self.button_new_keys = Button(
+                frame_custom, text='Get New Keys for Selection',
+                command=self.get_new_keys, state='disabled')
+        # frame_key_sets.
+        frames = [Frame(frame_key_sets, padding=2, borderwidth=0)
+                  for i in range(2)]
+        self.builtin_keyset_on = Radiobutton(
+                frames[0], variable=self.keyset_source, value=1,
+                command=self.set_keys_type, text='Use a Built-in Key Set')
+        self.custom_keyset_on = Radiobutton(
+                frames[0], variable=self.keyset_source, value=0,
+                command=self.set_keys_type, text='Use a Custom Key Set')
+        self.builtinlist = DynOptionMenu(
+                frames[0], self.builtin_name, None, command=None)
+        self.customlist = DynOptionMenu(
+                frames[0], self.custom_name, None, command=None)
+        self.button_delete_custom_keys = Button(
+                frames[1], text='Delete Custom Key Set',
+                command=self.delete_custom_keys)
+        self.button_save_custom_keys = Button(
+                frames[1], text='Save as New Custom Key Set',
+                command=self.save_as_new_key_set)
+        self.keys_message = Label(frames[0], borderwidth=2)
+
+        # Pack widgets:
+        # body.
+        frame_custom.pack(side=BOTTOM, padx=5, pady=5, expand=TRUE, fill=BOTH)
+        frame_key_sets.pack(side=BOTTOM, padx=5, pady=5, fill=BOTH)
+        # frame_custom.
+        self.button_new_keys.pack(side=BOTTOM, fill=X, padx=5, pady=5)
+        frame_target.pack(side=LEFT, padx=5, pady=5, expand=TRUE, fill=BOTH)
+        # frame_target.
+        frame_target.columnconfigure(0, weight=1)
+        frame_target.rowconfigure(1, weight=1)
+        target_title.grid(row=0, column=0, columnspan=2, sticky=W)
+        self.bindingslist.grid(row=1, column=0, sticky=NSEW)
+        scroll_target_y.grid(row=1, column=1, sticky=NS)
+        scroll_target_x.grid(row=2, column=0, sticky=EW)
+        # frame_key_sets.
+        self.builtin_keyset_on.grid(row=0, column=0, sticky=W+NS)
+        self.custom_keyset_on.grid(row=1, column=0, sticky=W+NS)
+        self.builtinlist.grid(row=0, column=1, sticky=NSEW)
+        self.customlist.grid(row=1, column=1, sticky=NSEW)
+        self.keys_message.grid(row=0, column=2, sticky=NSEW, padx=5, pady=5)
+        self.button_delete_custom_keys.pack(side=LEFT, fill=X, expand=True, padx=2)
+        self.button_save_custom_keys.pack(side=LEFT, fill=X, expand=True, padx=2)
+        frames[0].pack(side=TOP, fill=BOTH, expand=True)
+        frames[1].pack(side=TOP, fill=X, expand=True, pady=2)
+
+    def load_key_cfg(self):
+        "Load current configuration settings for the keybinding options."
+        # Set current keys type radiobutton.
+        self.keyset_source.set(idleConf.GetOption(
+                'main', 'Keys', 'default', type='bool', default=1))
+        # Set current keys.
+        current_option = idleConf.CurrentKeys()
+        # Load available keyset option menus.
+        if self.keyset_source.get():  # Default theme selected.
+            item_list = idleConf.GetSectionList('default', 'keys')
+            item_list.sort()
+            self.builtinlist.SetMenu(item_list, current_option)
+            item_list = idleConf.GetSectionList('user', 'keys')
+            item_list.sort()
+            if not item_list:
+                self.custom_keyset_on.state(('disabled',))
+                self.custom_name.set('- no custom keys -')
+            else:
+                self.customlist.SetMenu(item_list, item_list[0])
+        else:  # User key set selected.
+            item_list = idleConf.GetSectionList('user', 'keys')
+            item_list.sort()
+            self.customlist.SetMenu(item_list, current_option)
+            item_list = idleConf.GetSectionList('default', 'keys')
+            item_list.sort()
+            self.builtinlist.SetMenu(item_list, idleConf.default_keys())
+        self.set_keys_type()
+        # Load keyset element list.
+        keyset_name = idleConf.CurrentKeys()
+        self.load_keys_list(keyset_name)
+
+    def var_changed_builtin_name(self, *params):
+        "Process selection of builtin key set."
+        old_keys = (
+            'IDLE Classic Windows',
+            'IDLE Classic Unix',
+            'IDLE Classic Mac',
+            'IDLE Classic OSX',
+        )
+        value = self.builtin_name.get()
+        if value not in old_keys:
+            if idleConf.GetOption('main', 'Keys', 'name') not in old_keys:
+                changes.add_option('main', 'Keys', 'name', old_keys[0])
+            changes.add_option('main', 'Keys', 'name2', value)
+            self.keys_message['text'] = 'New key set, see Help'
+        else:
+            changes.add_option('main', 'Keys', 'name', value)
+            changes.add_option('main', 'Keys', 'name2', '')
+            self.keys_message['text'] = ''
+        self.load_keys_list(value)
+
+    def var_changed_custom_name(self, *params):
+        "Process selection of custom key set."
+        value = self.custom_name.get()
+        if value != '- no custom keys -':
+            changes.add_option('main', 'Keys', 'name', value)
+            self.load_keys_list(value)
+
+    def var_changed_keyset_source(self, *params):
+        "Process toggle between builtin key set and custom key set."
+        value = self.keyset_source.get()
+        changes.add_option('main', 'Keys', 'default', value)
+        if value:
+            self.var_changed_builtin_name()
+        else:
+            self.var_changed_custom_name()
+
+    def var_changed_keybinding(self, *params):
+        "Store change to a keybinding."
+        value = self.keybinding.get()
+        key_set = self.custom_name.get()
+        event = self.bindingslist.get(ANCHOR).split()[0]
+        if idleConf.IsCoreBinding(event):
+            changes.add_option('keys', key_set, event, value)
+        else:  # Event is an extension binding.
+            ext_name = idleConf.GetExtnNameForEvent(event)
+            ext_keybind_section = ext_name + '_cfgBindings'
+            changes.add_option('extensions', ext_keybind_section, event, value)
+
+    def set_keys_type(self):
+        "Set available screen options based on builtin or custom key set."
+        if self.keyset_source.get():
+            self.builtinlist['state'] = 'normal'
+            self.customlist['state'] = 'disabled'
+            self.button_delete_custom_keys.state(('disabled',))
+        else:
+            self.builtinlist['state'] = 'disabled'
+            self.custom_keyset_on.state(('!disabled',))
+            self.customlist['state'] = 'normal'
+            self.button_delete_custom_keys.state(('!disabled',))
+
+    def get_new_keys(self):
+        """Handle event to change key binding for selected line.
+
+        A selection of a key/binding in the list of current
+        bindings pops up a dialog to enter a new binding.  If
+        the current key set is builtin and a binding has
+        changed, then a name for a custom key set needs to be
+        entered for the change to be applied.
+        """
+        list_index = self.bindingslist.index(ANCHOR)
+        binding = self.bindingslist.get(list_index)
+        bind_name = binding.split()[0]
+        if self.keyset_source.get():
+            current_key_set_name = self.builtin_name.get()
+        else:
+            current_key_set_name = self.custom_name.get()
+        current_bindings = idleConf.GetCurrentKeySet()
+        if current_key_set_name in changes['keys']:  # unsaved changes
+            key_set_changes = changes['keys'][current_key_set_name]
+            for event in key_set_changes:
+                current_bindings[event] = key_set_changes[event].split()
+        current_key_sequences = list(current_bindings.values())
+        new_keys = GetKeysDialog(self, 'Get New Keys', bind_name,
+                current_key_sequences).result
+        if new_keys:
+            if self.keyset_source.get():  # Current key set is a built-in.
+                message = ('Your changes will be saved as a new Custom Key Set.'
+                           ' Enter a name for your new Custom Key Set below.')
+                new_keyset = self.get_new_keys_name(message)
+                if not new_keyset:  # User cancelled custom key set creation.
+                    self.bindingslist.select_set(list_index)
+                    self.bindingslist.select_anchor(list_index)
+                    return
+                else:  # Create new custom key set based on previously active key set.
+                    self.create_new_key_set(new_keyset)
+            self.bindingslist.delete(list_index)
+            self.bindingslist.insert(list_index, bind_name+' - '+new_keys)
+            self.bindingslist.select_set(list_index)
+            self.bindingslist.select_anchor(list_index)
+            self.keybinding.set(new_keys)
+        else:
+            self.bindingslist.select_set(list_index)
+            self.bindingslist.select_anchor(list_index)
+
+    def get_new_keys_name(self, message):
+        "Return new key set name from query popup."
+        used_names = (idleConf.GetSectionList('user', 'keys') +
+                idleConf.GetSectionList('default', 'keys'))
+        new_keyset = SectionName(
+                self, 'New Custom Key Set', message, used_names).result
+        return new_keyset
+
+    def save_as_new_key_set(self):
+        "Prompt for name of new key set and save changes using that name."
+        new_keys_name = self.get_new_keys_name('New Key Set Name:')
+        if new_keys_name:
+            self.create_new_key_set(new_keys_name)
+
+    def on_bindingslist_select(self, event):
+        "Activate button to assign new keys to selected action."
+        self.button_new_keys.state(('!disabled',))
+
+    def create_new_key_set(self, new_key_set_name):
+        """Create a new custom key set with the given name.
+
+        Copy the bindings/keys from the previously active keyset
+        to the new keyset and activate the new custom keyset.
+        """
+        if self.keyset_source.get():
+            prev_key_set_name = self.builtin_name.get()
+        else:
+            prev_key_set_name = self.custom_name.get()
+        prev_keys = idleConf.GetCoreKeys(prev_key_set_name)
+        new_keys = {}
+        for event in prev_keys:  # Add key set to changed items.
+            event_name = event[2:-2]  # Trim off the angle brackets.
+            binding = ' '.join(prev_keys[event])
+            new_keys[event_name] = binding
+        # Handle any unsaved changes to prev key set.
+        if prev_key_set_name in changes['keys']:
+            key_set_changes = changes['keys'][prev_key_set_name]
+            for event in key_set_changes:
+                new_keys[event] = key_set_changes[event]
+        # Save the new key set.
+        self.save_new_key_set(new_key_set_name, new_keys)
+        # Change GUI over to the new key set.
+        custom_key_list = idleConf.GetSectionList('user', 'keys')
+        custom_key_list.sort()
+        self.customlist.SetMenu(custom_key_list, new_key_set_name)
+        self.keyset_source.set(0)
+        self.set_keys_type()
+
+    def load_keys_list(self, keyset_name):
+        """Reload the list of action/key binding pairs for the active key set.
+
+        An action/key binding can be selected to change the key binding.
+        """
+        reselect = False
+        if self.bindingslist.curselection():
+            reselect = True
+            list_index = self.bindingslist.index(ANCHOR)
+        keyset = idleConf.GetKeySet(keyset_name)
+        bind_names = list(keyset.keys())
+        bind_names.sort()
+        self.bindingslist.delete(0, END)
+        for bind_name in bind_names:
+            key = ' '.join(keyset[bind_name])
+            bind_name = bind_name[2:-2]  # Trim off the angle brackets.
+            if keyset_name in changes['keys']:
+                # Handle any unsaved changes to this key set.
+                if bind_name in changes['keys'][keyset_name]:
+                    key = changes['keys'][keyset_name][bind_name]
+            self.bindingslist.insert(END, bind_name+' - '+key)
+        if reselect:
+            self.bindingslist.see(list_index)
+            self.bindingslist.select_set(list_index)
+            self.bindingslist.select_anchor(list_index)
+
+    @staticmethod
+    def save_new_key_set(keyset_name, keyset):
+        """Save a newly created core key set.
+
+        Add keyset to idleConf.userCfg['keys'], not to disk.
+        If the keyset doesn't exist, it is created.  The
+        binding/keys are taken from the keyset argument.
+
+        keyset_name - string, the name of the new key set
+        keyset - dictionary containing the new keybindings
+        """
+        if not idleConf.userCfg['keys'].has_section(keyset_name):
+            idleConf.userCfg['keys'].add_section(keyset_name)
+        for event in keyset:
+            value = keyset[event]
+            idleConf.userCfg['keys'].SetOption(keyset_name, event, value)
+
+    def askyesno(self, *args, **kwargs):
+        # Make testing easier.  Could change implementation.
+        return messagebox.askyesno(*args, **kwargs)
+
+    def delete_custom_keys(self):
+        """Handle event to delete a custom key set.
+
+        Applying the delete deactivates the current configuration and
+        reverts to the default.  The custom key set is permanently
+        deleted from the config file.
+        """
+        keyset_name = self.custom_name.get()
+        delmsg = 'Are you sure you wish to delete the key set %r ?'
+        if not self.askyesno(
+                'Delete Key Set',  delmsg % keyset_name, parent=self):
+            return
+        self.cd.deactivate_current_config()
+        # Remove key set from changes, config, and file.
+        changes.delete_section('keys', keyset_name)
+        # Reload user key set list.
+        item_list = idleConf.GetSectionList('user', 'keys')
+        item_list.sort()
+        if not item_list:
+            self.custom_keyset_on.state(('disabled',))
+            self.customlist.SetMenu(item_list, '- no custom keys -')
+        else:
+            self.customlist.SetMenu(item_list, item_list[0])
+        # Revert to default key set.
+        self.keyset_source.set(idleConf.defaultCfg['main']
+                               .Get('Keys', 'default'))
+        self.builtin_name.set(idleConf.defaultCfg['main'].Get('Keys', 'name')
+                              or idleConf.default_keys())
+        # User can't back out of these changes, they must be applied now.
+        changes.save_all()
+        self.cd.save_all_changed_extensions()
+        self.cd.activate_config_changes()
+        self.set_keys_type()
+
+
+class GenPage(Frame):
+
+    def __init__(self, master):
+        super().__init__(master)
+
+        self.init_validators()
+        self.create_page_general()
+        self.load_general_cfg()
+
+    def init_validators(self):
+        digits_or_empty_re = re.compile(r'[0-9]*')
+        def is_digits_or_empty(s):
+            "Return 's is blank or contains only digits'"
+            return digits_or_empty_re.fullmatch(s) is not None
+        self.digits_only = (self.register(is_digits_or_empty), '%P',)
+
+    def create_page_general(self):
+        """Return frame of widgets for General tab.
+
+        Enable users to provisionally change general options. Function
+        load_general_cfg initializes tk variables and helplist using
+        idleConf.  Radiobuttons startup_shell_on and startup_editor_on
+        set var startup_edit. Radiobuttons save_ask_on and save_auto_on
+        set var autosave. Entry boxes win_width_int and win_height_int
+        set var win_width and win_height.  Setting var_name invokes the
+        default callback that adds option to changes.
+
+        Helplist: load_general_cfg loads list user_helplist with
+        name, position pairs and copies names to listbox helplist.
+        Clicking a name invokes help_source selected. Clicking
+        button_helplist_name invokes helplist_item_name, which also
+        changes user_helplist.  These functions all call
+        set_add_delete_state. All but load call update_help_changes to
+        rewrite changes['main']['HelpFiles'].
+
+        Widgets for GenPage(Frame):  (*) widgets bound to self
+            frame_window: LabelFrame
+                frame_run: Frame
+                    startup_title: Label
+                    (*)startup_editor_on: Radiobutton - startup_edit
+                    (*)startup_shell_on: Radiobutton - startup_edit
+                frame_win_size: Frame
+                    win_size_title: Label
+                    win_width_title: Label
+                    (*)win_width_int: Entry - win_width
+                    win_height_title: Label
+                    (*)win_height_int: Entry - win_height
+                frame_cursor_blink: Frame
+                    cursor_blink_title: Label
+                    (*)cursor_blink_bool: Checkbutton - cursor_blink
+                frame_autocomplete: Frame
+                    auto_wait_title: Label
+                    (*)auto_wait_int: Entry - autocomplete_wait
+                frame_paren1: Frame
+                    paren_style_title: Label
+                    (*)paren_style_type: OptionMenu - paren_style
+                frame_paren2: Frame
+                    paren_time_title: Label
+                    (*)paren_flash_time: Entry - flash_delay
+                    (*)bell_on: Checkbutton - paren_bell
+            frame_editor: LabelFrame
+                frame_save: Frame
+                    run_save_title: Label
+                    (*)save_ask_on: Radiobutton - autosave
+                    (*)save_auto_on: Radiobutton - autosave
+                frame_format: Frame
+                    format_width_title: Label
+                    (*)format_width_int: Entry - format_width
+                frame_line_numbers_default: Frame
+                    line_numbers_default_title: Label
+                    (*)line_numbers_default_bool: Checkbutton - line_numbers_default
+                frame_context: Frame
+                    context_title: Label
+                    (*)context_int: Entry - context_lines
+            frame_shell: LabelFrame
+                frame_auto_squeeze_min_lines: Frame
+                    auto_squeeze_min_lines_title: Label
+                    (*)auto_squeeze_min_lines_int: Entry - auto_squeeze_min_lines
+            frame_help: LabelFrame
+                frame_helplist: Frame
+                    frame_helplist_buttons: Frame
+                        (*)button_helplist_edit
+                        (*)button_helplist_add
+                        (*)button_helplist_remove
+                    (*)helplist: ListBox
+                    scroll_helplist: Scrollbar
+        """
+        # Integer values need StringVar because int('') raises.
+        self.startup_edit = tracers.add(
+                IntVar(self), ('main', 'General', 'editor-on-startup'))
+        self.win_width = tracers.add(
+                StringVar(self), ('main', 'EditorWindow', 'width'))
+        self.win_height = tracers.add(
+                StringVar(self), ('main', 'EditorWindow', 'height'))
+        self.cursor_blink = tracers.add(
+                BooleanVar(self), ('main', 'EditorWindow', 'cursor-blink'))
+        self.autocomplete_wait = tracers.add(
+                StringVar(self), ('extensions', 'AutoComplete', 'popupwait'))
+        self.paren_style = tracers.add(
+                StringVar(self), ('extensions', 'ParenMatch', 'style'))
+        self.flash_delay = tracers.add(
+                StringVar(self), ('extensions', 'ParenMatch', 'flash-delay'))
+        self.paren_bell = tracers.add(
+                BooleanVar(self), ('extensions', 'ParenMatch', 'bell'))
+
+        self.auto_squeeze_min_lines = tracers.add(
+                StringVar(self), ('main', 'PyShell', 'auto-squeeze-min-lines'))
+
+        self.autosave = tracers.add(
+                IntVar(self), ('main', 'General', 'autosave'))
+        self.format_width = tracers.add(
+                StringVar(self), ('extensions', 'FormatParagraph', 'max-width'))
+        self.line_numbers_default = tracers.add(
+                BooleanVar(self),
+                ('main', 'EditorWindow', 'line-numbers-default'))
+        self.context_lines = tracers.add(
+                StringVar(self), ('extensions', 'CodeContext', 'maxlines'))
+
+        # Create widgets:
+        # Section frames.
+        frame_window = LabelFrame(self, borderwidth=2, relief=GROOVE,
+                                  text=' Window Preferences')
+        frame_editor = LabelFrame(self, borderwidth=2, relief=GROOVE,
+                                  text=' Editor Preferences')
+        frame_shell = LabelFrame(self, borderwidth=2, relief=GROOVE,
+                                 text=' Shell Preferences')
+        frame_help = LabelFrame(self, borderwidth=2, relief=GROOVE,
+                                text=' Additional Help Sources ')
+        # Frame_window.
+        frame_run = Frame(frame_window, borderwidth=0)
+        startup_title = Label(frame_run, text='At Startup')
+        self.startup_editor_on = Radiobutton(
+                frame_run, variable=self.startup_edit, value=1,
+                text="Open Edit Window")
+        self.startup_shell_on = Radiobutton(
+                frame_run, variable=self.startup_edit, value=0,
+                text='Open Shell Window')
+
+        frame_win_size = Frame(frame_window, borderwidth=0)
+        win_size_title = Label(
+                frame_win_size, text='Initial Window Size  (in characters)')
+        win_width_title = Label(frame_win_size, text='Width')
+        self.win_width_int = Entry(
+                frame_win_size, textvariable=self.win_width, width=3,
+                validatecommand=self.digits_only, validate='key',
+        )
+        win_height_title = Label(frame_win_size, text='Height')
+        self.win_height_int = Entry(
+                frame_win_size, textvariable=self.win_height, width=3,
+                validatecommand=self.digits_only, validate='key',
+        )
+
+        frame_cursor_blink = Frame(frame_window, borderwidth=0)
+        cursor_blink_title = Label(frame_cursor_blink, text='Cursor Blink')
+        self.cursor_blink_bool = Checkbutton(frame_cursor_blink,
+                variable=self.cursor_blink, width=1)
+
+        frame_autocomplete = Frame(frame_window, borderwidth=0,)
+        auto_wait_title = Label(frame_autocomplete,
+                               text='Completions Popup Wait (milliseconds)')
+        self.auto_wait_int = Entry(frame_autocomplete, width=6,
+                                   textvariable=self.autocomplete_wait,
+                                   validatecommand=self.digits_only,
+                                   validate='key',
+                                   )
+
+        frame_paren1 = Frame(frame_window, borderwidth=0)
+        paren_style_title = Label(frame_paren1, text='Paren Match Style')
+        self.paren_style_type = OptionMenu(
+                frame_paren1, self.paren_style, 'expression',
+                "opener","parens","expression")
+        frame_paren2 = Frame(frame_window, borderwidth=0)
+        paren_time_title = Label(
+                frame_paren2, text='Time Match Displayed (milliseconds)\n'
+                                  '(0 is until next input)')
+        self.paren_flash_time = Entry(
+                frame_paren2, textvariable=self.flash_delay, width=6)
+        self.bell_on = Checkbutton(
+                frame_paren2, text="Bell on Mismatch", variable=self.paren_bell)
+
+        # Frame_editor.
+        frame_save = Frame(frame_editor, borderwidth=0)
+        run_save_title = Label(frame_save, text='At Start of Run (F5)  ')
+        self.save_ask_on = Radiobutton(
+                frame_save, variable=self.autosave, value=0,
+                text="Prompt to Save")
+        self.save_auto_on = Radiobutton(
+                frame_save, variable=self.autosave, value=1,
+                text='No Prompt')
+
+        frame_format = Frame(frame_editor, borderwidth=0)
+        format_width_title = Label(frame_format,
+                                   text='Format Paragraph Max Width')
+        self.format_width_int = Entry(
+                frame_format, textvariable=self.format_width, width=4,
+                validatecommand=self.digits_only, validate='key',
+        )
+
+        frame_line_numbers_default = Frame(frame_editor, borderwidth=0)
+        line_numbers_default_title = Label(
+            frame_line_numbers_default, text='Show line numbers in new windows')
+        self.line_numbers_default_bool = Checkbutton(
+                frame_line_numbers_default,
+                variable=self.line_numbers_default,
+                width=1)
+
+        frame_context = Frame(frame_editor, borderwidth=0)
+        context_title = Label(frame_context, text='Max Context Lines :')
+        self.context_int = Entry(
+                frame_context, textvariable=self.context_lines, width=3,
+                validatecommand=self.digits_only, validate='key',
+        )
+
+        # Frame_shell.
+        frame_auto_squeeze_min_lines = Frame(frame_shell, borderwidth=0)
+        auto_squeeze_min_lines_title = Label(frame_auto_squeeze_min_lines,
+                                             text='Auto-Squeeze Min. Lines:')
+        self.auto_squeeze_min_lines_int = Entry(
+                frame_auto_squeeze_min_lines, width=4,
+                textvariable=self.auto_squeeze_min_lines,
+                validatecommand=self.digits_only, validate='key',
+        )
+
+        # frame_help.
+        frame_helplist = Frame(frame_help)
+        frame_helplist_buttons = Frame(frame_helplist)
+        self.helplist = Listbox(
+                frame_helplist, height=5, takefocus=True,
+                exportselection=FALSE)
+        scroll_helplist = Scrollbar(frame_helplist)
+        scroll_helplist['command'] = self.helplist.yview
+        self.helplist['yscrollcommand'] = scroll_helplist.set
+        self.helplist.bind('<ButtonRelease-1>', self.help_source_selected)
+        self.button_helplist_edit = Button(
+                frame_helplist_buttons, text='Edit', state='disabled',
+                width=8, command=self.helplist_item_edit)
+        self.button_helplist_add = Button(
+                frame_helplist_buttons, text='Add',
+                width=8, command=self.helplist_item_add)
+        self.button_helplist_remove = Button(
+                frame_helplist_buttons, text='Remove', state='disabled',
+                width=8, command=self.helplist_item_remove)
+
+        # Pack widgets:
+        # Body.
+        frame_window.pack(side=TOP, padx=5, pady=5, expand=TRUE, fill=BOTH)
+        frame_editor.pack(side=TOP, padx=5, pady=5, expand=TRUE, fill=BOTH)
+        frame_shell.pack(side=TOP, padx=5, pady=5, expand=TRUE, fill=BOTH)
+        frame_help.pack(side=TOP, padx=5, pady=5, expand=TRUE, fill=BOTH)
+        # frame_run.
+        frame_run.pack(side=TOP, padx=5, pady=0, fill=X)
+        startup_title.pack(side=LEFT, anchor=W, padx=5, pady=5)
+        self.startup_shell_on.pack(side=RIGHT, anchor=W, padx=5, pady=5)
+        self.startup_editor_on.pack(side=RIGHT, anchor=W, padx=5, pady=5)
+        # frame_win_size.
+        frame_win_size.pack(side=TOP, padx=5, pady=0, fill=X)
+        win_size_title.pack(side=LEFT, anchor=W, padx=5, pady=5)
+        self.win_height_int.pack(side=RIGHT, anchor=E, padx=10, pady=5)
+        win_height_title.pack(side=RIGHT, anchor=E, pady=5)
+        self.win_width_int.pack(side=RIGHT, anchor=E, padx=10, pady=5)
+        win_width_title.pack(side=RIGHT, anchor=E, pady=5)
+        # frame_cursor_blink.
+        frame_cursor_blink.pack(side=TOP, padx=5, pady=0, fill=X)
+        cursor_blink_title.pack(side=LEFT, anchor=W, padx=5, pady=5)
+        self.cursor_blink_bool.pack(side=LEFT, padx=5, pady=5)
+        # frame_autocomplete.
+        frame_autocomplete.pack(side=TOP, padx=5, pady=0, fill=X)
+        auto_wait_title.pack(side=LEFT, anchor=W, padx=5, pady=5)
+        self.auto_wait_int.pack(side=TOP, padx=10, pady=5)
+        # frame_paren.
+        frame_paren1.pack(side=TOP, padx=5, pady=0, fill=X)
+        paren_style_title.pack(side=LEFT, anchor=W, padx=5, pady=5)
+        self.paren_style_type.pack(side=TOP, padx=10, pady=5)
+        frame_paren2.pack(side=TOP, padx=5, pady=0, fill=X)
+        paren_time_title.pack(side=LEFT, anchor=W, padx=5)
+        self.bell_on.pack(side=RIGHT, anchor=E, padx=15, pady=5)
+        self.paren_flash_time.pack(side=TOP, anchor=W, padx=15, pady=5)
+
+        # frame_save.
+        frame_save.pack(side=TOP, padx=5, pady=0, fill=X)
+        run_save_title.pack(side=LEFT, anchor=W, padx=5, pady=5)
+        self.save_auto_on.pack(side=RIGHT, anchor=W, padx=5, pady=5)
+        self.save_ask_on.pack(side=RIGHT, anchor=W, padx=5, pady=5)
+        # frame_format.
+        frame_format.pack(side=TOP, padx=5, pady=0, fill=X)
+        format_width_title.pack(side=LEFT, anchor=W, padx=5, pady=5)
+        self.format_width_int.pack(side=TOP, padx=10, pady=5)
+        # frame_line_numbers_default.
+        frame_line_numbers_default.pack(side=TOP, padx=5, pady=0, fill=X)
+        line_numbers_default_title.pack(side=LEFT, anchor=W, padx=5, pady=5)
+        self.line_numbers_default_bool.pack(side=LEFT, padx=5, pady=5)
+        # frame_context.
+        frame_context.pack(side=TOP, padx=5, pady=0, fill=X)
+        context_title.pack(side=LEFT, anchor=W, padx=5, pady=5)
+        self.context_int.pack(side=TOP, padx=5, pady=5)
+
+        # frame_auto_squeeze_min_lines
+        frame_auto_squeeze_min_lines.pack(side=TOP, padx=5, pady=0, fill=X)
+        auto_squeeze_min_lines_title.pack(side=LEFT, anchor=W, padx=5, pady=5)
+        self.auto_squeeze_min_lines_int.pack(side=TOP, padx=5, pady=5)
+
+        # frame_help.
+        frame_helplist_buttons.pack(side=RIGHT, padx=5, pady=5, fill=Y)
+        frame_helplist.pack(side=TOP, padx=5, pady=5, expand=TRUE, fill=BOTH)
+        scroll_helplist.pack(side=RIGHT, anchor=W, fill=Y)
+        self.helplist.pack(side=LEFT, anchor=E, expand=TRUE, fill=BOTH)
+        self.button_helplist_edit.pack(side=TOP, anchor=W, pady=5)
+        self.button_helplist_add.pack(side=TOP, anchor=W)
+        self.button_helplist_remove.pack(side=TOP, anchor=W, pady=5)
+
+    def load_general_cfg(self):
+        "Load current configuration settings for the general options."
+        # Set variables for all windows.
+        self.startup_edit.set(idleConf.GetOption(
+                'main', 'General', 'editor-on-startup', type='bool'))
+        self.win_width.set(idleConf.GetOption(
+                'main', 'EditorWindow', 'width', type='int'))
+        self.win_height.set(idleConf.GetOption(
+                'main', 'EditorWindow', 'height', type='int'))
+        self.cursor_blink.set(idleConf.GetOption(
+                'main', 'EditorWindow', 'cursor-blink', type='bool'))
+        self.autocomplete_wait.set(idleConf.GetOption(
+                'extensions', 'AutoComplete', 'popupwait', type='int'))
+        self.paren_style.set(idleConf.GetOption(
+                'extensions', 'ParenMatch', 'style'))
+        self.flash_delay.set(idleConf.GetOption(
+                'extensions', 'ParenMatch', 'flash-delay', type='int'))
+        self.paren_bell.set(idleConf.GetOption(
+                'extensions', 'ParenMatch', 'bell'))
+
+        # Set variables for editor windows.
+        self.autosave.set(idleConf.GetOption(
+                'main', 'General', 'autosave', default=0, type='bool'))
+        self.format_width.set(idleConf.GetOption(
+                'extensions', 'FormatParagraph', 'max-width', type='int'))
+        self.line_numbers_default.set(idleConf.GetOption(
+                'main', 'EditorWindow', 'line-numbers-default', type='bool'))
+        self.context_lines.set(idleConf.GetOption(
+                'extensions', 'CodeContext', 'maxlines', type='int'))
+
+        # Set variables for shell windows.
+        self.auto_squeeze_min_lines.set(idleConf.GetOption(
+                'main', 'PyShell', 'auto-squeeze-min-lines', type='int'))
+
+        # Set additional help sources.
+        self.user_helplist = idleConf.GetAllExtraHelpSourcesList()
+        self.helplist.delete(0, 'end')
+        for help_item in self.user_helplist:
+            self.helplist.insert(END, help_item[0])
+        self.set_add_delete_state()
+
+    def help_source_selected(self, event):
+        "Handle event for selecting additional help."
+        self.set_add_delete_state()
+
+    def set_add_delete_state(self):
+        "Toggle the state for the help list buttons based on list entries."
+        if self.helplist.size() < 1:  # No entries in list.
+            self.button_helplist_edit.state(('disabled',))
+            self.button_helplist_remove.state(('disabled',))
+        else:  # Some entries.
+            if self.helplist.curselection():  # There currently is a selection.
+                self.button_helplist_edit.state(('!disabled',))
+                self.button_helplist_remove.state(('!disabled',))
+            else:  # There currently is not a selection.
+                self.button_helplist_edit.state(('disabled',))
+                self.button_helplist_remove.state(('disabled',))
+
+    def helplist_item_add(self):
+        """Handle add button for the help list.
+
+        Query for name and location of new help sources and add
+        them to the list.
+        """
+        help_source = HelpSource(self, 'New Help Source').result
+        if help_source:
+            self.user_helplist.append(help_source)
+            self.helplist.insert(END, help_source[0])
+            self.update_help_changes()
+
+    def helplist_item_edit(self):
+        """Handle edit button for the help list.
+
+        Query with existing help source information and update
+        config if the values are changed.
+        """
+        item_index = self.helplist.index(ANCHOR)
+        help_source = self.user_helplist[item_index]
+        new_help_source = HelpSource(
+                self, 'Edit Help Source',
+                menuitem=help_source[0],
+                filepath=help_source[1],
+                ).result
+        if new_help_source and new_help_source != help_source:
+            self.user_helplist[item_index] = new_help_source
+            self.helplist.delete(item_index)
+            self.helplist.insert(item_index, new_help_source[0])
+            self.update_help_changes()
+            self.set_add_delete_state()  # Selected will be un-selected
+
+    def helplist_item_remove(self):
+        """Handle remove button for the help list.
+
+        Delete the help list item from config.
+        """
+        item_index = self.helplist.index(ANCHOR)
+        del(self.user_helplist[item_index])
+        self.helplist.delete(item_index)
+        self.update_help_changes()
+        self.set_add_delete_state()
+
+    def update_help_changes(self):
+        "Clear and rebuild the HelpFiles section in changes"
+        changes['main']['HelpFiles'] = {}
+        for num in range(1, len(self.user_helplist) + 1):
+            changes.add_option(
+                    'main', 'HelpFiles', str(num),
+                    ';'.join(self.user_helplist[num-1][:2]))
+
+
+class VarTrace:
+    """Maintain Tk variables trace state."""
+
+    def __init__(self):
+        """Store Tk variables and callbacks.
+
+        untraced: List of tuples (var, callback)
+            that do not have the callback attached
+            to the Tk var.
+        traced: List of tuples (var, callback) where
+            that callback has been attached to the var.
+        """
+        self.untraced = []
+        self.traced = []
+
+    def clear(self):
+        "Clear lists (for tests)."
+        # Call after all tests in a module to avoid memory leaks.
+        self.untraced.clear()
+        self.traced.clear()
+
+    def add(self, var, callback):
+        """Add (var, callback) tuple to untraced list.
+
+        Args:
+            var: Tk variable instance.
+            callback: Either function name to be used as a callback
+                or a tuple with IdleConf config-type, section, and
+                option names used in the default callback.
+
+        Return:
+            Tk variable instance.
+        """
+        if isinstance(callback, tuple):
+            callback = self.make_callback(var, callback)
+        self.untraced.append((var, callback))
+        return var
+
+    @staticmethod
+    def make_callback(var, config):
+        "Return default callback function to add values to changes instance."
+        def default_callback(*params):
+            "Add config values to changes instance."
+            changes.add_option(*config, var.get())
+        return default_callback
+
+    def attach(self):
+        "Attach callback to all vars that are not traced."
+        while self.untraced:
+            var, callback = self.untraced.pop()
+            var.trace_add('write', callback)
+            self.traced.append((var, callback))
+
+    def detach(self):
+        "Remove callback from traced vars."
+        while self.traced:
+            var, callback = self.traced.pop()
+            var.trace_remove('write', var.trace_info()[0][1])
+            self.untraced.append((var, callback))
+
+
+tracers = VarTrace()
+
+help_common = '''\
+When you click either the Apply or Ok buttons, settings in this
+dialog that are different from IDLE's default are saved in
+a .idlerc directory in your home directory. Except as noted,
+these changes apply to all versions of IDLE installed on this
+machine. [Cancel] only cancels changes made since the last save.
+'''
+help_pages = {
+    'Fonts/Tabs':'''
+Font sample: This shows what a selection of Basic Multilingual Plane
+unicode characters look like for the current font selection.  If the
+selected font does not define a character, Tk attempts to find another
+font that does.  Substitute glyphs depend on what is available on a
+particular system and will not necessarily have the same size as the
+font selected.  Line contains 20 characters up to Devanagari, 14 for
+Tamil, and 10 for East Asia.
+
+Hebrew and Arabic letters should display right to left, starting with
+alef, \u05d0 and \u0627.  Arabic digits display left to right.  The
+Devanagari and Tamil lines start with digits.  The East Asian lines
+are Chinese digits, Chinese Hanzi, Korean Hangul, and Japanese
+Hiragana and Katakana.
+
+You can edit the font sample. Changes remain until IDLE is closed.
+''',
+    'Highlights': '''
+Highlighting:
+The IDLE Dark color theme is new in October 2015.  It can only
+be used with older IDLE releases if it is saved as a custom
+theme, with a different name.
+''',
+    'Keys': '''
+Keys:
+The IDLE Modern Unix key set is new in June 2016.  It can only
+be used with older IDLE releases if it is saved as a custom
+key set, with a different name.
+''',
+     'General': '''
+General:
+
+AutoComplete: Popupwait is milliseconds to wait after key char, without
+cursor movement, before popping up completion box.  Key char is '.' after
+identifier or a '/' (or '\\' on Windows) within a string.
+
+FormatParagraph: Max-width is max chars in lines after re-formatting.
+Use with paragraphs in both strings and comment blocks.
+
+ParenMatch: Style indicates what is highlighted when closer is entered:
+'opener' - opener '({[' corresponding to closer; 'parens' - both chars;
+'expression' (default) - also everything in between.  Flash-delay is how
+long to highlight if cursor is not moved (0 means forever).
+
+CodeContext: Maxlines is the maximum number of code context lines to
+display when Code Context is turned on for an editor window.
+
+Shell Preferences: Auto-Squeeze Min. Lines is the minimum number of lines
+of output to automatically "squeeze".
+'''
+}
+
+
+def is_int(s):
+    "Return 's is blank or represents an int'"
+    if not s:
+        return True
+    try:
+        int(s)
+        return True
+    except ValueError:
+        return False
+
+
+class VerticalScrolledFrame(Frame):
+    """A pure Tkinter vertically scrollable frame.
+
+    * Use the 'interior' attribute to place widgets inside the scrollable frame
+    * Construct and pack/place/grid normally
+    * This frame only allows vertical scrolling
+    """
+    def __init__(self, parent, *args, **kw):
+        Frame.__init__(self, parent, *args, **kw)
+
+        # Create a canvas object and a vertical scrollbar for scrolling it.
+        vscrollbar = Scrollbar(self, orient=VERTICAL)
+        vscrollbar.pack(fill=Y, side=RIGHT, expand=FALSE)
+        canvas = Canvas(self, borderwidth=0, highlightthickness=0,
+                        yscrollcommand=vscrollbar.set, width=240)
+        canvas.pack(side=LEFT, fill=BOTH, expand=TRUE)
+        vscrollbar.config(command=canvas.yview)
+
+        # Reset the view.
+        canvas.xview_moveto(0)
+        canvas.yview_moveto(0)
+
+        # Create a frame inside the canvas which will be scrolled with it.
+        self.interior = interior = Frame(canvas)
+        interior_id = canvas.create_window(0, 0, window=interior, anchor=NW)
+
+        # Track changes to the canvas and frame width and sync them,
+        # also updating the scrollbar.
+        def _configure_interior(event):
+            # Update the scrollbars to match the size of the inner frame.
+            size = (interior.winfo_reqwidth(), interior.winfo_reqheight())
+            canvas.config(scrollregion="0 0 %s %s" % size)
+        interior.bind('<Configure>', _configure_interior)
+
+        def _configure_canvas(event):
+            if interior.winfo_reqwidth() != canvas.winfo_width():
+                # Update the inner frame's width to fill the canvas.
+                canvas.itemconfigure(interior_id, width=canvas.winfo_width())
+        canvas.bind('<Configure>', _configure_canvas)
+
+        return
+
+
+if __name__ == '__main__':
+    from unittest import main
+    main('idlelib.idle_test.test_configdialog', verbosity=2, exit=False)
+
+    from idlelib.idle_test.htest import run
+    run(ConfigDialog)