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