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 *
13import tkMessageBox, tkColorChooser, tkFont
14
15from idlelib.configHandler import idleConf
16from idlelib.dynOptionMenuWidget import DynOptionMenu
17from idlelib.keybindingDialog import GetKeysDialog
18from idlelib.configSectionNameDialog import GetCfgSectionNameDialog
19from idlelib.configHelpSourceEdit import GetHelpSourceDialog
20from idlelib.tabbedpages import TabbedPageSet
21from idlelib.textView import view_text
22from idlelib import macosxSupport
23
24class 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.grab_release()
1201        self.destroy()
1202
1203    def Ok(self):
1204        self.Apply()
1205        self.grab_release()
1206        self.destroy()
1207
1208    def Apply(self):
1209        self.DeactivateCurrentConfig()
1210        self.SaveAllChangedConfigs()
1211        self.ActivateConfigChanges()
1212
1213    def Help(self):
1214        page = self.tabPages._current_page
1215        view_text(self, title='Help for IDLE preferences',
1216                 text=help_common+help_pages.get(page, ''))
1217
1218    def CreatePageExtensions(self):
1219        """Part of the config dialog used for configuring IDLE extensions.
1220
1221        This code is generic - it works for any and all IDLE extensions.
1222
1223        IDLE extensions save their configuration options using idleConf.
1224        This code reads the current configuration using idleConf, supplies a
1225        GUI interface to change the configuration values, and saves the
1226        changes using idleConf.
1227
1228        Not all changes take effect immediately - some may require restarting IDLE.
1229        This depends on each extension's implementation.
1230
1231        All values are treated as text, and it is up to the user to supply
1232        reasonable values. The only exception to this are the 'enable*' options,
1233        which are boolean, and can be toggled with a True/False button.
1234        """
1235        parent = self.parent
1236        frame = self.tabPages.pages['Extensions'].frame
1237        self.ext_defaultCfg = idleConf.defaultCfg['extensions']
1238        self.ext_userCfg = idleConf.userCfg['extensions']
1239        self.is_int = self.register(is_int)
1240        self.load_extensions()
1241        # create widgets - a listbox shows all available extensions, with the
1242        # controls for the extension selected in the listbox to the right
1243        self.extension_names = StringVar(self)
1244        frame.rowconfigure(0, weight=1)
1245        frame.columnconfigure(2, weight=1)
1246        self.extension_list = Listbox(frame, listvariable=self.extension_names,
1247                                      selectmode='browse')
1248        self.extension_list.bind('<<ListboxSelect>>', self.extension_selected)
1249        scroll = Scrollbar(frame, command=self.extension_list.yview)
1250        self.extension_list.yscrollcommand=scroll.set
1251        self.details_frame = LabelFrame(frame, width=250, height=250)
1252        self.extension_list.grid(column=0, row=0, sticky='nws')
1253        scroll.grid(column=1, row=0, sticky='ns')
1254        self.details_frame.grid(column=2, row=0, sticky='nsew', padx=[10, 0])
1255        frame.configure(padx=10, pady=10)
1256        self.config_frame = {}
1257        self.current_extension = None
1258
1259        self.outerframe = self                      # TEMPORARY
1260        self.tabbed_page_set = self.extension_list  # TEMPORARY
1261
1262        # create the frame holding controls for each extension
1263        ext_names = ''
1264        for ext_name in sorted(self.extensions):
1265            self.create_extension_frame(ext_name)
1266            ext_names = ext_names + '{' + ext_name + '} '
1267        self.extension_names.set(ext_names)
1268        self.extension_list.selection_set(0)
1269        self.extension_selected(None)
1270
1271    def load_extensions(self):
1272        "Fill self.extensions with data from the default and user configs."
1273        self.extensions = {}
1274        for ext_name in idleConf.GetExtensions(active_only=False):
1275            self.extensions[ext_name] = []
1276
1277        for ext_name in self.extensions:
1278            opt_list = sorted(self.ext_defaultCfg.GetOptionList(ext_name))
1279
1280            # bring 'enable' options to the beginning of the list
1281            enables = [opt_name for opt_name in opt_list
1282                       if opt_name.startswith('enable')]
1283            for opt_name in enables:
1284                opt_list.remove(opt_name)
1285            opt_list = enables + opt_list
1286
1287            for opt_name in opt_list:
1288                def_str = self.ext_defaultCfg.Get(
1289                        ext_name, opt_name, raw=True)
1290                try:
1291                    def_obj = {'True':True, 'False':False}[def_str]
1292                    opt_type = 'bool'
1293                except KeyError:
1294                    try:
1295                        def_obj = int(def_str)
1296                        opt_type = 'int'
1297                    except ValueError:
1298                        def_obj = def_str
1299                        opt_type = None
1300                try:
1301                    value = self.ext_userCfg.Get(
1302                            ext_name, opt_name, type=opt_type, raw=True,
1303                            default=def_obj)
1304                except ValueError:  # Need this until .Get fixed
1305                    value = def_obj  # bad values overwritten by entry
1306                var = StringVar(self)
1307                var.set(str(value))
1308
1309                self.extensions[ext_name].append({'name': opt_name,
1310                                                  'type': opt_type,
1311                                                  'default': def_str,
1312                                                  'value': value,
1313                                                  'var': var,
1314                                                 })
1315
1316    def extension_selected(self, event):
1317        newsel = self.extension_list.curselection()
1318        if newsel:
1319            newsel = self.extension_list.get(newsel)
1320        if newsel is None or newsel != self.current_extension:
1321            if self.current_extension:
1322                self.details_frame.config(text='')
1323                self.config_frame[self.current_extension].grid_forget()
1324                self.current_extension = None
1325        if newsel:
1326            self.details_frame.config(text=newsel)
1327            self.config_frame[newsel].grid(column=0, row=0, sticky='nsew')
1328            self.current_extension = newsel
1329
1330    def create_extension_frame(self, ext_name):
1331        """Create a frame holding the widgets to configure one extension"""
1332        f = VerticalScrolledFrame(self.details_frame, height=250, width=250)
1333        self.config_frame[ext_name] = f
1334        entry_area = f.interior
1335        # create an entry for each configuration option
1336        for row, opt in enumerate(self.extensions[ext_name]):
1337            # create a row with a label and entry/checkbutton
1338            label = Label(entry_area, text=opt['name'])
1339            label.grid(row=row, column=0, sticky=NW)
1340            var = opt['var']
1341            if opt['type'] == 'bool':
1342                Checkbutton(entry_area, textvariable=var, variable=var,
1343                            onvalue='True', offvalue='False',
1344                            indicatoron=FALSE, selectcolor='', width=8
1345                            ).grid(row=row, column=1, sticky=W, padx=7)
1346            elif opt['type'] == 'int':
1347                Entry(entry_area, textvariable=var, validate='key',
1348                      validatecommand=(self.is_int, '%P')
1349                      ).grid(row=row, column=1, sticky=NSEW, padx=7)
1350
1351            else:
1352                Entry(entry_area, textvariable=var
1353                      ).grid(row=row, column=1, sticky=NSEW, padx=7)
1354        return
1355
1356    def set_extension_value(self, section, opt):
1357        name = opt['name']
1358        default = opt['default']
1359        value = opt['var'].get().strip() or default
1360        opt['var'].set(value)
1361        # if self.defaultCfg.has_section(section):
1362        # Currently, always true; if not, indent to return
1363        if (value == default):
1364            return self.ext_userCfg.RemoveOption(section, name)
1365        # set the option
1366        return self.ext_userCfg.SetOption(section, name, value)
1367
1368    def save_all_changed_extensions(self):
1369        """Save configuration changes to the user config file."""
1370        has_changes = False
1371        for ext_name in self.extensions:
1372            options = self.extensions[ext_name]
1373            for opt in options:
1374                if self.set_extension_value(ext_name, opt):
1375                    has_changes = True
1376        if has_changes:
1377            self.ext_userCfg.Save()
1378
1379
1380help_common = '''\
1381When you click either the Apply or Ok buttons, settings in this
1382dialog that are different from IDLE's default are saved in
1383a .idlerc directory in your home directory. Except as noted,
1384these changes apply to all versions of IDLE installed on this
1385machine. Some do not take affect until IDLE is restarted.
1386[Cancel] only cancels changes made since the last save.
1387'''
1388help_pages = {
1389    'Highlighting':'''
1390Highlighting:
1391The IDLE Dark color theme is new in October 2015.  It can only
1392be used with older IDLE releases if it is saved as a custom
1393theme, with a different name.
1394'''
1395}
1396
1397
1398def is_int(s):
1399    "Return 's is blank or represents an int'"
1400    if not s:
1401        return True
1402    try:
1403        int(s)
1404        return True
1405    except ValueError:
1406        return False
1407
1408
1409class VerticalScrolledFrame(Frame):
1410    """A pure Tkinter vertically scrollable frame.
1411
1412    * Use the 'interior' attribute to place widgets inside the scrollable frame
1413    * Construct and pack/place/grid normally
1414    * This frame only allows vertical scrolling
1415    """
1416    def __init__(self, parent, *args, **kw):
1417        Frame.__init__(self, parent, *args, **kw)
1418
1419        # create a canvas object and a vertical scrollbar for scrolling it
1420        vscrollbar = Scrollbar(self, orient=VERTICAL)
1421        vscrollbar.pack(fill=Y, side=RIGHT, expand=FALSE)
1422        canvas = Canvas(self, bd=0, highlightthickness=0,
1423                        yscrollcommand=vscrollbar.set, width=240)
1424        canvas.pack(side=LEFT, fill=BOTH, expand=TRUE)
1425        vscrollbar.config(command=canvas.yview)
1426
1427        # reset the view
1428        canvas.xview_moveto(0)
1429        canvas.yview_moveto(0)
1430
1431        # create a frame inside the canvas which will be scrolled with it
1432        self.interior = interior = Frame(canvas)
1433        interior_id = canvas.create_window(0, 0, window=interior, anchor=NW)
1434
1435        # track changes to the canvas and frame width and sync them,
1436        # also updating the scrollbar
1437        def _configure_interior(event):
1438            # update the scrollbars to match the size of the inner frame
1439            size = (interior.winfo_reqwidth(), interior.winfo_reqheight())
1440            canvas.config(scrollregion="0 0 %s %s" % size)
1441        interior.bind('<Configure>', _configure_interior)
1442
1443        def _configure_canvas(event):
1444            if interior.winfo_reqwidth() != canvas.winfo_width():
1445                # update the inner frame's width to fill the canvas
1446                canvas.itemconfigure(interior_id, width=canvas.winfo_width())
1447        canvas.bind('<Configure>', _configure_canvas)
1448
1449        return
1450
1451
1452if __name__ == '__main__':
1453    import unittest
1454    unittest.main('idlelib.idle_test.test_configdialog',
1455                  verbosity=2, exit=False)
1456    from idlelib.idle_test.htest import run
1457    run(ConfigDialog)
1458