Home | History | Annotate | Download | only in idlelib
      1 """IDLE Configuration Dialog: support user customization of IDLE by GUI
      2 
      3 Customize font faces, sizes, and colorization attributes.  Set indentation
      4 defaults.  Customize keybindings.  Colorization and keybindings can be
      5 saved as user defined sets.  Select startup options including shell/editor
      6 and default window size.  Define additional help sources.
      7 
      8 Note that tab width in IDLE is currently fixed at eight due to Tk issues.
      9 Refer to comments in EditorWindow autoindent code for details.
     10 
     11 """
     12 from tkinter import *
     13 from tkinter.ttk import Scrollbar
     14 import tkinter.colorchooser as tkColorChooser
     15 import tkinter.font as tkFont
     16 import tkinter.messagebox as tkMessageBox
     17 
     18 from idlelib.config import idleConf
     19 from idlelib.config_key import GetKeysDialog
     20 from idlelib.dynoption import DynOptionMenu
     21 from idlelib import macosx
     22 from idlelib.query import SectionName, HelpSource
     23 from idlelib.tabbedpages import TabbedPageSet
     24 from idlelib.textview import view_text
     25 
     26 class ConfigDialog(Toplevel):
     27 
     28     def __init__(self, parent, title='', _htest=False, _utest=False):
     29         """
     30         _htest - bool, change box location when running htest
     31         _utest - bool, don't wait_window when running unittest
     32         """
     33         Toplevel.__init__(self, parent)
     34         self.parent = parent
     35         if _htest:
     36             parent.instance_dict = {}
     37         self.wm_withdraw()
     38 
     39         self.configure(borderwidth=5)
     40         self.title(title or 'IDLE Preferences')
     41         self.geometry(
     42                 "+%d+%d" % (parent.winfo_rootx() + 20,
     43                 parent.winfo_rooty() + (30 if not _htest else 150)))
     44         #Theme Elements. Each theme element key is its display name.
     45         #The first value of the tuple is the sample area tag name.
     46         #The second value is the display name list sort index.
     47         self.themeElements={
     48             'Normal Text': ('normal', '00'),
     49             'Python Keywords': ('keyword', '01'),
     50             'Python Definitions': ('definition', '02'),
     51             'Python Builtins': ('builtin', '03'),
     52             'Python Comments': ('comment', '04'),
     53             'Python Strings': ('string', '05'),
     54             'Selected Text': ('hilite', '06'),
     55             'Found Text': ('hit', '07'),
     56             'Cursor': ('cursor', '08'),
     57             'Editor Breakpoint': ('break', '09'),
     58             'Shell Normal Text': ('console', '10'),
     59             'Shell Error Text': ('error', '11'),
     60             'Shell Stdout Text': ('stdout', '12'),
     61             'Shell Stderr Text': ('stderr', '13'),
     62             }
     63         self.ResetChangedItems() #load initial values in changed items dict
     64         self.CreateWidgets()
     65         self.resizable(height=FALSE, width=FALSE)
     66         self.transient(parent)
     67         self.grab_set()
     68         self.protocol("WM_DELETE_WINDOW", self.Cancel)
     69         self.tabPages.focus_set()
     70         #key bindings for this dialog
     71         #self.bind('<Escape>', self.Cancel) #dismiss dialog, no save
     72         #self.bind('<Alt-a>', self.Apply) #apply changes, save
     73         #self.bind('<F1>', self.Help) #context help
     74         self.LoadConfigs()
     75         self.AttachVarCallbacks() #avoid callbacks during LoadConfigs
     76 
     77         if not _utest:
     78             self.wm_deiconify()
     79             self.wait_window()
     80 
     81     def CreateWidgets(self):
     82         self.tabPages = TabbedPageSet(self,
     83                 page_names=['Fonts/Tabs', 'Highlighting', 'Keys', 'General',
     84                             'Extensions'])
     85         self.tabPages.pack(side=TOP, expand=TRUE, fill=BOTH)
     86         self.CreatePageFontTab()
     87         self.CreatePageHighlight()
     88         self.CreatePageKeys()
     89         self.CreatePageGeneral()
     90         self.CreatePageExtensions()
     91         self.create_action_buttons().pack(side=BOTTOM)
     92 
     93     def create_action_buttons(self):
     94         if macosx.isAquaTk():
     95             # Changing the default padding on OSX results in unreadable
     96             # text in the buttons
     97             paddingArgs = {}
     98         else:
     99             paddingArgs = {'padx':6, 'pady':3}
    100         outer = Frame(self, pady=2)
    101         buttons = Frame(outer, pady=2)
    102         for txt, cmd in (
    103             ('Ok', self.Ok),
    104             ('Apply', self.Apply),
    105             ('Cancel', self.Cancel),
    106             ('Help', self.Help)):
    107             Button(buttons, text=txt, command=cmd, takefocus=FALSE,
    108                    **paddingArgs).pack(side=LEFT, padx=5)
    109         # add space above buttons
    110         Frame(outer, height=2, borderwidth=0).pack(side=TOP)
    111         buttons.pack(side=BOTTOM)
    112         return outer
    113 
    114     def CreatePageFontTab(self):
    115         parent = self.parent
    116         self.fontSize = StringVar(parent)
    117         self.fontBold = BooleanVar(parent)
    118         self.fontName = StringVar(parent)
    119         self.spaceNum = IntVar(parent)
    120         self.editFont = tkFont.Font(parent, ('courier', 10, 'normal'))
    121 
    122         ##widget creation
    123         #body frame
    124         frame = self.tabPages.pages['Fonts/Tabs'].frame
    125         #body section frames
    126         frameFont = LabelFrame(
    127                 frame, borderwidth=2, relief=GROOVE, text=' Base Editor Font ')
    128         frameIndent = LabelFrame(
    129                 frame, borderwidth=2, relief=GROOVE, text=' Indentation Width ')
    130         #frameFont
    131         frameFontName = Frame(frameFont)
    132         frameFontParam = Frame(frameFont)
    133         labelFontNameTitle = Label(
    134                 frameFontName, justify=LEFT, text='Font Face :')
    135         self.listFontName = Listbox(
    136                 frameFontName, height=5, takefocus=FALSE, exportselection=FALSE)
    137         self.listFontName.bind(
    138                 '<ButtonRelease-1>', self.OnListFontButtonRelease)
    139         scrollFont = Scrollbar(frameFontName)
    140         scrollFont.config(command=self.listFontName.yview)
    141         self.listFontName.config(yscrollcommand=scrollFont.set)
    142         labelFontSizeTitle = Label(frameFontParam, text='Size :')
    143         self.optMenuFontSize = DynOptionMenu(
    144                 frameFontParam, self.fontSize, None, command=self.SetFontSample)
    145         checkFontBold = Checkbutton(
    146                 frameFontParam, variable=self.fontBold, onvalue=1,
    147                 offvalue=0, text='Bold', command=self.SetFontSample)
    148         frameFontSample = Frame(frameFont, relief=SOLID, borderwidth=1)
    149         self.labelFontSample = Label(
    150                 frameFontSample, justify=LEFT, font=self.editFont,
    151                 text='AaBbCcDdEe\nFfGgHhIiJjK\n1234567890\n#:+=(){}[]')
    152         #frameIndent
    153         frameIndentSize = Frame(frameIndent)
    154         labelSpaceNumTitle = Label(
    155                 frameIndentSize, justify=LEFT,
    156                 text='Python Standard: 4 Spaces!')
    157         self.scaleSpaceNum = Scale(
    158                 frameIndentSize, variable=self.spaceNum,
    159                 orient='horizontal', tickinterval=2, from_=2, to=16)
    160 
    161         #widget packing
    162         #body
    163         frameFont.pack(side=LEFT, padx=5, pady=5, expand=TRUE, fill=BOTH)
    164         frameIndent.pack(side=LEFT, padx=5, pady=5, fill=Y)
    165         #frameFont
    166         frameFontName.pack(side=TOP, padx=5, pady=5, fill=X)
    167         frameFontParam.pack(side=TOP, padx=5, pady=5, fill=X)
    168         labelFontNameTitle.pack(side=TOP, anchor=W)
    169         self.listFontName.pack(side=LEFT, expand=TRUE, fill=X)
    170         scrollFont.pack(side=LEFT, fill=Y)
    171         labelFontSizeTitle.pack(side=LEFT, anchor=W)
    172         self.optMenuFontSize.pack(side=LEFT, anchor=W)
    173         checkFontBold.pack(side=LEFT, anchor=W, padx=20)
    174         frameFontSample.pack(side=TOP, padx=5, pady=5, expand=TRUE, fill=BOTH)
    175         self.labelFontSample.pack(expand=TRUE, fill=BOTH)
    176         #frameIndent
    177         frameIndentSize.pack(side=TOP, fill=X)
    178         labelSpaceNumTitle.pack(side=TOP, anchor=W, padx=5)
    179         self.scaleSpaceNum.pack(side=TOP, padx=5, fill=X)
    180         return frame
    181 
    182     def CreatePageHighlight(self):
    183         parent = self.parent
    184         self.builtinTheme = StringVar(parent)
    185         self.customTheme = StringVar(parent)
    186         self.fgHilite = BooleanVar(parent)
    187         self.colour = StringVar(parent)
    188         self.fontName = StringVar(parent)
    189         self.themeIsBuiltin = BooleanVar(parent)
    190         self.highlightTarget = StringVar(parent)
    191 
    192         ##widget creation
    193         #body frame
    194         frame = self.tabPages.pages['Highlighting'].frame
    195         #body section frames
    196         frameCustom = LabelFrame(frame, borderwidth=2, relief=GROOVE,
    197                                  text=' Custom Highlighting ')
    198         frameTheme = LabelFrame(frame, borderwidth=2, relief=GROOVE,
    199                                 text=' Highlighting Theme ')
    200         #frameCustom
    201         self.textHighlightSample=Text(
    202                 frameCustom, relief=SOLID, borderwidth=1,
    203                 font=('courier', 12, ''), cursor='hand2', width=21, height=11,
    204                 takefocus=FALSE, highlightthickness=0, wrap=NONE)
    205         text=self.textHighlightSample
    206         text.bind('<Double-Button-1>', lambda e: 'break')
    207         text.bind('<B1-Motion>', lambda e: 'break')
    208         textAndTags=(
    209             ('#you can click here', 'comment'), ('\n', 'normal'),
    210             ('#to choose items', 'comment'), ('\n', 'normal'),
    211             ('def', 'keyword'), (' ', 'normal'),
    212             ('func', 'definition'), ('(param):\n  ', 'normal'),
    213             ('"""string"""', 'string'), ('\n  var0 = ', 'normal'),
    214             ("'string'", 'string'), ('\n  var1 = ', 'normal'),
    215             ("'selected'", 'hilite'), ('\n  var2 = ', 'normal'),
    216             ("'found'", 'hit'), ('\n  var3 = ', 'normal'),
    217             ('list', 'builtin'), ('(', 'normal'),
    218             ('None', 'keyword'), (')\n', 'normal'),
    219             ('  breakpoint("line")', 'break'), ('\n\n', 'normal'),
    220             (' error ', 'error'), (' ', 'normal'),
    221             ('cursor |', 'cursor'), ('\n ', 'normal'),
    222             ('shell', 'console'), (' ', 'normal'),
    223             ('stdout', 'stdout'), (' ', 'normal'),
    224             ('stderr', 'stderr'), ('\n', 'normal'))
    225         for txTa in textAndTags:
    226             text.insert(END, txTa[0], txTa[1])
    227         for element in self.themeElements:
    228             def tem(event, elem=element):
    229                 event.widget.winfo_toplevel().highlightTarget.set(elem)
    230             text.tag_bind(
    231                     self.themeElements[element][0], '<ButtonPress-1>', tem)
    232         text.config(state=DISABLED)
    233         self.frameColourSet = Frame(frameCustom, relief=SOLID, borderwidth=1)
    234         frameFgBg = Frame(frameCustom)
    235         buttonSetColour = Button(
    236                 self.frameColourSet, text='Choose Colour for :',
    237                 command=self.GetColour, highlightthickness=0)
    238         self.optMenuHighlightTarget = DynOptionMenu(
    239                 self.frameColourSet, self.highlightTarget, None,
    240                 highlightthickness=0) #, command=self.SetHighlightTargetBinding
    241         self.radioFg = Radiobutton(
    242                 frameFgBg, variable=self.fgHilite, value=1,
    243                 text='Foreground', command=self.SetColourSampleBinding)
    244         self.radioBg=Radiobutton(
    245                 frameFgBg, variable=self.fgHilite, value=0,
    246                 text='Background', command=self.SetColourSampleBinding)
    247         self.fgHilite.set(1)
    248         buttonSaveCustomTheme = Button(
    249                 frameCustom, text='Save as New Custom Theme',
    250                 command=self.SaveAsNewTheme)
    251         #frameTheme
    252         labelTypeTitle = Label(frameTheme, text='Select : ')
    253         self.radioThemeBuiltin = Radiobutton(
    254                 frameTheme, variable=self.themeIsBuiltin, value=1,
    255                 command=self.SetThemeType, text='a Built-in Theme')
    256         self.radioThemeCustom = Radiobutton(
    257                 frameTheme, variable=self.themeIsBuiltin, value=0,
    258                 command=self.SetThemeType, text='a Custom Theme')
    259         self.optMenuThemeBuiltin = DynOptionMenu(
    260                 frameTheme, self.builtinTheme, None, command=None)
    261         self.optMenuThemeCustom=DynOptionMenu(
    262                 frameTheme, self.customTheme, None, command=None)
    263         self.buttonDeleteCustomTheme=Button(
    264                 frameTheme, text='Delete Custom Theme',
    265                 command=self.DeleteCustomTheme)
    266         self.new_custom_theme = Label(frameTheme, bd=2)
    267 
    268         ##widget packing
    269         #body
    270         frameCustom.pack(side=LEFT, padx=5, pady=5, expand=TRUE, fill=BOTH)
    271         frameTheme.pack(side=LEFT, padx=5, pady=5, fill=Y)
    272         #frameCustom
    273         self.frameColourSet.pack(side=TOP, padx=5, pady=5, expand=TRUE, fill=X)
    274         frameFgBg.pack(side=TOP, padx=5, pady=0)
    275         self.textHighlightSample.pack(
    276                 side=TOP, padx=5, pady=5, expand=TRUE, fill=BOTH)
    277         buttonSetColour.pack(side=TOP, expand=TRUE, fill=X, padx=8, pady=4)
    278         self.optMenuHighlightTarget.pack(
    279                 side=TOP, expand=TRUE, fill=X, padx=8, pady=3)
    280         self.radioFg.pack(side=LEFT, anchor=E)
    281         self.radioBg.pack(side=RIGHT, anchor=W)
    282         buttonSaveCustomTheme.pack(side=BOTTOM, fill=X, padx=5, pady=5)
    283         #frameTheme
    284         labelTypeTitle.pack(side=TOP, anchor=W, padx=5, pady=5)
    285         self.radioThemeBuiltin.pack(side=TOP, anchor=W, padx=5)
    286         self.radioThemeCustom.pack(side=TOP, anchor=W, padx=5, pady=2)
    287         self.optMenuThemeBuiltin.pack(side=TOP, fill=X, padx=5, pady=5)
    288         self.optMenuThemeCustom.pack(side=TOP, fill=X, anchor=W, padx=5, pady=5)
    289         self.buttonDeleteCustomTheme.pack(side=TOP, fill=X, padx=5, pady=5)
    290         self.new_custom_theme.pack(side=TOP, fill=X, pady=5)
    291         return frame
    292 
    293     def CreatePageKeys(self):
    294         parent = self.parent
    295         self.bindingTarget = StringVar(parent)
    296         self.builtinKeys = StringVar(parent)
    297         self.customKeys = StringVar(parent)
    298         self.keysAreBuiltin = BooleanVar(parent)
    299         self.keyBinding = StringVar(parent)
    300 
    301         ##widget creation
    302         #body frame
    303         frame = self.tabPages.pages['Keys'].frame
    304         #body section frames
    305         frameCustom = LabelFrame(
    306                 frame, borderwidth=2, relief=GROOVE,
    307                 text=' Custom Key Bindings ')
    308         frameKeySets = LabelFrame(
    309                 frame, borderwidth=2, relief=GROOVE, text=' Key Set ')
    310         #frameCustom
    311         frameTarget = Frame(frameCustom)
    312         labelTargetTitle = Label(frameTarget, text='Action - Key(s)')
    313         scrollTargetY = Scrollbar(frameTarget)
    314         scrollTargetX = Scrollbar(frameTarget, orient=HORIZONTAL)
    315         self.listBindings = Listbox(
    316                 frameTarget, takefocus=FALSE, exportselection=FALSE)
    317         self.listBindings.bind('<ButtonRelease-1>', self.KeyBindingSelected)
    318         scrollTargetY.config(command=self.listBindings.yview)
    319         scrollTargetX.config(command=self.listBindings.xview)
    320         self.listBindings.config(yscrollcommand=scrollTargetY.set)
    321         self.listBindings.config(xscrollcommand=scrollTargetX.set)
    322         self.buttonNewKeys = Button(
    323                 frameCustom, text='Get New Keys for Selection',
    324                 command=self.GetNewKeys, state=DISABLED)
    325         #frameKeySets
    326         frames = [Frame(frameKeySets, padx=2, pady=2, borderwidth=0)
    327                   for i in range(2)]
    328         self.radioKeysBuiltin = Radiobutton(
    329                 frames[0], variable=self.keysAreBuiltin, value=1,
    330                 command=self.SetKeysType, text='Use a Built-in Key Set')
    331         self.radioKeysCustom = Radiobutton(
    332                 frames[0], variable=self.keysAreBuiltin,  value=0,
    333                 command=self.SetKeysType, text='Use a Custom Key Set')
    334         self.optMenuKeysBuiltin = DynOptionMenu(
    335                 frames[0], self.builtinKeys, None, command=None)
    336         self.optMenuKeysCustom = DynOptionMenu(
    337                 frames[0], self.customKeys, None, command=None)
    338         self.buttonDeleteCustomKeys = Button(
    339                 frames[1], text='Delete Custom Key Set',
    340                 command=self.DeleteCustomKeys)
    341         buttonSaveCustomKeys = Button(
    342                 frames[1], text='Save as New Custom Key Set',
    343                 command=self.SaveAsNewKeySet)
    344         self.new_custom_keys = Label(frames[0], bd=2)
    345 
    346         ##widget packing
    347         #body
    348         frameCustom.pack(side=BOTTOM, padx=5, pady=5, expand=TRUE, fill=BOTH)
    349         frameKeySets.pack(side=BOTTOM, padx=5, pady=5, fill=BOTH)
    350         #frameCustom
    351         self.buttonNewKeys.pack(side=BOTTOM, fill=X, padx=5, pady=5)
    352         frameTarget.pack(side=LEFT, padx=5, pady=5, expand=TRUE, fill=BOTH)
    353         #frame target
    354         frameTarget.columnconfigure(0, weight=1)
    355         frameTarget.rowconfigure(1, weight=1)
    356         labelTargetTitle.grid(row=0, column=0, columnspan=2, sticky=W)
    357         self.listBindings.grid(row=1, column=0, sticky=NSEW)
    358         scrollTargetY.grid(row=1, column=1, sticky=NS)
    359         scrollTargetX.grid(row=2, column=0, sticky=EW)
    360         #frameKeySets
    361         self.radioKeysBuiltin.grid(row=0, column=0, sticky=W+NS)
    362         self.radioKeysCustom.grid(row=1, column=0, sticky=W+NS)
    363         self.optMenuKeysBuiltin.grid(row=0, column=1, sticky=NSEW)
    364         self.optMenuKeysCustom.grid(row=1, column=1, sticky=NSEW)
    365         self.new_custom_keys.grid(row=0, column=2, sticky=NSEW, padx=5, pady=5)
    366         self.buttonDeleteCustomKeys.pack(side=LEFT, fill=X, expand=True, padx=2)
    367         buttonSaveCustomKeys.pack(side=LEFT, fill=X, expand=True, padx=2)
    368         frames[0].pack(side=TOP, fill=BOTH, expand=True)
    369         frames[1].pack(side=TOP, fill=X, expand=True, pady=2)
    370         return frame
    371 
    372     def CreatePageGeneral(self):
    373         parent = self.parent
    374         self.winWidth = StringVar(parent)
    375         self.winHeight = StringVar(parent)
    376         self.startupEdit = IntVar(parent)
    377         self.autoSave = IntVar(parent)
    378         self.encoding = StringVar(parent)
    379         self.userHelpBrowser = BooleanVar(parent)
    380         self.helpBrowser = StringVar(parent)
    381 
    382         #widget creation
    383         #body
    384         frame = self.tabPages.pages['General'].frame
    385         #body section frames
    386         frameRun = LabelFrame(frame, borderwidth=2, relief=GROOVE,
    387                               text=' Startup Preferences ')
    388         frameSave = LabelFrame(frame, borderwidth=2, relief=GROOVE,
    389                                text=' Autosave Preferences ')
    390         frameWinSize = Frame(frame, borderwidth=2, relief=GROOVE)
    391         frameHelp = LabelFrame(frame, borderwidth=2, relief=GROOVE,
    392                                text=' Additional Help Sources ')
    393         #frameRun
    394         labelRunChoiceTitle = Label(frameRun, text='At Startup')
    395         self.radioStartupEdit = Radiobutton(
    396                 frameRun, variable=self.startupEdit, value=1,
    397                 text="Open Edit Window")
    398         self.radioStartupShell = Radiobutton(
    399                 frameRun, variable=self.startupEdit, value=0,
    400                 text='Open Shell Window')
    401         #frameSave
    402         labelRunSaveTitle = Label(frameSave, text='At Start of Run (F5)  ')
    403         self.radioSaveAsk = Radiobutton(
    404                 frameSave, variable=self.autoSave, value=0,
    405                 text="Prompt to Save")
    406         self.radioSaveAuto = Radiobutton(
    407                 frameSave, variable=self.autoSave, value=1,
    408                 text='No Prompt')
    409         #frameWinSize
    410         labelWinSizeTitle = Label(
    411                 frameWinSize, text='Initial Window Size  (in characters)')
    412         labelWinWidthTitle = Label(frameWinSize, text='Width')
    413         self.entryWinWidth = Entry(
    414                 frameWinSize, textvariable=self.winWidth, width=3)
    415         labelWinHeightTitle = Label(frameWinSize, text='Height')
    416         self.entryWinHeight = Entry(
    417                 frameWinSize, textvariable=self.winHeight, width=3)
    418         #frameHelp
    419         frameHelpList = Frame(frameHelp)
    420         frameHelpListButtons = Frame(frameHelpList)
    421         scrollHelpList = Scrollbar(frameHelpList)
    422         self.listHelp = Listbox(
    423                 frameHelpList, height=5, takefocus=FALSE,
    424                 exportselection=FALSE)
    425         scrollHelpList.config(command=self.listHelp.yview)
    426         self.listHelp.config(yscrollcommand=scrollHelpList.set)
    427         self.listHelp.bind('<ButtonRelease-1>', self.HelpSourceSelected)
    428         self.buttonHelpListEdit = Button(
    429                 frameHelpListButtons, text='Edit', state=DISABLED,
    430                 width=8, command=self.HelpListItemEdit)
    431         self.buttonHelpListAdd = Button(
    432                 frameHelpListButtons, text='Add',
    433                 width=8, command=self.HelpListItemAdd)
    434         self.buttonHelpListRemove = Button(
    435                 frameHelpListButtons, text='Remove', state=DISABLED,
    436                 width=8, command=self.HelpListItemRemove)
    437 
    438         #widget packing
    439         #body
    440         frameRun.pack(side=TOP, padx=5, pady=5, fill=X)
    441         frameSave.pack(side=TOP, padx=5, pady=5, fill=X)
    442         frameWinSize.pack(side=TOP, padx=5, pady=5, fill=X)
    443         frameHelp.pack(side=TOP, padx=5, pady=5, expand=TRUE, fill=BOTH)
    444         #frameRun
    445         labelRunChoiceTitle.pack(side=LEFT, anchor=W, padx=5, pady=5)
    446         self.radioStartupShell.pack(side=RIGHT, anchor=W, padx=5, pady=5)
    447         self.radioStartupEdit.pack(side=RIGHT, anchor=W, padx=5, pady=5)
    448         #frameSave
    449         labelRunSaveTitle.pack(side=LEFT, anchor=W, padx=5, pady=5)
    450         self.radioSaveAuto.pack(side=RIGHT, anchor=W, padx=5, pady=5)
    451         self.radioSaveAsk.pack(side=RIGHT, anchor=W, padx=5, pady=5)
    452         #frameWinSize
    453         labelWinSizeTitle.pack(side=LEFT, anchor=W, padx=5, pady=5)
    454         self.entryWinHeight.pack(side=RIGHT, anchor=E, padx=10, pady=5)
    455         labelWinHeightTitle.pack(side=RIGHT, anchor=E, pady=5)
    456         self.entryWinWidth.pack(side=RIGHT, anchor=E, padx=10, pady=5)
    457         labelWinWidthTitle.pack(side=RIGHT, anchor=E, pady=5)
    458         #frameHelp
    459         frameHelpListButtons.pack(side=RIGHT, padx=5, pady=5, fill=Y)
    460         frameHelpList.pack(side=TOP, padx=5, pady=5, expand=TRUE, fill=BOTH)
    461         scrollHelpList.pack(side=RIGHT, anchor=W, fill=Y)
    462         self.listHelp.pack(side=LEFT, anchor=E, expand=TRUE, fill=BOTH)
    463         self.buttonHelpListEdit.pack(side=TOP, anchor=W, pady=5)
    464         self.buttonHelpListAdd.pack(side=TOP, anchor=W)
    465         self.buttonHelpListRemove.pack(side=TOP, anchor=W, pady=5)
    466         return frame
    467 
    468     def AttachVarCallbacks(self):
    469         self.fontSize.trace_add('write', self.VarChanged_font)
    470         self.fontName.trace_add('write', self.VarChanged_font)
    471         self.fontBold.trace_add('write', self.VarChanged_font)
    472         self.spaceNum.trace_add('write', self.VarChanged_spaceNum)
    473         self.colour.trace_add('write', self.VarChanged_colour)
    474         self.builtinTheme.trace_add('write', self.VarChanged_builtinTheme)
    475         self.customTheme.trace_add('write', self.VarChanged_customTheme)
    476         self.themeIsBuiltin.trace_add('write', self.VarChanged_themeIsBuiltin)
    477         self.highlightTarget.trace_add('write', self.VarChanged_highlightTarget)
    478         self.keyBinding.trace_add('write', self.VarChanged_keyBinding)
    479         self.builtinKeys.trace_add('write', self.VarChanged_builtinKeys)
    480         self.customKeys.trace_add('write', self.VarChanged_customKeys)
    481         self.keysAreBuiltin.trace_add('write', self.VarChanged_keysAreBuiltin)
    482         self.winWidth.trace_add('write', self.VarChanged_winWidth)
    483         self.winHeight.trace_add('write', self.VarChanged_winHeight)
    484         self.startupEdit.trace_add('write', self.VarChanged_startupEdit)
    485         self.autoSave.trace_add('write', self.VarChanged_autoSave)
    486         self.encoding.trace_add('write', self.VarChanged_encoding)
    487 
    488     def remove_var_callbacks(self):
    489         "Remove callbacks to prevent memory leaks."
    490         for var in (
    491                 self.fontSize, self.fontName, self.fontBold,
    492                 self.spaceNum, self.colour, self.builtinTheme,
    493                 self.customTheme, self.themeIsBuiltin, self.highlightTarget,
    494                 self.keyBinding, self.builtinKeys, self.customKeys,
    495                 self.keysAreBuiltin, self.winWidth, self.winHeight,
    496                 self.startupEdit, self.autoSave, self.encoding,):
    497             var.trace_remove('write', var.trace_info()[0][1])
    498 
    499     def VarChanged_font(self, *params):
    500         '''When one font attribute changes, save them all, as they are
    501         not independent from each other. In particular, when we are
    502         overriding the default font, we need to write out everything.
    503         '''
    504         value = self.fontName.get()
    505         self.AddChangedItem('main', 'EditorWindow', 'font', value)
    506         value = self.fontSize.get()
    507         self.AddChangedItem('main', 'EditorWindow', 'font-size', value)
    508         value = self.fontBold.get()
    509         self.AddChangedItem('main', 'EditorWindow', 'font-bold', value)
    510 
    511     def VarChanged_spaceNum(self, *params):
    512         value = self.spaceNum.get()
    513         self.AddChangedItem('main', 'Indent', 'num-spaces', value)
    514 
    515     def VarChanged_colour(self, *params):
    516         self.OnNewColourSet()
    517 
    518     def VarChanged_builtinTheme(self, *params):
    519         oldthemes = ('IDLE Classic', 'IDLE New')
    520         value = self.builtinTheme.get()
    521         if value not in oldthemes:
    522             if idleConf.GetOption('main', 'Theme', 'name') not in oldthemes:
    523                 self.AddChangedItem('main', 'Theme', 'name', oldthemes[0])
    524             self.AddChangedItem('main', 'Theme', 'name2', value)
    525             self.new_custom_theme.config(text='New theme, see Help',
    526                                          fg='#500000')
    527         else:
    528             self.AddChangedItem('main', 'Theme', 'name', value)
    529             self.AddChangedItem('main', 'Theme', 'name2', '')
    530             self.new_custom_theme.config(text='', fg='black')
    531         self.PaintThemeSample()
    532 
    533     def VarChanged_customTheme(self, *params):
    534         value = self.customTheme.get()
    535         if value != '- no custom themes -':
    536             self.AddChangedItem('main', 'Theme', 'name', value)
    537             self.PaintThemeSample()
    538 
    539     def VarChanged_themeIsBuiltin(self, *params):
    540         value = self.themeIsBuiltin.get()
    541         self.AddChangedItem('main', 'Theme', 'default', value)
    542         if value:
    543             self.VarChanged_builtinTheme()
    544         else:
    545             self.VarChanged_customTheme()
    546 
    547     def VarChanged_highlightTarget(self, *params):
    548         self.SetHighlightTarget()
    549 
    550     def VarChanged_keyBinding(self, *params):
    551         value = self.keyBinding.get()
    552         keySet = self.customKeys.get()
    553         event = self.listBindings.get(ANCHOR).split()[0]
    554         if idleConf.IsCoreBinding(event):
    555             #this is a core keybinding
    556             self.AddChangedItem('keys', keySet, event, value)
    557         else: #this is an extension key binding
    558             extName = idleConf.GetExtnNameForEvent(event)
    559             extKeybindSection = extName + '_cfgBindings'
    560             self.AddChangedItem('extensions', extKeybindSection, event, value)
    561 
    562     def VarChanged_builtinKeys(self, *params):
    563         oldkeys = (
    564             'IDLE Classic Windows',
    565             'IDLE Classic Unix',
    566             'IDLE Classic Mac',
    567             'IDLE Classic OSX',
    568         )
    569         value = self.builtinKeys.get()
    570         if value not in oldkeys:
    571             if idleConf.GetOption('main', 'Keys', 'name') not in oldkeys:
    572                 self.AddChangedItem('main', 'Keys', 'name', oldkeys[0])
    573             self.AddChangedItem('main', 'Keys', 'name2', value)
    574             self.new_custom_keys.config(text='New key set, see Help',
    575                                         fg='#500000')
    576         else:
    577             self.AddChangedItem('main', 'Keys', 'name', value)
    578             self.AddChangedItem('main', 'Keys', 'name2', '')
    579             self.new_custom_keys.config(text='', fg='black')
    580         self.LoadKeysList(value)
    581 
    582     def VarChanged_customKeys(self, *params):
    583         value = self.customKeys.get()
    584         if value != '- no custom keys -':
    585             self.AddChangedItem('main', 'Keys', 'name', value)
    586             self.LoadKeysList(value)
    587 
    588     def VarChanged_keysAreBuiltin(self, *params):
    589         value = self.keysAreBuiltin.get()
    590         self.AddChangedItem('main', 'Keys', 'default', value)
    591         if value:
    592             self.VarChanged_builtinKeys()
    593         else:
    594             self.VarChanged_customKeys()
    595 
    596     def VarChanged_winWidth(self, *params):
    597         value = self.winWidth.get()
    598         self.AddChangedItem('main', 'EditorWindow', 'width', value)
    599 
    600     def VarChanged_winHeight(self, *params):
    601         value = self.winHeight.get()
    602         self.AddChangedItem('main', 'EditorWindow', 'height', value)
    603 
    604     def VarChanged_startupEdit(self, *params):
    605         value = self.startupEdit.get()
    606         self.AddChangedItem('main', 'General', 'editor-on-startup', value)
    607 
    608     def VarChanged_autoSave(self, *params):
    609         value = self.autoSave.get()
    610         self.AddChangedItem('main', 'General', 'autosave', value)
    611 
    612     def VarChanged_encoding(self, *params):
    613         value = self.encoding.get()
    614         self.AddChangedItem('main', 'EditorWindow', 'encoding', value)
    615 
    616     def ResetChangedItems(self):
    617         #When any config item is changed in this dialog, an entry
    618         #should be made in the relevant section (config type) of this
    619         #dictionary. The key should be the config file section name and the
    620         #value a dictionary, whose key:value pairs are item=value pairs for
    621         #that config file section.
    622         self.changedItems = {'main':{}, 'highlight':{}, 'keys':{},
    623                              'extensions':{}}
    624 
    625     def AddChangedItem(self, typ, section, item, value):
    626         value = str(value) #make sure we use a string
    627         if section not in self.changedItems[typ]:
    628             self.changedItems[typ][section] = {}
    629         self.changedItems[typ][section][item] = value
    630 
    631     def GetDefaultItems(self):
    632         dItems={'main':{}, 'highlight':{}, 'keys':{}, 'extensions':{}}
    633         for configType in dItems:
    634             sections = idleConf.GetSectionList('default', configType)
    635             for section in sections:
    636                 dItems[configType][section] = {}
    637                 options = idleConf.defaultCfg[configType].GetOptionList(section)
    638                 for option in options:
    639                     dItems[configType][section][option] = (
    640                             idleConf.defaultCfg[configType].Get(section, option))
    641         return dItems
    642 
    643     def SetThemeType(self):
    644         if self.themeIsBuiltin.get():
    645             self.optMenuThemeBuiltin.config(state=NORMAL)
    646             self.optMenuThemeCustom.config(state=DISABLED)
    647             self.buttonDeleteCustomTheme.config(state=DISABLED)
    648         else:
    649             self.optMenuThemeBuiltin.config(state=DISABLED)
    650             self.radioThemeCustom.config(state=NORMAL)
    651             self.optMenuThemeCustom.config(state=NORMAL)
    652             self.buttonDeleteCustomTheme.config(state=NORMAL)
    653 
    654     def SetKeysType(self):
    655         if self.keysAreBuiltin.get():
    656             self.optMenuKeysBuiltin.config(state=NORMAL)
    657             self.optMenuKeysCustom.config(state=DISABLED)
    658             self.buttonDeleteCustomKeys.config(state=DISABLED)
    659         else:
    660             self.optMenuKeysBuiltin.config(state=DISABLED)
    661             self.radioKeysCustom.config(state=NORMAL)
    662             self.optMenuKeysCustom.config(state=NORMAL)
    663             self.buttonDeleteCustomKeys.config(state=NORMAL)
    664 
    665     def GetNewKeys(self):
    666         listIndex = self.listBindings.index(ANCHOR)
    667         binding = self.listBindings.get(listIndex)
    668         bindName = binding.split()[0] #first part, up to first space
    669         if self.keysAreBuiltin.get():
    670             currentKeySetName = self.builtinKeys.get()
    671         else:
    672             currentKeySetName = self.customKeys.get()
    673         currentBindings = idleConf.GetCurrentKeySet()
    674         if currentKeySetName in self.changedItems['keys']: #unsaved changes
    675             keySetChanges = self.changedItems['keys'][currentKeySetName]
    676             for event in keySetChanges:
    677                 currentBindings[event] = keySetChanges[event].split()
    678         currentKeySequences = list(currentBindings.values())
    679         newKeys = GetKeysDialog(self, 'Get New Keys', bindName,
    680                 currentKeySequences).result
    681         if newKeys: #new keys were specified
    682             if self.keysAreBuiltin.get(): #current key set is a built-in
    683                 message = ('Your changes will be saved as a new Custom Key Set.'
    684                            ' Enter a name for your new Custom Key Set below.')
    685                 newKeySet = self.GetNewKeysName(message)
    686                 if not newKeySet: #user cancelled custom key set creation
    687                     self.listBindings.select_set(listIndex)
    688                     self.listBindings.select_anchor(listIndex)
    689                     return
    690                 else: #create new custom key set based on previously active key set
    691                     self.CreateNewKeySet(newKeySet)
    692             self.listBindings.delete(listIndex)
    693             self.listBindings.insert(listIndex, bindName+' - '+newKeys)
    694             self.listBindings.select_set(listIndex)
    695             self.listBindings.select_anchor(listIndex)
    696             self.keyBinding.set(newKeys)
    697         else:
    698             self.listBindings.select_set(listIndex)
    699             self.listBindings.select_anchor(listIndex)
    700 
    701     def GetNewKeysName(self, message):
    702         usedNames = (idleConf.GetSectionList('user', 'keys') +
    703                 idleConf.GetSectionList('default', 'keys'))
    704         newKeySet = SectionName(
    705                 self, 'New Custom Key Set', message, usedNames).result
    706         return newKeySet
    707 
    708     def SaveAsNewKeySet(self):
    709         newKeysName = self.GetNewKeysName('New Key Set Name:')
    710         if newKeysName:
    711             self.CreateNewKeySet(newKeysName)
    712 
    713     def KeyBindingSelected(self, event):
    714         self.buttonNewKeys.config(state=NORMAL)
    715 
    716     def CreateNewKeySet(self, newKeySetName):
    717         #creates new custom key set based on the previously active key set,
    718         #and makes the new key set active
    719         if self.keysAreBuiltin.get():
    720             prevKeySetName = self.builtinKeys.get()
    721         else:
    722             prevKeySetName = self.customKeys.get()
    723         prevKeys = idleConf.GetCoreKeys(prevKeySetName)
    724         newKeys = {}
    725         for event in prevKeys: #add key set to changed items
    726             eventName = event[2:-2] #trim off the angle brackets
    727             binding = ' '.join(prevKeys[event])
    728             newKeys[eventName] = binding
    729         #handle any unsaved changes to prev key set
    730         if prevKeySetName in self.changedItems['keys']:
    731             keySetChanges = self.changedItems['keys'][prevKeySetName]
    732             for event in keySetChanges:
    733                 newKeys[event] = keySetChanges[event]
    734         #save the new theme
    735         self.SaveNewKeySet(newKeySetName, newKeys)
    736         #change gui over to the new key set
    737         customKeyList = idleConf.GetSectionList('user', 'keys')
    738         customKeyList.sort()
    739         self.optMenuKeysCustom.SetMenu(customKeyList, newKeySetName)
    740         self.keysAreBuiltin.set(0)
    741         self.SetKeysType()
    742 
    743     def LoadKeysList(self, keySetName):
    744         reselect = 0
    745         newKeySet = 0
    746         if self.listBindings.curselection():
    747             reselect = 1
    748             listIndex = self.listBindings.index(ANCHOR)
    749         keySet = idleConf.GetKeySet(keySetName)
    750         bindNames = list(keySet.keys())
    751         bindNames.sort()
    752         self.listBindings.delete(0, END)
    753         for bindName in bindNames:
    754             key = ' '.join(keySet[bindName]) #make key(s) into a string
    755             bindName = bindName[2:-2] #trim off the angle brackets
    756             if keySetName in self.changedItems['keys']:
    757                 #handle any unsaved changes to this key set
    758                 if bindName in self.changedItems['keys'][keySetName]:
    759                     key = self.changedItems['keys'][keySetName][bindName]
    760             self.listBindings.insert(END, bindName+' - '+key)
    761         if reselect:
    762             self.listBindings.see(listIndex)
    763             self.listBindings.select_set(listIndex)
    764             self.listBindings.select_anchor(listIndex)
    765 
    766     def DeleteCustomKeys(self):
    767         keySetName=self.customKeys.get()
    768         delmsg = 'Are you sure you wish to delete the key set %r ?'
    769         if not tkMessageBox.askyesno(
    770                 'Delete Key Set',  delmsg % keySetName, parent=self):
    771             return
    772         self.DeactivateCurrentConfig()
    773         #remove key set from config
    774         idleConf.userCfg['keys'].remove_section(keySetName)
    775         if keySetName in self.changedItems['keys']:
    776             del(self.changedItems['keys'][keySetName])
    777         #write changes
    778         idleConf.userCfg['keys'].Save()
    779         #reload user key set list
    780         itemList = idleConf.GetSectionList('user', 'keys')
    781         itemList.sort()
    782         if not itemList:
    783             self.radioKeysCustom.config(state=DISABLED)
    784             self.optMenuKeysCustom.SetMenu(itemList, '- no custom keys -')
    785         else:
    786             self.optMenuKeysCustom.SetMenu(itemList, itemList[0])
    787         #revert to default key set
    788         self.keysAreBuiltin.set(idleConf.defaultCfg['main']
    789                                 .Get('Keys', 'default'))
    790         self.builtinKeys.set(idleConf.defaultCfg['main'].Get('Keys', 'name')
    791                              or idleConf.default_keys())
    792         #user can't back out of these changes, they must be applied now
    793         self.SaveAllChangedConfigs()
    794         self.ActivateConfigChanges()
    795         self.SetKeysType()
    796 
    797     def DeleteCustomTheme(self):
    798         themeName = self.customTheme.get()
    799         delmsg = 'Are you sure you wish to delete the theme %r ?'
    800         if not tkMessageBox.askyesno(
    801                 'Delete Theme',  delmsg % themeName, parent=self):
    802             return
    803         self.DeactivateCurrentConfig()
    804         #remove theme from config
    805         idleConf.userCfg['highlight'].remove_section(themeName)
    806         if themeName in self.changedItems['highlight']:
    807             del(self.changedItems['highlight'][themeName])
    808         #write changes
    809         idleConf.userCfg['highlight'].Save()
    810         #reload user theme list
    811         itemList = idleConf.GetSectionList('user', 'highlight')
    812         itemList.sort()
    813         if not itemList:
    814             self.radioThemeCustom.config(state=DISABLED)
    815             self.optMenuThemeCustom.SetMenu(itemList, '- no custom themes -')
    816         else:
    817             self.optMenuThemeCustom.SetMenu(itemList, itemList[0])
    818         #revert to default theme
    819         self.themeIsBuiltin.set(idleConf.defaultCfg['main'].Get('Theme', 'default'))
    820         self.builtinTheme.set(idleConf.defaultCfg['main'].Get('Theme', 'name'))
    821         #user can't back out of these changes, they must be applied now
    822         self.SaveAllChangedConfigs()
    823         self.ActivateConfigChanges()
    824         self.SetThemeType()
    825 
    826     def GetColour(self):
    827         target = self.highlightTarget.get()
    828         prevColour = self.frameColourSet.cget('bg')
    829         rgbTuplet, colourString = tkColorChooser.askcolor(
    830                 parent=self, title='Pick new colour for : '+target,
    831                 initialcolor=prevColour)
    832         if colourString and (colourString != prevColour):
    833             #user didn't cancel, and they chose a new colour
    834             if self.themeIsBuiltin.get():  #current theme is a built-in
    835                 message = ('Your changes will be saved as a new Custom Theme. '
    836                            'Enter a name for your new Custom Theme below.')
    837                 newTheme = self.GetNewThemeName(message)
    838                 if not newTheme:  #user cancelled custom theme creation
    839                     return
    840                 else:  #create new custom theme based on previously active theme
    841                     self.CreateNewTheme(newTheme)
    842                     self.colour.set(colourString)
    843             else:  #current theme is user defined
    844                 self.colour.set(colourString)
    845 
    846     def OnNewColourSet(self):
    847         newColour=self.colour.get()
    848         self.frameColourSet.config(bg=newColour)  #set sample
    849         plane ='foreground' if self.fgHilite.get() else 'background'
    850         sampleElement = self.themeElements[self.highlightTarget.get()][0]
    851         self.textHighlightSample.tag_config(sampleElement, **{plane:newColour})
    852         theme = self.customTheme.get()
    853         themeElement = sampleElement + '-' + plane
    854         self.AddChangedItem('highlight', theme, themeElement, newColour)
    855 
    856     def GetNewThemeName(self, message):
    857         usedNames = (idleConf.GetSectionList('user', 'highlight') +
    858                 idleConf.GetSectionList('default', 'highlight'))
    859         newTheme = SectionName(
    860                 self, 'New Custom Theme', message, usedNames).result
    861         return newTheme
    862 
    863     def SaveAsNewTheme(self):
    864         newThemeName = self.GetNewThemeName('New Theme Name:')
    865         if newThemeName:
    866             self.CreateNewTheme(newThemeName)
    867 
    868     def CreateNewTheme(self, newThemeName):
    869         #creates new custom theme based on the previously active theme,
    870         #and makes the new theme active
    871         if self.themeIsBuiltin.get():
    872             themeType = 'default'
    873             themeName = self.builtinTheme.get()
    874         else:
    875             themeType = 'user'
    876             themeName = self.customTheme.get()
    877         newTheme = idleConf.GetThemeDict(themeType, themeName)
    878         #apply any of the old theme's unsaved changes to the new theme
    879         if themeName in self.changedItems['highlight']:
    880             themeChanges = self.changedItems['highlight'][themeName]
    881             for element in themeChanges:
    882                 newTheme[element] = themeChanges[element]
    883         #save the new theme
    884         self.SaveNewTheme(newThemeName, newTheme)
    885         #change gui over to the new theme
    886         customThemeList = idleConf.GetSectionList('user', 'highlight')
    887         customThemeList.sort()
    888         self.optMenuThemeCustom.SetMenu(customThemeList, newThemeName)
    889         self.themeIsBuiltin.set(0)
    890         self.SetThemeType()
    891 
    892     def OnListFontButtonRelease(self, event):
    893         font = self.listFontName.get(ANCHOR)
    894         self.fontName.set(font.lower())
    895         self.SetFontSample()
    896 
    897     def SetFontSample(self, event=None):
    898         fontName = self.fontName.get()
    899         fontWeight = tkFont.BOLD if self.fontBold.get() else tkFont.NORMAL
    900         newFont = (fontName, self.fontSize.get(), fontWeight)
    901         self.labelFontSample.config(font=newFont)
    902         self.textHighlightSample.configure(font=newFont)
    903 
    904     def SetHighlightTarget(self):
    905         if self.highlightTarget.get() == 'Cursor':  #bg not possible
    906             self.radioFg.config(state=DISABLED)
    907             self.radioBg.config(state=DISABLED)
    908             self.fgHilite.set(1)
    909         else:  #both fg and bg can be set
    910             self.radioFg.config(state=NORMAL)
    911             self.radioBg.config(state=NORMAL)
    912             self.fgHilite.set(1)
    913         self.SetColourSample()
    914 
    915     def SetColourSampleBinding(self, *args):
    916         self.SetColourSample()
    917 
    918     def SetColourSample(self):
    919         #set the colour smaple area
    920         tag = self.themeElements[self.highlightTarget.get()][0]
    921         plane = 'foreground' if self.fgHilite.get() else 'background'
    922         colour = self.textHighlightSample.tag_cget(tag, plane)
    923         self.frameColourSet.config(bg=colour)
    924 
    925     def PaintThemeSample(self):
    926         if self.themeIsBuiltin.get():  #a default theme
    927             theme = self.builtinTheme.get()
    928         else:  #a user theme
    929             theme = self.customTheme.get()
    930         for elementTitle in self.themeElements:
    931             element = self.themeElements[elementTitle][0]
    932             colours = idleConf.GetHighlight(theme, element)
    933             if element == 'cursor': #cursor sample needs special painting
    934                 colours['background'] = idleConf.GetHighlight(
    935                         theme, 'normal', fgBg='bg')
    936             #handle any unsaved changes to this theme
    937             if theme in self.changedItems['highlight']:
    938                 themeDict = self.changedItems['highlight'][theme]
    939                 if element + '-foreground' in themeDict:
    940                     colours['foreground'] = themeDict[element + '-foreground']
    941                 if element + '-background' in themeDict:
    942                     colours['background'] = themeDict[element + '-background']
    943             self.textHighlightSample.tag_config(element, **colours)
    944         self.SetColourSample()
    945 
    946     def HelpSourceSelected(self, event):
    947         self.SetHelpListButtonStates()
    948 
    949     def SetHelpListButtonStates(self):
    950         if self.listHelp.size() < 1:  #no entries in list
    951             self.buttonHelpListEdit.config(state=DISABLED)
    952             self.buttonHelpListRemove.config(state=DISABLED)
    953         else: #there are some entries
    954             if self.listHelp.curselection():  #there currently is a selection
    955                 self.buttonHelpListEdit.config(state=NORMAL)
    956                 self.buttonHelpListRemove.config(state=NORMAL)
    957             else:  #there currently is not a selection
    958                 self.buttonHelpListEdit.config(state=DISABLED)
    959                 self.buttonHelpListRemove.config(state=DISABLED)
    960 
    961     def HelpListItemAdd(self):
    962         helpSource = HelpSource(self, 'New Help Source',
    963                                 ).result
    964         if helpSource:
    965             self.userHelpList.append((helpSource[0], helpSource[1]))
    966             self.listHelp.insert(END, helpSource[0])
    967             self.UpdateUserHelpChangedItems()
    968         self.SetHelpListButtonStates()
    969 
    970     def HelpListItemEdit(self):
    971         itemIndex = self.listHelp.index(ANCHOR)
    972         helpSource = self.userHelpList[itemIndex]
    973         newHelpSource = HelpSource(
    974                 self, 'Edit Help Source',
    975                 menuitem=helpSource[0],
    976                 filepath=helpSource[1],
    977                 ).result
    978         if newHelpSource and newHelpSource != helpSource:
    979             self.userHelpList[itemIndex] = newHelpSource
    980             self.listHelp.delete(itemIndex)
    981             self.listHelp.insert(itemIndex, newHelpSource[0])
    982             self.UpdateUserHelpChangedItems()
    983             self.SetHelpListButtonStates()
    984 
    985     def HelpListItemRemove(self):
    986         itemIndex = self.listHelp.index(ANCHOR)
    987         del(self.userHelpList[itemIndex])
    988         self.listHelp.delete(itemIndex)
    989         self.UpdateUserHelpChangedItems()
    990         self.SetHelpListButtonStates()
    991 
    992     def UpdateUserHelpChangedItems(self):
    993         "Clear and rebuild the HelpFiles section in self.changedItems"
    994         self.changedItems['main']['HelpFiles'] = {}
    995         for num in range(1, len(self.userHelpList) + 1):
    996             self.AddChangedItem(
    997                     'main', 'HelpFiles', str(num),
    998                     ';'.join(self.userHelpList[num-1][:2]))
    999 
   1000     def LoadFontCfg(self):
   1001         ##base editor font selection list
   1002         fonts = list(tkFont.families(self))
   1003         fonts.sort()
   1004         for font in fonts:
   1005             self.listFontName.insert(END, font)
   1006         configuredFont = idleConf.GetFont(self, 'main', 'EditorWindow')
   1007         fontName = configuredFont[0].lower()
   1008         fontSize = configuredFont[1]
   1009         fontBold  = configuredFont[2]=='bold'
   1010         self.fontName.set(fontName)
   1011         lc_fonts = [s.lower() for s in fonts]
   1012         try:
   1013             currentFontIndex = lc_fonts.index(fontName)
   1014             self.listFontName.see(currentFontIndex)
   1015             self.listFontName.select_set(currentFontIndex)
   1016             self.listFontName.select_anchor(currentFontIndex)
   1017         except ValueError:
   1018             pass
   1019         ##font size dropdown
   1020         self.optMenuFontSize.SetMenu(('7', '8', '9', '10', '11', '12', '13',
   1021                                       '14', '16', '18', '20', '22',
   1022                                       '25', '29', '34', '40'), fontSize )
   1023         ##fontWeight
   1024         self.fontBold.set(fontBold)
   1025         ##font sample
   1026         self.SetFontSample()
   1027 
   1028     def LoadTabCfg(self):
   1029         ##indent sizes
   1030         spaceNum = idleConf.GetOption(
   1031             'main', 'Indent', 'num-spaces', default=4, type='int')
   1032         self.spaceNum.set(spaceNum)
   1033 
   1034     def LoadThemeCfg(self):
   1035         ##current theme type radiobutton
   1036         self.themeIsBuiltin.set(idleConf.GetOption(
   1037                 'main', 'Theme', 'default', type='bool', default=1))
   1038         ##currently set theme
   1039         currentOption = idleConf.CurrentTheme()
   1040         ##load available theme option menus
   1041         if self.themeIsBuiltin.get(): #default theme selected
   1042             itemList = idleConf.GetSectionList('default', 'highlight')
   1043             itemList.sort()
   1044             self.optMenuThemeBuiltin.SetMenu(itemList, currentOption)
   1045             itemList = idleConf.GetSectionList('user', 'highlight')
   1046             itemList.sort()
   1047             if not itemList:
   1048                 self.radioThemeCustom.config(state=DISABLED)
   1049                 self.customTheme.set('- no custom themes -')
   1050             else:
   1051                 self.optMenuThemeCustom.SetMenu(itemList, itemList[0])
   1052         else: #user theme selected
   1053             itemList = idleConf.GetSectionList('user', 'highlight')
   1054             itemList.sort()
   1055             self.optMenuThemeCustom.SetMenu(itemList, currentOption)
   1056             itemList = idleConf.GetSectionList('default', 'highlight')
   1057             itemList.sort()
   1058             self.optMenuThemeBuiltin.SetMenu(itemList, itemList[0])
   1059         self.SetThemeType()
   1060         ##load theme element option menu
   1061         themeNames = list(self.themeElements.keys())
   1062         themeNames.sort(key=lambda x: self.themeElements[x][1])
   1063         self.optMenuHighlightTarget.SetMenu(themeNames, themeNames[0])
   1064         self.PaintThemeSample()
   1065         self.SetHighlightTarget()
   1066 
   1067     def LoadKeyCfg(self):
   1068         ##current keys type radiobutton
   1069         self.keysAreBuiltin.set(idleConf.GetOption(
   1070                 'main', 'Keys', 'default', type='bool', default=1))
   1071         ##currently set keys
   1072         currentOption = idleConf.CurrentKeys()
   1073         ##load available keyset option menus
   1074         if self.keysAreBuiltin.get(): #default theme selected
   1075             itemList = idleConf.GetSectionList('default', 'keys')
   1076             itemList.sort()
   1077             self.optMenuKeysBuiltin.SetMenu(itemList, currentOption)
   1078             itemList = idleConf.GetSectionList('user', 'keys')
   1079             itemList.sort()
   1080             if not itemList:
   1081                 self.radioKeysCustom.config(state=DISABLED)
   1082                 self.customKeys.set('- no custom keys -')
   1083             else:
   1084                 self.optMenuKeysCustom.SetMenu(itemList, itemList[0])
   1085         else: #user key set selected
   1086             itemList = idleConf.GetSectionList('user', 'keys')
   1087             itemList.sort()
   1088             self.optMenuKeysCustom.SetMenu(itemList, currentOption)
   1089             itemList = idleConf.GetSectionList('default', 'keys')
   1090             itemList.sort()
   1091             self.optMenuKeysBuiltin.SetMenu(itemList, idleConf.default_keys())
   1092         self.SetKeysType()
   1093         ##load keyset element list
   1094         keySetName = idleConf.CurrentKeys()
   1095         self.LoadKeysList(keySetName)
   1096 
   1097     def LoadGeneralCfg(self):
   1098         #startup state
   1099         self.startupEdit.set(idleConf.GetOption(
   1100                 'main', 'General', 'editor-on-startup', default=1, type='bool'))
   1101         #autosave state
   1102         self.autoSave.set(idleConf.GetOption(
   1103                 'main', 'General', 'autosave', default=0, type='bool'))
   1104         #initial window size
   1105         self.winWidth.set(idleConf.GetOption(
   1106                 'main', 'EditorWindow', 'width', type='int'))
   1107         self.winHeight.set(idleConf.GetOption(
   1108                 'main', 'EditorWindow', 'height', type='int'))
   1109         # default source encoding
   1110         self.encoding.set(idleConf.GetOption(
   1111                 'main', 'EditorWindow', 'encoding', default='none'))
   1112         # additional help sources
   1113         self.userHelpList = idleConf.GetAllExtraHelpSourcesList()
   1114         for helpItem in self.userHelpList:
   1115             self.listHelp.insert(END, helpItem[0])
   1116         self.SetHelpListButtonStates()
   1117 
   1118     def LoadConfigs(self):
   1119         """
   1120         load configuration from default and user config files and populate
   1121         the widgets on the config dialog pages.
   1122         """
   1123         ### fonts / tabs page
   1124         self.LoadFontCfg()
   1125         self.LoadTabCfg()
   1126         ### highlighting page
   1127         self.LoadThemeCfg()
   1128         ### keys page
   1129         self.LoadKeyCfg()
   1130         ### general page
   1131         self.LoadGeneralCfg()
   1132         # note: extension page handled separately
   1133 
   1134     def SaveNewKeySet(self, keySetName, keySet):
   1135         """
   1136         save a newly created core key set.
   1137         keySetName - string, the name of the new key set
   1138         keySet - dictionary containing the new key set
   1139         """
   1140         if not idleConf.userCfg['keys'].has_section(keySetName):
   1141             idleConf.userCfg['keys'].add_section(keySetName)
   1142         for event in keySet:
   1143             value = keySet[event]
   1144             idleConf.userCfg['keys'].SetOption(keySetName, event, value)
   1145 
   1146     def SaveNewTheme(self, themeName, theme):
   1147         """
   1148         save a newly created theme.
   1149         themeName - string, the name of the new theme
   1150         theme - dictionary containing the new theme
   1151         """
   1152         if not idleConf.userCfg['highlight'].has_section(themeName):
   1153             idleConf.userCfg['highlight'].add_section(themeName)
   1154         for element in theme:
   1155             value = theme[element]
   1156             idleConf.userCfg['highlight'].SetOption(themeName, element, value)
   1157 
   1158     def SetUserValue(self, configType, section, item, value):
   1159         if idleConf.defaultCfg[configType].has_option(section, item):
   1160             if idleConf.defaultCfg[configType].Get(section, item) == value:
   1161                 #the setting equals a default setting, remove it from user cfg
   1162                 return idleConf.userCfg[configType].RemoveOption(section, item)
   1163         #if we got here set the option
   1164         return idleConf.userCfg[configType].SetOption(section, item, value)
   1165 
   1166     def SaveAllChangedConfigs(self):
   1167         "Save configuration changes to the user config file."
   1168         idleConf.userCfg['main'].Save()
   1169         for configType in self.changedItems:
   1170             cfgTypeHasChanges = False
   1171             for section in self.changedItems[configType]:
   1172                 if section == 'HelpFiles':
   1173                     #this section gets completely replaced
   1174                     idleConf.userCfg['main'].remove_section('HelpFiles')
   1175                     cfgTypeHasChanges = True
   1176                 for item in self.changedItems[configType][section]:
   1177                     value = self.changedItems[configType][section][item]
   1178                     if self.SetUserValue(configType, section, item, value):
   1179                         cfgTypeHasChanges = True
   1180             if cfgTypeHasChanges:
   1181                 idleConf.userCfg[configType].Save()
   1182         for configType in ['keys', 'highlight']:
   1183             # save these even if unchanged!
   1184             idleConf.userCfg[configType].Save()
   1185         self.ResetChangedItems() #clear the changed items dict
   1186         self.save_all_changed_extensions()  # uses a different mechanism
   1187 
   1188     def DeactivateCurrentConfig(self):
   1189         #Before a config is saved, some cleanup of current
   1190         #config must be done - remove the previous keybindings
   1191         winInstances = self.parent.instance_dict.keys()
   1192         for instance in winInstances:
   1193             instance.RemoveKeybindings()
   1194 
   1195     def ActivateConfigChanges(self):
   1196         "Dynamically apply configuration changes"
   1197         winInstances = self.parent.instance_dict.keys()
   1198         for instance in winInstances:
   1199             instance.ResetColorizer()
   1200             instance.ResetFont()
   1201             instance.set_notabs_indentwidth()
   1202             instance.ApplyKeybindings()
   1203             instance.reset_help_menu_entries()
   1204 
   1205     def Cancel(self):
   1206         self.destroy()
   1207 
   1208     def Ok(self):
   1209         self.Apply()
   1210         self.destroy()
   1211 
   1212     def Apply(self):
   1213         self.DeactivateCurrentConfig()
   1214         self.SaveAllChangedConfigs()
   1215         self.ActivateConfigChanges()
   1216 
   1217     def Help(self):
   1218         page = self.tabPages._current_page
   1219         view_text(self, title='Help for IDLE preferences',
   1220                  text=help_common+help_pages.get(page, ''))
   1221 
   1222     def CreatePageExtensions(self):
   1223         """Part of the config dialog used for configuring IDLE extensions.
   1224 
   1225         This code is generic - it works for any and all IDLE extensions.
   1226 
   1227         IDLE extensions save their configuration options using idleConf.
   1228         This code reads the current configuration using idleConf, supplies a
   1229         GUI interface to change the configuration values, and saves the
   1230         changes using idleConf.
   1231 
   1232         Not all changes take effect immediately - some may require restarting IDLE.
   1233         This depends on each extension's implementation.
   1234 
   1235         All values are treated as text, and it is up to the user to supply
   1236         reasonable values. The only exception to this are the 'enable*' options,
   1237         which are boolean, and can be toggled with a True/False button.
   1238         """
   1239         parent = self.parent
   1240         frame = self.tabPages.pages['Extensions'].frame
   1241         self.ext_defaultCfg = idleConf.defaultCfg['extensions']
   1242         self.ext_userCfg = idleConf.userCfg['extensions']
   1243         self.is_int = self.register(is_int)
   1244         self.load_extensions()
   1245         # create widgets - a listbox shows all available extensions, with the
   1246         # controls for the extension selected in the listbox to the right
   1247         self.extension_names = StringVar(self)
   1248         frame.rowconfigure(0, weight=1)
   1249         frame.columnconfigure(2, weight=1)
   1250         self.extension_list = Listbox(frame, listvariable=self.extension_names,
   1251                                       selectmode='browse')
   1252         self.extension_list.bind('<<ListboxSelect>>', self.extension_selected)
   1253         scroll = Scrollbar(frame, command=self.extension_list.yview)
   1254         self.extension_list.yscrollcommand=scroll.set
   1255         self.details_frame = LabelFrame(frame, width=250, height=250)
   1256         self.extension_list.grid(column=0, row=0, sticky='nws')
   1257         scroll.grid(column=1, row=0, sticky='ns')
   1258         self.details_frame.grid(column=2, row=0, sticky='nsew', padx=[10, 0])
   1259         frame.configure(padx=10, pady=10)
   1260         self.config_frame = {}
   1261         self.current_extension = None
   1262 
   1263         self.outerframe = self                      # TEMPORARY
   1264         self.tabbed_page_set = self.extension_list  # TEMPORARY
   1265 
   1266         # create the frame holding controls for each extension
   1267         ext_names = ''
   1268         for ext_name in sorted(self.extensions):
   1269             self.create_extension_frame(ext_name)
   1270             ext_names = ext_names + '{' + ext_name + '} '
   1271         self.extension_names.set(ext_names)
   1272         self.extension_list.selection_set(0)
   1273         self.extension_selected(None)
   1274 
   1275     def load_extensions(self):
   1276         "Fill self.extensions with data from the default and user configs."
   1277         self.extensions = {}
   1278         for ext_name in idleConf.GetExtensions(active_only=False):
   1279             self.extensions[ext_name] = []
   1280 
   1281         for ext_name in self.extensions:
   1282             opt_list = sorted(self.ext_defaultCfg.GetOptionList(ext_name))
   1283 
   1284             # bring 'enable' options to the beginning of the list
   1285             enables = [opt_name for opt_name in opt_list
   1286                        if opt_name.startswith('enable')]
   1287             for opt_name in enables:
   1288                 opt_list.remove(opt_name)
   1289             opt_list = enables + opt_list
   1290 
   1291             for opt_name in opt_list:
   1292                 def_str = self.ext_defaultCfg.Get(
   1293                         ext_name, opt_name, raw=True)
   1294                 try:
   1295                     def_obj = {'True':True, 'False':False}[def_str]
   1296                     opt_type = 'bool'
   1297                 except KeyError:
   1298                     try:
   1299                         def_obj = int(def_str)
   1300                         opt_type = 'int'
   1301                     except ValueError:
   1302                         def_obj = def_str
   1303                         opt_type = None
   1304                 try:
   1305                     value = self.ext_userCfg.Get(
   1306                             ext_name, opt_name, type=opt_type, raw=True,
   1307                             default=def_obj)
   1308                 except ValueError:  # Need this until .Get fixed
   1309                     value = def_obj  # bad values overwritten by entry
   1310                 var = StringVar(self)
   1311                 var.set(str(value))
   1312 
   1313                 self.extensions[ext_name].append({'name': opt_name,
   1314                                                   'type': opt_type,
   1315                                                   'default': def_str,
   1316                                                   'value': value,
   1317                                                   'var': var,
   1318                                                  })
   1319 
   1320     def extension_selected(self, event):
   1321         newsel = self.extension_list.curselection()
   1322         if newsel:
   1323             newsel = self.extension_list.get(newsel)
   1324         if newsel is None or newsel != self.current_extension:
   1325             if self.current_extension:
   1326                 self.details_frame.config(text='')
   1327                 self.config_frame[self.current_extension].grid_forget()
   1328                 self.current_extension = None
   1329         if newsel:
   1330             self.details_frame.config(text=newsel)
   1331             self.config_frame[newsel].grid(column=0, row=0, sticky='nsew')
   1332             self.current_extension = newsel
   1333 
   1334     def create_extension_frame(self, ext_name):
   1335         """Create a frame holding the widgets to configure one extension"""
   1336         f = VerticalScrolledFrame(self.details_frame, height=250, width=250)
   1337         self.config_frame[ext_name] = f
   1338         entry_area = f.interior
   1339         # create an entry for each configuration option
   1340         for row, opt in enumerate(self.extensions[ext_name]):
   1341             # create a row with a label and entry/checkbutton
   1342             label = Label(entry_area, text=opt['name'])
   1343             label.grid(row=row, column=0, sticky=NW)
   1344             var = opt['var']
   1345             if opt['type'] == 'bool':
   1346                 Checkbutton(entry_area, textvariable=var, variable=var,
   1347                             onvalue='True', offvalue='False',
   1348                             indicatoron=FALSE, selectcolor='', width=8
   1349                             ).grid(row=row, column=1, sticky=W, padx=7)
   1350             elif opt['type'] == 'int':
   1351                 Entry(entry_area, textvariable=var, validate='key',
   1352                       validatecommand=(self.is_int, '%P')
   1353                       ).grid(row=row, column=1, sticky=NSEW, padx=7)
   1354 
   1355             else:
   1356                 Entry(entry_area, textvariable=var
   1357                       ).grid(row=row, column=1, sticky=NSEW, padx=7)
   1358         return
   1359 
   1360     def set_extension_value(self, section, opt):
   1361         name = opt['name']
   1362         default = opt['default']
   1363         value = opt['var'].get().strip() or default
   1364         opt['var'].set(value)
   1365         # if self.defaultCfg.has_section(section):
   1366         # Currently, always true; if not, indent to return
   1367         if (value == default):
   1368             return self.ext_userCfg.RemoveOption(section, name)
   1369         # set the option
   1370         return self.ext_userCfg.SetOption(section, name, value)
   1371 
   1372     def save_all_changed_extensions(self):
   1373         """Save configuration changes to the user config file."""
   1374         has_changes = False
   1375         for ext_name in self.extensions:
   1376             options = self.extensions[ext_name]
   1377             for opt in options:
   1378                 if self.set_extension_value(ext_name, opt):
   1379                     has_changes = True
   1380         if has_changes:
   1381             self.ext_userCfg.Save()
   1382 
   1383 
   1384 help_common = '''\
   1385 When you click either the Apply or Ok buttons, settings in this
   1386 dialog that are different from IDLE's default are saved in
   1387 a .idlerc directory in your home directory. Except as noted,
   1388 these changes apply to all versions of IDLE installed on this
   1389 machine. Some do not take affect until IDLE is restarted.
   1390 [Cancel] only cancels changes made since the last save.
   1391 '''
   1392 help_pages = {
   1393     'Highlighting': '''
   1394 Highlighting:
   1395 The IDLE Dark color theme is new in October 2015.  It can only
   1396 be used with older IDLE releases if it is saved as a custom
   1397 theme, with a different name.
   1398 ''',
   1399     'Keys': '''
   1400 Keys:
   1401 The IDLE Modern Unix key set is new in June 2016.  It can only
   1402 be used with older IDLE releases if it is saved as a custom
   1403 key set, with a different name.
   1404 ''',
   1405 }
   1406 
   1407 
   1408 def is_int(s):
   1409     "Return 's is blank or represents an int'"
   1410     if not s:
   1411         return True
   1412     try:
   1413         int(s)
   1414         return True
   1415     except ValueError:
   1416         return False
   1417 
   1418 
   1419 class VerticalScrolledFrame(Frame):
   1420     """A pure Tkinter vertically scrollable frame.
   1421 
   1422     * Use the 'interior' attribute to place widgets inside the scrollable frame
   1423     * Construct and pack/place/grid normally
   1424     * This frame only allows vertical scrolling
   1425     """
   1426     def __init__(self, parent, *args, **kw):
   1427         Frame.__init__(self, parent, *args, **kw)
   1428 
   1429         # create a canvas object and a vertical scrollbar for scrolling it
   1430         vscrollbar = Scrollbar(self, orient=VERTICAL)
   1431         vscrollbar.pack(fill=Y, side=RIGHT, expand=FALSE)
   1432         canvas = Canvas(self, bd=0, highlightthickness=0,
   1433                         yscrollcommand=vscrollbar.set, width=240)
   1434         canvas.pack(side=LEFT, fill=BOTH, expand=TRUE)
   1435         vscrollbar.config(command=canvas.yview)
   1436 
   1437         # reset the view
   1438         canvas.xview_moveto(0)
   1439         canvas.yview_moveto(0)
   1440 
   1441         # create a frame inside the canvas which will be scrolled with it
   1442         self.interior = interior = Frame(canvas)
   1443         interior_id = canvas.create_window(0, 0, window=interior, anchor=NW)
   1444 
   1445         # track changes to the canvas and frame width and sync them,
   1446         # also updating the scrollbar
   1447         def _configure_interior(event):
   1448             # update the scrollbars to match the size of the inner frame
   1449             size = (interior.winfo_reqwidth(), interior.winfo_reqheight())
   1450             canvas.config(scrollregion="0 0 %s %s" % size)
   1451         interior.bind('<Configure>', _configure_interior)
   1452 
   1453         def _configure_canvas(event):
   1454             if interior.winfo_reqwidth() != canvas.winfo_width():
   1455                 # update the inner frame's width to fill the canvas
   1456                 canvas.itemconfigure(interior_id, width=canvas.winfo_width())
   1457         canvas.bind('<Configure>', _configure_canvas)
   1458 
   1459         return
   1460 
   1461 
   1462 if __name__ == '__main__':
   1463     import unittest
   1464     unittest.main('idlelib.idle_test.test_configdialog',
   1465                   verbosity=2, exit=False)
   1466     from idlelib.idle_test.htest import run
   1467     run(ConfigDialog)
   1468