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