[Python-checkins] bpo-31205: IDLE: Factor KeysPage class from ConfigDialog (#3096)

Terry Jan Reedy webhook-mailer at python.org
Tue Aug 15 18:26:36 EDT 2017


https://github.com/python/cpython/commit/e36d9f5568093b3885da62a0bf0fdfbe3771672b
commit: e36d9f5568093b3885da62a0bf0fdfbe3771672b
branch: master
author: Cheryl Sabella <cheryl.sabella at gmail.com>
committer: Terry Jan Reedy <tjreedy at udel.edu>
date: 2017-08-15T18:26:23-04:00
summary:

bpo-31205: IDLE: Factor KeysPage class from ConfigDialog (#3096)

  The slightly modified tests continue to pass.  Patch by Cheryl Sabella.

files:
A Misc/NEWS.d/next/IDLE/2017-08-15-12-58-23.bpo-31205.iuziZ5.rst
M Lib/idlelib/configdialog.py
M Lib/idlelib/idle_test/test_configdialog.py

diff --git a/Lib/idlelib/configdialog.py b/Lib/idlelib/configdialog.py
index 66219f1820e..e1ac82b7df3 100644
--- a/Lib/idlelib/configdialog.py
+++ b/Lib/idlelib/configdialog.py
@@ -92,9 +92,9 @@ def create_widgets(self):
             note: Notebook
             highpage: self.create_page_highlight
             fontpage: FontPage
-            keyspage: self.create_page_keys
+            keyspage: KeysPage
             genpage: GenPage
-            extpageL self.create_page_extensions
+            extpage: self.create_page_extensions
 
         Methods:
             create_action_buttons
@@ -104,7 +104,7 @@ def create_widgets(self):
         self.note = note = Notebook(self, width=450, height=450)
         self.highpage = self.create_page_highlight()
         self.fontpage = FontPage(note, self.highpage)
-        self.keyspage = self.create_page_keys()
+        self.keyspage = KeysPage(note)
         self.genpage = GenPage(note)
         self.extpage = self.create_page_extensions()
         note.add(self.fontpage, text='Fonts/Tabs')
@@ -132,7 +132,7 @@ def load_configs(self):
         #self.load_font_cfg()
         #self.load_tab_cfg()
         self.load_theme_cfg()
-        self.load_key_cfg()
+        # self.load_key_cfg()
         # self.load_general_cfg()
         # note: extension page handled separately
 
@@ -791,863 +791,869 @@ def delete_custom_theme(self):
         self.activate_config_changes()
         self.set_theme_type()
 
+    def deactivate_current_config(self):
+        """Remove current key bindings.
 
-    def create_page_keys(self):
-        """Return frame of widgets for Keys tab.
+        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()
 
-        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.
+    def activate_config_changes(self):
+        """Apply configuration changes to current windows.
 
-        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.
+        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()
 
-        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'].
+    def create_page_extensions(self):
+        """Part of the config dialog used for configuring IDLE extensions.
 
-        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.
+        This code is generic - it works for any and all IDLE extensions.
 
-        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 always 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.
+        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.
 
-        Tk Variables:
-            keybinding: Action/key bindings.
+        Not all changes take effect immediately - some may require restarting IDLE.
+        This depends on each extension's implementation.
 
-        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.
+        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.
 
-        Widgets for keys page 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
+        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
-        self.builtin_name = tracers.add(
-                StringVar(parent), self.var_changed_builtin_name)
-        self.custom_name = tracers.add(
-                StringVar(parent), self.var_changed_custom_name)
-        self.keyset_source = tracers.add(
-                BooleanVar(parent), self.var_changed_keyset_source)
-        self.keybinding = tracers.add(
-                StringVar(parent), self.var_changed_keybinding)
-
-        # Widget creation:
-        # body and section frames.
         frame = Frame(self.note)
-        frame_custom = LabelFrame(
-                frame, borderwidth=2, relief=GROOVE,
-                text=' Custom Key Bindings ')
-        frame_key_sets = LabelFrame(
-                frame, 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, padx=2, pady=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], bd=2)
+        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(padx=10, pady=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)
 
-        ##widget packing
-        #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)
         return frame
 
-    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 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):
+            self.extensions[ext_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'
-            self.keys_message['fg'] = '#500000'
-        else:
-            changes.add_option('main', 'Keys', 'name', value)
-            changes.add_option('main', 'Keys', 'name2', '')
-            self.keys_message['text'] = ''
-            self.keys_message['fg'] = 'black'
-        self.load_keys_list(value)
+        for ext_name in self.extensions:
+            opt_list = sorted(self.ext_defaultCfg.GetOptionList(ext_name))
 
-    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)
+            # 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
 
