• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1"""IDLE Configuration Dialog: support user customization of IDLE by GUI
2
3Customize font faces, sizes, and colorization attributes.  Set indentation
4defaults.  Customize keybindings.  Colorization and keybindings can be
5saved as user defined sets.  Select startup options including shell/editor
6and default window size.  Define additional help sources.
7
8Note that tab width in IDLE is currently fixed at eight due to Tk issues.
9Refer to comments in EditorWindow autoindent code for details.
10
11"""
12from tkinter import *
13from tkinter.ttk import Scrollbar
14import tkinter.colorchooser as tkColorChooser
15import tkinter.font as tkFont
16import tkinter.messagebox as tkMessageBox
17
18from idlelib.config import idleConf
19from idlelib.config_key import GetKeysDialog
20from idlelib.dynoption import DynOptionMenu
21from idlelib import macosx
22from idlelib.query import SectionName, HelpSource
23from idlelib.tabbedpages import TabbedPageSet
24from idlelib.textview import view_text
25
26class 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
1384help_common = '''\
1385When you click either the Apply or Ok buttons, settings in this
1386dialog that are different from IDLE's default are saved in
1387a .idlerc directory in your home directory. Except as noted,
1388these changes apply to all versions of IDLE installed on this
1389machine. Some do not take affect until IDLE is restarted.
1390[Cancel] only cancels changes made since the last save.
1391'''
1392help_pages = {
1393    'Highlighting': '''
1394Highlighting:
1395The IDLE Dark color theme is new in October 2015.  It can only
1396be used with older IDLE releases if it is saved as a custom
1397theme, with a different name.
1398''',
1399    'Keys': '''
1400Keys:
1401The IDLE Modern Unix key set is new in June 2016.  It can only
1402be used with older IDLE releases if it is saved as a custom
1403key set, with a different name.
1404''',
1405}
1406
1407
1408def 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
1419class 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
1462if __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