mirror of https://github.com/python/cpython
1468 lines
65 KiB
Python
1468 lines
65 KiB
Python
"""IDLE Configuration Dialog: support user customization of IDLE by GUI
|
|
|
|
Customize font faces, sizes, and colorization attributes. Set indentation
|
|
defaults. Customize keybindings. Colorization and keybindings can be
|
|
saved as user defined sets. Select startup options including shell/editor
|
|
and default window size. Define additional help sources.
|
|
|
|
Note that tab width in IDLE is currently fixed at eight due to Tk issues.
|
|
Refer to comments in EditorWindow autoindent code for details.
|
|
|
|
"""
|
|
from tkinter import *
|
|
from tkinter.ttk import Scrollbar
|
|
import tkinter.colorchooser as tkColorChooser
|
|
import tkinter.font as tkFont
|
|
import tkinter.messagebox as tkMessageBox
|
|
|
|
from idlelib.config import idleConf
|
|
from idlelib.config_key import GetKeysDialog
|
|
from idlelib.dynoption import DynOptionMenu
|
|
from idlelib import macosx
|
|
from idlelib.query import SectionName, HelpSource
|
|
from idlelib.tabbedpages import TabbedPageSet
|
|
from idlelib.textview import view_text
|
|
|
|
class ConfigDialog(Toplevel):
|
|
|
|
def __init__(self, parent, title='', _htest=False, _utest=False):
|
|
"""
|
|
_htest - bool, change box location when running htest
|
|
_utest - bool, don't wait_window when running unittest
|
|
"""
|
|
Toplevel.__init__(self, parent)
|
|
self.parent = parent
|
|
if _htest:
|
|
parent.instance_dict = {}
|
|
self.wm_withdraw()
|
|
|
|
self.configure(borderwidth=5)
|
|
self.title(title or 'IDLE Preferences')
|
|
self.geometry(
|
|
"+%d+%d" % (parent.winfo_rootx() + 20,
|
|
parent.winfo_rooty() + (30 if not _htest else 150)))
|
|
#Theme Elements. Each theme element key is its display name.
|
|
#The first value of the tuple is the sample area tag name.
|
|
#The second value is the display name list sort index.
|
|
self.themeElements={
|
|
'Normal Text': ('normal', '00'),
|
|
'Python Keywords': ('keyword', '01'),
|
|
'Python Definitions': ('definition', '02'),
|
|
'Python Builtins': ('builtin', '03'),
|
|
'Python Comments': ('comment', '04'),
|
|
'Python Strings': ('string', '05'),
|
|
'Selected Text': ('hilite', '06'),
|
|
'Found Text': ('hit', '07'),
|
|
'Cursor': ('cursor', '08'),
|
|
'Editor Breakpoint': ('break', '09'),
|
|
'Shell Normal Text': ('console', '10'),
|
|
'Shell Error Text': ('error', '11'),
|
|
'Shell Stdout Text': ('stdout', '12'),
|
|
'Shell Stderr Text': ('stderr', '13'),
|
|
}
|
|
self.ResetChangedItems() #load initial values in changed items dict
|
|
self.CreateWidgets()
|
|
self.resizable(height=FALSE, width=FALSE)
|
|
self.transient(parent)
|
|
self.grab_set()
|
|
self.protocol("WM_DELETE_WINDOW", self.Cancel)
|
|
self.tabPages.focus_set()
|
|
#key bindings for this dialog
|
|
#self.bind('<Escape>', self.Cancel) #dismiss dialog, no save
|
|
#self.bind('<Alt-a>', self.Apply) #apply changes, save
|
|
#self.bind('<F1>', self.Help) #context help
|
|
self.LoadConfigs()
|
|
self.AttachVarCallbacks() #avoid callbacks during LoadConfigs
|
|
|
|
if not _utest:
|
|
self.wm_deiconify()
|
|
self.wait_window()
|
|
|
|
def CreateWidgets(self):
|
|
self.tabPages = TabbedPageSet(self,
|
|
page_names=['Fonts/Tabs', 'Highlighting', 'Keys', 'General',
|
|
'Extensions'])
|
|
self.tabPages.pack(side=TOP, expand=TRUE, fill=BOTH)
|
|
self.CreatePageFontTab()
|
|
self.CreatePageHighlight()
|
|
self.CreatePageKeys()
|
|
self.CreatePageGeneral()
|
|
self.CreatePageExtensions()
|
|
self.create_action_buttons().pack(side=BOTTOM)
|
|
|
|
def create_action_buttons(self):
|
|
if macosx.isAquaTk():
|
|
# Changing the default padding on OSX results in unreadable
|
|
# text in the buttons
|
|
paddingArgs = {}
|
|
else:
|
|
paddingArgs = {'padx':6, 'pady':3}
|
|
outer = Frame(self, pady=2)
|
|
buttons = Frame(outer, pady=2)
|
|
for txt, cmd in (
|
|
('Ok', self.Ok),
|
|
('Apply', self.Apply),
|
|
('Cancel', self.Cancel),
|
|
('Help', self.Help)):
|
|
Button(buttons, text=txt, command=cmd, takefocus=FALSE,
|
|
**paddingArgs).pack(side=LEFT, padx=5)
|
|
# add space above buttons
|
|
Frame(outer, height=2, borderwidth=0).pack(side=TOP)
|
|
buttons.pack(side=BOTTOM)
|
|
return outer
|
|
|
|
def CreatePageFontTab(self):
|
|
parent = self.parent
|
|
self.fontSize = StringVar(parent)
|
|
self.fontBold = BooleanVar(parent)
|
|
self.fontName = StringVar(parent)
|
|
self.spaceNum = IntVar(parent)
|
|
self.editFont = tkFont.Font(parent, ('courier', 10, 'normal'))
|
|
|
|
##widget creation
|
|
#body frame
|
|
frame = self.tabPages.pages['Fonts/Tabs'].frame
|
|
#body section frames
|
|
frameFont = LabelFrame(
|
|
frame, borderwidth=2, relief=GROOVE, text=' Base Editor Font ')
|
|
frameIndent = LabelFrame(
|
|
frame, borderwidth=2, relief=GROOVE, text=' Indentation Width ')
|
|
#frameFont
|
|
frameFontName = Frame(frameFont)
|
|
frameFontParam = Frame(frameFont)
|
|
labelFontNameTitle = Label(
|
|
frameFontName, justify=LEFT, text='Font Face :')
|
|
self.listFontName = Listbox(
|
|
frameFontName, height=5, takefocus=FALSE, exportselection=FALSE)
|
|
self.listFontName.bind(
|
|
'<ButtonRelease-1>', self.OnListFontButtonRelease)
|
|
scrollFont = Scrollbar(frameFontName)
|
|
scrollFont.config(command=self.listFontName.yview)
|
|
self.listFontName.config(yscrollcommand=scrollFont.set)
|
|
labelFontSizeTitle = Label(frameFontParam, text='Size :')
|
|
self.optMenuFontSize = DynOptionMenu(
|
|
frameFontParam, self.fontSize, None, command=self.SetFontSample)
|
|
checkFontBold = Checkbutton(
|
|
frameFontParam, variable=self.fontBold, onvalue=1,
|
|
offvalue=0, text='Bold', command=self.SetFontSample)
|
|
frameFontSample = Frame(frameFont, relief=SOLID, borderwidth=1)
|
|
self.labelFontSample = Label(
|
|
frameFontSample, justify=LEFT, font=self.editFont,
|
|
text='AaBbCcDdEe\nFfGgHhIiJjK\n1234567890\n#:+=(){}[]')
|
|
#frameIndent
|
|
frameIndentSize = Frame(frameIndent)
|
|
labelSpaceNumTitle = Label(
|
|
frameIndentSize, justify=LEFT,
|
|
text='Python Standard: 4 Spaces!')
|
|
self.scaleSpaceNum = Scale(
|
|
frameIndentSize, variable=self.spaceNum,
|
|
orient='horizontal', tickinterval=2, from_=2, to=16)
|
|
|
|
#widget packing
|
|
#body
|
|
frameFont.pack(side=LEFT, padx=5, pady=5, expand=TRUE, fill=BOTH)
|
|
frameIndent.pack(side=LEFT, padx=5, pady=5, fill=Y)
|
|
#frameFont
|
|
frameFontName.pack(side=TOP, padx=5, pady=5, fill=X)
|
|
frameFontParam.pack(side=TOP, padx=5, pady=5, fill=X)
|
|
labelFontNameTitle.pack(side=TOP, anchor=W)
|
|
self.listFontName.pack(side=LEFT, expand=TRUE, fill=X)
|
|
scrollFont.pack(side=LEFT, fill=Y)
|
|
labelFontSizeTitle.pack(side=LEFT, anchor=W)
|
|
self.optMenuFontSize.pack(side=LEFT, anchor=W)
|
|
checkFontBold.pack(side=LEFT, anchor=W, padx=20)
|
|
frameFontSample.pack(side=TOP, padx=5, pady=5, expand=TRUE, fill=BOTH)
|
|
self.labelFontSample.pack(expand=TRUE, fill=BOTH)
|
|
#frameIndent
|
|
frameIndentSize.pack(side=TOP, fill=X)
|
|
labelSpaceNumTitle.pack(side=TOP, anchor=W, padx=5)
|
|
self.scaleSpaceNum.pack(side=TOP, padx=5, fill=X)
|
|
return frame
|
|
|
|
def CreatePageHighlight(self):
|
|
parent = self.parent
|
|
self.builtinTheme = StringVar(parent)
|
|
self.customTheme = StringVar(parent)
|
|
self.fgHilite = BooleanVar(parent)
|
|
self.colour = StringVar(parent)
|
|
self.fontName = StringVar(parent)
|
|
self.themeIsBuiltin = BooleanVar(parent)
|
|
self.highlightTarget = StringVar(parent)
|
|
|
|
##widget creation
|
|
#body frame
|
|
frame = self.tabPages.pages['Highlighting'].frame
|
|
#body section frames
|
|
frameCustom = LabelFrame(frame, borderwidth=2, relief=GROOVE,
|
|
text=' Custom Highlighting ')
|
|
frameTheme = LabelFrame(frame, borderwidth=2, relief=GROOVE,
|
|
text=' Highlighting Theme ')
|
|
#frameCustom
|
|
self.textHighlightSample=Text(
|
|
frameCustom, relief=SOLID, borderwidth=1,
|
|
font=('courier', 12, ''), cursor='hand2', width=21, height=11,
|
|
takefocus=FALSE, highlightthickness=0, wrap=NONE)
|
|
text=self.textHighlightSample
|
|
text.bind('<Double-Button-1>', lambda e: 'break')
|
|
text.bind('<B1-Motion>', lambda e: 'break')
|
|
textAndTags=(
|
|
('#you can click here', 'comment'), ('\n', 'normal'),
|
|
('#to choose items', 'comment'), ('\n', 'normal'),
|
|
('def', 'keyword'), (' ', 'normal'),
|
|
('func', 'definition'), ('(param):\n ', 'normal'),
|
|
('"""string"""', 'string'), ('\n var0 = ', 'normal'),
|
|
("'string'", 'string'), ('\n var1 = ', 'normal'),
|
|
("'selected'", 'hilite'), ('\n var2 = ', 'normal'),
|
|
("'found'", 'hit'), ('\n var3 = ', 'normal'),
|
|
('list', 'builtin'), ('(', 'normal'),
|
|
('None', 'keyword'), (')\n', 'normal'),
|
|
(' breakpoint("line")', 'break'), ('\n\n', 'normal'),
|
|
(' error ', 'error'), (' ', 'normal'),
|
|
('cursor |', 'cursor'), ('\n ', 'normal'),
|
|
('shell', 'console'), (' ', 'normal'),
|
|
('stdout', 'stdout'), (' ', 'normal'),
|
|
('stderr', 'stderr'), ('\n', 'normal'))
|
|
for txTa in textAndTags:
|
|
text.insert(END, txTa[0], txTa[1])
|
|
for element in self.themeElements:
|
|
def tem(event, elem=element):
|
|
event.widget.winfo_toplevel().highlightTarget.set(elem)
|
|
text.tag_bind(
|
|
self.themeElements[element][0], '<ButtonPress-1>', tem)
|
|
text.config(state=DISABLED)
|
|
self.frameColourSet = Frame(frameCustom, relief=SOLID, borderwidth=1)
|
|
frameFgBg = Frame(frameCustom)
|
|
buttonSetColour = Button(
|
|
self.frameColourSet, text='Choose Colour for :',
|
|
command=self.GetColour, highlightthickness=0)
|
|
self.optMenuHighlightTarget = DynOptionMenu(
|
|
self.frameColourSet, self.highlightTarget, None,
|
|
highlightthickness=0) #, command=self.SetHighlightTargetBinding
|
|
self.radioFg = Radiobutton(
|
|
frameFgBg, variable=self.fgHilite, value=1,
|
|
text='Foreground', command=self.SetColourSampleBinding)
|
|
self.radioBg=Radiobutton(
|
|
frameFgBg, variable=self.fgHilite, value=0,
|
|
text='Background', command=self.SetColourSampleBinding)
|
|
self.fgHilite.set(1)
|
|
buttonSaveCustomTheme = Button(
|
|
frameCustom, text='Save as New Custom Theme',
|
|
command=self.SaveAsNewTheme)
|
|
#frameTheme
|
|
labelTypeTitle = Label(frameTheme, text='Select : ')
|
|
self.radioThemeBuiltin = Radiobutton(
|
|
frameTheme, variable=self.themeIsBuiltin, value=1,
|
|
command=self.SetThemeType, text='a Built-in Theme')
|
|
self.radioThemeCustom = Radiobutton(
|
|
frameTheme, variable=self.themeIsBuiltin, value=0,
|
|
command=self.SetThemeType, text='a Custom Theme')
|
|
self.optMenuThemeBuiltin = DynOptionMenu(
|
|
frameTheme, self.builtinTheme, None, command=None)
|
|
self.optMenuThemeCustom=DynOptionMenu(
|
|
frameTheme, self.customTheme, None, command=None)
|
|
self.buttonDeleteCustomTheme=Button(
|
|
frameTheme, text='Delete Custom Theme',
|
|
command=self.DeleteCustomTheme)
|
|
self.new_custom_theme = Label(frameTheme, bd=2)
|
|
|
|
##widget packing
|
|
#body
|
|
frameCustom.pack(side=LEFT, padx=5, pady=5, expand=TRUE, fill=BOTH)
|
|
frameTheme.pack(side=LEFT, padx=5, pady=5, fill=Y)
|
|
#frameCustom
|
|
self.frameColourSet.pack(side=TOP, padx=5, pady=5, expand=TRUE, fill=X)
|
|
frameFgBg.pack(side=TOP, padx=5, pady=0)
|
|
self.textHighlightSample.pack(
|
|
side=TOP, padx=5, pady=5, expand=TRUE, fill=BOTH)
|
|
buttonSetColour.pack(side=TOP, expand=TRUE, fill=X, padx=8, pady=4)
|
|
self.optMenuHighlightTarget.pack(
|
|
side=TOP, expand=TRUE, fill=X, padx=8, pady=3)
|
|
self.radioFg.pack(side=LEFT, anchor=E)
|
|
self.radioBg.pack(side=RIGHT, anchor=W)
|
|
buttonSaveCustomTheme.pack(side=BOTTOM, fill=X, padx=5, pady=5)
|
|
#frameTheme
|
|
labelTypeTitle.pack(side=TOP, anchor=W, padx=5, pady=5)
|
|
self.radioThemeBuiltin.pack(side=TOP, anchor=W, padx=5)
|
|
self.radioThemeCustom.pack(side=TOP, anchor=W, padx=5, pady=2)
|
|
self.optMenuThemeBuiltin.pack(side=TOP, fill=X, padx=5, pady=5)
|
|
self.optMenuThemeCustom.pack(side=TOP, fill=X, anchor=W, padx=5, pady=5)
|
|
self.buttonDeleteCustomTheme.pack(side=TOP, fill=X, padx=5, pady=5)
|
|
self.new_custom_theme.pack(side=TOP, fill=X, pady=5)
|
|
return frame
|
|
|
|
def CreatePageKeys(self):
|
|
parent = self.parent
|
|
self.bindingTarget = StringVar(parent)
|
|
self.builtinKeys = StringVar(parent)
|
|
self.customKeys = StringVar(parent)
|
|
self.keysAreBuiltin = BooleanVar(parent)
|
|
self.keyBinding = StringVar(parent)
|
|
|
|
##widget creation
|
|
#body frame
|
|
frame = self.tabPages.pages['Keys'].frame
|
|
#body section frames
|
|
frameCustom = LabelFrame(
|
|
frame, borderwidth=2, relief=GROOVE,
|
|
text=' Custom Key Bindings ')
|
|
frameKeySets = LabelFrame(
|
|
frame, borderwidth=2, relief=GROOVE, text=' Key Set ')
|
|
#frameCustom
|
|
frameTarget = Frame(frameCustom)
|
|
labelTargetTitle = Label(frameTarget, text='Action - Key(s)')
|
|
scrollTargetY = Scrollbar(frameTarget)
|
|
scrollTargetX = Scrollbar(frameTarget, orient=HORIZONTAL)
|
|
self.listBindings = Listbox(
|
|
frameTarget, takefocus=FALSE, exportselection=FALSE)
|
|
self.listBindings.bind('<ButtonRelease-1>', self.KeyBindingSelected)
|
|
scrollTargetY.config(command=self.listBindings.yview)
|
|
scrollTargetX.config(command=self.listBindings.xview)
|
|
self.listBindings.config(yscrollcommand=scrollTargetY.set)
|
|
self.listBindings.config(xscrollcommand=scrollTargetX.set)
|
|
self.buttonNewKeys = Button(
|
|
frameCustom, text='Get New Keys for Selection',
|
|
command=self.GetNewKeys, state=DISABLED)
|
|
#frameKeySets
|
|
frames = [Frame(frameKeySets, padx=2, pady=2, borderwidth=0)
|
|
for i in range(2)]
|
|
self.radioKeysBuiltin = Radiobutton(
|
|
frames[0], variable=self.keysAreBuiltin, value=1,
|
|
command=self.SetKeysType, text='Use a Built-in Key Set')
|
|
self.radioKeysCustom = Radiobutton(
|
|
frames[0], variable=self.keysAreBuiltin, value=0,
|
|
command=self.SetKeysType, text='Use a Custom Key Set')
|
|
self.optMenuKeysBuiltin = DynOptionMenu(
|
|
frames[0], self.builtinKeys, None, command=None)
|
|
self.optMenuKeysCustom = DynOptionMenu(
|
|
frames[0], self.customKeys, None, command=None)
|
|
self.buttonDeleteCustomKeys = Button(
|
|
frames[1], text='Delete Custom Key Set',
|
|
command=self.DeleteCustomKeys)
|
|
buttonSaveCustomKeys = Button(
|
|
frames[1], text='Save as New Custom Key Set',
|
|
command=self.SaveAsNewKeySet)
|
|
self.new_custom_keys = Label(frames[0], bd=2)
|
|
|
|
##widget packing
|
|
#body
|
|
frameCustom.pack(side=BOTTOM, padx=5, pady=5, expand=TRUE, fill=BOTH)
|
|
frameKeySets.pack(side=BOTTOM, padx=5, pady=5, fill=BOTH)
|
|
#frameCustom
|
|
self.buttonNewKeys.pack(side=BOTTOM, fill=X, padx=5, pady=5)
|
|
frameTarget.pack(side=LEFT, padx=5, pady=5, expand=TRUE, fill=BOTH)
|
|
#frame target
|
|
frameTarget.columnconfigure(0, weight=1)
|
|
frameTarget.rowconfigure(1, weight=1)
|
|
labelTargetTitle.grid(row=0, column=0, columnspan=2, sticky=W)
|
|
self.listBindings.grid(row=1, column=0, sticky=NSEW)
|
|
scrollTargetY.grid(row=1, column=1, sticky=NS)
|
|
scrollTargetX.grid(row=2, column=0, sticky=EW)
|
|
#frameKeySets
|
|
self.radioKeysBuiltin.grid(row=0, column=0, sticky=W+NS)
|
|
self.radioKeysCustom.grid(row=1, column=0, sticky=W+NS)
|
|
self.optMenuKeysBuiltin.grid(row=0, column=1, sticky=NSEW)
|
|
self.optMenuKeysCustom.grid(row=1, column=1, sticky=NSEW)
|
|
self.new_custom_keys.grid(row=0, column=2, sticky=NSEW, padx=5, pady=5)
|
|
self.buttonDeleteCustomKeys.pack(side=LEFT, fill=X, expand=True, padx=2)
|
|
buttonSaveCustomKeys.pack(side=LEFT, fill=X, expand=True, padx=2)
|
|
frames[0].pack(side=TOP, fill=BOTH, expand=True)
|
|
frames[1].pack(side=TOP, fill=X, expand=True, pady=2)
|
|
return frame
|
|
|
|
def CreatePageGeneral(self):
|
|
parent = self.parent
|
|
self.winWidth = StringVar(parent)
|
|
self.winHeight = StringVar(parent)
|
|
self.startupEdit = IntVar(parent)
|
|
self.autoSave = IntVar(parent)
|
|
self.encoding = StringVar(parent)
|
|
self.userHelpBrowser = BooleanVar(parent)
|
|
self.helpBrowser = StringVar(parent)
|
|
|
|
#widget creation
|
|
#body
|
|
frame = self.tabPages.pages['General'].frame
|
|
#body section frames
|
|
frameRun = LabelFrame(frame, borderwidth=2, relief=GROOVE,
|
|
text=' Startup Preferences ')
|
|
frameSave = LabelFrame(frame, borderwidth=2, relief=GROOVE,
|
|
text=' Autosave Preferences ')
|
|
frameWinSize = Frame(frame, borderwidth=2, relief=GROOVE)
|
|
frameHelp = LabelFrame(frame, borderwidth=2, relief=GROOVE,
|
|
text=' Additional Help Sources ')
|
|
#frameRun
|
|
labelRunChoiceTitle = Label(frameRun, text='At Startup')
|
|
self.radioStartupEdit = Radiobutton(
|
|
frameRun, variable=self.startupEdit, value=1,
|
|
text="Open Edit Window")
|
|
self.radioStartupShell = Radiobutton(
|
|
frameRun, variable=self.startupEdit, value=0,
|
|
text='Open Shell Window')
|
|
#frameSave
|
|
labelRunSaveTitle = Label(frameSave, text='At Start of Run (F5) ')
|
|
self.radioSaveAsk = Radiobutton(
|
|
frameSave, variable=self.autoSave, value=0,
|
|
text="Prompt to Save")
|
|
self.radioSaveAuto = Radiobutton(
|
|
frameSave, variable=self.autoSave, value=1,
|
|
text='No Prompt')
|
|
#frameWinSize
|
|
labelWinSizeTitle = Label(
|
|
frameWinSize, text='Initial Window Size (in characters)')
|
|
labelWinWidthTitle = Label(frameWinSize, text='Width')
|
|
self.entryWinWidth = Entry(
|
|
frameWinSize, textvariable=self.winWidth, width=3)
|
|
labelWinHeightTitle = Label(frameWinSize, text='Height')
|
|
self.entryWinHeight = Entry(
|
|
frameWinSize, textvariable=self.winHeight, width=3)
|
|
#frameHelp
|
|
frameHelpList = Frame(frameHelp)
|
|
frameHelpListButtons = Frame(frameHelpList)
|
|
scrollHelpList = Scrollbar(frameHelpList)
|
|
self.listHelp = Listbox(
|
|
frameHelpList, height=5, takefocus=FALSE,
|
|
exportselection=FALSE)
|
|
scrollHelpList.config(command=self.listHelp.yview)
|
|
self.listHelp.config(yscrollcommand=scrollHelpList.set)
|
|
self.listHelp.bind('<ButtonRelease-1>', self.HelpSourceSelected)
|
|
self.buttonHelpListEdit = Button(
|
|
frameHelpListButtons, text='Edit', state=DISABLED,
|
|
width=8, command=self.HelpListItemEdit)
|
|
self.buttonHelpListAdd = Button(
|
|
frameHelpListButtons, text='Add',
|
|
width=8, command=self.HelpListItemAdd)
|
|
self.buttonHelpListRemove = Button(
|
|
frameHelpListButtons, text='Remove', state=DISABLED,
|
|
width=8, command=self.HelpListItemRemove)
|
|
|
|
#widget packing
|
|
#body
|
|
frameRun.pack(side=TOP, padx=5, pady=5, fill=X)
|
|
frameSave.pack(side=TOP, padx=5, pady=5, fill=X)
|
|
frameWinSize.pack(side=TOP, padx=5, pady=5, fill=X)
|
|
frameHelp.pack(side=TOP, padx=5, pady=5, expand=TRUE, fill=BOTH)
|
|
#frameRun
|
|
labelRunChoiceTitle.pack(side=LEFT, anchor=W, padx=5, pady=5)
|
|
self.radioStartupShell.pack(side=RIGHT, anchor=W, padx=5, pady=5)
|
|
self.radioStartupEdit.pack(side=RIGHT, anchor=W, padx=5, pady=5)
|
|
#frameSave
|
|
labelRunSaveTitle.pack(side=LEFT, anchor=W, padx=5, pady=5)
|
|
self.radioSaveAuto.pack(side=RIGHT, anchor=W, padx=5, pady=5)
|
|
self.radioSaveAsk.pack(side=RIGHT, anchor=W, padx=5, pady=5)
|
|
#frameWinSize
|
|
labelWinSizeTitle.pack(side=LEFT, anchor=W, padx=5, pady=5)
|
|
self.entryWinHeight.pack(side=RIGHT, anchor=E, padx=10, pady=5)
|
|
labelWinHeightTitle.pack(side=RIGHT, anchor=E, pady=5)
|
|
self.entryWinWidth.pack(side=RIGHT, anchor=E, padx=10, pady=5)
|
|
labelWinWidthTitle.pack(side=RIGHT, anchor=E, pady=5)
|
|
#frameHelp
|
|
frameHelpListButtons.pack(side=RIGHT, padx=5, pady=5, fill=Y)
|
|
frameHelpList.pack(side=TOP, padx=5, pady=5, expand=TRUE, fill=BOTH)
|
|
scrollHelpList.pack(side=RIGHT, anchor=W, fill=Y)
|
|
self.listHelp.pack(side=LEFT, anchor=E, expand=TRUE, fill=BOTH)
|
|
self.buttonHelpListEdit.pack(side=TOP, anchor=W, pady=5)
|
|
self.buttonHelpListAdd.pack(side=TOP, anchor=W)
|
|
self.buttonHelpListRemove.pack(side=TOP, anchor=W, pady=5)
|
|
return frame
|
|
|
|
def AttachVarCallbacks(self):
|
|
self.fontSize.trace_add('write', self.VarChanged_font)
|
|
self.fontName.trace_add('write', self.VarChanged_font)
|
|
self.fontBold.trace_add('write', self.VarChanged_font)
|
|
self.spaceNum.trace_add('write', self.VarChanged_spaceNum)
|
|
self.colour.trace_add('write', self.VarChanged_colour)
|
|
self.builtinTheme.trace_add('write', self.VarChanged_builtinTheme)
|
|
self.customTheme.trace_add('write', self.VarChanged_customTheme)
|
|
self.themeIsBuiltin.trace_add('write', self.VarChanged_themeIsBuiltin)
|
|
self.highlightTarget.trace_add('write', self.VarChanged_highlightTarget)
|
|
self.keyBinding.trace_add('write', self.VarChanged_keyBinding)
|
|
self.builtinKeys.trace_add('write', self.VarChanged_builtinKeys)
|
|
self.customKeys.trace_add('write', self.VarChanged_customKeys)
|
|
self.keysAreBuiltin.trace_add('write', self.VarChanged_keysAreBuiltin)
|
|
self.winWidth.trace_add('write', self.VarChanged_winWidth)
|
|
self.winHeight.trace_add('write', self.VarChanged_winHeight)
|
|
self.startupEdit.trace_add('write', self.VarChanged_startupEdit)
|
|
self.autoSave.trace_add('write', self.VarChanged_autoSave)
|
|
self.encoding.trace_add('write', self.VarChanged_encoding)
|
|
|
|
def remove_var_callbacks(self):
|
|
"Remove callbacks to prevent memory leaks."
|
|
for var in (
|
|
self.fontSize, self.fontName, self.fontBold,
|
|
self.spaceNum, self.colour, self.builtinTheme,
|
|
self.customTheme, self.themeIsBuiltin, self.highlightTarget,
|
|
self.keyBinding, self.builtinKeys, self.customKeys,
|
|
self.keysAreBuiltin, self.winWidth, self.winHeight,
|
|
self.startupEdit, self.autoSave, self.encoding,):
|
|
var.trace_remove('write', var.trace_info()[0][1])
|
|
|
|
def VarChanged_font(self, *params):
|
|
'''When one font attribute changes, save them all, as they are
|
|
not independent from each other. In particular, when we are
|
|
overriding the default font, we need to write out everything.
|
|
'''
|
|
value = self.fontName.get()
|
|
self.AddChangedItem('main', 'EditorWindow', 'font', value)
|
|
value = self.fontSize.get()
|
|
self.AddChangedItem('main', 'EditorWindow', 'font-size', value)
|
|
value = self.fontBold.get()
|
|
self.AddChangedItem('main', 'EditorWindow', 'font-bold', value)
|
|
|
|
def VarChanged_spaceNum(self, *params):
|
|
value = self.spaceNum.get()
|
|
self.AddChangedItem('main', 'Indent', 'num-spaces', value)
|
|
|
|
def VarChanged_colour(self, *params):
|
|
self.OnNewColourSet()
|
|
|
|
def VarChanged_builtinTheme(self, *params):
|
|
oldthemes = ('IDLE Classic', 'IDLE New')
|
|
value = self.builtinTheme.get()
|
|
if value not in oldthemes:
|
|
if idleConf.GetOption('main', 'Theme', 'name') not in oldthemes:
|
|
self.AddChangedItem('main', 'Theme', 'name', oldthemes[0])
|
|
self.AddChangedItem('main', 'Theme', 'name2', value)
|
|
self.new_custom_theme.config(text='New theme, see Help',
|
|
fg='#500000')
|
|
else:
|
|
self.AddChangedItem('main', 'Theme', 'name', value)
|
|
self.AddChangedItem('main', 'Theme', 'name2', '')
|
|
self.new_custom_theme.config(text='', fg='black')
|
|
self.PaintThemeSample()
|
|
|
|
def VarChanged_customTheme(self, *params):
|
|
value = self.customTheme.get()
|
|
if value != '- no custom themes -':
|
|
self.AddChangedItem('main', 'Theme', 'name', value)
|
|
self.PaintThemeSample()
|
|
|
|
def VarChanged_themeIsBuiltin(self, *params):
|
|
value = self.themeIsBuiltin.get()
|
|
self.AddChangedItem('main', 'Theme', 'default', value)
|
|
if value:
|
|
self.VarChanged_builtinTheme()
|
|
else:
|
|
self.VarChanged_customTheme()
|
|
|
|
def VarChanged_highlightTarget(self, *params):
|
|
self.SetHighlightTarget()
|
|
|
|
def VarChanged_keyBinding(self, *params):
|
|
value = self.keyBinding.get()
|
|
keySet = self.customKeys.get()
|
|
event = self.listBindings.get(ANCHOR).split()[0]
|
|
if idleConf.IsCoreBinding(event):
|
|
#this is a core keybinding
|
|
self.AddChangedItem('keys', keySet, event, value)
|
|
else: #this is an extension key binding
|
|
extName = idleConf.GetExtnNameForEvent(event)
|
|
extKeybindSection = extName + '_cfgBindings'
|
|
self.AddChangedItem('extensions', extKeybindSection, event, value)
|
|
|
|
def VarChanged_builtinKeys(self, *params):
|
|
oldkeys = (
|
|
'IDLE Classic Windows',
|
|
'IDLE Classic Unix',
|
|
'IDLE Classic Mac',
|
|
'IDLE Classic OSX',
|
|
)
|
|
value = self.builtinKeys.get()
|
|
if value not in oldkeys:
|
|
if idleConf.GetOption('main', 'Keys', 'name') not in oldkeys:
|
|
self.AddChangedItem('main', 'Keys', 'name', oldkeys[0])
|
|
self.AddChangedItem('main', 'Keys', 'name2', value)
|
|
self.new_custom_keys.config(text='New key set, see Help',
|
|
fg='#500000')
|
|
else:
|
|
self.AddChangedItem('main', 'Keys', 'name', value)
|
|
self.AddChangedItem('main', 'Keys', 'name2', '')
|
|
self.new_custom_keys.config(text='', fg='black')
|
|
self.LoadKeysList(value)
|
|
|
|
def VarChanged_customKeys(self, *params):
|
|
value = self.customKeys.get()
|
|
if value != '- no custom keys -':
|
|
self.AddChangedItem('main', 'Keys', 'name', value)
|
|
self.LoadKeysList(value)
|
|
|
|
def VarChanged_keysAreBuiltin(self, *params):
|
|
value = self.keysAreBuiltin.get()
|
|
self.AddChangedItem('main', 'Keys', 'default', value)
|
|
if value:
|
|
self.VarChanged_builtinKeys()
|
|
else:
|
|
self.VarChanged_customKeys()
|
|
|
|
def VarChanged_winWidth(self, *params):
|
|
value = self.winWidth.get()
|
|
self.AddChangedItem('main', 'EditorWindow', 'width', value)
|
|
|
|
def VarChanged_winHeight(self, *params):
|
|
value = self.winHeight.get()
|
|
self.AddChangedItem('main', 'EditorWindow', 'height', value)
|
|
|
|
def VarChanged_startupEdit(self, *params):
|
|
value = self.startupEdit.get()
|
|
self.AddChangedItem('main', 'General', 'editor-on-startup', value)
|
|
|
|
def VarChanged_autoSave(self, *params):
|
|
value = self.autoSave.get()
|
|
self.AddChangedItem('main', 'General', 'autosave', value)
|
|
|
|
def VarChanged_encoding(self, *params):
|
|
value = self.encoding.get()
|
|
self.AddChangedItem('main', 'EditorWindow', 'encoding', value)
|
|
|
|
def ResetChangedItems(self):
|
|
#When any config item is changed in this dialog, an entry
|
|
#should be made in the relevant section (config type) of this
|
|
#dictionary. The key should be the config file section name and the
|
|
#value a dictionary, whose key:value pairs are item=value pairs for
|
|
#that config file section.
|
|
self.changedItems = {'main':{}, 'highlight':{}, 'keys':{},
|
|
'extensions':{}}
|
|
|
|
def AddChangedItem(self, typ, section, item, value):
|
|
value = str(value) #make sure we use a string
|
|
if section not in self.changedItems[typ]:
|
|
self.changedItems[typ][section] = {}
|
|
self.changedItems[typ][section][item] = value
|
|
|
|
def GetDefaultItems(self):
|
|
dItems={'main':{}, 'highlight':{}, 'keys':{}, 'extensions':{}}
|
|
for configType in dItems:
|
|
sections = idleConf.GetSectionList('default', configType)
|
|
for section in sections:
|
|
dItems[configType][section] = {}
|
|
options = idleConf.defaultCfg[configType].GetOptionList(section)
|
|
for option in options:
|
|
dItems[configType][section][option] = (
|
|
idleConf.defaultCfg[configType].Get(section, option))
|
|
return dItems
|
|
|
|
def SetThemeType(self):
|
|
if self.themeIsBuiltin.get():
|
|
self.optMenuThemeBuiltin.config(state=NORMAL)
|
|
self.optMenuThemeCustom.config(state=DISABLED)
|
|
self.buttonDeleteCustomTheme.config(state=DISABLED)
|
|
else:
|
|
self.optMenuThemeBuiltin.config(state=DISABLED)
|
|
self.radioThemeCustom.config(state=NORMAL)
|
|
self.optMenuThemeCustom.config(state=NORMAL)
|
|
self.buttonDeleteCustomTheme.config(state=NORMAL)
|
|
|
|
def SetKeysType(self):
|
|
if self.keysAreBuiltin.get():
|
|
self.optMenuKeysBuiltin.config(state=NORMAL)
|
|
self.optMenuKeysCustom.config(state=DISABLED)
|
|
self.buttonDeleteCustomKeys.config(state=DISABLED)
|
|
else:
|
|
self.optMenuKeysBuiltin.config(state=DISABLED)
|
|
self.radioKeysCustom.config(state=NORMAL)
|
|
self.optMenuKeysCustom.config(state=NORMAL)
|
|
self.buttonDeleteCustomKeys.config(state=NORMAL)
|
|
|
|
def GetNewKeys(self):
|
|
listIndex = self.listBindings.index(ANCHOR)
|
|
binding = self.listBindings.get(listIndex)
|
|
bindName = binding.split()[0] #first part, up to first space
|
|
if self.keysAreBuiltin.get():
|
|
currentKeySetName = self.builtinKeys.get()
|
|
else:
|
|
currentKeySetName = self.customKeys.get()
|
|
currentBindings = idleConf.GetCurrentKeySet()
|
|
if currentKeySetName in self.changedItems['keys']: #unsaved changes
|
|
keySetChanges = self.changedItems['keys'][currentKeySetName]
|
|
for event in keySetChanges:
|
|
currentBindings[event] = keySetChanges[event].split()
|
|
currentKeySequences = list(currentBindings.values())
|
|
newKeys = GetKeysDialog(self, 'Get New Keys', bindName,
|
|
currentKeySequences).result
|
|
if newKeys: #new keys were specified
|
|
if self.keysAreBuiltin.get(): #current key set is a built-in
|
|
message = ('Your changes will be saved as a new Custom Key Set.'
|
|
' Enter a name for your new Custom Key Set below.')
|
|
newKeySet = self.GetNewKeysName(message)
|
|
if not newKeySet: #user cancelled custom key set creation
|
|
self.listBindings.select_set(listIndex)
|
|
self.listBindings.select_anchor(listIndex)
|
|
return
|
|
else: #create new custom key set based on previously active key set
|
|
self.CreateNewKeySet(newKeySet)
|
|
self.listBindings.delete(listIndex)
|
|
self.listBindings.insert(listIndex, bindName+' - '+newKeys)
|
|
self.listBindings.select_set(listIndex)
|
|
self.listBindings.select_anchor(listIndex)
|
|
self.keyBinding.set(newKeys)
|
|
else:
|
|
self.listBindings.select_set(listIndex)
|
|
self.listBindings.select_anchor(listIndex)
|
|
|
|
def GetNewKeysName(self, message):
|
|
usedNames = (idleConf.GetSectionList('user', 'keys') +
|
|
idleConf.GetSectionList('default', 'keys'))
|
|
newKeySet = SectionName(
|
|
self, 'New Custom Key Set', message, usedNames).result
|
|
return newKeySet
|
|
|
|
def SaveAsNewKeySet(self):
|
|
newKeysName = self.GetNewKeysName('New Key Set Name:')
|
|
if newKeysName:
|
|
self.CreateNewKeySet(newKeysName)
|
|
|
|
def KeyBindingSelected(self, event):
|
|
self.buttonNewKeys.config(state=NORMAL)
|
|
|
|
def CreateNewKeySet(self, newKeySetName):
|
|
#creates new custom key set based on the previously active key set,
|
|
#and makes the new key set active
|
|
if self.keysAreBuiltin.get():
|
|
prevKeySetName = self.builtinKeys.get()
|
|
else:
|
|
prevKeySetName = self.customKeys.get()
|
|
prevKeys = idleConf.GetCoreKeys(prevKeySetName)
|
|
newKeys = {}
|
|
for event in prevKeys: #add key set to changed items
|
|
eventName = event[2:-2] #trim off the angle brackets
|
|
binding = ' '.join(prevKeys[event])
|
|
newKeys[eventName] = binding
|
|
#handle any unsaved changes to prev key set
|
|
if prevKeySetName in self.changedItems['keys']:
|
|
keySetChanges = self.changedItems['keys'][prevKeySetName]
|
|
for event in keySetChanges:
|
|
newKeys[event] = keySetChanges[event]
|
|
#save the new theme
|
|
self.SaveNewKeySet(newKeySetName, newKeys)
|
|
#change gui over to the new key set
|
|
customKeyList = idleConf.GetSectionList('user', 'keys')
|
|
customKeyList.sort()
|
|
self.optMenuKeysCustom.SetMenu(customKeyList, newKeySetName)
|
|
self.keysAreBuiltin.set(0)
|
|
self.SetKeysType()
|
|
|
|
def LoadKeysList(self, keySetName):
|
|
reselect = 0
|
|
newKeySet = 0
|
|
if self.listBindings.curselection():
|
|
reselect = 1
|
|
listIndex = self.listBindings.index(ANCHOR)
|
|
keySet = idleConf.GetKeySet(keySetName)
|
|
bindNames = list(keySet.keys())
|
|
bindNames.sort()
|
|
self.listBindings.delete(0, END)
|
|
for bindName in bindNames:
|
|
key = ' '.join(keySet[bindName]) #make key(s) into a string
|
|
bindName = bindName[2:-2] #trim off the angle brackets
|
|
if keySetName in self.changedItems['keys']:
|
|
#handle any unsaved changes to this key set
|
|
if bindName in self.changedItems['keys'][keySetName]:
|
|
key = self.changedItems['keys'][keySetName][bindName]
|
|
self.listBindings.insert(END, bindName+' - '+key)
|
|
if reselect:
|
|
self.listBindings.see(listIndex)
|
|
self.listBindings.select_set(listIndex)
|
|
self.listBindings.select_anchor(listIndex)
|
|
|
|
def DeleteCustomKeys(self):
|
|
keySetName=self.customKeys.get()
|
|
delmsg = 'Are you sure you wish to delete the key set %r ?'
|
|
if not tkMessageBox.askyesno(
|
|
'Delete Key Set', delmsg % keySetName, parent=self):
|
|
return
|
|
self.DeactivateCurrentConfig()
|
|
#remove key set from config
|
|
idleConf.userCfg['keys'].remove_section(keySetName)
|
|
if keySetName in self.changedItems['keys']:
|
|
del(self.changedItems['keys'][keySetName])
|
|
#write changes
|
|
idleConf.userCfg['keys'].Save()
|
|
#reload user key set list
|
|
itemList = idleConf.GetSectionList('user', 'keys')
|
|
itemList.sort()
|
|
if not itemList:
|
|
self.radioKeysCustom.config(state=DISABLED)
|
|
self.optMenuKeysCustom.SetMenu(itemList, '- no custom keys -')
|
|
else:
|
|
self.optMenuKeysCustom.SetMenu(itemList, itemList[0])
|
|
#revert to default key set
|
|
self.keysAreBuiltin.set(idleConf.defaultCfg['main']
|
|
.Get('Keys', 'default'))
|
|
self.builtinKeys.set(idleConf.defaultCfg['main'].Get('Keys', 'name')
|
|
or idleConf.default_keys())
|
|
#user can't back out of these changes, they must be applied now
|
|
self.SaveAllChangedConfigs()
|
|
self.ActivateConfigChanges()
|
|
self.SetKeysType()
|
|
|
|
def DeleteCustomTheme(self):
|
|
themeName = self.customTheme.get()
|
|
delmsg = 'Are you sure you wish to delete the theme %r ?'
|
|
if not tkMessageBox.askyesno(
|
|
'Delete Theme', delmsg % themeName, parent=self):
|
|
return
|
|
self.DeactivateCurrentConfig()
|
|
#remove theme from config
|
|
idleConf.userCfg['highlight'].remove_section(themeName)
|
|
if themeName in self.changedItems['highlight']:
|
|
del(self.changedItems['highlight'][themeName])
|
|
#write changes
|
|
idleConf.userCfg['highlight'].Save()
|
|
#reload user theme list
|
|
itemList = idleConf.GetSectionList('user', 'highlight')
|
|
itemList.sort()
|
|
if not itemList:
|
|
self.radioThemeCustom.config(state=DISABLED)
|
|
self.optMenuThemeCustom.SetMenu(itemList, '- no custom themes -')
|
|
else:
|
|
self.optMenuThemeCustom.SetMenu(itemList, itemList[0])
|
|
#revert to default theme
|
|
self.themeIsBuiltin.set(idleConf.defaultCfg['main'].Get('Theme', 'default'))
|
|
self.builtinTheme.set(idleConf.defaultCfg['main'].Get('Theme', 'name'))
|
|
#user can't back out of these changes, they must be applied now
|
|
self.SaveAllChangedConfigs()
|
|
self.ActivateConfigChanges()
|
|
self.SetThemeType()
|
|
|
|
def GetColour(self):
|
|
target = self.highlightTarget.get()
|
|
prevColour = self.frameColourSet.cget('bg')
|
|
rgbTuplet, colourString = tkColorChooser.askcolor(
|
|
parent=self, title='Pick new colour for : '+target,
|
|
initialcolor=prevColour)
|
|
if colourString and (colourString != prevColour):
|
|
#user didn't cancel, and they chose a new colour
|
|
if self.themeIsBuiltin.get(): #current theme is a built-in
|
|
message = ('Your changes will be saved as a new Custom Theme. '
|
|
'Enter a name for your new Custom Theme below.')
|
|
newTheme = self.GetNewThemeName(message)
|
|
if not newTheme: #user cancelled custom theme creation
|
|
return
|
|
else: #create new custom theme based on previously active theme
|
|
self.CreateNewTheme(newTheme)
|
|
self.colour.set(colourString)
|
|
else: #current theme is user defined
|
|
self.colour.set(colourString)
|
|
|
|
def OnNewColourSet(self):
|
|
newColour=self.colour.get()
|
|
self.frameColourSet.config(bg=newColour) #set sample
|
|
plane ='foreground' if self.fgHilite.get() else 'background'
|
|
sampleElement = self.themeElements[self.highlightTarget.get()][0]
|
|
self.textHighlightSample.tag_config(sampleElement, **{plane:newColour})
|
|
theme = self.customTheme.get()
|
|
themeElement = sampleElement + '-' + plane
|
|
self.AddChangedItem('highlight', theme, themeElement, newColour)
|
|
|
|
def GetNewThemeName(self, message):
|
|
usedNames = (idleConf.GetSectionList('user', 'highlight') +
|
|
idleConf.GetSectionList('default', 'highlight'))
|
|
newTheme = SectionName(
|
|
self, 'New Custom Theme', message, usedNames).result
|
|
return newTheme
|
|
|
|
def SaveAsNewTheme(self):
|
|
newThemeName = self.GetNewThemeName('New Theme Name:')
|
|
if newThemeName:
|
|
self.CreateNewTheme(newThemeName)
|
|
|
|
def CreateNewTheme(self, newThemeName):
|
|
#creates new custom theme based on the previously active theme,
|
|
#and makes the new theme active
|
|
if self.themeIsBuiltin.get():
|
|
themeType = 'default'
|
|
themeName = self.builtinTheme.get()
|
|
else:
|
|
themeType = 'user'
|
|
themeName = self.customTheme.get()
|
|
newTheme = idleConf.GetThemeDict(themeType, themeName)
|
|
#apply any of the old theme's unsaved changes to the new theme
|
|
if themeName in self.changedItems['highlight']:
|
|
themeChanges = self.changedItems['highlight'][themeName]
|
|
for element in themeChanges:
|
|
newTheme[element] = themeChanges[element]
|
|
#save the new theme
|
|
self.SaveNewTheme(newThemeName, newTheme)
|
|
#change gui over to the new theme
|
|
customThemeList = idleConf.GetSectionList('user', 'highlight')
|
|
customThemeList.sort()
|
|
self.optMenuThemeCustom.SetMenu(customThemeList, newThemeName)
|
|
self.themeIsBuiltin.set(0)
|
|
self.SetThemeType()
|
|
|
|
def OnListFontButtonRelease(self, event):
|
|
font = self.listFontName.get(ANCHOR)
|
|
self.fontName.set(font.lower())
|
|
self.SetFontSample()
|
|
|
|
def SetFontSample(self, event=None):
|
|
fontName = self.fontName.get()
|
|
fontWeight = tkFont.BOLD if self.fontBold.get() else tkFont.NORMAL
|
|
newFont = (fontName, self.fontSize.get(), fontWeight)
|
|
self.labelFontSample.config(font=newFont)
|
|
self.textHighlightSample.configure(font=newFont)
|
|
|
|
def SetHighlightTarget(self):
|
|
if self.highlightTarget.get() == 'Cursor': #bg not possible
|
|
self.radioFg.config(state=DISABLED)
|
|
self.radioBg.config(state=DISABLED)
|
|
self.fgHilite.set(1)
|
|
else: #both fg and bg can be set
|
|
self.radioFg.config(state=NORMAL)
|
|
self.radioBg.config(state=NORMAL)
|
|
self.fgHilite.set(1)
|
|
self.SetColourSample()
|
|
|
|
def SetColourSampleBinding(self, *args):
|
|
self.SetColourSample()
|
|
|
|
def SetColourSample(self):
|
|
#set the colour smaple area
|
|
tag = self.themeElements[self.highlightTarget.get()][0]
|
|
plane = 'foreground' if self.fgHilite.get() else 'background'
|
|
colour = self.textHighlightSample.tag_cget(tag, plane)
|
|
self.frameColourSet.config(bg=colour)
|
|
|
|
def PaintThemeSample(self):
|
|
if self.themeIsBuiltin.get(): #a default theme
|
|
theme = self.builtinTheme.get()
|
|
else: #a user theme
|
|
theme = self.customTheme.get()
|
|
for elementTitle in self.themeElements:
|
|
element = self.themeElements[elementTitle][0]
|
|
colours = idleConf.GetHighlight(theme, element)
|
|
if element == 'cursor': #cursor sample needs special painting
|
|
colours['background'] = idleConf.GetHighlight(
|
|
theme, 'normal', fgBg='bg')
|
|
#handle any unsaved changes to this theme
|
|
if theme in self.changedItems['highlight']:
|
|
themeDict = self.changedItems['highlight'][theme]
|
|
if element + '-foreground' in themeDict:
|
|
colours['foreground'] = themeDict[element + '-foreground']
|
|
if element + '-background' in themeDict:
|
|
colours['background'] = themeDict[element + '-background']
|
|
self.textHighlightSample.tag_config(element, **colours)
|
|
self.SetColourSample()
|
|
|
|
def HelpSourceSelected(self, event):
|
|
self.SetHelpListButtonStates()
|
|
|
|
def SetHelpListButtonStates(self):
|
|
if self.listHelp.size() < 1: #no entries in list
|
|
self.buttonHelpListEdit.config(state=DISABLED)
|
|
self.buttonHelpListRemove.config(state=DISABLED)
|
|
else: #there are some entries
|
|
if self.listHelp.curselection(): #there currently is a selection
|
|
self.buttonHelpListEdit.config(state=NORMAL)
|
|
self.buttonHelpListRemove.config(state=NORMAL)
|
|
else: #there currently is not a selection
|
|
self.buttonHelpListEdit.config(state=DISABLED)
|
|
self.buttonHelpListRemove.config(state=DISABLED)
|
|
|
|
def HelpListItemAdd(self):
|
|
helpSource = HelpSource(self, 'New Help Source',
|
|
).result
|
|
if helpSource:
|
|
self.userHelpList.append((helpSource[0], helpSource[1]))
|
|
self.listHelp.insert(END, helpSource[0])
|
|
self.UpdateUserHelpChangedItems()
|
|
self.SetHelpListButtonStates()
|
|
|
|
def HelpListItemEdit(self):
|
|
itemIndex = self.listHelp.index(ANCHOR)
|
|
helpSource = self.userHelpList[itemIndex]
|
|
newHelpSource = HelpSource(
|
|
self, 'Edit Help Source',
|
|
menuitem=helpSource[0],
|
|
filepath=helpSource[1],
|
|
).result
|
|
if newHelpSource and newHelpSource != helpSource:
|
|
self.userHelpList[itemIndex] = newHelpSource
|
|
self.listHelp.delete(itemIndex)
|
|
self.listHelp.insert(itemIndex, newHelpSource[0])
|
|
self.UpdateUserHelpChangedItems()
|
|
self.SetHelpListButtonStates()
|
|
|
|
def HelpListItemRemove(self):
|
|
itemIndex = self.listHelp.index(ANCHOR)
|
|
del(self.userHelpList[itemIndex])
|
|
self.listHelp.delete(itemIndex)
|
|
self.UpdateUserHelpChangedItems()
|
|
self.SetHelpListButtonStates()
|
|
|
|
def UpdateUserHelpChangedItems(self):
|
|
"Clear and rebuild the HelpFiles section in self.changedItems"
|
|
self.changedItems['main']['HelpFiles'] = {}
|
|
for num in range(1, len(self.userHelpList) + 1):
|
|
self.AddChangedItem(
|
|
'main', 'HelpFiles', str(num),
|
|
';'.join(self.userHelpList[num-1][:2]))
|
|
|
|
def LoadFontCfg(self):
|
|
##base editor font selection list
|
|
fonts = list(tkFont.families(self))
|
|
fonts.sort()
|
|
for font in fonts:
|
|
self.listFontName.insert(END, font)
|
|
configuredFont = idleConf.GetFont(self, 'main', 'EditorWindow')
|
|
fontName = configuredFont[0].lower()
|
|
fontSize = configuredFont[1]
|
|
fontBold = configuredFont[2]=='bold'
|
|
self.fontName.set(fontName)
|
|
lc_fonts = [s.lower() for s in fonts]
|
|
try:
|
|
currentFontIndex = lc_fonts.index(fontName)
|
|
self.listFontName.see(currentFontIndex)
|
|
self.listFontName.select_set(currentFontIndex)
|
|
self.listFontName.select_anchor(currentFontIndex)
|
|
except ValueError:
|
|
pass
|
|
##font size dropdown
|
|
self.optMenuFontSize.SetMenu(('7', '8', '9', '10', '11', '12', '13',
|
|
'14', '16', '18', '20', '22',
|
|
'25', '29', '34', '40'), fontSize )
|
|
##fontWeight
|
|
self.fontBold.set(fontBold)
|
|
##font sample
|
|
self.SetFontSample()
|
|
|
|
def LoadTabCfg(self):
|
|
##indent sizes
|
|
spaceNum = idleConf.GetOption(
|
|
'main', 'Indent', 'num-spaces', default=4, type='int')
|
|
self.spaceNum.set(spaceNum)
|
|
|
|
def LoadThemeCfg(self):
|
|
##current theme type radiobutton
|
|
self.themeIsBuiltin.set(idleConf.GetOption(
|
|
'main', 'Theme', 'default', type='bool', default=1))
|
|
##currently set theme
|
|
currentOption = idleConf.CurrentTheme()
|
|
##load available theme option menus
|
|
if self.themeIsBuiltin.get(): #default theme selected
|
|
itemList = idleConf.GetSectionList('default', 'highlight')
|
|
itemList.sort()
|
|
self.optMenuThemeBuiltin.SetMenu(itemList, currentOption)
|
|
itemList = idleConf.GetSectionList('user', 'highlight')
|
|
itemList.sort()
|
|
if not itemList:
|
|
self.radioThemeCustom.config(state=DISABLED)
|
|
self.customTheme.set('- no custom themes -')
|
|
else:
|
|
self.optMenuThemeCustom.SetMenu(itemList, itemList[0])
|
|
else: #user theme selected
|
|
itemList = idleConf.GetSectionList('user', 'highlight')
|
|
itemList.sort()
|
|
self.optMenuThemeCustom.SetMenu(itemList, currentOption)
|
|
itemList = idleConf.GetSectionList('default', 'highlight')
|
|
itemList.sort()
|
|
self.optMenuThemeBuiltin.SetMenu(itemList, itemList[0])
|
|
self.SetThemeType()
|
|
##load theme element option menu
|
|
themeNames = list(self.themeElements.keys())
|
|
themeNames.sort(key=lambda x: self.themeElements[x][1])
|
|
self.optMenuHighlightTarget.SetMenu(themeNames, themeNames[0])
|
|
self.PaintThemeSample()
|
|
self.SetHighlightTarget()
|
|
|
|
def LoadKeyCfg(self):
|
|
##current keys type radiobutton
|
|
self.keysAreBuiltin.set(idleConf.GetOption(
|
|
'main', 'Keys', 'default', type='bool', default=1))
|
|
##currently set keys
|
|
currentOption = idleConf.CurrentKeys()
|
|
##load available keyset option menus
|
|
if self.keysAreBuiltin.get(): #default theme selected
|
|
itemList = idleConf.GetSectionList('default', 'keys')
|
|
itemList.sort()
|
|
self.optMenuKeysBuiltin.SetMenu(itemList, currentOption)
|
|
itemList = idleConf.GetSectionList('user', 'keys')
|
|
itemList.sort()
|
|
if not itemList:
|
|
self.radioKeysCustom.config(state=DISABLED)
|
|
self.customKeys.set('- no custom keys -')
|
|
else:
|
|
self.optMenuKeysCustom.SetMenu(itemList, itemList[0])
|
|
else: #user key set selected
|
|
itemList = idleConf.GetSectionList('user', 'keys')
|
|
itemList.sort()
|
|
self.optMenuKeysCustom.SetMenu(itemList, currentOption)
|
|
itemList = idleConf.GetSectionList('default', 'keys')
|
|
itemList.sort()
|
|
self.optMenuKeysBuiltin.SetMenu(itemList, idleConf.default_keys())
|
|
self.SetKeysType()
|
|
##load keyset element list
|
|
keySetName = idleConf.CurrentKeys()
|
|
self.LoadKeysList(keySetName)
|
|
|
|
def LoadGeneralCfg(self):
|
|
#startup state
|
|
self.startupEdit.set(idleConf.GetOption(
|
|
'main', 'General', 'editor-on-startup', default=1, type='bool'))
|
|
#autosave state
|
|
self.autoSave.set(idleConf.GetOption(
|
|
'main', 'General', 'autosave', default=0, type='bool'))
|
|
#initial window size
|
|
self.winWidth.set(idleConf.GetOption(
|
|
'main', 'EditorWindow', 'width', type='int'))
|
|
self.winHeight.set(idleConf.GetOption(
|
|
'main', 'EditorWindow', 'height', type='int'))
|
|
# default source encoding
|
|
self.encoding.set(idleConf.GetOption(
|
|
'main', 'EditorWindow', 'encoding', default='none'))
|
|
# additional help sources
|
|
self.userHelpList = idleConf.GetAllExtraHelpSourcesList()
|
|
for helpItem in self.userHelpList:
|
|
self.listHelp.insert(END, helpItem[0])
|
|
self.SetHelpListButtonStates()
|
|
|
|
def LoadConfigs(self):
|
|
"""
|
|
load configuration from default and user config files and populate
|
|
the widgets on the config dialog pages.
|
|
"""
|
|
### fonts / tabs page
|
|
self.LoadFontCfg()
|
|
self.LoadTabCfg()
|
|
### highlighting page
|
|
self.LoadThemeCfg()
|
|
### keys page
|
|
self.LoadKeyCfg()
|
|
### general page
|
|
self.LoadGeneralCfg()
|
|
# note: extension page handled separately
|
|
|
|
def SaveNewKeySet(self, keySetName, keySet):
|
|
"""
|
|
save a newly created core key set.
|
|
keySetName - string, the name of the new key set
|
|
keySet - dictionary containing the new key set
|
|
"""
|
|
if not idleConf.userCfg['keys'].has_section(keySetName):
|
|
idleConf.userCfg['keys'].add_section(keySetName)
|
|
for event in keySet:
|
|
value = keySet[event]
|
|
idleConf.userCfg['keys'].SetOption(keySetName, event, value)
|
|
|
|
def SaveNewTheme(self, themeName, theme):
|
|
"""
|
|
save a newly created theme.
|
|
themeName - string, the name of the new theme
|
|
theme - dictionary containing the new theme
|
|
"""
|
|
if not idleConf.userCfg['highlight'].has_section(themeName):
|
|
idleConf.userCfg['highlight'].add_section(themeName)
|
|
for element in theme:
|
|
value = theme[element]
|
|
idleConf.userCfg['highlight'].SetOption(themeName, element, value)
|
|
|
|
def SetUserValue(self, configType, section, item, value):
|
|
if idleConf.defaultCfg[configType].has_option(section, item):
|
|
if idleConf.defaultCfg[configType].Get(section, item) == value:
|
|
#the setting equals a default setting, remove it from user cfg
|
|
return idleConf.userCfg[configType].RemoveOption(section, item)
|
|
#if we got here set the option
|
|
return idleConf.userCfg[configType].SetOption(section, item, value)
|
|
|
|
def SaveAllChangedConfigs(self):
|
|
"Save configuration changes to the user config file."
|
|
idleConf.userCfg['main'].Save()
|
|
for configType in self.changedItems:
|
|
cfgTypeHasChanges = False
|
|
for section in self.changedItems[configType]:
|
|
if section == 'HelpFiles':
|
|
#this section gets completely replaced
|
|
idleConf.userCfg['main'].remove_section('HelpFiles')
|
|
cfgTypeHasChanges = True
|
|
for item in self.changedItems[configType][section]:
|
|
value = self.changedItems[configType][section][item]
|
|
if self.SetUserValue(configType, section, item, value):
|
|
cfgTypeHasChanges = True
|
|
if cfgTypeHasChanges:
|
|
idleConf.userCfg[configType].Save()
|
|
for configType in ['keys', 'highlight']:
|
|
# save these even if unchanged!
|
|
idleConf.userCfg[configType].Save()
|
|
self.ResetChangedItems() #clear the changed items dict
|
|
self.save_all_changed_extensions() # uses a different mechanism
|
|
|
|
def DeactivateCurrentConfig(self):
|
|
#Before a config is saved, some cleanup of current
|
|
#config must be done - remove the previous keybindings
|
|
winInstances = self.parent.instance_dict.keys()
|
|
for instance in winInstances:
|
|
instance.RemoveKeybindings()
|
|
|
|
def ActivateConfigChanges(self):
|
|
"Dynamically apply configuration changes"
|
|
winInstances = self.parent.instance_dict.keys()
|
|
for instance in winInstances:
|
|
instance.ResetColorizer()
|
|
instance.ResetFont()
|
|
instance.set_notabs_indentwidth()
|
|
instance.ApplyKeybindings()
|
|
instance.reset_help_menu_entries()
|
|
|
|
def Cancel(self):
|
|
self.destroy()
|
|
|
|
def Ok(self):
|
|
self.Apply()
|
|
self.destroy()
|
|
|
|
def Apply(self):
|
|
self.DeactivateCurrentConfig()
|
|
self.SaveAllChangedConfigs()
|
|
self.ActivateConfigChanges()
|
|
|
|
def Help(self):
|
|
page = self.tabPages._current_page
|
|
view_text(self, title='Help for IDLE preferences',
|
|
text=help_common+help_pages.get(page, ''))
|
|
|
|
def CreatePageExtensions(self):
|
|
"""Part of the config dialog used for configuring IDLE extensions.
|
|
|
|
This code is generic - it works for any and all IDLE extensions.
|
|
|
|
IDLE extensions save their configuration options using idleConf.
|
|
This code reads the current configuration using idleConf, supplies a
|
|
GUI interface to change the configuration values, and saves the
|
|
changes using idleConf.
|
|
|
|
Not all changes take effect immediately - some may require restarting IDLE.
|
|
This depends on each extension's implementation.
|
|
|
|
All values are treated as text, and it is up to the user to supply
|
|
reasonable values. The only exception to this are the 'enable*' options,
|
|
which are boolean, and can be toggled with a True/False button.
|
|
"""
|
|
parent = self.parent
|
|
frame = self.tabPages.pages['Extensions'].frame
|
|
self.ext_defaultCfg = idleConf.defaultCfg['extensions']
|
|
self.ext_userCfg = idleConf.userCfg['extensions']
|
|
self.is_int = self.register(is_int)
|
|
self.load_extensions()
|
|
# create widgets - a listbox shows all available extensions, with the
|
|
# controls for the extension selected in the listbox to the right
|
|
self.extension_names = StringVar(self)
|
|
frame.rowconfigure(0, weight=1)
|
|
frame.columnconfigure(2, weight=1)
|
|
self.extension_list = Listbox(frame, listvariable=self.extension_names,
|
|
selectmode='browse')
|
|
self.extension_list.bind('<<ListboxSelect>>', self.extension_selected)
|
|
scroll = Scrollbar(frame, command=self.extension_list.yview)
|
|
self.extension_list.yscrollcommand=scroll.set
|
|
self.details_frame = LabelFrame(frame, width=250, height=250)
|
|
self.extension_list.grid(column=0, row=0, sticky='nws')
|
|
scroll.grid(column=1, row=0, sticky='ns')
|
|
self.details_frame.grid(column=2, row=0, sticky='nsew', padx=[10, 0])
|
|
frame.configure(padx=10, pady=10)
|
|
self.config_frame = {}
|
|
self.current_extension = None
|
|
|
|
self.outerframe = self # TEMPORARY
|
|
self.tabbed_page_set = self.extension_list # TEMPORARY
|
|
|
|
# create the frame holding controls for each extension
|
|
ext_names = ''
|
|
for ext_name in sorted(self.extensions):
|
|
self.create_extension_frame(ext_name)
|
|
ext_names = ext_names + '{' + ext_name + '} '
|
|
self.extension_names.set(ext_names)
|
|
self.extension_list.selection_set(0)
|
|
self.extension_selected(None)
|
|
|
|
def load_extensions(self):
|
|
"Fill self.extensions with data from the default and user configs."
|
|
self.extensions = {}
|
|
for ext_name in idleConf.GetExtensions(active_only=False):
|
|
self.extensions[ext_name] = []
|
|
|
|
for ext_name in self.extensions:
|
|
opt_list = sorted(self.ext_defaultCfg.GetOptionList(ext_name))
|
|
|
|
# bring 'enable' options to the beginning of the list
|
|
enables = [opt_name for opt_name in opt_list
|
|
if opt_name.startswith('enable')]
|
|
for opt_name in enables:
|
|
opt_list.remove(opt_name)
|
|
opt_list = enables + opt_list
|
|
|
|
for opt_name in opt_list:
|
|
def_str = self.ext_defaultCfg.Get(
|
|
ext_name, opt_name, raw=True)
|
|
try:
|
|
def_obj = {'True':True, 'False':False}[def_str]
|
|
opt_type = 'bool'
|
|
except KeyError:
|
|
try:
|
|
def_obj = int(def_str)
|
|
opt_type = 'int'
|
|
except ValueError:
|
|
def_obj = def_str
|
|
opt_type = None
|
|
try:
|
|
value = self.ext_userCfg.Get(
|
|
ext_name, opt_name, type=opt_type, raw=True,
|
|
default=def_obj)
|
|
except ValueError: # Need this until .Get fixed
|
|
value = def_obj # bad values overwritten by entry
|
|
var = StringVar(self)
|
|
var.set(str(value))
|
|
|
|
self.extensions[ext_name].append({'name': opt_name,
|
|
'type': opt_type,
|
|
'default': def_str,
|
|
'value': value,
|
|
'var': var,
|
|
})
|
|
|
|
def extension_selected(self, event):
|
|
newsel = self.extension_list.curselection()
|
|
if newsel:
|
|
newsel = self.extension_list.get(newsel)
|
|
if newsel is None or newsel != self.current_extension:
|
|
if self.current_extension:
|
|
self.details_frame.config(text='')
|
|
self.config_frame[self.current_extension].grid_forget()
|
|
self.current_extension = None
|
|
if newsel:
|
|
self.details_frame.config(text=newsel)
|
|
self.config_frame[newsel].grid(column=0, row=0, sticky='nsew')
|
|
self.current_extension = newsel
|
|
|
|
def create_extension_frame(self, ext_name):
|
|
"""Create a frame holding the widgets to configure one extension"""
|
|
f = VerticalScrolledFrame(self.details_frame, height=250, width=250)
|
|
self.config_frame[ext_name] = f
|
|
entry_area = f.interior
|
|
# create an entry for each configuration option
|
|
for row, opt in enumerate(self.extensions[ext_name]):
|
|
# create a row with a label and entry/checkbutton
|
|
label = Label(entry_area, text=opt['name'])
|
|
label.grid(row=row, column=0, sticky=NW)
|
|
var = opt['var']
|
|
if opt['type'] == 'bool':
|
|
Checkbutton(entry_area, textvariable=var, variable=var,
|
|
onvalue='True', offvalue='False',
|
|
indicatoron=FALSE, selectcolor='', width=8
|
|
).grid(row=row, column=1, sticky=W, padx=7)
|
|
elif opt['type'] == 'int':
|
|
Entry(entry_area, textvariable=var, validate='key',
|
|
validatecommand=(self.is_int, '%P')
|
|
).grid(row=row, column=1, sticky=NSEW, padx=7)
|
|
|
|
else:
|
|
Entry(entry_area, textvariable=var
|
|
).grid(row=row, column=1, sticky=NSEW, padx=7)
|
|
return
|
|
|
|
def set_extension_value(self, section, opt):
|
|
name = opt['name']
|
|
default = opt['default']
|
|
value = opt['var'].get().strip() or default
|
|
opt['var'].set(value)
|
|
# if self.defaultCfg.has_section(section):
|
|
# Currently, always true; if not, indent to return
|
|
if (value == default):
|
|
return self.ext_userCfg.RemoveOption(section, name)
|
|
# set the option
|
|
return self.ext_userCfg.SetOption(section, name, value)
|
|
|
|
def save_all_changed_extensions(self):
|
|
"""Save configuration changes to the user config file."""
|
|
has_changes = False
|
|
for ext_name in self.extensions:
|
|
options = self.extensions[ext_name]
|
|
for opt in options:
|
|
if self.set_extension_value(ext_name, opt):
|
|
has_changes = True
|
|
if has_changes:
|
|
self.ext_userCfg.Save()
|
|
|
|
|
|
help_common = '''\
|
|
When you click either the Apply or Ok buttons, settings in this
|
|
dialog that are different from IDLE's default are saved in
|
|
a .idlerc directory in your home directory. Except as noted,
|
|
these changes apply to all versions of IDLE installed on this
|
|
machine. Some do not take affect until IDLE is restarted.
|
|
[Cancel] only cancels changes made since the last save.
|
|
'''
|
|
help_pages = {
|
|
'Highlighting': '''
|
|
Highlighting:
|
|
The IDLE Dark color theme is new in October 2015. It can only
|
|
be used with older IDLE releases if it is saved as a custom
|
|
theme, with a different name.
|
|
''',
|
|
'Keys': '''
|
|
Keys:
|
|
The IDLE Modern Unix key set is new in June 2016. It can only
|
|
be used with older IDLE releases if it is saved as a custom
|
|
key set, with a different name.
|
|
''',
|
|
}
|
|
|
|
|
|
def is_int(s):
|
|
"Return 's is blank or represents an int'"
|
|
if not s:
|
|
return True
|
|
try:
|
|
int(s)
|
|
return True
|
|
except ValueError:
|
|
return False
|
|
|
|
|
|
class VerticalScrolledFrame(Frame):
|
|
"""A pure Tkinter vertically scrollable frame.
|
|
|
|
* Use the 'interior' attribute to place widgets inside the scrollable frame
|
|
* Construct and pack/place/grid normally
|
|
* This frame only allows vertical scrolling
|
|
"""
|
|
def __init__(self, parent, *args, **kw):
|
|
Frame.__init__(self, parent, *args, **kw)
|
|
|
|
# create a canvas object and a vertical scrollbar for scrolling it
|
|
vscrollbar = Scrollbar(self, orient=VERTICAL)
|
|
vscrollbar.pack(fill=Y, side=RIGHT, expand=FALSE)
|
|
canvas = Canvas(self, bd=0, highlightthickness=0,
|
|
yscrollcommand=vscrollbar.set, width=240)
|
|
canvas.pack(side=LEFT, fill=BOTH, expand=TRUE)
|
|
vscrollbar.config(command=canvas.yview)
|
|
|
|
# reset the view
|
|
canvas.xview_moveto(0)
|
|
canvas.yview_moveto(0)
|
|
|
|
# create a frame inside the canvas which will be scrolled with it
|
|
self.interior = interior = Frame(canvas)
|
|
interior_id = canvas.create_window(0, 0, window=interior, anchor=NW)
|
|
|
|
# track changes to the canvas and frame width and sync them,
|
|
# also updating the scrollbar
|
|
def _configure_interior(event):
|
|
# update the scrollbars to match the size of the inner frame
|
|
size = (interior.winfo_reqwidth(), interior.winfo_reqheight())
|
|
canvas.config(scrollregion="0 0 %s %s" % size)
|
|
interior.bind('<Configure>', _configure_interior)
|
|
|
|
def _configure_canvas(event):
|
|
if interior.winfo_reqwidth() != canvas.winfo_width():
|
|
# update the inner frame's width to fill the canvas
|
|
canvas.itemconfigure(interior_id, width=canvas.winfo_width())
|
|
canvas.bind('<Configure>', _configure_canvas)
|
|
|
|
return
|
|
|
|
|
|
if __name__ == '__main__':
|
|
import unittest
|
|
unittest.main('idlelib.idle_test.test_configdialog',
|
|
verbosity=2, exit=False)
|
|
from idlelib.idle_test.htest import run
|
|
run(ConfigDialog)
|