-    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()
+            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))
 
-    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)
+                self.extensions[ext_name].append({'name': opt_name,
+                                                  'type': opt_type,
+                                                  'default': def_str,
+                                                  'value': value,
+                                                  'var': var,
+                                                 })
 
-    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'] = NORMAL
-            self.customlist['state'] = NORMAL
-            self.button_delete_custom_keys['state'] = NORMAL
+    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 get_new_keys(self):
-        """Handle event to change key binding for selected line.
+    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, textvariable=var, variable=var,
+                            onvalue='True', offvalue='False',
+                            indicatoron=FALSE, selectcolor='', 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')
+                      ).grid(row=row, column=1, sticky=NSEW, padx=7)
 
-        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)
+            else:
+                Entry(entry_area, textvariable=var
+                      ).grid(row=row, column=1, sticky=NSEW, padx=7)
+        return
 
-    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 set_extension_value(self, section, opt):
+        """Return True if the configuration was added or changed.
 
-    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)
+        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 on_bindingslist_select(self, event):
-        "Activate button to assign new keys to selected action."
-        self.button_new_keys['state'] = NORMAL
+    def save_all_changed_extensions(self):
+        """Save configuration changes to the user config file.
 
-    def create_new_key_set(self, new_key_set_name):
-        """Create a new custom key set with the given name.
+        Attributes accessed:
+            extensions
 
-        Copy the bindings/keys from the previously active keyset
-        to the new keyset and activate the new custom keyset.
+        Methods:
+            set_extension_value
         """
-        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()
+        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()
 
-    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)
-
-    def save_new_key_set(self, 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)
+# 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.
 
-    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 tkMessageBox.askyesno(
-                'Delete Key Set',  delmsg % keyset_name, parent=self):
-            return
-        self.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.save_all_changed_extensions()
-        self.activate_config_changes()
-        self.set_keys_type()
+class FontPage(Frame):
 
-    def deactivate_current_config(self):
-        """Remove current key bindings.
+    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()
 
-        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 create_page_font_tab(self):
+        """Return frame of widgets for Font/Tabs tab.
 
-    def activate_config_changes(self):
-        """Apply configuration changes to current windows.
+        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.
 
-        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()
+        Function load_font_cfg initializes font vars and widgets from
+        idleConf entries and tk.
 
-    def create_page_extensions(self):
-        """Part of the config dialog used for configuring IDLE extensions.
+        Fontlist: mouse button 1 click or up or down key invoke
+        on_fontlist_select(), which sets var font_name.
 
-        This code is generic - it works for any and all IDLE extensions.
+        Sizelist: clicking the menubutton opens the dropdown menu. A
+        mouse button 1 click or return key sets var font_size.
 
-        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.
+        Bold_toggle: clicking the box toggles var font_bold.
 
-        Not all changes take effect immediately - some may require restarting IDLE.
-        This depends on each extension's implementation.
+        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 hightlight page.
 
-        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.
+        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.
 
-        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().
+        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_font_sample: Frame
+                    (*)font_sample: Label
+            frame_indent: LabelFrame
+                    indent_title: Label
+                    (*)indent_scale: Scale - space_num
         """
-        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(padx=10, pady=10)
-        self.config_frame = {}
-        self.current_extension = None
+        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'))
 
