Author: Carl Friedrich Bolz-Tereick <[email protected]>
Branch: py3.6
Changeset: r95996:9387f96a5518
Date: 2019-02-13 13:07 +0100
http://bitbucket.org/pypy/pypy/changeset/9387f96a5518/

Log:    seems we never updated idlelib with the 3.6 changes!

diff too long, truncating to 2000 out of 7060 lines

diff --git a/lib-python/3/idlelib/autocomplete.py 
b/lib-python/3/idlelib/autocomplete.py
new file mode 100644
--- /dev/null
+++ b/lib-python/3/idlelib/autocomplete.py
@@ -0,0 +1,232 @@
+"""autocomplete.py - An IDLE extension for automatically completing names.
+
+This extension can complete either attribute names or file names. It can pop
+a window with all available names, for the user to select from.
+"""
+import os
+import string
+import sys
+
+# These constants represent the two different types of completions.
+# They must be defined here so autocomple_w can import them.
+COMPLETE_ATTRIBUTES, COMPLETE_FILES = range(1, 2+1)
+
+from idlelib import autocomplete_w
+from idlelib.config import idleConf
+from idlelib.hyperparser import HyperParser
+import __main__
+
+# This string includes all chars that may be in an identifier.
+# TODO Update this here and elsewhere.
+ID_CHARS = string.ascii_letters + string.digits + "_"
+
+SEPS = os.sep
+if os.altsep:  # e.g. '/' on Windows...
+    SEPS += os.altsep
+
+
+class AutoComplete:
+
+    menudefs = [
+        ('edit', [
+            ("Show Completions", "<<force-open-completions>>"),
+        ])
+    ]
+
+    popupwait = idleConf.GetOption("extensions", "AutoComplete",
+                                   "popupwait", type="int", default=0)
+
+    def __init__(self, editwin=None):
+        self.editwin = editwin
+        if editwin is not None:  # not in subprocess or test
+            self.text = editwin.text
+            self.autocompletewindow = None
+            # id of delayed call, and the index of the text insert when
+            # the delayed call was issued. If _delayed_completion_id is
+            # None, there is no delayed call.
+            self._delayed_completion_id = None
+            self._delayed_completion_index = None
+
+    def _make_autocomplete_window(self):
+        return autocomplete_w.AutoCompleteWindow(self.text)
+
+    def _remove_autocomplete_window(self, event=None):
+        if self.autocompletewindow:
+            self.autocompletewindow.hide_window()
+            self.autocompletewindow = None
+
+    def force_open_completions_event(self, event):
+        """Happens when the user really wants to open a completion list, even
+        if a function call is needed.
+        """
+        self.open_completions(True, False, True)
+
+    def try_open_completions_event(self, event):
+        """Happens when it would be nice to open a completion list, but not
+        really necessary, for example after a dot, so function
+        calls won't be made.
+        """
+        lastchar = self.text.get("insert-1c")
+        if lastchar == ".":
+            self._open_completions_later(False, False, False,
+                                         COMPLETE_ATTRIBUTES)
+        elif lastchar in SEPS:
+            self._open_completions_later(False, False, False,
+                                         COMPLETE_FILES)
+
+    def autocomplete_event(self, event):
+        """Happens when the user wants to complete his word, and if necessary,
+        open a completion list after that (if there is more than one
+        completion)
+        """
+        if hasattr(event, "mc_state") and event.mc_state or\
+                not self.text.get("insert linestart", "insert").strip():
+            # A modifier was pressed along with the tab or
+            # there is only previous whitespace on this line, so tab.
+            return None
+        if self.autocompletewindow and self.autocompletewindow.is_active():
+            self.autocompletewindow.complete()
+            return "break"
+        else:
+            opened = self.open_completions(False, True, True)
+            return "break" if opened else None
+
+    def _open_completions_later(self, *args):
+        self._delayed_completion_index = self.text.index("insert")
+        if self._delayed_completion_id is not None:
+            self.text.after_cancel(self._delayed_completion_id)
+        self._delayed_completion_id = \
+            self.text.after(self.popupwait, self._delayed_open_completions,
+                            *args)
+
+    def _delayed_open_completions(self, *args):
+        self._delayed_completion_id = None
+        if self.text.index("insert") == self._delayed_completion_index:
+            self.open_completions(*args)
+
+    def open_completions(self, evalfuncs, complete, userWantsWin, mode=None):
+        """Find the completions and create the AutoCompleteWindow.
+        Return True if successful (no syntax error or so found).
+        if complete is True, then if there's nothing to complete and no
+        start of completion, won't open completions and return False.
+        If mode is given, will open a completion list only in this mode.
+        """
+        # Cancel another delayed call, if it exists.
+        if self._delayed_completion_id is not None:
+            self.text.after_cancel(self._delayed_completion_id)
+            self._delayed_completion_id = None
+
+        hp = HyperParser(self.editwin, "insert")
+        curline = self.text.get("insert linestart", "insert")
+        i = j = len(curline)
+        if hp.is_in_string() and (not mode or mode==COMPLETE_FILES):
+            # Find the beginning of the string
+            # fetch_completions will look at the file system to determine 
whether the
+            # string value constitutes an actual file name
+            # XXX could consider raw strings here and unescape the string 
value if it's
+            # not raw.
+            self._remove_autocomplete_window()
+            mode = COMPLETE_FILES
+            # Find last separator or string start
+            while i and curline[i-1] not in "'\"" + SEPS:
+                i -= 1
+            comp_start = curline[i:j]
+            j = i
+            # Find string start
+            while i and curline[i-1] not in "'\"":
+                i -= 1
+            comp_what = curline[i:j]
+        elif hp.is_in_code() and (not mode or mode==COMPLETE_ATTRIBUTES):
+            self._remove_autocomplete_window()
+            mode = COMPLETE_ATTRIBUTES
+            while i and (curline[i-1] in ID_CHARS or ord(curline[i-1]) > 127):
+                i -= 1
+            comp_start = curline[i:j]
+            if i and curline[i-1] == '.':
+                hp.set_index("insert-%dc" % (len(curline)-(i-1)))
+                comp_what = hp.get_expression()
+                if not comp_what or \
+                   (not evalfuncs and comp_what.find('(') != -1):
+                    return None
+            else:
+                comp_what = ""
+        else:
+            return None
+
+        if complete and not comp_what and not comp_start:
+            return None
+        comp_lists = self.fetch_completions(comp_what, mode)
+        if not comp_lists[0]:
+            return None
+        self.autocompletewindow = self._make_autocomplete_window()
+        return not self.autocompletewindow.show_window(
+                comp_lists, "insert-%dc" % len(comp_start),
+                complete, mode, userWantsWin)
+
+    def fetch_completions(self, what, mode):
+        """Return a pair of lists of completions for something. The first list
+        is a sublist of the second. Both are sorted.
+
+        If there is a Python subprocess, get the comp. list there.  Otherwise,
+        either fetch_completions() is running in the subprocess itself or it
+        was called in an IDLE EditorWindow before any script had been run.
+
+        The subprocess environment is that of the most recently run script.  If
+        two unrelated modules are being edited some calltips in the current
+        module may be inoperative if the module was not the last to run.
+        """
+        try:
+            rpcclt = self.editwin.flist.pyshell.interp.rpcclt
+        except:
+            rpcclt = None
+        if rpcclt:
+            return rpcclt.remotecall("exec", "get_the_completion_list",
+                                     (what, mode), {})
+        else:
+            if mode == COMPLETE_ATTRIBUTES:
+                if what == "":
+                    namespace = __main__.__dict__.copy()
+                    namespace.update(__main__.__builtins__.__dict__)
+                    bigl = eval("dir()", namespace)
+                    bigl.sort()
+                    if "__all__" in bigl:
+                        smalll = sorted(eval("__all__", namespace))
+                    else:
+                        smalll = [s for s in bigl if s[:1] != '_']
+                else:
+                    try:
+                        entity = self.get_entity(what)
+                        bigl = dir(entity)
+                        bigl.sort()
+                        if "__all__" in bigl:
+                            smalll = sorted(entity.__all__)
+                        else:
+                            smalll = [s for s in bigl if s[:1] != '_']
+                    except:
+                        return [], []
+
+            elif mode == COMPLETE_FILES:
+                if what == "":
+                    what = "."
+                try:
+                    expandedpath = os.path.expanduser(what)
+                    bigl = os.listdir(expandedpath)
+                    bigl.sort()
+                    smalll = [s for s in bigl if s[:1] != '.']
+                except OSError:
+                    return [], []
+
+            if not smalll:
+                smalll = bigl
+            return smalll, bigl
+
+    def get_entity(self, name):
+        """Lookup name in a namespace spanning sys.modules and __main.dict__"""
+        namespace = sys.modules.copy()
+        namespace.update(__main__.__dict__)
+        return eval(name, namespace)
+
+
+if __name__ == '__main__':
+    from unittest import main
+    main('idlelib.idle_test.test_autocomplete', verbosity=2)
diff --git a/lib-python/3/idlelib/autoexpand.py 
b/lib-python/3/idlelib/autoexpand.py
new file mode 100644
--- /dev/null
+++ b/lib-python/3/idlelib/autoexpand.py
@@ -0,0 +1,105 @@
+'''Complete the current word before the cursor with words in the editor.
+
+Each menu selection or shortcut key selection replaces the word with a
+different word with the same prefix. The search for matches begins
+before the target and moves toward the top of the editor. It then starts
+after the cursor and moves down. It then returns to the original word and
+the cycle starts again.
+
+Changing the current text line or leaving the cursor in a different
+place before requesting the next selection causes AutoExpand to reset
+its state.
+
+This is an extension file and there is only one instance of AutoExpand.
+'''
+import re
+import string
+
+###$ event <<expand-word>>
+###$ win <Alt-slash>
+###$ unix <Alt-slash>
+
+class AutoExpand:
+
+    menudefs = [
+        ('edit', [
+            ('E_xpand Word', '<<expand-word>>'),
+         ]),
+    ]
+
+    wordchars = string.ascii_letters + string.digits + "_"
+
+    def __init__(self, editwin):
+        self.text = editwin.text
+        self.bell = self.text.bell
+        self.state = None
+
+    def expand_word_event(self, event):
+        "Replace the current word with the next expansion."
+        curinsert = self.text.index("insert")
+        curline = self.text.get("insert linestart", "insert lineend")
+        if not self.state:
+            words = self.getwords()
+            index = 0
+        else:
+            words, index, insert, line = self.state
+            if insert != curinsert or line != curline:
+                words = self.getwords()
+                index = 0
+        if not words:
+            self.bell()
+            return "break"
+        word = self.getprevword()
+        self.text.delete("insert - %d chars" % len(word), "insert")
+        newword = words[index]
+        index = (index + 1) % len(words)
+        if index == 0:
+            self.bell()            # Warn we cycled around
+        self.text.insert("insert", newword)
+        curinsert = self.text.index("insert")
+        curline = self.text.get("insert linestart", "insert lineend")
+        self.state = words, index, curinsert, curline
+        return "break"
+
+    def getwords(self):
+        "Return a list of words that match the prefix before the cursor."
+        word = self.getprevword()
+        if not word:
+            return []
+        before = self.text.get("1.0", "insert wordstart")
+        wbefore = re.findall(r"\b" + word + r"\w+\b", before)
+        del before
+        after = self.text.get("insert wordend", "end")
+        wafter = re.findall(r"\b" + word + r"\w+\b", after)
+        del after
+        if not wbefore and not wafter:
+            return []
+        words = []
+        dict = {}
+        # search backwards through words before
+        wbefore.reverse()
+        for w in wbefore:
+            if dict.get(w):
+                continue
+            words.append(w)
+            dict[w] = w
+        # search onwards through words after
+        for w in wafter:
+            if dict.get(w):
+                continue
+            words.append(w)
+            dict[w] = w
+        words.append(word)
+        return words
+
+    def getprevword(self):
+        "Return the word prefix before the cursor."
+        line = self.text.get("insert linestart", "insert")
+        i = len(line)
+        while i > 0 and line[i-1] in self.wordchars:
+            i = i-1
+        return line[i:]
+
+if __name__ == '__main__':
+    import unittest
+    unittest.main('idlelib.idle_test.test_autoexpand', verbosity=2)
diff --git a/lib-python/3/idlelib/calltips.py b/lib-python/3/idlelib/calltips.py
new file mode 100644
--- /dev/null
+++ b/lib-python/3/idlelib/calltips.py
@@ -0,0 +1,175 @@
+"""calltips.py - An IDLE Extension to Jog Your Memory
+
+Call Tips are floating windows which display function, class, and method
+parameter and docstring information when you type an opening parenthesis, and
+which disappear when you type a closing parenthesis.
+
+"""
+import inspect
+import re
+import sys
+import textwrap
+import types
+
+from idlelib import calltip_w
+from idlelib.hyperparser import HyperParser
+import __main__
+
+class CallTips:
+
+    menudefs = [
+        ('edit', [
+            ("Show call tip", "<<force-open-calltip>>"),
+        ])
+    ]
+
+    def __init__(self, editwin=None):
+        if editwin is None:  # subprocess and test
+            self.editwin = None
+        else:
+            self.editwin = editwin
+            self.text = editwin.text
+            self.active_calltip = None
+            self._calltip_window = self._make_tk_calltip_window
+
+    def close(self):
+        self._calltip_window = None
+
+    def _make_tk_calltip_window(self):
+        # See __init__ for usage
+        return calltip_w.CallTip(self.text)
+
+    def _remove_calltip_window(self, event=None):
+        if self.active_calltip:
+            self.active_calltip.hidetip()
+            self.active_calltip = None
+
+    def force_open_calltip_event(self, event):
+        "The user selected the menu entry or hotkey, open the tip."
+        self.open_calltip(True)
+
+    def try_open_calltip_event(self, event):
+        """Happens when it would be nice to open a CallTip, but not really
+        necessary, for example after an opening bracket, so function calls
+        won't be made.
+        """
+        self.open_calltip(False)
+
+    def refresh_calltip_event(self, event):
+        if self.active_calltip and self.active_calltip.is_active():
+            self.open_calltip(False)
+
+    def open_calltip(self, evalfuncs):
+        self._remove_calltip_window()
+
+        hp = HyperParser(self.editwin, "insert")
+        sur_paren = hp.get_surrounding_brackets('(')
+        if not sur_paren:
+            return
+        hp.set_index(sur_paren[0])
+        expression  = hp.get_expression()
+        if not expression:
+            return
+        if not evalfuncs and (expression.find('(') != -1):
+            return
+        argspec = self.fetch_tip(expression)
+        if not argspec:
+            return
+        self.active_calltip = self._calltip_window()
+        self.active_calltip.showtip(argspec, sur_paren[0], sur_paren[1])
+
+    def fetch_tip(self, expression):
+        """Return the argument list and docstring of a function or class.
+
+        If there is a Python subprocess, get the calltip there.  Otherwise,
+        either this fetch_tip() is running in the subprocess or it was
+        called in an IDLE running without the subprocess.
+
+        The subprocess environment is that of the most recently run script.  If
+        two unrelated modules are being edited some calltips in the current
+        module may be inoperative if the module was not the last to run.
+
+        To find methods, fetch_tip must be fed a fully qualified name.
+
+        """
+        try:
+            rpcclt = self.editwin.flist.pyshell.interp.rpcclt
+        except AttributeError:
+            rpcclt = None
+        if rpcclt:
+            return rpcclt.remotecall("exec", "get_the_calltip",
+                                     (expression,), {})
+        else:
+            return get_argspec(get_entity(expression))
+
+def get_entity(expression):
+    """Return the object corresponding to expression evaluated
+    in a namespace spanning sys.modules and __main.dict__.
+    """
+    if expression:
+        namespace = sys.modules.copy()
+        namespace.update(__main__.__dict__)
+        try:
+            return eval(expression, namespace)
+        except BaseException:
+            # An uncaught exception closes idle, and eval can raise any
+            # exception, especially if user classes are involved.
+            return None
+
+# The following are used in get_argspec and some in tests
+_MAX_COLS = 85
+_MAX_LINES = 5  # enough for bytes
+_INDENT = ' '*4  # for wrapped signatures
+_first_param = re.compile(r'(?<=\()\w*\,?\s*')
+_default_callable_argspec = "See source or doc"
+
+
+def get_argspec(ob):
+    '''Return a string describing the signature of a callable object, or ''.
+
+    For Python-coded functions and methods, the first line is introspected.
+    Delete 'self' parameter for classes (.__init__) and bound methods.
+    The next lines are the first lines of the doc string up to the first
+    empty line or _MAX_LINES.    For builtins, this typically includes
+    the arguments in addition to the return value.
+    '''
+    argspec = ""
+    try:
+        ob_call = ob.__call__
+    except BaseException:
+        return argspec
+    if isinstance(ob, type):
+        fob = ob.__init__
+    elif isinstance(ob_call, types.MethodType):
+        fob = ob_call
+    else:
+        fob = ob
+    if isinstance(fob, (types.FunctionType, types.MethodType)):
+        argspec = inspect.formatargspec(*inspect.getfullargspec(fob))
+        if (isinstance(ob, (type, types.MethodType)) or
+                isinstance(ob_call, types.MethodType)):
+            argspec = _first_param.sub("", argspec)
+
+    lines = (textwrap.wrap(argspec, _MAX_COLS, subsequent_indent=_INDENT)
+            if len(argspec) > _MAX_COLS else [argspec] if argspec else [])
+
+    if isinstance(ob_call, types.MethodType):
+        doc = ob_call.__doc__
+    else:
+        doc = getattr(ob, "__doc__", "")
+    if doc:
+        for line in doc.split('\n', _MAX_LINES)[:_MAX_LINES]:
+            line = line.strip()
+            if not line:
+                break
+            if len(line) > _MAX_COLS:
+                line = line[: _MAX_COLS - 3] + '...'
+            lines.append(line)
+        argspec = '\n'.join(lines)
+    if not argspec:
+        argspec = _default_callable_argspec
+    return argspec
+
+if __name__ == '__main__':
+    from unittest import main
+    main('idlelib.idle_test.test_calltips', verbosity=2)
diff --git a/lib-python/3/idlelib/codecontext.py 
b/lib-python/3/idlelib/codecontext.py
new file mode 100644
--- /dev/null
+++ b/lib-python/3/idlelib/codecontext.py
@@ -0,0 +1,178 @@
+"""codecontext - Extension to display the block context above the edit window
+
+Once code has scrolled off the top of a window, it can be difficult to
+determine which block you are in.  This extension implements a pane at the top
+of each IDLE edit window which provides block structure hints.  These hints are
+the lines which contain the block opening keywords, e.g. 'if', for the
+enclosing block.  The number of hint lines is determined by the numlines
+variable in the codecontext section of config-extensions.def. Lines which do
+not open blocks are not shown in the context hints pane.
+
+"""
+import re
+from sys import maxsize as INFINITY
+
+import tkinter
+from tkinter.constants import TOP, LEFT, X, W, SUNKEN
+
+from idlelib.config import idleConf
+
+BLOCKOPENERS = {"class", "def", "elif", "else", "except", "finally", "for",
+                    "if", "try", "while", "with"}
+UPDATEINTERVAL = 100 # millisec
+FONTUPDATEINTERVAL = 1000 # millisec
+
+getspacesfirstword =\
+                   lambda s, c=re.compile(r"^(\s*)(\w*)"): c.match(s).groups()
+
+class CodeContext:
+    menudefs = [('options', [('!Code Conte_xt', '<<toggle-code-context>>')])]
+    context_depth = idleConf.GetOption("extensions", "CodeContext",
+                                       "numlines", type="int", default=3)
+    bgcolor = idleConf.GetOption("extensions", "CodeContext",
+                                 "bgcolor", type="str", default="LightGray")
+    fgcolor = idleConf.GetOption("extensions", "CodeContext",
+                                 "fgcolor", type="str", default="Black")
+    def __init__(self, editwin):
+        self.editwin = editwin
+        self.text = editwin.text
+        self.textfont = self.text["font"]
+        self.label = None
+        # self.info is a list of (line number, indent level, line text, block
+        # keyword) tuples providing the block structure associated with
+        # self.topvisible (the linenumber of the line displayed at the top of
+        # the edit window). self.info[0] is initialized as a 'dummy' line which
+        # starts the toplevel 'block' of the module.
+        self.info = [(0, -1, "", False)]
+        self.topvisible = 1
+        visible = idleConf.GetOption("extensions", "CodeContext",
+                                     "visible", type="bool", default=False)
+        if visible:
+            self.toggle_code_context_event()
+            self.editwin.setvar('<<toggle-code-context>>', True)
+        # Start two update cycles, one for context lines, one for font changes.
+        self.text.after(UPDATEINTERVAL, self.timer_event)
+        self.text.after(FONTUPDATEINTERVAL, self.font_timer_event)
+
+    def toggle_code_context_event(self, event=None):
+        if not self.label:
+            # Calculate the border width and horizontal padding required to
+            # align the context with the text in the main Text widget.
+            #
+            # All values are passed through getint(), since some
+            # values may be pixel objects, which can't simply be added to ints.
+            widgets = self.editwin.text, self.editwin.text_frame
+            # Calculate the required vertical padding
+            padx = 0
+            for widget in widgets:
+                padx += widget.tk.getint(widget.pack_info()['padx'])
+                padx += widget.tk.getint(widget.cget('padx'))
+            # Calculate the required border width
+            border = 0
+            for widget in widgets:
+                border += widget.tk.getint(widget.cget('border'))
+            self.label = tkinter.Label(self.editwin.top,
+                                       text="\n" * (self.context_depth - 1),
+                                       anchor=W, justify=LEFT,
+                                       font=self.textfont,
+                                       bg=self.bgcolor, fg=self.fgcolor,
+                                       width=1, #don't request more than we get
+                                       padx=padx, border=border,
+                                       relief=SUNKEN)
+            # Pack the label widget before and above the text_frame widget,
+            # thus ensuring that it will appear directly above text_frame
+            self.label.pack(side=TOP, fill=X, expand=False,
+                            before=self.editwin.text_frame)
+        else:
+            self.label.destroy()
+            self.label = None
+        idleConf.SetOption("extensions", "CodeContext", "visible",
+                           str(self.label is not None))
+        idleConf.SaveUserCfgFiles()
+
+    def get_line_info(self, linenum):
+        """Get the line indent value, text, and any block start keyword
+
+        If the line does not start a block, the keyword value is False.
+        The indentation of empty lines (or comment lines) is INFINITY.
+
+        """
+        text = self.text.get("%d.0" % linenum, "%d.end" % linenum)
+        spaces, firstword = getspacesfirstword(text)
+        opener = firstword in BLOCKOPENERS and firstword
+        if len(text) == len(spaces) or text[len(spaces)] == '#':
+            indent = INFINITY
+        else:
+            indent = len(spaces)
+        return indent, text, opener
+
+    def get_context(self, new_topvisible, stopline=1, stopindent=0):
+        """Get context lines, starting at new_topvisible and working backwards.
+
+        Stop when stopline or stopindent is reached. Return a tuple of context
+        data and the indent level at the top of the region inspected.
+
+        """
+        assert stopline > 0
+        lines = []
+        # The indentation level we are currently in:
+        lastindent = INFINITY
+        # For a line to be interesting, it must begin with a block opening
+        # keyword, and have less indentation than lastindent.
+        for linenum in range(new_topvisible, stopline-1, -1):
+            indent, text, opener = self.get_line_info(linenum)
+            if indent < lastindent:
+                lastindent = indent
+                if opener in ("else", "elif"):
+                    # We also show the if statement
+                    lastindent += 1
+                if opener and linenum < new_topvisible and indent >= 
stopindent:
+                    lines.append((linenum, indent, text, opener))
+                if lastindent <= stopindent:
+                    break
+        lines.reverse()
+        return lines, lastindent
+
+    def update_code_context(self):
+        """Update context information and lines visible in the context pane.
+
+        """
+        new_topvisible = int(self.text.index("@0,0").split('.')[0])
+        if self.topvisible == new_topvisible:      # haven't scrolled
+            return
+        if self.topvisible < new_topvisible:       # scroll down
+            lines, lastindent = self.get_context(new_topvisible,
+                                                 self.topvisible)
+            # retain only context info applicable to the region
+            # between topvisible and new_topvisible:
+            while self.info[-1][1] >= lastindent:
+                del self.info[-1]
+        elif self.topvisible > new_topvisible:     # scroll up
+            stopindent = self.info[-1][1] + 1
+            # retain only context info associated
+            # with lines above new_topvisible:
+            while self.info[-1][0] >= new_topvisible:
+                stopindent = self.info[-1][1]
+                del self.info[-1]
+            lines, lastindent = self.get_context(new_topvisible,
+                                                 self.info[-1][0]+1,
+                                                 stopindent)
+        self.info.extend(lines)
+        self.topvisible = new_topvisible
+        # empty lines in context pane:
+        context_strings = [""] * max(0, self.context_depth - len(self.info))
+        # followed by the context hint lines:
+        context_strings += [x[2] for x in self.info[-self.context_depth:]]
+        self.label["text"] = '\n'.join(context_strings)
+
+    def timer_event(self):
+        if self.label:
+            self.update_code_context()
+        self.text.after(UPDATEINTERVAL, self.timer_event)
+
+    def font_timer_event(self):
+        newtextfont = self.text["font"]
+        if self.label and newtextfont != self.textfont:
+            self.textfont = newtextfont
+            self.label["font"] = self.textfont
+        self.text.after(FONTUPDATEINTERVAL, self.font_timer_event)
diff --git a/lib-python/3/idlelib/configdialog.py 
b/lib-python/3/idlelib/configdialog.py
new file mode 100644
--- /dev/null
+++ b/lib-python/3/idlelib/configdialog.py
@@ -0,0 +1,1467 @@
+"""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.
+
+"""
+from tkinter import *
+from tkinter.ttk import Scrollbar
+import tkinter.colorchooser as tkColorChooser
+import tkinter.font as tkFont
+import tkinter.messagebox as tkMessageBox
+
+from idlelib.config import idleConf
+from idlelib.config_key import GetKeysDialog
+from idlelib.dynoption import DynOptionMenu
+from idlelib import macosx
+from idlelib.query import SectionName, HelpSource
+from idlelib.tabbedpages import TabbedPageSet
+from idlelib.textview import view_text
+
+class ConfigDialog(Toplevel):
+
+    def __init__(self, parent, title='', _htest=False, _utest=False):
+        """
+        _htest - bool, change box location when running htest
+        _utest - bool, don't wait_window when running unittest
+        """
+        Toplevel.__init__(self, parent)
+        self.parent = parent
+        if _htest:
+            parent.instance_dict = {}
+        self.wm_withdraw()
+
+        self.configure(borderwidth=5)
+        self.title(title or 'IDLE Preferences')
+        self.geometry(
+                "+%d+%d" % (parent.winfo_rootx() + 20,
+                parent.winfo_rooty() + (30 if not _htest else 150)))
+        #Theme Elements. 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.themeElements={
+            'Normal Text': ('normal', '00'),
+            'Python Keywords': ('keyword', '01'),
+            'Python Definitions': ('definition', '02'),
+            'Python Builtins': ('builtin', '03'),
+            'Python Comments': ('comment', '04'),
+            'Python Strings': ('string', '05'),
+            'Selected Text': ('hilite', '06'),
+            'Found Text': ('hit', '07'),
+            'Cursor': ('cursor', '08'),
+            'Editor Breakpoint': ('break', '09'),
+            'Shell Normal Text': ('console', '10'),
+            'Shell Error Text': ('error', '11'),
+            'Shell Stdout Text': ('stdout', '12'),
+            'Shell Stderr Text': ('stderr', '13'),
+            }
+        self.ResetChangedItems() #load initial values in changed items dict
+        self.CreateWidgets()
+        self.resizable(height=FALSE, width=FALSE)
+        self.transient(parent)
+        self.grab_set()
+        self.protocol("WM_DELETE_WINDOW", self.Cancel)
+        self.tabPages.focus_set()
+        #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
+        self.LoadConfigs()
+        self.AttachVarCallbacks() #avoid callbacks during LoadConfigs
+
+        if not _utest:
+            self.wm_deiconify()
+            self.wait_window()
+
+    def CreateWidgets(self):
+        self.tabPages = TabbedPageSet(self,
+                page_names=['Fonts/Tabs', 'Highlighting', 'Keys', 'General',
+                            'Extensions'])
+        self.tabPages.pack(side=TOP, expand=TRUE, fill=BOTH)
+        self.CreatePageFontTab()
+        self.CreatePageHighlight()
+        self.CreatePageKeys()
+        self.CreatePageGeneral()
+        self.CreatePageExtensions()
+        self.create_action_buttons().pack(side=BOTTOM)
+
+    def create_action_buttons(self):
+        if macosx.isAquaTk():
+            # Changing the default padding on OSX results in unreadable
+            # text in the buttons
+            paddingArgs = {}
+        else:
+            paddingArgs = {'padx':6, 'pady':3}
+        outer = Frame(self, pady=2)
+        buttons = Frame(outer, pady=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,
+                   **paddingArgs).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 CreatePageFontTab(self):
+        parent = self.parent
+        self.fontSize = StringVar(parent)
+        self.fontBold = BooleanVar(parent)
+        self.fontName = StringVar(parent)
+        self.spaceNum = IntVar(parent)
+        self.editFont = tkFont.Font(parent, ('courier', 10, 'normal'))
+
+        ##widget creation
+        #body frame
+        frame = self.tabPages.pages['Fonts/Tabs'].frame
+        #body section frames
+        frameFont = LabelFrame(
+                frame, borderwidth=2, relief=GROOVE, text=' Base Editor Font ')
+        frameIndent = LabelFrame(
+                frame, borderwidth=2, relief=GROOVE, text=' Indentation Width 
')
+        #frameFont
+        frameFontName = Frame(frameFont)
+        frameFontParam = Frame(frameFont)
+        labelFontNameTitle = Label(
+                frameFontName, justify=LEFT, text='Font Face :')
+        self.listFontName = Listbox(
+                frameFontName, height=5, takefocus=FALSE, 
exportselection=FALSE)
+        self.listFontName.bind(
+                '<ButtonRelease-1>', self.OnListFontButtonRelease)
+        scrollFont = Scrollbar(frameFontName)
+        scrollFont.config(command=self.listFontName.yview)
+        self.listFontName.config(yscrollcommand=scrollFont.set)
+        labelFontSizeTitle = Label(frameFontParam, text='Size :')
+        self.optMenuFontSize = DynOptionMenu(
+                frameFontParam, self.fontSize, None, 
command=self.SetFontSample)
+        checkFontBold = Checkbutton(
+                frameFontParam, variable=self.fontBold, onvalue=1,
+                offvalue=0, text='Bold', command=self.SetFontSample)
+        frameFontSample = Frame(frameFont, relief=SOLID, borderwidth=1)
+        self.labelFontSample = Label(
+                frameFontSample, justify=LEFT, font=self.editFont,
+                text='AaBbCcDdEe\nFfGgHhIiJjK\n1234567890\n#:+=(){}[]')
+        #frameIndent
+        frameIndentSize = Frame(frameIndent)
+        labelSpaceNumTitle = Label(
+                frameIndentSize, justify=LEFT,
+                text='Python Standard: 4 Spaces!')
+        self.scaleSpaceNum = Scale(
+                frameIndentSize, variable=self.spaceNum,
+                orient='horizontal', tickinterval=2, from_=2, to=16)
+
+        #widget packing
+        #body
+        frameFont.pack(side=LEFT, padx=5, pady=5, expand=TRUE, fill=BOTH)
+        frameIndent.pack(side=LEFT, padx=5, pady=5, fill=Y)
+        #frameFont
+        frameFontName.pack(side=TOP, padx=5, pady=5, fill=X)
+        frameFontParam.pack(side=TOP, padx=5, pady=5, fill=X)
+        labelFontNameTitle.pack(side=TOP, anchor=W)
+        self.listFontName.pack(side=LEFT, expand=TRUE, fill=X)
+        scrollFont.pack(side=LEFT, fill=Y)
+        labelFontSizeTitle.pack(side=LEFT, anchor=W)
+        self.optMenuFontSize.pack(side=LEFT, anchor=W)
+        checkFontBold.pack(side=LEFT, anchor=W, padx=20)
+        frameFontSample.pack(side=TOP, padx=5, pady=5, expand=TRUE, fill=BOTH)
+        self.labelFontSample.pack(expand=TRUE, fill=BOTH)
+        #frameIndent
+        frameIndentSize.pack(side=TOP, fill=X)
+        labelSpaceNumTitle.pack(side=TOP, anchor=W, padx=5)
+        self.scaleSpaceNum.pack(side=TOP, padx=5, fill=X)
+        return frame
+
+    def CreatePageHighlight(self):
+        parent = self.parent
+        self.builtinTheme = StringVar(parent)
+        self.customTheme = StringVar(parent)
+        self.fgHilite = BooleanVar(parent)
+        self.colour = StringVar(parent)
+        self.fontName = StringVar(parent)
+        self.themeIsBuiltin = BooleanVar(parent)
+        self.highlightTarget = StringVar(parent)
+
+        ##widget creation
+        #body frame
+        frame = self.tabPages.pages['Highlighting'].frame
+        #body section frames
+        frameCustom = LabelFrame(frame, borderwidth=2, relief=GROOVE,
+                                 text=' Custom Highlighting ')
+        frameTheme = LabelFrame(frame, borderwidth=2, relief=GROOVE,
+                                text=' Highlighting Theme ')
+        #frameCustom
+        self.textHighlightSample=Text(
+                frameCustom, relief=SOLID, borderwidth=1,
+                font=('courier', 12, ''), cursor='hand2', width=21, height=11,
+                takefocus=FALSE, highlightthickness=0, wrap=NONE)
+        text=self.textHighlightSample
+        text.bind('<Double-Button-1>', lambda e: 'break')
+        text.bind('<B1-Motion>', lambda e: 'break')
+        textAndTags=(
+            ('#you can click here', 'comment'), ('\n', 'normal'),
+            ('#to choose items', 'comment'), ('\n', 'normal'),
+            ('def', 'keyword'), (' ', 'normal'),
+            ('func', 'definition'), ('(param):\n  ', 'normal'),
+            ('"""string"""', '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'),
+            (' error ', 'error'), (' ', 'normal'),
+            ('cursor |', 'cursor'), ('\n ', 'normal'),
+            ('shell', 'console'), (' ', 'normal'),
+            ('stdout', 'stdout'), (' ', 'normal'),
+            ('stderr', 'stderr'), ('\n', 'normal'))
+        for txTa in textAndTags:
+            text.insert(END, txTa[0], txTa[1])
+        for element in self.themeElements:
+            def tem(event, elem=element):
+                event.widget.winfo_toplevel().highlightTarget.set(elem)
+            text.tag_bind(
+                    self.themeElements[element][0], '<ButtonPress-1>', tem)
+        text.config(state=DISABLED)
+        self.frameColourSet = Frame(frameCustom, relief=SOLID, borderwidth=1)
+        frameFgBg = Frame(frameCustom)
+        buttonSetColour = Button(
+                self.frameColourSet, text='Choose Colour for :',
+                command=self.GetColour, highlightthickness=0)
+        self.optMenuHighlightTarget = DynOptionMenu(
+                self.frameColourSet, self.highlightTarget, None,
+                highlightthickness=0) #, command=self.SetHighlightTargetBinding
+        self.radioFg = Radiobutton(
+                frameFgBg, variable=self.fgHilite, value=1,
+                text='Foreground', command=self.SetColourSampleBinding)
+        self.radioBg=Radiobutton(
+                frameFgBg, variable=self.fgHilite, value=0,
+                text='Background', command=self.SetColourSampleBinding)
+        self.fgHilite.set(1)
+        buttonSaveCustomTheme = Button(
+                frameCustom, text='Save as New Custom Theme',
+                command=self.SaveAsNewTheme)
+        #frameTheme
+        labelTypeTitle = Label(frameTheme, text='Select : ')
+        self.radioThemeBuiltin = Radiobutton(
+                frameTheme, variable=self.themeIsBuiltin, value=1,
+                command=self.SetThemeType, text='a Built-in Theme')
+        self.radioThemeCustom = Radiobutton(
+                frameTheme, variable=self.themeIsBuiltin, value=0,
+                command=self.SetThemeType, text='a Custom Theme')
+        self.optMenuThemeBuiltin = DynOptionMenu(
+                frameTheme, self.builtinTheme, None, command=None)
+        self.optMenuThemeCustom=DynOptionMenu(
+                frameTheme, self.customTheme, None, command=None)
+        self.buttonDeleteCustomTheme=Button(
+                frameTheme, text='Delete Custom Theme',
+                command=self.DeleteCustomTheme)
+        self.new_custom_theme = Label(frameTheme, bd=2)
+
+        ##widget packing
+        #body
+        frameCustom.pack(side=LEFT, padx=5, pady=5, expand=TRUE, fill=BOTH)
+        frameTheme.pack(side=LEFT, padx=5, pady=5, fill=Y)
+        #frameCustom
+        self.frameColourSet.pack(side=TOP, padx=5, pady=5, expand=TRUE, fill=X)
+        frameFgBg.pack(side=TOP, padx=5, pady=0)
+        self.textHighlightSample.pack(
+                side=TOP, padx=5, pady=5, expand=TRUE, fill=BOTH)
+        buttonSetColour.pack(side=TOP, expand=TRUE, fill=X, padx=8, pady=4)
+        self.optMenuHighlightTarget.pack(
+                side=TOP, expand=TRUE, fill=X, padx=8, pady=3)
+        self.radioFg.pack(side=LEFT, anchor=E)
+        self.radioBg.pack(side=RIGHT, anchor=W)
+        buttonSaveCustomTheme.pack(side=BOTTOM, fill=X, padx=5, pady=5)
+        #frameTheme
+        labelTypeTitle.pack(side=TOP, anchor=W, padx=5, pady=5)
+        self.radioThemeBuiltin.pack(side=TOP, anchor=W, padx=5)
+        self.radioThemeCustom.pack(side=TOP, anchor=W, padx=5, pady=2)
+        self.optMenuThemeBuiltin.pack(side=TOP, fill=X, padx=5, pady=5)
+        self.optMenuThemeCustom.pack(side=TOP, fill=X, anchor=W, padx=5, 
pady=5)
+        self.buttonDeleteCustomTheme.pack(side=TOP, fill=X, padx=5, pady=5)
+        self.new_custom_theme.pack(side=TOP, fill=X, pady=5)
+        return frame
+
+    def CreatePageKeys(self):
+        parent = self.parent
+        self.bindingTarget = StringVar(parent)
+        self.builtinKeys = StringVar(parent)
+        self.customKeys = StringVar(parent)
+        self.keysAreBuiltin = BooleanVar(parent)
+        self.keyBinding = StringVar(parent)
+
+        ##widget creation
+        #body frame
+        frame = self.tabPages.pages['Keys'].frame
+        #body section frames
+        frameCustom = LabelFrame(
+                frame, borderwidth=2, relief=GROOVE,
+                text=' Custom Key Bindings ')
+        frameKeySets = LabelFrame(
+                frame, borderwidth=2, relief=GROOVE, text=' Key Set ')
+        #frameCustom
+        frameTarget = Frame(frameCustom)
+        labelTargetTitle = Label(frameTarget, text='Action - Key(s)')
+        scrollTargetY = Scrollbar(frameTarget)
+        scrollTargetX = Scrollbar(frameTarget, orient=HORIZONTAL)
+        self.listBindings = Listbox(
+                frameTarget, takefocus=FALSE, exportselection=FALSE)
+        self.listBindings.bind('<ButtonRelease-1>', self.KeyBindingSelected)
+        scrollTargetY.config(command=self.listBindings.yview)
+        scrollTargetX.config(command=self.listBindings.xview)
+        self.listBindings.config(yscrollcommand=scrollTargetY.set)
+        self.listBindings.config(xscrollcommand=scrollTargetX.set)
+        self.buttonNewKeys = Button(
+                frameCustom, text='Get New Keys for Selection',
+                command=self.GetNewKeys, state=DISABLED)
+        #frameKeySets
+        frames = [Frame(frameKeySets, padx=2, pady=2, borderwidth=0)
+                  for i in range(2)]
+        self.radioKeysBuiltin = Radiobutton(
+                frames[0], variable=self.keysAreBuiltin, value=1,
+                command=self.SetKeysType, text='Use a Built-in Key Set')
+        self.radioKeysCustom = Radiobutton(
+                frames[0], variable=self.keysAreBuiltin,  value=0,
+                command=self.SetKeysType, text='Use a Custom Key Set')
+        self.optMenuKeysBuiltin = DynOptionMenu(
+                frames[0], self.builtinKeys, None, command=None)
+        self.optMenuKeysCustom = DynOptionMenu(
+                frames[0], self.customKeys, None, command=None)
+        self.buttonDeleteCustomKeys = Button(
+                frames[1], text='Delete Custom Key Set',
+                command=self.DeleteCustomKeys)
+        buttonSaveCustomKeys = Button(
+                frames[1], text='Save as New Custom Key Set',
+                command=self.SaveAsNewKeySet)
+        self.new_custom_keys = Label(frames[0], bd=2)
+
+        ##widget packing
+        #body
+        frameCustom.pack(side=BOTTOM, padx=5, pady=5, expand=TRUE, fill=BOTH)
+        frameKeySets.pack(side=BOTTOM, padx=5, pady=5, fill=BOTH)
+        #frameCustom
+        self.buttonNewKeys.pack(side=BOTTOM, fill=X, padx=5, pady=5)
+        frameTarget.pack(side=LEFT, padx=5, pady=5, expand=TRUE, fill=BOTH)
+        #frame target
+        frameTarget.columnconfigure(0, weight=1)
+        frameTarget.rowconfigure(1, weight=1)
+        labelTargetTitle.grid(row=0, column=0, columnspan=2, sticky=W)
+        self.listBindings.grid(row=1, column=0, sticky=NSEW)
+        scrollTargetY.grid(row=1, column=1, sticky=NS)
+        scrollTargetX.grid(row=2, column=0, sticky=EW)
+        #frameKeySets
+        self.radioKeysBuiltin.grid(row=0, column=0, sticky=W+NS)
+        self.radioKeysCustom.grid(row=1, column=0, sticky=W+NS)
+        self.optMenuKeysBuiltin.grid(row=0, column=1, sticky=NSEW)
+        self.optMenuKeysCustom.grid(row=1, column=1, sticky=NSEW)
+        self.new_custom_keys.grid(row=0, column=2, sticky=NSEW, padx=5, pady=5)
+        self.buttonDeleteCustomKeys.pack(side=LEFT, fill=X, expand=True, 
padx=2)
+        buttonSaveCustomKeys.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 CreatePageGeneral(self):
+        parent = self.parent
+        self.winWidth = StringVar(parent)
+        self.winHeight = StringVar(parent)
+        self.startupEdit = IntVar(parent)
+        self.autoSave = IntVar(parent)
+        self.encoding = StringVar(parent)
+        self.userHelpBrowser = BooleanVar(parent)
+        self.helpBrowser = StringVar(parent)
+
+        #widget creation
+        #body
+        frame = self.tabPages.pages['General'].frame
+        #body section frames
+        frameRun = LabelFrame(frame, borderwidth=2, relief=GROOVE,
+                              text=' Startup Preferences ')
+        frameSave = LabelFrame(frame, borderwidth=2, relief=GROOVE,
+                               text=' Autosave Preferences ')
+        frameWinSize = Frame(frame, borderwidth=2, relief=GROOVE)
+        frameHelp = LabelFrame(frame, borderwidth=2, relief=GROOVE,
+                               text=' Additional Help Sources ')
+        #frameRun
+        labelRunChoiceTitle = Label(frameRun, text='At Startup')
+        self.radioStartupEdit = Radiobutton(
+                frameRun, variable=self.startupEdit, value=1,
+                text="Open Edit Window")
+        self.radioStartupShell = Radiobutton(
+                frameRun, variable=self.startupEdit, value=0,
+                text='Open Shell Window')
+        #frameSave
+        labelRunSaveTitle = Label(frameSave, text='At Start of Run (F5)  ')
+        self.radioSaveAsk = Radiobutton(
+                frameSave, variable=self.autoSave, value=0,
+                text="Prompt to Save")
+        self.radioSaveAuto = Radiobutton(
+                frameSave, variable=self.autoSave, value=1,
+                text='No Prompt')
+        #frameWinSize
+        labelWinSizeTitle = Label(
+                frameWinSize, text='Initial Window Size  (in characters)')
+        labelWinWidthTitle = Label(frameWinSize, text='Width')
+        self.entryWinWidth = Entry(
+                frameWinSize, textvariable=self.winWidth, width=3)
+        labelWinHeightTitle = Label(frameWinSize, text='Height')
+        self.entryWinHeight = Entry(
+                frameWinSize, textvariable=self.winHeight, width=3)
+        #frameHelp
+        frameHelpList = Frame(frameHelp)
+        frameHelpListButtons = Frame(frameHelpList)
+        scrollHelpList = Scrollbar(frameHelpList)
+        self.listHelp = Listbox(
+                frameHelpList, height=5, takefocus=FALSE,
+                exportselection=FALSE)
+        scrollHelpList.config(command=self.listHelp.yview)
+        self.listHelp.config(yscrollcommand=scrollHelpList.set)
+        self.listHelp.bind('<ButtonRelease-1>', self.HelpSourceSelected)
+        self.buttonHelpListEdit = Button(
+                frameHelpListButtons, text='Edit', state=DISABLED,
+                width=8, command=self.HelpListItemEdit)
+        self.buttonHelpListAdd = Button(
+                frameHelpListButtons, text='Add',
+                width=8, command=self.HelpListItemAdd)
+        self.buttonHelpListRemove = Button(
+                frameHelpListButtons, text='Remove', state=DISABLED,
+                width=8, command=self.HelpListItemRemove)
+
+        #widget packing
+        #body
+        frameRun.pack(side=TOP, padx=5, pady=5, fill=X)
+        frameSave.pack(side=TOP, padx=5, pady=5, fill=X)
+        frameWinSize.pack(side=TOP, padx=5, pady=5, fill=X)
+        frameHelp.pack(side=TOP, padx=5, pady=5, expand=TRUE, fill=BOTH)
+        #frameRun
+        labelRunChoiceTitle.pack(side=LEFT, anchor=W, padx=5, pady=5)
+        self.radioStartupShell.pack(side=RIGHT, anchor=W, padx=5, pady=5)
+        self.radioStartupEdit.pack(side=RIGHT, anchor=W, padx=5, pady=5)
+        #frameSave
+        labelRunSaveTitle.pack(side=LEFT, anchor=W, padx=5, pady=5)
+        self.radioSaveAuto.pack(side=RIGHT, anchor=W, padx=5, pady=5)
+        self.radioSaveAsk.pack(side=RIGHT, anchor=W, padx=5, pady=5)
+        #frameWinSize
+        labelWinSizeTitle.pack(side=LEFT, anchor=W, padx=5, pady=5)
+        self.entryWinHeight.pack(side=RIGHT, anchor=E, padx=10, pady=5)
+        labelWinHeightTitle.pack(side=RIGHT, anchor=E, pady=5)
+        self.entryWinWidth.pack(side=RIGHT, anchor=E, padx=10, pady=5)
+        labelWinWidthTitle.pack(side=RIGHT, anchor=E, pady=5)
+        #frameHelp
+        frameHelpListButtons.pack(side=RIGHT, padx=5, pady=5, fill=Y)
+        frameHelpList.pack(side=TOP, padx=5, pady=5, expand=TRUE, fill=BOTH)
+        scrollHelpList.pack(side=RIGHT, anchor=W, fill=Y)
+        self.listHelp.pack(side=LEFT, anchor=E, expand=TRUE, fill=BOTH)
+        self.buttonHelpListEdit.pack(side=TOP, anchor=W, pady=5)
+        self.buttonHelpListAdd.pack(side=TOP, anchor=W)
+        self.buttonHelpListRemove.pack(side=TOP, anchor=W, pady=5)
+        return frame
+
+    def AttachVarCallbacks(self):
+        self.fontSize.trace_add('write', self.VarChanged_font)
+        self.fontName.trace_add('write', self.VarChanged_font)
+        self.fontBold.trace_add('write', self.VarChanged_font)
+        self.spaceNum.trace_add('write', self.VarChanged_spaceNum)
+        self.colour.trace_add('write', self.VarChanged_colour)
+        self.builtinTheme.trace_add('write', self.VarChanged_builtinTheme)
+        self.customTheme.trace_add('write', self.VarChanged_customTheme)
+        self.themeIsBuiltin.trace_add('write', self.VarChanged_themeIsBuiltin)
+        self.highlightTarget.trace_add('write', 
self.VarChanged_highlightTarget)
+        self.keyBinding.trace_add('write', self.VarChanged_keyBinding)
+        self.builtinKeys.trace_add('write', self.VarChanged_builtinKeys)
+        self.customKeys.trace_add('write', self.VarChanged_customKeys)
+        self.keysAreBuiltin.trace_add('write', self.VarChanged_keysAreBuiltin)
+        self.winWidth.trace_add('write', self.VarChanged_winWidth)
+        self.winHeight.trace_add('write', self.VarChanged_winHeight)
+        self.startupEdit.trace_add('write', self.VarChanged_startupEdit)
+        self.autoSave.trace_add('write', self.VarChanged_autoSave)
+        self.encoding.trace_add('write', self.VarChanged_encoding)
+
+    def remove_var_callbacks(self):
+        "Remove callbacks to prevent memory leaks."
+        for var in (
+                self.fontSize, self.fontName, self.fontBold,
+                self.spaceNum, self.colour, self.builtinTheme,
+                self.customTheme, self.themeIsBuiltin, self.highlightTarget,
+                self.keyBinding, self.builtinKeys, self.customKeys,
+                self.keysAreBuiltin, self.winWidth, self.winHeight,
+                self.startupEdit, self.autoSave, self.encoding,):
+            var.trace_remove('write', var.trace_info()[0][1])
+
+    def VarChanged_font(self, *params):
+        '''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.fontName.get()
+        self.AddChangedItem('main', 'EditorWindow', 'font', value)
+        value = self.fontSize.get()
+        self.AddChangedItem('main', 'EditorWindow', 'font-size', value)
+        value = self.fontBold.get()
+        self.AddChangedItem('main', 'EditorWindow', 'font-bold', value)
+
+    def VarChanged_spaceNum(self, *params):
+        value = self.spaceNum.get()
+        self.AddChangedItem('main', 'Indent', 'num-spaces', value)
+
+    def VarChanged_colour(self, *params):
+        self.OnNewColourSet()
+
+    def VarChanged_builtinTheme(self, *params):
+        oldthemes = ('IDLE Classic', 'IDLE New')
+        value = self.builtinTheme.get()
+        if value not in oldthemes:
+            if idleConf.GetOption('main', 'Theme', 'name') not in oldthemes:
+                self.AddChangedItem('main', 'Theme', 'name', oldthemes[0])
+            self.AddChangedItem('main', 'Theme', 'name2', value)
+            self.new_custom_theme.config(text='New theme, see Help',
+                                         fg='#500000')
+        else:
+            self.AddChangedItem('main', 'Theme', 'name', value)
+            self.AddChangedItem('main', 'Theme', 'name2', '')
+            self.new_custom_theme.config(text='', fg='black')
+        self.PaintThemeSample()
+
+    def VarChanged_customTheme(self, *params):
+        value = self.customTheme.get()
+        if value != '- no custom themes -':
+            self.AddChangedItem('main', 'Theme', 'name', value)
+            self.PaintThemeSample()
+
+    def VarChanged_themeIsBuiltin(self, *params):
+        value = self.themeIsBuiltin.get()
+        self.AddChangedItem('main', 'Theme', 'default', value)
+        if value:
+            self.VarChanged_builtinTheme()
+        else:
+            self.VarChanged_customTheme()
+
+    def VarChanged_highlightTarget(self, *params):
+        self.SetHighlightTarget()
+
+    def VarChanged_keyBinding(self, *params):
+        value = self.keyBinding.get()
+        keySet = self.customKeys.get()
+        event = self.listBindings.get(ANCHOR).split()[0]
+        if idleConf.IsCoreBinding(event):
+            #this is a core keybinding
+            self.AddChangedItem('keys', keySet, event, value)
+        else: #this is an extension key binding
+            extName = idleConf.GetExtnNameForEvent(event)
+            extKeybindSection = extName + '_cfgBindings'
+            self.AddChangedItem('extensions', extKeybindSection, event, value)
+
+    def VarChanged_builtinKeys(self, *params):
+        oldkeys = (
+            'IDLE Classic Windows',
+            'IDLE Classic Unix',
+            'IDLE Classic Mac',
+            'IDLE Classic OSX',
+        )
+        value = self.builtinKeys.get()
+        if value not in oldkeys:
+            if idleConf.GetOption('main', 'Keys', 'name') not in oldkeys:
+                self.AddChangedItem('main', 'Keys', 'name', oldkeys[0])
+            self.AddChangedItem('main', 'Keys', 'name2', value)
+            self.new_custom_keys.config(text='New key set, see Help',
+                                        fg='#500000')
+        else:
+            self.AddChangedItem('main', 'Keys', 'name', value)
+            self.AddChangedItem('main', 'Keys', 'name2', '')
+            self.new_custom_keys.config(text='', fg='black')
+        self.LoadKeysList(value)
+
+    def VarChanged_customKeys(self, *params):
+        value = self.customKeys.get()
+        if value != '- no custom keys -':
+            self.AddChangedItem('main', 'Keys', 'name', value)
+            self.LoadKeysList(value)
+
+    def VarChanged_keysAreBuiltin(self, *params):
+        value = self.keysAreBuiltin.get()
+        self.AddChangedItem('main', 'Keys', 'default', value)
+        if value:
+            self.VarChanged_builtinKeys()
+        else:
+            self.VarChanged_customKeys()
+
+    def VarChanged_winWidth(self, *params):
+        value = self.winWidth.get()
+        self.AddChangedItem('main', 'EditorWindow', 'width', value)
+
+    def VarChanged_winHeight(self, *params):
+        value = self.winHeight.get()
+        self.AddChangedItem('main', 'EditorWindow', 'height', value)
+
+    def VarChanged_startupEdit(self, *params):
+        value = self.startupEdit.get()
+        self.AddChangedItem('main', 'General', 'editor-on-startup', value)
+
+    def VarChanged_autoSave(self, *params):
+        value = self.autoSave.get()
+        self.AddChangedItem('main', 'General', 'autosave', value)
+
+    def VarChanged_encoding(self, *params):
+        value = self.encoding.get()
+        self.AddChangedItem('main', 'EditorWindow', 'encoding', value)
+
+    def ResetChangedItems(self):
+        #When any config item is changed in this dialog, an entry
+        #should be made in the relevant section (config type) of this
+        #dictionary. The key should be the config file section name and the
+        #value a dictionary, whose key:value pairs are item=value pairs for
+        #that config file section.
+        self.changedItems = {'main':{}, 'highlight':{}, 'keys':{},
+                             'extensions':{}}
+
+    def AddChangedItem(self, typ, section, item, value):
+        value = str(value) #make sure we use a string
+        if section not in self.changedItems[typ]:
+            self.changedItems[typ][section] = {}
+        self.changedItems[typ][section][item] = value
+
+    def GetDefaultItems(self):
+        dItems={'main':{}, 'highlight':{}, 'keys':{}, 'extensions':{}}
+        for configType in dItems:
+            sections = idleConf.GetSectionList('default', configType)
+            for section in sections:
+                dItems[configType][section] = {}
+                options = 
idleConf.defaultCfg[configType].GetOptionList(section)
+                for option in options:
+                    dItems[configType][section][option] = (
+                            idleConf.defaultCfg[configType].Get(section, 
option))
+        return dItems
+
+    def SetThemeType(self):
+        if self.themeIsBuiltin.get():
+            self.optMenuThemeBuiltin.config(state=NORMAL)
+            self.optMenuThemeCustom.config(state=DISABLED)
+            self.buttonDeleteCustomTheme.config(state=DISABLED)
+        else:
+            self.optMenuThemeBuiltin.config(state=DISABLED)
+            self.radioThemeCustom.config(state=NORMAL)
+            self.optMenuThemeCustom.config(state=NORMAL)
+            self.buttonDeleteCustomTheme.config(state=NORMAL)
+
+    def SetKeysType(self):
+        if self.keysAreBuiltin.get():
+            self.optMenuKeysBuiltin.config(state=NORMAL)
+            self.optMenuKeysCustom.config(state=DISABLED)
+            self.buttonDeleteCustomKeys.config(state=DISABLED)
+        else:
+            self.optMenuKeysBuiltin.config(state=DISABLED)
+            self.radioKeysCustom.config(state=NORMAL)
+            self.optMenuKeysCustom.config(state=NORMAL)
+            self.buttonDeleteCustomKeys.config(state=NORMAL)
+
+    def GetNewKeys(self):
+        listIndex = self.listBindings.index(ANCHOR)
+        binding = self.listBindings.get(listIndex)
+        bindName = binding.split()[0] #first part, up to first space
+        if self.keysAreBuiltin.get():
+            currentKeySetName = self.builtinKeys.get()
+        else:
+            currentKeySetName = self.customKeys.get()
+        currentBindings = idleConf.GetCurrentKeySet()
+        if currentKeySetName in self.changedItems['keys']: #unsaved changes
+            keySetChanges = self.changedItems['keys'][currentKeySetName]
+            for event in keySetChanges:
+                currentBindings[event] = keySetChanges[event].split()
+        currentKeySequences = list(currentBindings.values())
+        newKeys = GetKeysDialog(self, 'Get New Keys', bindName,
+                currentKeySequences).result
+        if newKeys: #new keys were specified
+            if self.keysAreBuiltin.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.')
+                newKeySet = self.GetNewKeysName(message)
+                if not newKeySet: #user cancelled custom key set creation
+                    self.listBindings.select_set(listIndex)
+                    self.listBindings.select_anchor(listIndex)
+                    return
+                else: #create new custom key set based on previously active 
key set
+                    self.CreateNewKeySet(newKeySet)
+            self.listBindings.delete(listIndex)
+            self.listBindings.insert(listIndex, bindName+' - '+newKeys)
+            self.listBindings.select_set(listIndex)
+            self.listBindings.select_anchor(listIndex)
+            self.keyBinding.set(newKeys)
+        else:
+            self.listBindings.select_set(listIndex)
+            self.listBindings.select_anchor(listIndex)
+
+    def GetNewKeysName(self, message):
+        usedNames = (idleConf.GetSectionList('user', 'keys') +
+                idleConf.GetSectionList('default', 'keys'))
+        newKeySet = SectionName(
+                self, 'New Custom Key Set', message, usedNames).result
+        return newKeySet
+
+    def SaveAsNewKeySet(self):
+        newKeysName = self.GetNewKeysName('New Key Set Name:')
+        if newKeysName:
+            self.CreateNewKeySet(newKeysName)
+
+    def KeyBindingSelected(self, event):
+        self.buttonNewKeys.config(state=NORMAL)
+
+    def CreateNewKeySet(self, newKeySetName):
+        #creates new custom key set based on the previously active key set,
+        #and makes the new key set active
+        if self.keysAreBuiltin.get():
+            prevKeySetName = self.builtinKeys.get()
+        else:
+            prevKeySetName = self.customKeys.get()
+        prevKeys = idleConf.GetCoreKeys(prevKeySetName)
+        newKeys = {}
+        for event in prevKeys: #add key set to changed items
+            eventName = event[2:-2] #trim off the angle brackets
+            binding = ' '.join(prevKeys[event])
+            newKeys[eventName] = binding
+        #handle any unsaved changes to prev key set
+        if prevKeySetName in self.changedItems['keys']:
+            keySetChanges = self.changedItems['keys'][prevKeySetName]
+            for event in keySetChanges:
+                newKeys[event] = keySetChanges[event]
+        #save the new theme
+        self.SaveNewKeySet(newKeySetName, newKeys)
+        #change gui over to the new key set
+        customKeyList = idleConf.GetSectionList('user', 'keys')
+        customKeyList.sort()
+        self.optMenuKeysCustom.SetMenu(customKeyList, newKeySetName)
+        self.keysAreBuiltin.set(0)
+        self.SetKeysType()
+
+    def LoadKeysList(self, keySetName):
+        reselect = 0
+        newKeySet = 0
+        if self.listBindings.curselection():
+            reselect = 1
+            listIndex = self.listBindings.index(ANCHOR)
+        keySet = idleConf.GetKeySet(keySetName)
+        bindNames = list(keySet.keys())
+        bindNames.sort()
+        self.listBindings.delete(0, END)
+        for bindName in bindNames:
+            key = ' '.join(keySet[bindName]) #make key(s) into a string
+            bindName = bindName[2:-2] #trim off the angle brackets
+            if keySetName in self.changedItems['keys']:
+                #handle any unsaved changes to this key set
+                if bindName in self.changedItems['keys'][keySetName]:
+                    key = self.changedItems['keys'][keySetName][bindName]
+            self.listBindings.insert(END, bindName+' - '+key)
+        if reselect:
+            self.listBindings.see(listIndex)
+            self.listBindings.select_set(listIndex)
+            self.listBindings.select_anchor(listIndex)
+
+    def DeleteCustomKeys(self):
+        keySetName=self.customKeys.get()
+        delmsg = 'Are you sure you wish to delete the key set %r ?'
+        if not tkMessageBox.askyesno(
+                'Delete Key Set',  delmsg % keySetName, parent=self):
+            return
+        self.DeactivateCurrentConfig()
+        #remove key set from config
+        idleConf.userCfg['keys'].remove_section(keySetName)
+        if keySetName in self.changedItems['keys']:
+            del(self.changedItems['keys'][keySetName])
+        #write changes
+        idleConf.userCfg['keys'].Save()
+        #reload user key set list
+        itemList = idleConf.GetSectionList('user', 'keys')
+        itemList.sort()
+        if not itemList:
+            self.radioKeysCustom.config(state=DISABLED)
+            self.optMenuKeysCustom.SetMenu(itemList, '- no custom keys -')
+        else:
+            self.optMenuKeysCustom.SetMenu(itemList, itemList[0])
+        #revert to default key set
+        self.keysAreBuiltin.set(idleConf.defaultCfg['main']
+                                .Get('Keys', 'default'))
+        self.builtinKeys.set(idleConf.defaultCfg['main'].Get('Keys', 'name')
+                             or idleConf.default_keys())
+        #user can't back out of these changes, they must be applied now
+        self.SaveAllChangedConfigs()
+        self.ActivateConfigChanges()
+        self.SetKeysType()
+
+    def DeleteCustomTheme(self):
+        themeName = self.customTheme.get()
+        delmsg = 'Are you sure you wish to delete the theme %r ?'
+        if not tkMessageBox.askyesno(
+                'Delete Theme',  delmsg % themeName, parent=self):
+            return
+        self.DeactivateCurrentConfig()
+        #remove theme from config
+        idleConf.userCfg['highlight'].remove_section(themeName)
+        if themeName in self.changedItems['highlight']:
+            del(self.changedItems['highlight'][themeName])
+        #write changes
+        idleConf.userCfg['highlight'].Save()
+        #reload user theme list
+        itemList = idleConf.GetSectionList('user', 'highlight')
+        itemList.sort()
+        if not itemList:
+            self.radioThemeCustom.config(state=DISABLED)
+            self.optMenuThemeCustom.SetMenu(itemList, '- no custom themes -')
+        else:
+            self.optMenuThemeCustom.SetMenu(itemList, itemList[0])
+        #revert to default theme
+        self.themeIsBuiltin.set(idleConf.defaultCfg['main'].Get('Theme', 
'default'))
+        self.builtinTheme.set(idleConf.defaultCfg['main'].Get('Theme', 'name'))
+        #user can't back out of these changes, they must be applied now
+        self.SaveAllChangedConfigs()
+        self.ActivateConfigChanges()
+        self.SetThemeType()
+
+    def GetColour(self):
+        target = self.highlightTarget.get()
+        prevColour = self.frameColourSet.cget('bg')
+        rgbTuplet, colourString = tkColorChooser.askcolor(
+                parent=self, title='Pick new colour for : '+target,
+                initialcolor=prevColour)
+        if colourString and (colourString != prevColour):
+            #user didn't cancel, and they chose a new colour
+            if self.themeIsBuiltin.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.')
+                newTheme = self.GetNewThemeName(message)
+                if not newTheme:  #user cancelled custom theme creation
+                    return
+                else:  #create new custom theme based on previously active 
theme
+                    self.CreateNewTheme(newTheme)
+                    self.colour.set(colourString)
+            else:  #current theme is user defined
+                self.colour.set(colourString)
+
+    def OnNewColourSet(self):
+        newColour=self.colour.get()
+        self.frameColourSet.config(bg=newColour)  #set sample
+        plane ='foreground' if self.fgHilite.get() else 'background'
+        sampleElement = self.themeElements[self.highlightTarget.get()][0]
+        self.textHighlightSample.tag_config(sampleElement, **{plane:newColour})
+        theme = self.customTheme.get()
+        themeElement = sampleElement + '-' + plane
+        self.AddChangedItem('highlight', theme, themeElement, newColour)
+
+    def GetNewThemeName(self, message):
+        usedNames = (idleConf.GetSectionList('user', 'highlight') +
+                idleConf.GetSectionList('default', 'highlight'))
+        newTheme = SectionName(
+                self, 'New Custom Theme', message, usedNames).result
+        return newTheme
+
+    def SaveAsNewTheme(self):
+        newThemeName = self.GetNewThemeName('New Theme Name:')
+        if newThemeName:
+            self.CreateNewTheme(newThemeName)
+
+    def CreateNewTheme(self, newThemeName):
+        #creates new custom theme based on the previously active theme,
+        #and makes the new theme active
+        if self.themeIsBuiltin.get():
+            themeType = 'default'
+            themeName = self.builtinTheme.get()
+        else:
+            themeType = 'user'
+            themeName = self.customTheme.get()
+        newTheme = idleConf.GetThemeDict(themeType, themeName)
+        #apply any of the old theme's unsaved changes to the new theme
+        if themeName in self.changedItems['highlight']:
+            themeChanges = self.changedItems['highlight'][themeName]
+            for element in themeChanges:
+                newTheme[element] = themeChanges[element]
+        #save the new theme
+        self.SaveNewTheme(newThemeName, newTheme)
+        #change gui over to the new theme
+        customThemeList = idleConf.GetSectionList('user', 'highlight')
+        customThemeList.sort()
+        self.optMenuThemeCustom.SetMenu(customThemeList, newThemeName)
+        self.themeIsBuiltin.set(0)
+        self.SetThemeType()
+
+    def OnListFontButtonRelease(self, event):
+        font = self.listFontName.get(ANCHOR)
+        self.fontName.set(font.lower())
+        self.SetFontSample()
+
+    def SetFontSample(self, event=None):
+        fontName = self.fontName.get()
+        fontWeight = tkFont.BOLD if self.fontBold.get() else tkFont.NORMAL
+        newFont = (fontName, self.fontSize.get(), fontWeight)
+        self.labelFontSample.config(font=newFont)
+        self.textHighlightSample.configure(font=newFont)
+
+    def SetHighlightTarget(self):
+        if self.highlightTarget.get() == 'Cursor':  #bg not possible
+            self.radioFg.config(state=DISABLED)
+            self.radioBg.config(state=DISABLED)
+            self.fgHilite.set(1)
+        else:  #both fg and bg can be set
+            self.radioFg.config(state=NORMAL)
+            self.radioBg.config(state=NORMAL)
+            self.fgHilite.set(1)
+        self.SetColourSample()
+
+    def SetColourSampleBinding(self, *args):
+        self.SetColourSample()
+
+    def SetColourSample(self):
+        #set the colour smaple area
+        tag = self.themeElements[self.highlightTarget.get()][0]
+        plane = 'foreground' if self.fgHilite.get() else 'background'
+        colour = self.textHighlightSample.tag_cget(tag, plane)
+        self.frameColourSet.config(bg=colour)
+
+    def PaintThemeSample(self):
+        if self.themeIsBuiltin.get():  #a default theme
+            theme = self.builtinTheme.get()
+        else:  #a user theme
+            theme = self.customTheme.get()
+        for elementTitle in self.themeElements:
+            element = self.themeElements[elementTitle][0]
+            colours = idleConf.GetHighlight(theme, element)
+            if element == 'cursor': #cursor sample needs special painting
+                colours['background'] = idleConf.GetHighlight(
+                        theme, 'normal', fgBg='bg')
+            #handle any unsaved changes to this theme
+            if theme in self.changedItems['highlight']:
+                themeDict = self.changedItems['highlight'][theme]
+                if element + '-foreground' in themeDict:
+                    colours['foreground'] = themeDict[element + '-foreground']
+                if element + '-background' in themeDict:
+                    colours['background'] = themeDict[element + '-background']
+            self.textHighlightSample.tag_config(element, **colours)
+        self.SetColourSample()
+
+    def HelpSourceSelected(self, event):
+        self.SetHelpListButtonStates()
+
+    def SetHelpListButtonStates(self):
+        if self.listHelp.size() < 1:  #no entries in list
+            self.buttonHelpListEdit.config(state=DISABLED)
+            self.buttonHelpListRemove.config(state=DISABLED)
+        else: #there are some entries
+            if self.listHelp.curselection():  #there currently is a selection
+                self.buttonHelpListEdit.config(state=NORMAL)
+                self.buttonHelpListRemove.config(state=NORMAL)
+            else:  #there currently is not a selection
+                self.buttonHelpListEdit.config(state=DISABLED)
+                self.buttonHelpListRemove.config(state=DISABLED)
+
+    def HelpListItemAdd(self):
+        helpSource = HelpSource(self, 'New Help Source',
+                                ).result
+        if helpSource:
+            self.userHelpList.append((helpSource[0], helpSource[1]))
+            self.listHelp.insert(END, helpSource[0])
+            self.UpdateUserHelpChangedItems()
+        self.SetHelpListButtonStates()
+
+    def HelpListItemEdit(self):
+        itemIndex = self.listHelp.index(ANCHOR)
+        helpSource = self.userHelpList[itemIndex]
+        newHelpSource = HelpSource(
+                self, 'Edit Help Source',
+                menuitem=helpSource[0],
+                filepath=helpSource[1],
+                ).result
+        if newHelpSource and newHelpSource != helpSource:
+            self.userHelpList[itemIndex] = newHelpSource
+            self.listHelp.delete(itemIndex)
+            self.listHelp.insert(itemIndex, newHelpSource[0])
+            self.UpdateUserHelpChangedItems()
+            self.SetHelpListButtonStates()
+
+    def HelpListItemRemove(self):
+        itemIndex = self.listHelp.index(ANCHOR)
+        del(self.userHelpList[itemIndex])
+        self.listHelp.delete(itemIndex)
+        self.UpdateUserHelpChangedItems()
+        self.SetHelpListButtonStates()
+
+    def UpdateUserHelpChangedItems(self):
+        "Clear and rebuild the HelpFiles section in self.changedItems"
+        self.changedItems['main']['HelpFiles'] = {}
+        for num in range(1, len(self.userHelpList) + 1):
+            self.AddChangedItem(
+                    'main', 'HelpFiles', str(num),
+                    ';'.join(self.userHelpList[num-1][:2]))
+
+    def LoadFontCfg(self):
+        ##base editor font selection list
+        fonts = list(tkFont.families(self))
+        fonts.sort()
+        for font in fonts:
+            self.listFontName.insert(END, font)
+        configuredFont = idleConf.GetFont(self, 'main', 'EditorWindow')
+        fontName = configuredFont[0].lower()
+        fontSize = configuredFont[1]
+        fontBold  = configuredFont[2]=='bold'
+        self.fontName.set(fontName)
+        lc_fonts = [s.lower() for s in fonts]
+        try:
+            currentFontIndex = lc_fonts.index(fontName)
+            self.listFontName.see(currentFontIndex)
+            self.listFontName.select_set(currentFontIndex)
+            self.listFontName.select_anchor(currentFontIndex)
+        except ValueError:
+            pass
+        ##font size dropdown
+        self.optMenuFontSize.SetMenu(('7', '8', '9', '10', '11', '12', '13',
+                                      '14', '16', '18', '20', '22',
+                                      '25', '29', '34', '40'), fontSize )
+        ##fontWeight
+        self.fontBold.set(fontBold)
+        ##font sample
+        self.SetFontSample()
+
+    def LoadTabCfg(self):
+        ##indent sizes
+        spaceNum = idleConf.GetOption(
+            'main', 'Indent', 'num-spaces', default=4, type='int')
+        self.spaceNum.set(spaceNum)
+
+    def LoadThemeCfg(self):
+        ##current theme type radiobutton
+        self.themeIsBuiltin.set(idleConf.GetOption(
+                'main', 'Theme', 'default', type='bool', default=1))
+        ##currently set theme
+        currentOption = idleConf.CurrentTheme()
+        ##load available theme option menus
+        if self.themeIsBuiltin.get(): #default theme selected
+            itemList = idleConf.GetSectionList('default', 'highlight')
+            itemList.sort()
+            self.optMenuThemeBuiltin.SetMenu(itemList, currentOption)
+            itemList = idleConf.GetSectionList('user', 'highlight')
+            itemList.sort()
+            if not itemList:
+                self.radioThemeCustom.config(state=DISABLED)
+                self.customTheme.set('- no custom themes -')
+            else:
+                self.optMenuThemeCustom.SetMenu(itemList, itemList[0])
+        else: #user theme selected
+            itemList = idleConf.GetSectionList('user', 'highlight')
+            itemList.sort()
+            self.optMenuThemeCustom.SetMenu(itemList, currentOption)
+            itemList = idleConf.GetSectionList('default', 'highlight')
+            itemList.sort()
+            self.optMenuThemeBuiltin.SetMenu(itemList, itemList[0])
+        self.SetThemeType()
+        ##load theme element option menu
+        themeNames = list(self.themeElements.keys())
+        themeNames.sort(key=lambda x: self.themeElements[x][1])
+        self.optMenuHighlightTarget.SetMenu(themeNames, themeNames[0])
+        self.PaintThemeSample()
+        self.SetHighlightTarget()
+
+    def LoadKeyCfg(self):
+        ##current keys type radiobutton
+        self.keysAreBuiltin.set(idleConf.GetOption(
+                'main', 'Keys', 'default', type='bool', default=1))
+        ##currently set keys
+        currentOption = idleConf.CurrentKeys()
+        ##load available keyset option menus
+        if self.keysAreBuiltin.get(): #default theme selected
+            itemList = idleConf.GetSectionList('default', 'keys')
+            itemList.sort()
+            self.optMenuKeysBuiltin.SetMenu(itemList, currentOption)
+            itemList = idleConf.GetSectionList('user', 'keys')
+            itemList.sort()
+            if not itemList:
+                self.radioKeysCustom.config(state=DISABLED)
+                self.customKeys.set('- no custom keys -')
+            else:
+                self.optMenuKeysCustom.SetMenu(itemList, itemList[0])
+        else: #user key set selected
+            itemList = idleConf.GetSectionList('user', 'keys')
+            itemList.sort()
+            self.optMenuKeysCustom.SetMenu(itemList, currentOption)
+            itemList = idleConf.GetSectionList('default', 'keys')
+            itemList.sort()
+            self.optMenuKeysBuiltin.SetMenu(itemList, idleConf.default_keys())
+        self.SetKeysType()
+        ##load keyset element list
+        keySetName = idleConf.CurrentKeys()
+        self.LoadKeysList(keySetName)
+
+    def LoadGeneralCfg(self):
+        #startup state
+        self.startupEdit.set(idleConf.GetOption(
+                'main', 'General', 'editor-on-startup', default=1, 
type='bool'))
+        #autosave state
+        self.autoSave.set(idleConf.GetOption(
+                'main', 'General', 'autosave', default=0, type='bool'))
+        #initial window size
+        self.winWidth.set(idleConf.GetOption(
+                'main', 'EditorWindow', 'width', type='int'))
+        self.winHeight.set(idleConf.GetOption(
+                'main', 'EditorWindow', 'height', type='int'))
+        # default source encoding
+        self.encoding.set(idleConf.GetOption(
+                'main', 'EditorWindow', 'encoding', default='none'))
+        # additional help sources
+        self.userHelpList = idleConf.GetAllExtraHelpSourcesList()
+        for helpItem in self.userHelpList:
+            self.listHelp.insert(END, helpItem[0])
+        self.SetHelpListButtonStates()
+
+    def LoadConfigs(self):
+        """
+        load configuration from default and user config files and populate
+        the widgets on the config dialog pages.
+        """
+        ### fonts / tabs page
+        self.LoadFontCfg()
+        self.LoadTabCfg()
+        ### highlighting page
+        self.LoadThemeCfg()
+        ### keys page
+        self.LoadKeyCfg()
+        ### general page
+        self.LoadGeneralCfg()
+        # note: extension page handled separately
+
+    def SaveNewKeySet(self, keySetName, keySet):
+        """
+        save a newly created core key set.
+        keySetName - string, the name of the new key set
+        keySet - dictionary containing the new key set
+        """
+        if not idleConf.userCfg['keys'].has_section(keySetName):
+            idleConf.userCfg['keys'].add_section(keySetName)
+        for event in keySet:
+            value = keySet[event]
+            idleConf.userCfg['keys'].SetOption(keySetName, event, value)
+
+    def SaveNewTheme(self, themeName, theme):
+        """
+        save a newly created theme.
+        themeName - string, the name of the new theme
+        theme - dictionary containing the new theme
+        """
+        if not idleConf.userCfg['highlight'].has_section(themeName):
+            idleConf.userCfg['highlight'].add_section(themeName)
+        for element in theme:
+            value = theme[element]
+            idleConf.userCfg['highlight'].SetOption(themeName, element, value)
+
+    def SetUserValue(self, configType, section, item, value):
+        if idleConf.defaultCfg[configType].has_option(section, item):
+            if idleConf.defaultCfg[configType].Get(section, item) == value:
+                #the setting equals a default setting, remove it from user cfg
+                return idleConf.userCfg[configType].RemoveOption(section, item)
+        #if we got here set the option
+        return idleConf.userCfg[configType].SetOption(section, item, value)
+
+    def SaveAllChangedConfigs(self):
+        "Save configuration changes to the user config file."
+        idleConf.userCfg['main'].Save()
+        for configType in self.changedItems:
+            cfgTypeHasChanges = False
+            for section in self.changedItems[configType]:
+                if section == 'HelpFiles':
+                    #this section gets completely replaced
+                    idleConf.userCfg['main'].remove_section('HelpFiles')
+                    cfgTypeHasChanges = True
+                for item in self.changedItems[configType][section]:
+                    value = self.changedItems[configType][section][item]
+                    if self.SetUserValue(configType, section, item, value):
+                        cfgTypeHasChanges = True
+            if cfgTypeHasChanges:
+                idleConf.userCfg[configType].Save()
+        for configType in ['keys', 'highlight']:
+            # save these even if unchanged!
+            idleConf.userCfg[configType].Save()
+        self.ResetChangedItems() #clear the changed items dict
+        self.save_all_changed_extensions()  # uses a different mechanism
+
+    def DeactivateCurrentConfig(self):
+        #Before a config is saved, some cleanup of current
+        #config must be done - remove the previous keybindings
+        winInstances = self.parent.instance_dict.keys()
+        for instance in winInstances:
+            instance.RemoveKeybindings()
+
+    def ActivateConfigChanges(self):
+        "Dynamically apply configuration changes"
+        winInstances = self.parent.instance_dict.keys()
+        for instance in winInstances:
+            instance.ResetColorizer()
+            instance.ResetFont()
+            instance.set_notabs_indentwidth()
+            instance.ApplyKeybindings()
+            instance.reset_help_menu_entries()
+
+    def Cancel(self):
+        self.destroy()
+
+    def Ok(self):
+        self.Apply()
+        self.destroy()
+
+    def Apply(self):
+        self.DeactivateCurrentConfig()
+        self.SaveAllChangedConfigs()
+        self.ActivateConfigChanges()
+
+    def Help(self):
+        page = self.tabPages._current_page
+        view_text(self, title='Help for IDLE preferences',
+                 text=help_common+help_pages.get(page, ''))
+
+    def CreatePageExtensions(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.
+        """
+        parent = self.parent
+        frame = self.tabPages.pages['Extensions'].frame
+        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)
+
+    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] = []
+
+        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
_______________________________________________
pypy-commit mailing list
[email protected]
https://mail.python.org/mailman/listinfo/pypy-commit

Reply via email to