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