-        self.outerframe = self                      # TEMPORARY
-        self.tabbed_page_set = self.extension_list  # TEMPORARY
+        # Create widgets:
+        # body and body section frames.
+        frame_font = LabelFrame(
+                self, borderwidth=2, relief=GROOVE, text=' Base Editor Font ')
+        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=5,
+                                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_font_sample = Frame(frame_font, relief=SOLID, borderwidth=1)
+        temp_font = tkFont.Font(self, ('courier', 10, 'normal'))
+        self.font_sample = Label(
+                frame_font_sample, justify=LEFT, font=temp_font,
+                text='AaBbCcDdEe\nFfGgHhIiJj\n1234567890\n#:+=(){}[]')
+        # 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)
 
-        # 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)
+        # Pack widgets:
+        # body.
+        frame_font.pack(side=LEFT, padx=5, pady=5, expand=TRUE, fill=BOTH)
+        frame_indent.pack(side=LEFT, padx=5, pady=5, fill=Y)
+        # 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_font_sample.pack(side=TOP, padx=5, pady=5, expand=TRUE, fill=BOTH)
+        self.font_sample.pack(expand=TRUE, fill=BOTH)
+        # frame_indent.
+        frame_indent.pack(side=TOP, fill=X)
+        indent_title.pack(side=TOP, anchor=W, padx=5)
+        self.indent_scale.pack(side=TOP, padx=5, fill=X)
 
-        return frame
+    def load_font_cfg(self):
+        """Load current configuration settings for the font options.
 
-    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):
-            self.extensions[ext_name] = []
+        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'
 
-        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))
+        # 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()
 
-                self.extensions[ext_name].append({'name': opt_name,
-                                                  'type': opt_type,
-                                                  'default': def_str,
-                                                  'value': value,
-                                                  'var': var,
-                                                 })
+    def var_changed_font(self, *params):
+        """Store changes to font attributes.
 
-    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
+        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 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, textvariable=var, variable=var,
-                            onvalue='True', offvalue='False',
-                            indicatoron=FALSE, selectcolor='', 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')
-                      ).grid(row=row, column=1, sticky=NSEW, padx=7)
+    def on_fontlist_select(self, event):
+        """Handle selecting a font from the list.
 
-            else:
-                Entry(entry_area, textvariable=var
-                      ).grid(row=row, column=1, sticky=NSEW, padx=7)
-        return
+        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_extension_value(self, section, opt):
-        """Return True if the configuration was added or changed.
+    def set_samples(self, event=None):
+        """Update update both screen samples with the font settings.
 
-        If the value is the same as the default, then remove it
-        from user config file.
+        Called on font initialization and change events.
+        Accesses font_name, font_size, and font_bold Variables.
+        Updates font_sample and hightlight page highlight_sample.
         """
-        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.
+        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
 
-        Attributes accessed:
-            extensions
+    def load_tab_cfg(self):
+        """Load current configuration settings for the tab options.
 
-        Methods:
-            set_extension_value
+        Attributes updated:
+            space_num: Set to value from idleConf.
         """
-        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()
-
+        # Set indent sizes.
+        space_num = idleConf.GetOption(
+            'main', 'Indent', 'num-spaces', default=4, type='int')
+        self.space_num.set(space_num)
 
-# 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.
+    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 FontPage(Frame):
+class KeysPage(Frame):
 
-    def __init__(self, master, highpage):
+    def __init__(self, master):
         super().__init__(master)
-        self.highlight_sample = highpage.highlight_sample
-        self.create_page_font_tab()
-        self.load_font_cfg()
-        self.load_tab_cfg()
+        self.cd = master.master
+        self.create_page_keys()
+        self.load_key_cfg()
 
-    def create_page_font_tab(self):
-        """Return frame of widgets for Font/Tabs tab.
+    def create_page_keys(self):
+        """Return frame of widgets for Keys 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.
+        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_font_cfg initializes font vars and widgets from
-        idleConf entries and tk.
+        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.
 
-        Fontlist: mouse button 1 click or up or down key invoke
-        on_fontlist_select(), which sets var font_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'].
 
-        Sizelist: clicking the menubutton opens the dropdown menu. A
-        mouse button 1 click or return key sets var font_size.
+        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.
 
-        Bold_toggle: clicking the box toggles var font_bold.
+        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 always 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.
 
-        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 hightlight page.
+        Tk Variables:
+            keybinding: Action/key bindings.
 
-        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.
+        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 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_font_sample: Frame
-                    (*)font_sample: Label
-            frame_indent: LabelFrame
-                    indent_title: Label
-                    (*)indent_scale: Scale - space_num
+        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.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'))
+        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 body section frames.
-        frame_font = LabelFrame(
-                self, borderwidth=2, relief=GROOVE, text=' Base Editor Font ')
-        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=5,
-                                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_font_sample = Frame(frame_font, relief=SOLID, borderwidth=1)
-        temp_font = tkFont.Font(self, ('courier', 10, 'normal'))
-        self.font_sample = Label(
-                frame_font_sample, justify=LEFT, font=temp_font,
-                text='AaBbCcDdEe\nFfGgHhIiJj\n1234567890\n#:+=(){}[]')
-        # 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)
+        # 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, padx=2, pady=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], bd=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'
+            self.keys_message['fg'] = '#500000'
+        else:
+            changes.add_option('main', 'Keys', 'name', value)
+            changes.add_option('main', 'Keys', 'name2', '')
+            self.keys_message['text'] = ''
+            self.keys_message['fg'] = 'black'
+        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)
 
-        # Pack widgets:
-        # body.
-        frame_font.pack(side=LEFT, padx=5, pady=5, expand=TRUE, fill=BOTH)
-        frame_indent.pack(side=LEFT, padx=5, pady=5, fill=Y)
-        # 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_font_sample.pack(side=TOP, padx=5, pady=5, expand=TRUE, fill=BOTH)
-        self.font_sample.pack(expand=TRUE, fill=BOTH)
-        # frame_indent.
-        frame_indent.pack(side=TOP, fill=X)
-        indent_title.pack(side=TOP, anchor=W, padx=5)
-        self.indent_scale.pack(side=TOP, padx=5, fill=X)
+    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'] = NORMAL
+            self.customlist['state'] = NORMAL
+            self.button_delete_custom_keys['state'] = NORMAL
 
-    def load_font_cfg(self):
-        """Load current configuration settings for the font options.
+    def get_new_keys(self):
+        """Handle event to change key binding for selected line.
 
-        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.
+        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.
         """
-        configured_font = idleConf.GetFont(self, 'main', 'EditorWindow')
-        font_name = configured_font[0].lower()
-        font_size = configured_font[1]
-        font_bold  = configured_font[2]=='bold'
+        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)
 
-        # 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 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 var_changed_font(self, *params):
-        """Store changes to font attributes.
+    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)
 
-        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_bindingslist_select(self, event):
+        "Activate button to assign new keys to selected action."
+        self.button_new_keys['state'] = NORMAL
 
-    def on_fontlist_select(self, event):
-        """Handle selecting a font from the list.
+    def create_new_key_set(self, new_key_set_name):
+        """Create a new custom key set with the given name.
 
-        Event can result from either mouse click or Up or Down key.
-        Set font_name and example displays to selection.
+        Copy the bindings/keys from the previously active keyset
+        to the new keyset and activate the new custom keyset.
         """
-        font = self.fontlist.get(
-                ACTIVE if event.type.name == 'KeyRelease' else ANCHOR)
-        self.font_name.set(font.lower())
+        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 set_samples(self, event=None):
-        """Update update both screen samples with the font settings.
+    def load_keys_list(self, keyset_name):
+        """Reload the list of action/key binding pairs for the active key set.
 
-        Called on font initialization and change events.
-        Accesses font_name, font_size, and font_bold Variables.
-        Updates font_sample and hightlight page highlight_sample.
+        An action/key binding can be selected to change the key binding.
         """
-        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
+        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)
 
-    def load_tab_cfg(self):
-        """Load current configuration settings for the tab options.
+    @staticmethod
+    def save_new_key_set(keyset_name, keyset):
+        """Save a newly created core key set.
 
-        Attributes updated:
-            space_num: Set to value from idleConf.
+        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
         """
-        # Set indent sizes.
-        space_num = idleConf.GetOption(
-            'main', 'Indent', 'num-spaces', default=4, type='int')
-        self.space_num.set(space_num)
+        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 var_changed_space_num(self, *params):
-        "Store change to indentation size."
-        value = self.space_num.get()
-        changes.add_option('main', 'Indent', 'num-spaces', value)
+    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 tkMessageBox.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):
diff --git a/Lib/idlelib/idle_test/test_configdialog.py b/Lib/idlelib/idle_test/test_configdialog.py
index b07a65cf56a..964784508f8 100644
--- a/Lib/idlelib/idle_test/test_configdialog.py
+++ b/Lib/idlelib/idle_test/test_configdialog.py
@@ -232,22 +232,27 @@ def setUp(self):
         changes.clear()
 
 
-class KeyTest(unittest.TestCase):
+class KeysPageTest(unittest.TestCase):
+    """Test that keys tab widgets enable users to make changes.
+
+    Test that widget actions set vars, that var changes add
+    options to changes and that key sets works correctly.
+    """
 
     @classmethod
     def setUpClass(cls):
-        d = dialog
-        dialog.note.select(d.keyspage)
-        d.set_keys_type = Func()
-        d.load_keys_list = Func()
+        page = cls.page = dialog.keyspage
+        dialog.note.select(page)
+        page.set_keys_type = Func()
+        page.load_keys_list = Func()
 
     @classmethod
     def tearDownClass(cls):
-        d = dialog
-        del d.set_keys_type, d.load_keys_list
+        page = cls.page
+        del page.set_keys_type, page.load_keys_list
 
     def setUp(self):
-        d = dialog
+        d = self.page
         # The following is needed for test_load_key_cfg, _delete_custom_keys.
         # This may indicate a defect in some test or function.
         for section in idleConf.GetSectionList('user', 'keys'):
@@ -258,7 +263,7 @@ def setUp(self):
 
     def test_load_key_cfg(self):
         tracers.detach()
-        d = dialog
+        d = self.page
         eq = self.assertEqual
 
         # Use builtin keyset with no user keysets created.
@@ -300,7 +305,7 @@ def test_load_key_cfg(self):
 
     def test_keyset_source(self):
         eq = self.assertEqual
-        d = dialog
+        d = self.page
         # Test these separately.
         d.var_changed_builtin_name = Func()
         d.var_changed_custom_name = Func()
@@ -321,7 +326,7 @@ def test_keyset_source(self):
 
     def test_builtin_name(self):
         eq = self.assertEqual
-        d = dialog
+        d = self.page
         idleConf.userCfg['main'].remove_section('Keys')
         item_list = ['IDLE Classic Windows', 'IDLE Classic OSX',
                      'IDLE Modern UNIX']
@@ -352,7 +357,7 @@ def test_builtin_name(self):
         eq(d.load_keys_list.args, ('IDLE Classic OSX', ))
 
     def test_custom_name(self):
-        d = dialog
+        d = self.page
 
         # If no selections, doesn't get added.
         d.customlist.SetMenu([], '- no custom keys -')
@@ -366,7 +371,7 @@ def test_custom_name(self):
         self.assertEqual(d.load_keys_list.called, 1)
 
     def test_keybinding(self):
-        d = dialog
+        d = self.page
         d.custom_name.set('my custom keys')
         d.bindingslist.delete(0, 'end')
         d.bindingslist.insert(0, 'copy')
@@ -386,7 +391,7 @@ def test_keybinding(self):
 
     def test_set_keys_type(self):
         eq = self.assertEqual
-        d = dialog
+        d = self.page
         del d.set_keys_type
 
         # Builtin keyset selected.
@@ -407,7 +412,7 @@ def test_set_keys_type(self):
 
     def test_get_new_keys(self):
         eq = self.assertEqual
-        d = dialog
+        d = self.page
         orig_getkeysdialog = configdialog.GetKeysDialog
         gkd = configdialog.GetKeysDialog = Func(return_self=True)
         gnkn = d.get_new_keys_name = Func()
@@ -456,7 +461,7 @@ def test_get_new_keys(self):
     def test_get_new_keys_name(self):
         orig_sectionname = configdialog.SectionName
         sn = configdialog.SectionName = Func(return_self=True)
-        d = dialog
+        d = self.page
 
         sn.result = 'New Keys'
         self.assertEqual(d.get_new_keys_name(''), 'New Keys')
@@ -464,7 +469,7 @@ def test_get_new_keys_name(self):
         configdialog.SectionName = orig_sectionname
 
     def test_save_as_new_key_set(self):
-        d = dialog
+        d = self.page
         gnkn = d.get_new_keys_name = Func()
         d.keyset_source.set(True)
 
@@ -482,7 +487,7 @@ def test_save_as_new_key_set(self):
         del d.get_new_keys_name
 
     def test_on_bindingslist_select(self):
-        d = dialog
+        d = self.page
         b = d.bindingslist
         b.delete(0, 'end')
         b.insert(0, 'copy')
@@ -504,7 +509,7 @@ def test_on_bindingslist_select(self):
 
     def test_create_new_key_set_and_save_new_key_set(self):
         eq = self.assertEqual
-        d = dialog
+        d = self.page
 
         # Use default as previously active keyset.
         d.keyset_source.set(True)
@@ -535,7 +540,7 @@ def test_create_new_key_set_and_save_new_key_set(self):
 
     def test_load_keys_list(self):
         eq = self.assertEqual
-        d = dialog
+        d = self.page
         gks = idleConf.GetKeySet = Func()
         del d.load_keys_list
         b = d.bindingslist
@@ -578,11 +583,11 @@ def test_load_keys_list(self):
 
     def test_delete_custom_keys(self):
         eq = self.assertEqual
-        d = dialog
+        d = self.page
         d.button_delete_custom_keys['state'] = NORMAL
         yesno = configdialog.tkMessageBox.askyesno = Func()
-        d.deactivate_current_config = Func()
-        d.activate_config_changes = Func()
+        dialog.deactivate_current_config = Func()
+        dialog.activate_config_changes = Func()
 
         keyset_name = 'spam key set'
         idleConf.userCfg['keys'].SetOption(keyset_name, 'name', 'value')
@@ -598,8 +603,8 @@ def test_delete_custom_keys(self):
         eq(yesno.called, 1)
         eq(keyspage[keyset_name], {'option': 'True'})
         eq(idleConf.GetSectionList('user', 'keys'), ['spam key set'])
-        eq(d.deactivate_current_config.called, 0)
-        eq(d.activate_config_changes.called, 0)
+        eq(dialog.deactivate_current_config.called, 0)
+        eq(dialog.activate_config_changes.called, 0)
         eq(d.set_keys_type.called, 0)
 
         # Confirm deletion.
@@ -610,11 +615,11 @@ def test_delete_custom_keys(self):
         eq(idleConf.GetSectionList('user', 'keys'), [])
         eq(d.custom_keyset_on['state'], DISABLED)
         eq(d.custom_name.get(), '- no custom keys -')
-        eq(d.deactivate_current_config.called, 1)
-        eq(d.activate_config_changes.called, 1)
+        eq(dialog.deactivate_current_config.called, 1)
+        eq(dialog.activate_config_changes.called, 1)
         eq(d.set_keys_type.called, 1)
 
-        del d.activate_config_changes, d.deactivate_current_config
+        del dialog.activate_config_changes, dialog.deactivate_current_config
         del configdialog.tkMessageBox.askyesno
 
 
diff --git a/Misc/NEWS.d/next/IDLE/2017-08-15-12-58-23.bpo-31205.iuziZ5.rst b/Misc/NEWS.d/next/IDLE/2017-08-15-12-58-23.bpo-31205.iuziZ5.rst
new file mode 100644
index 00000000000..007a2e2fc55
--- /dev/null
+++ b/Misc/NEWS.d/next/IDLE/2017-08-15-12-58-23.bpo-31205.iuziZ5.rst
@@ -0,0 +1,2 @@
+IDLE: Factor KeysPage(Frame) class from ConfigDialog.  The slightly
+modified tests continue to pass.  Patch by Cheryl Sabella.



More information about the Python-checkins mailing list