import re
from tkinter import (Toplevel, Listbox, Canvas,
StringVar, BooleanVar, IntVar, TRUE, FALSE,
TOP, BOTTOM, RIGHT, LEFT, SOLID, GROOVE,
NONE, BOTH, X, Y, W, E, EW, NS, NSEW, NW,
HORIZONTAL, VERTICAL, ANCHOR, ACTIVE, END, TclError)
from tkinter.ttk import (Frame, LabelFrame, Button, Checkbutton, Entry, Label,
OptionMenu, Notebook, Radiobutton, Scrollbar, Style,
Spinbox, Combobox)
from tkinter import colorchooser
import tkinter.font as tkfont
from tkinter import messagebox
from idlelib.config import idleConf, ConfigChanges
from idlelib.config_key import GetKeysWindow
from idlelib.dynoption import DynOptionMenu
from idlelib import macosx
from idlelib.query import SectionName, HelpSource
from idlelib.textview import view_text
from idlelib.autocomplete import AutoComplete
from idlelib.codecontext import CodeContext
from idlelib.parenmatch import ParenMatch
from idlelib.format import FormatParagraph
from idlelib.squeezer import Squeezer
from idlelib.textview import ScrollableTextFrame
changes = ConfigChanges()
reloadables = (AutoComplete, CodeContext, ParenMatch, FormatParagraph,
Squeezer)
class ConfigDialog(Toplevel):
def __init__(self, parent, title='', *, _htest=False, _utest=False):
Toplevel.__init__(self, parent)
self.parent = parent
if _htest:
parent.instance_dict = {}
if not _utest:
self.withdraw()
self.title(title or 'IDLE Preferences')
x = parent.winfo_rootx() + 20
y = parent.winfo_rooty() + (30 if not _htest else 150)
self.geometry(f'+{x}+{y}')
self.create_widgets()
self.resizable(height=FALSE, width=FALSE)
self.transient(parent)
self.protocol("WM_DELETE_WINDOW", self.cancel)
self.fontpage.fontlist.focus_set()
tracers.attach()
if not _utest:
self.grab_set()
self.wm_deiconify()
self.wait_window()
def create_widgets(self):
self.frame = frame = Frame(self, padding=5)
self.frame.grid(sticky="nwes")
self.note = note = Notebook(frame)
self.extpage = ExtPage(note)
self.highpage = HighPage(note, self.extpage)
self.fontpage = FontPage(note, self.highpage)
self.keyspage = KeysPage(note, self.extpage)
self.winpage = WinPage(note)
self.shedpage = ShedPage(note)
note.add(self.fontpage, text=' Fonts ')
note.add(self.highpage, text='Highlights')
note.add(self.keyspage, text=' Keys ')
note.add(self.winpage, text=' Windows ')
note.add(self.shedpage, text=' Shell/Ed ')
note.add(self.extpage, text='Extensions')
note.enable_traversal()
note.pack(side=TOP, expand=TRUE, fill=BOTH)
self.create_action_buttons().pack(side=BOTTOM)
def create_action_buttons(self):
if macosx.isAquaTk():
padding_args = {}
else:
padding_args = {'padding': (6, 3)}
outer = Frame(self.frame, padding=2)
buttons_frame = Frame(outer, padding=2)
self.buttons = {}
for txt, cmd in (
('Ok', self.ok),
('Apply', self.apply),
('Cancel', self.cancel),
('Help', self.help)):
self.buttons[txt] = Button(buttons_frame, text=txt, command=cmd,
takefocus=FALSE, **padding_args)
self.buttons[txt].pack(side=LEFT, padx=5)
Frame(outer, height=2, borderwidth=0).pack(side=TOP)
buttons_frame.pack(side=BOTTOM)
return outer
def ok(self):
self.apply()
self.destroy()
def apply(self):
self.deactivate_current_config()
changes.save_all()
self.extpage.save_all_changed_extensions()
self.activate_config_changes()
def cancel(self):
changes.clear()
self.destroy()
def destroy(self):
global font_sample_text
font_sample_text = self.fontpage.font_sample.get('1.0', 'end')
self.grab_release()
super().destroy()
def help(self):
page = self.note.tab(self.note.select(), option='text').strip()
view_text(self, title='Help for IDLE preferences',
contents=help_common+help_pages.get(page, ''))
def deactivate_current_config(self):
for instance in self.parent.instance_dict:
instance.RemoveKeybindings()
def activate_config_changes(self):
for instance in self.parent.instance_dict:
instance.ResetColorizer()
instance.ResetFont()
instance.set_notabs_indentwidth()
instance.ApplyKeybindings()
instance.reset_help_menu_entries()
instance.update_cursor_blink()
for klass in reloadables:
klass.reload()
font_sample_text = (
'<ASCII/Latin1>\n'
'AaBbCcDdEeFfGgHhIiJj\n1234567890#:+=(){}[]\n'
'\u00a2\u00a3\u00a5\u00a7\u00a9\u00ab\u00ae\u00b6\u00bd\u011e'
'\u00c0\u00c1\u00c2\u00c3\u00c4\u00c5\u00c7\u00d0\u00d8\u00df\n'
'\n<IPA,Greek,Cyrillic>\n'
'\u0250\u0255\u0258\u025e\u025f\u0264\u026b\u026e\u0270\u0277'
'\u027b\u0281\u0283\u0286\u028e\u029e\u02a2\u02ab\u02ad\u02af\n'
'\u0391\u03b1\u0392\u03b2\u0393\u03b3\u0394\u03b4\u0395\u03b5'
'\u0396\u03b6\u0397\u03b7\u0398\u03b8\u0399\u03b9\u039a\u03ba\n'
'\u0411\u0431\u0414\u0434\u0416\u0436\u041f\u043f\u0424\u0444'
'\u0427\u0447\u042a\u044a\u042d\u044d\u0460\u0464\u046c\u04dc\n'
'\n<Hebrew, Arabic>\n'
'\u05d0\u05d1\u05d2\u05d3\u05d4\u05d5\u05d6\u05d7\u05d8\u05d9'
'\u05da\u05db\u05dc\u05dd\u05de\u05df\u05e0\u05e1\u05e2\u05e3\n'
'\u0627\u0628\u062c\u062f\u0647\u0648\u0632\u062d\u0637\u064a'
'\u0660\u0661\u0662\u0663\u0664\u0665\u0666\u0667\u0668\u0669\n'
'\n<Devanagari, Tamil>\n'
'\u0966\u0967\u0968\u0969\u096a\u096b\u096c\u096d\u096e\u096f'
'\u0905\u0906\u0907\u0908\u0909\u090a\u090f\u0910\u0913\u0914\n'
'\u0be6\u0be7\u0be8\u0be9\u0bea\u0beb\u0bec\u0bed\u0bee\u0bef'
'\u0b85\u0b87\u0b89\u0b8e\n'
'\n<East Asian>\n'
'\u3007\u4e00\u4e8c\u4e09\u56db\u4e94\u516d\u4e03\u516b\u4e5d\n'
'\u6c49\u5b57\u6f22\u5b57\u4eba\u6728\u706b\u571f\u91d1\u6c34\n'
'\uac00\ub0d0\ub354\ub824\ubaa8\ubd64\uc218\uc720\uc988\uce58\n'
'\u3042\u3044\u3046\u3048\u304a\u30a2\u30a4\u30a6\u30a8\u30aa\n'
)
class FontPage(Frame):
def __init__(self, master, highpage):
super().__init__(master)
self.highlight_sample = highpage.highlight_sample
self.create_page_font()
self.load_font_cfg()
def create_page_font(self):
self.font_name = tracers.add(StringVar(self), self.var_changed_font)
self.font_size = tracers.add(StringVar(self), self.var_changed_font)
self.font_bold = tracers.add(BooleanVar(self), self.var_changed_font)
frame_font = LabelFrame(self, borderwidth=2, relief=GROOVE,
text=' Shell/Editor Font ')
frame_sample = LabelFrame(self, borderwidth=2, relief=GROOVE,
text=' Font Sample (Editable) ')
frame_font_name = Frame(frame_font)
frame_font_param = Frame(frame_font)
font_name_title = Label(
frame_font_name, justify=LEFT, text='Font Face :')
self.fontlist = Listbox(frame_font_name, height=15,
takefocus=True, exportselection=FALSE)
self.fontlist.bind('<ButtonRelease-1>', self.on_fontlist_select)
self.fontlist.bind('<KeyRelease-Up>', self.on_fontlist_select)
self.fontlist.bind('<KeyRelease-Down>', self.on_fontlist_select)
scroll_font = Scrollbar(frame_font_name)
scroll_font.config(command=self.fontlist.yview)
self.fontlist.config(yscrollcommand=scroll_font.set)
font_size_title = Label(frame_font_param, text='Size :')
self.sizelist = DynOptionMenu(frame_font_param, self.font_size, None)
self.bold_toggle = Checkbutton(
frame_font_param, variable=self.font_bold,
onvalue=1, offvalue=0, text='Bold')
font_sample_frame = ScrollableTextFrame(frame_sample)
self.font_sample = font_sample_frame.text
self.font_sample.config(wrap=NONE, width=1, height=1)
self.font_sample.insert(END, font_sample_text)
self.columnconfigure(1, weight=1)
self.rowconfigure(2, weight=1)
frame_font.grid(row=0, column=0, padx=5, pady=5)
frame_sample.grid(row=0, column=1, rowspan=3, padx=5, pady=5,
sticky='nsew')
frame_font_name.pack(side=TOP, padx=5, pady=5, fill=X)
frame_font_param.pack(side=TOP, padx=5, pady=5, fill=X)
font_name_title.pack(side=TOP, anchor=W)
self.fontlist.pack(side=LEFT, expand=TRUE, fill=X)
scroll_font.pack(side=LEFT, fill=Y)
font_size_title.pack(side=LEFT, anchor=W)
self.sizelist.pack(side=LEFT, anchor=W)
self.bold_toggle.pack(side=LEFT, anchor=W, padx=20)
font_sample_frame.pack(expand=TRUE, fill=BOTH)
def load_font_cfg(self):
configured_font = idleConf.GetFont(self, 'main', 'EditorWindow')
font_name = configured_font[0].lower()
font_size = configured_font[1]
font_bold = configured_font[2]=='bold'
fonts = sorted(set(tkfont.families(self)))
for font in fonts:
self.fontlist.insert(END, font)
self.font_name.set(font_name)
lc_fonts = [s.lower() for s in fonts]
try:
current_font_index = lc_fonts.index(font_name)
self.fontlist.see(current_font_index)
self.fontlist.select_set(current_font_index)
self.fontlist.select_anchor(current_font_index)
self.fontlist.activate(current_font_index)
except ValueError:
pass
self.sizelist.SetMenu(('7', '8', '9', '10', '11', '12', '13', '14',
'16', '18', '20', '22', '25', '29', '34', '40'),
font_size)
self.font_bold.set(font_bold)
self.set_samples()
def var_changed_font(self, *params):
value = self.font_name.get()
changes.add_option('main', 'EditorWindow', 'font', value)
value = self.font_size.get()
changes.add_option('main', 'EditorWindow', 'font-size', value)
value = self.font_bold.get()
changes.add_option('main', 'EditorWindow', 'font-bold', value)
self.set_samples()
def on_fontlist_select(self, event):
font = self.fontlist.get(
ACTIVE if event.type.name == 'KeyRelease' else ANCHOR)
self.font_name.set(font.lower())
def set_samples(self, event=None):
font_name = self.font_name.get()
font_weight = tkfont.BOLD if self.font_bold.get() else tkfont.NORMAL
new_font = (font_name, self.font_size.get(), font_weight)
self.font_sample['font'] = new_font
self.highlight_sample['font'] = new_font
class HighPage(Frame):
def __init__(self, master, extpage):
super().__init__(master)
self.extpage = extpage
self.cd = master.winfo_toplevel()
self.style = Style(master)
self.create_page_highlight()
self.load_theme_cfg()
def create_page_highlight(self):
self.theme_elements = {
'Normal Code or Text': 'normal',
'Code Context': 'context',
'Python Keywords': 'keyword',
'Python Definitions': 'definition',
'Python Builtins': 'builtin',
'Python Comments': 'comment',
'Python Strings': 'string',
'Selected Text': 'hilite',
'Found Text': 'hit',
'Cursor': 'cursor',
'Editor Breakpoint': 'break',
'Shell Prompt': 'console',
'Error Text': 'error',
'Shell User Output': 'stdout',
'Shell User Exception': 'stderr',
'Line Number': 'linenumber',
}
self.builtin_name = tracers.add(
StringVar(self), self.var_changed_builtin_name)
self.custom_name = tracers.add(
StringVar(self), self.var_changed_custom_name)
self.fg_bg_toggle = BooleanVar(self)
self.color = tracers.add(
StringVar(self), self.var_changed_color)
self.theme_source = tracers.add(
BooleanVar(self), self.var_changed_theme_source)
self.highlight_target = tracers.add(
StringVar(self), self.var_changed_highlight_target)
frame_custom = LabelFrame(self, borderwidth=2, relief=GROOVE,
text=' Custom Highlighting ')
frame_theme = LabelFrame(self, borderwidth=2, relief=GROOVE,
text=' Highlighting Theme ')
sample_frame = ScrollableTextFrame(
frame_custom, relief=SOLID, borderwidth=1)
text = self.highlight_sample = sample_frame.text
text.configure(
font=('courier', 12, ''), cursor='hand2', width=1, height=1,
takefocus=FALSE, highlightthickness=0, wrap=NONE)
text.bind('<Double-Button-1>', lambda e: 'break')
text.bind('<B1-Motion>', lambda e: 'break')
string_tags=(
('# Click selects item.', 'comment'), ('\n', 'normal'),
('code context section', 'context'), ('\n', 'normal'),
('| cursor', 'cursor'), ('\n', 'normal'),
('def', 'keyword'), (' ', 'normal'),
('func', 'definition'), ('(param):\n ', 'normal'),
('"Return None."', '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'),
('>>>', 'console'), (' 3.14**2\n', 'normal'),
('9.8596', 'stdout'), ('\n', 'normal'),
('>>>', 'console'), (' pri ', 'normal'),
('n', 'error'), ('t(\n', 'normal'),
('SyntaxError', 'stderr'), ('\n', 'normal'))
for string, tag in string_tags:
text.insert(END, string, tag)
n_lines = len(text.get('1.0', END).splitlines())
for lineno in range(1, n_lines):
text.insert(f'{lineno}.0',
f'{lineno:{len(str(n_lines))}d} ',
'linenumber')
for element in self.theme_elements:
def tem(event, elem=element):
self.highlight_target.set(elem)
text.tag_bind(
self.theme_elements[element], '<ButtonPress-1>', tem)
text['state'] = 'disabled'
self.style.configure('frame_color_set.TFrame', borderwidth=1,
relief='solid')
self.frame_color_set = Frame(frame_custom, style='frame_color_set.TFrame')
frame_fg_bg_toggle = Frame(frame_custom)
self.button_set_color = Button(
self.frame_color_set, text='Choose Color for :',
command=self.get_color)
self.targetlist = DynOptionMenu(
self.frame_color_set, self.highlight_target, None,
highlightthickness=0) self.fg_on = Radiobutton(
frame_fg_bg_toggle, variable=self.fg_bg_toggle, value=1,
text='Foreground', command=self.set_color_sample_binding)
self.bg_on = Radiobutton(
frame_fg_bg_toggle, variable=self.fg_bg_toggle, value=0,
text='Background', command=self.set_color_sample_binding)
self.fg_bg_toggle.set(1)
self.button_save_custom = Button(
frame_custom, text='Save as New Custom Theme',
command=self.save_as_new_theme)
theme_type_title = Label(frame_theme, text='Select : ')
self.builtin_theme_on = Radiobutton(
frame_theme, variable=self.theme_source, value=1,
command=self.set_theme_type, text='a Built-in Theme')
self.custom_theme_on = Radiobutton(
frame_theme, variable=self.theme_source, value=0,
command=self.set_theme_type, text='a Custom Theme')
self.builtinlist = DynOptionMenu(
frame_theme, self.builtin_name, None, command=None)
self.customlist = DynOptionMenu(
frame_theme, self.custom_name, None, command=None)
self.button_delete_custom = Button(
frame_theme, text='Delete Custom Theme',
command=self.delete_custom)
self.theme_message = Label(frame_theme, borderwidth=2)
frame_custom.pack(side=LEFT, padx=5, pady=5, expand=TRUE, fill=BOTH)
frame_theme.pack(side=TOP, padx=5, pady=5, fill=X)
self.frame_color_set.pack(side=TOP, padx=5, pady=5, fill=X)
frame_fg_bg_toggle.pack(side=TOP, padx=5, pady=0)
sample_frame.pack(
side=TOP, padx=5, pady=5, expand=TRUE, fill=BOTH)
self.button_set_color.pack(side=TOP, expand=TRUE, fill=X, padx=8, pady=4)
self.targetlist.pack(side=TOP, expand=TRUE, fill=X, padx=8, pady=3)
self.fg_on.pack(side=LEFT, anchor=E)
self.bg_on.pack(side=RIGHT, anchor=W)
self.button_save_custom.pack(side=BOTTOM, fill=X, padx=5, pady=5)
theme_type_title.pack(side=TOP, anchor=W, padx=5, pady=5)
self.builtin_theme_on.pack(side=TOP, anchor=W, padx=5)
self.custom_theme_on.pack(side=TOP, anchor=W, padx=5, pady=2)
self.builtinlist.pack(side=TOP, fill=X, padx=5, pady=5)
self.customlist.pack(side=TOP, fill=X, anchor=W, padx=5, pady=5)
self.button_delete_custom.pack(side=TOP, fill=X, padx=5, pady=5)
self.theme_message.pack(side=TOP, fill=X, pady=5)
def load_theme_cfg(self):
self.theme_source.set(idleConf.GetOption(
'main', 'Theme', 'default', type='bool', default=1))
current_option = idleConf.CurrentTheme()
if self.theme_source.get(): item_list = idleConf.GetSectionList('default', 'highlight')
item_list.sort()
self.builtinlist.SetMenu(item_list, current_option)
item_list = idleConf.GetSectionList('user', 'highlight')
item_list.sort()
if not item_list:
self.custom_theme_on.state(('disabled',))
self.custom_name.set('- no custom themes -')
else:
self.customlist.SetMenu(item_list, item_list[0])
else: item_list = idleConf.GetSectionList('user', 'highlight')
item_list.sort()
self.customlist.SetMenu(item_list, current_option)
item_list = idleConf.GetSectionList('default', 'highlight')
item_list.sort()
self.builtinlist.SetMenu(item_list, item_list[0])
self.set_theme_type()
theme_names = list(self.theme_elements)
self.targetlist.SetMenu(theme_names, theme_names[0])
self.paint_theme_sample()
self.set_highlight_target()
def var_changed_builtin_name(self, *params):
old_themes = ('IDLE Classic', 'IDLE New')
value = self.builtin_name.get()
if value not in old_themes:
if idleConf.GetOption('main', 'Theme', 'name') not in old_themes:
changes.add_option('main', 'Theme', 'name', old_themes[0])
changes.add_option('main', 'Theme', 'name2', value)
self.theme_message['text'] = 'New theme, see Help'
else:
changes.add_option('main', 'Theme', 'name', value)
changes.add_option('main', 'Theme', 'name2', '')
self.theme_message['text'] = ''
self.paint_theme_sample()
def var_changed_custom_name(self, *params):
value = self.custom_name.get()
if value != '- no custom themes -':
changes.add_option('main', 'Theme', 'name', value)
self.paint_theme_sample()
def var_changed_theme_source(self, *params):
value = self.theme_source.get()
changes.add_option('main', 'Theme', 'default', value)
if value:
self.var_changed_builtin_name()
else:
self.var_changed_custom_name()
def var_changed_color(self, *params):
"Process change to color choice."
self.on_new_color_set()
def var_changed_highlight_target(self, *params):
"Process selection of new target tag for highlighting."
self.set_highlight_target()
def set_theme_type(self):
if self.theme_source.get():
self.builtinlist['state'] = 'normal'
self.customlist['state'] = 'disabled'
self.button_delete_custom.state(('disabled',))
else:
self.builtinlist['state'] = 'disabled'
self.custom_theme_on.state(('!disabled',))
self.customlist['state'] = 'normal'
self.button_delete_custom.state(('!disabled',))
def get_color(self):
target = self.highlight_target.get()
prev_color = self.style.lookup(self.frame_color_set['style'],
'background')
rgbTuplet, color_string = colorchooser.askcolor(
parent=self, title='Pick new color for : '+target,
initialcolor=prev_color)
if color_string and (color_string != prev_color):
if self.theme_source.get(): message = ('Your changes will be saved as a new Custom Theme. '
'Enter a name for your new Custom Theme below.')
new_theme = self.get_new_theme_name(message)
if not new_theme: return
else: self.create_new(new_theme)
self.color.set(color_string)
else: self.color.set(color_string)
def on_new_color_set(self):
"Display sample of new color selection on the dialog."
new_color = self.color.get()
self.style.configure('frame_color_set.TFrame', background=new_color)
plane = 'foreground' if self.fg_bg_toggle.get() else 'background'
sample_element = self.theme_elements[self.highlight_target.get()]
self.highlight_sample.tag_config(sample_element, **{plane: new_color})
theme = self.custom_name.get()
theme_element = sample_element + '-' + plane
changes.add_option('highlight', theme, theme_element, new_color)
def get_new_theme_name(self, message):
"Return name of new theme from query popup."
used_names = (idleConf.GetSectionList('user', 'highlight') +
idleConf.GetSectionList('default', 'highlight'))
new_theme = SectionName(
self, 'New Custom Theme', message, used_names).result
return new_theme
def save_as_new_theme(self):
new_theme_name = self.get_new_theme_name('New Theme Name:')
if new_theme_name:
self.create_new(new_theme_name)
def create_new(self, new_theme_name):
if self.theme_source.get():
theme_type = 'default'
theme_name = self.builtin_name.get()
else:
theme_type = 'user'
theme_name = self.custom_name.get()
new_theme = idleConf.GetThemeDict(theme_type, theme_name)
if theme_name in changes['highlight']:
theme_changes = changes['highlight'][theme_name]
for element in theme_changes:
new_theme[element] = theme_changes[element]
self.save_new(new_theme_name, new_theme)
custom_theme_list = idleConf.GetSectionList('user', 'highlight')
custom_theme_list.sort()
self.customlist.SetMenu(custom_theme_list, new_theme_name)
self.theme_source.set(0)
self.set_theme_type()
def set_highlight_target(self):
if self.highlight_target.get() == 'Cursor': self.fg_on.state(('disabled',))
self.bg_on.state(('disabled',))
self.fg_bg_toggle.set(1)
else: self.fg_on.state(('!disabled',))
self.bg_on.state(('!disabled',))
self.fg_bg_toggle.set(1)
self.set_color_sample()
def set_color_sample_binding(self, *args):
self.set_color_sample()
def set_color_sample(self):
tag = self.theme_elements[self.highlight_target.get()]
plane = 'foreground' if self.fg_bg_toggle.get() else 'background'
color = self.highlight_sample.tag_cget(tag, plane)
self.style.configure('frame_color_set.TFrame', background=color)
def paint_theme_sample(self):
if self.theme_source.get(): theme = self.builtin_name.get()
else: theme = self.custom_name.get()
for element_title in self.theme_elements:
element = self.theme_elements[element_title]
colors = idleConf.GetHighlight(theme, element)
if element == 'cursor': colors['background'] = idleConf.GetHighlight(
theme, 'normal')['background']
if theme in changes['highlight']:
theme_dict = changes['highlight'][theme]
if element + '-foreground' in theme_dict:
colors['foreground'] = theme_dict[element + '-foreground']
if element + '-background' in theme_dict:
colors['background'] = theme_dict[element + '-background']
self.highlight_sample.tag_config(element, **colors)
self.set_color_sample()
def save_new(self, theme_name, theme):
idleConf.userCfg['highlight'].AddSection(theme_name)
for element in theme:
value = theme[element]
idleConf.userCfg['highlight'].SetOption(theme_name, element, value)
def askyesno(self, *args, **kwargs):
return messagebox.askyesno(*args, **kwargs)
def delete_custom(self):
theme_name = self.custom_name.get()
delmsg = 'Are you sure you wish to delete the theme %r ?'
if not self.askyesno(
'Delete Theme', delmsg % theme_name, parent=self):
return
self.cd.deactivate_current_config()
changes.delete_section('highlight', theme_name)
item_list = idleConf.GetSectionList('user', 'highlight')
item_list.sort()
if not item_list:
self.custom_theme_on.state(('disabled',))
self.customlist.SetMenu(item_list, '- no custom themes -')
else:
self.customlist.SetMenu(item_list, item_list[0])
self.theme_source.set(idleConf.defaultCfg['main'].Get('Theme', 'default'))
self.builtin_name.set(idleConf.defaultCfg['main'].Get('Theme', 'name'))
changes.save_all()
self.extpage.save_all_changed_extensions()
self.cd.activate_config_changes()
self.set_theme_type()
class KeysPage(Frame):
def __init__(self, master, extpage):
super().__init__(master)
self.extpage = extpage
self.cd = master.winfo_toplevel()
self.create_page_keys()
self.load_key_cfg()
def create_page_keys(self):
self.builtin_name = tracers.add(
StringVar(self), self.var_changed_builtin_name)
self.custom_name = tracers.add(
StringVar(self), self.var_changed_custom_name)
self.keyset_source = tracers.add(
BooleanVar(self), self.var_changed_keyset_source)
self.keybinding = tracers.add(
StringVar(self), self.var_changed_keybinding)
frame_custom = LabelFrame(
self, borderwidth=2, relief=GROOVE,
text=' Custom Key Bindings ')
frame_key_sets = LabelFrame(
self, borderwidth=2, relief=GROOVE, text=' Key Set ')
frame_target = Frame(frame_custom)
target_title = Label(frame_target, text='Action - Key(s)')
scroll_target_y = Scrollbar(frame_target)
scroll_target_x = Scrollbar(frame_target, orient=HORIZONTAL)
self.bindingslist = Listbox(
frame_target, takefocus=FALSE, exportselection=FALSE)
self.bindingslist.bind('<ButtonRelease-1>',
self.on_bindingslist_select)
scroll_target_y['command'] = self.bindingslist.yview
scroll_target_x['command'] = self.bindingslist.xview
self.bindingslist['yscrollcommand'] = scroll_target_y.set
self.bindingslist['xscrollcommand'] = scroll_target_x.set
self.button_new_keys = Button(
frame_custom, text='Get New Keys for Selection',
command=self.get_new_keys, state='disabled')
frames = [Frame(frame_key_sets, padding=2, borderwidth=0)
for i in range(2)]
self.builtin_keyset_on = Radiobutton(
frames[0], variable=self.keyset_source, value=1,
command=self.set_keys_type, text='Use a Built-in Key Set')
self.custom_keyset_on = Radiobutton(
frames[0], variable=self.keyset_source, value=0,
command=self.set_keys_type, text='Use a Custom Key Set')
self.builtinlist = DynOptionMenu(
frames[0], self.builtin_name, None, command=None)
self.customlist = DynOptionMenu(
frames[0], self.custom_name, None, command=None)
self.button_delete_custom_keys = Button(
frames[1], text='Delete Custom Key Set',
command=self.delete_custom_keys)
self.button_save_custom_keys = Button(
frames[1], text='Save as New Custom Key Set',
command=self.save_as_new_key_set)
self.keys_message = Label(frames[0], borderwidth=2)
frame_custom.pack(side=BOTTOM, padx=5, pady=5, expand=TRUE, fill=BOTH)
frame_key_sets.pack(side=BOTTOM, padx=5, pady=5, fill=BOTH)
self.button_new_keys.pack(side=BOTTOM, fill=X, padx=5, pady=5)
frame_target.pack(side=LEFT, padx=5, pady=5, expand=TRUE, fill=BOTH)
frame_target.columnconfigure(0, weight=1)
frame_target.rowconfigure(1, weight=1)
target_title.grid(row=0, column=0, columnspan=2, sticky=W)
self.bindingslist.grid(row=1, column=0, sticky=NSEW)
scroll_target_y.grid(row=1, column=1, sticky=NS)
scroll_target_x.grid(row=2, column=0, sticky=EW)
self.builtin_keyset_on.grid(row=0, column=0, sticky=W+NS)
self.custom_keyset_on.grid(row=1, column=0, sticky=W+NS)
self.builtinlist.grid(row=0, column=1, sticky=NSEW)
self.customlist.grid(row=1, column=1, sticky=NSEW)
self.keys_message.grid(row=0, column=2, sticky=NSEW, padx=5, pady=5)
self.button_delete_custom_keys.pack(side=LEFT, fill=X, expand=True, padx=2)
self.button_save_custom_keys.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)
def load_key_cfg(self):
"Load current configuration settings for the keybinding options."
self.keyset_source.set(idleConf.GetOption(
'main', 'Keys', 'default', type='bool', default=1))
current_option = idleConf.CurrentKeys()
if self.keyset_source.get(): item_list = idleConf.GetSectionList('default', 'keys')
item_list.sort()
self.builtinlist.SetMenu(item_list, current_option)
item_list = idleConf.GetSectionList('user', 'keys')
item_list.sort()
if not item_list:
self.custom_keyset_on.state(('disabled',))
self.custom_name.set('- no custom keys -')
else:
self.customlist.SetMenu(item_list, item_list[0])
else: item_list = idleConf.GetSectionList('user', 'keys')
item_list.sort()
self.customlist.SetMenu(item_list, current_option)
item_list = idleConf.GetSectionList('default', 'keys')
item_list.sort()
self.builtinlist.SetMenu(item_list, idleConf.default_keys())
self.set_keys_type()
keyset_name = idleConf.CurrentKeys()
self.load_keys_list(keyset_name)
def var_changed_builtin_name(self, *params):
"Process selection of builtin key set."
old_keys = (
'IDLE Classic Windows',
'IDLE Classic Unix',
'IDLE Classic Mac',
'IDLE Classic OSX',
)
value = self.builtin_name.get()
if value not in old_keys:
if idleConf.GetOption('main', 'Keys', 'name') not in old_keys:
changes.add_option('main', 'Keys', 'name', old_keys[0])
changes.add_option('main', 'Keys', 'name2', value)
self.keys_message['text'] = 'New key set, see Help'
else:
changes.add_option('main', 'Keys', 'name', value)
changes.add_option('main', 'Keys', 'name2', '')
self.keys_message['text'] = ''
self.load_keys_list(value)
def var_changed_custom_name(self, *params):
"Process selection of custom key set."
value = self.custom_name.get()
if value != '- no custom keys -':
changes.add_option('main', 'Keys', 'name', value)
self.load_keys_list(value)
def var_changed_keyset_source(self, *params):
"Process toggle between builtin key set and custom key set."
value = self.keyset_source.get()
changes.add_option('main', 'Keys', 'default', value)
if value:
self.var_changed_builtin_name()
else:
self.var_changed_custom_name()
def var_changed_keybinding(self, *params):
"Store change to a keybinding."
value = self.keybinding.get()
key_set = self.custom_name.get()
event = self.bindingslist.get(ANCHOR).split()[0]
if idleConf.IsCoreBinding(event):
changes.add_option('keys', key_set, event, value)
else: ext_name = idleConf.GetExtnNameForEvent(event)
ext_keybind_section = ext_name + '_cfgBindings'
changes.add_option('extensions', ext_keybind_section, event, value)
def set_keys_type(self):
"Set available screen options based on builtin or custom key set."
if self.keyset_source.get():
self.builtinlist['state'] = 'normal'
self.customlist['state'] = 'disabled'
self.button_delete_custom_keys.state(('disabled',))
else:
self.builtinlist['state'] = 'disabled'
self.custom_keyset_on.state(('!disabled',))
self.customlist['state'] = 'normal'
self.button_delete_custom_keys.state(('!disabled',))
def get_new_keys(self):
list_index = self.bindingslist.index(ANCHOR)
binding = self.bindingslist.get(list_index)
bind_name = binding.split()[0]
if self.keyset_source.get():
current_key_set_name = self.builtin_name.get()
else:
current_key_set_name = self.custom_name.get()
current_bindings = idleConf.GetCurrentKeySet()
if current_key_set_name in changes['keys']: key_set_changes = changes['keys'][current_key_set_name]
for event in key_set_changes:
current_bindings[event] = key_set_changes[event].split()
current_key_sequences = list(current_bindings.values())
new_keys = GetKeysWindow(self, 'Get New Keys', bind_name,
current_key_sequences).result
if new_keys:
if self.keyset_source.get(): message = ('Your changes will be saved as a new Custom Key Set.'
' Enter a name for your new Custom Key Set below.')
new_keyset = self.get_new_keys_name(message)
if not new_keyset: self.bindingslist.select_set(list_index)
self.bindingslist.select_anchor(list_index)
return
else: self.create_new_key_set(new_keyset)
self.bindingslist.delete(list_index)
self.bindingslist.insert(list_index, bind_name+' - '+new_keys)
self.bindingslist.select_set(list_index)
self.bindingslist.select_anchor(list_index)
self.keybinding.set(new_keys)
else:
self.bindingslist.select_set(list_index)
self.bindingslist.select_anchor(list_index)
def get_new_keys_name(self, message):
"Return new key set name from query popup."
used_names = (idleConf.GetSectionList('user', 'keys') +
idleConf.GetSectionList('default', 'keys'))
new_keyset = SectionName(
self, 'New Custom Key Set', message, used_names).result
return new_keyset
def save_as_new_key_set(self):
"Prompt for name of new key set and save changes using that name."
new_keys_name = self.get_new_keys_name('New Key Set Name:')
if new_keys_name:
self.create_new_key_set(new_keys_name)
def on_bindingslist_select(self, event):
"Activate button to assign new keys to selected action."
self.button_new_keys.state(('!disabled',))
def create_new_key_set(self, new_key_set_name):
if self.keyset_source.get():
prev_key_set_name = self.builtin_name.get()
else:
prev_key_set_name = self.custom_name.get()
prev_keys = idleConf.GetCoreKeys(prev_key_set_name)
new_keys = {}
for event in prev_keys: event_name = event[2:-2] binding = ' '.join(prev_keys[event])
new_keys[event_name] = binding
if prev_key_set_name in changes['keys']:
key_set_changes = changes['keys'][prev_key_set_name]
for event in key_set_changes:
new_keys[event] = key_set_changes[event]
self.save_new_key_set(new_key_set_name, new_keys)
custom_key_list = idleConf.GetSectionList('user', 'keys')
custom_key_list.sort()
self.customlist.SetMenu(custom_key_list, new_key_set_name)
self.keyset_source.set(0)
self.set_keys_type()
def load_keys_list(self, keyset_name):
reselect = False
if self.bindingslist.curselection():
reselect = True
list_index = self.bindingslist.index(ANCHOR)
keyset = idleConf.GetKeySet(keyset_name)
bind_names = list(keyset)
bind_names.sort()
self.bindingslist.delete(0, END)
for bind_name in bind_names:
key = ' '.join(keyset[bind_name])
bind_name = bind_name[2:-2] if keyset_name in changes['keys']:
if bind_name in changes['keys'][keyset_name]:
key = changes['keys'][keyset_name][bind_name]
self.bindingslist.insert(END, bind_name+' - '+key)
if reselect:
self.bindingslist.see(list_index)
self.bindingslist.select_set(list_index)
self.bindingslist.select_anchor(list_index)
@staticmethod
def save_new_key_set(keyset_name, keyset):
idleConf.userCfg['keys'].AddSection(keyset_name)
for event in keyset:
value = keyset[event]
idleConf.userCfg['keys'].SetOption(keyset_name, event, value)
def askyesno(self, *args, **kwargs):
return messagebox.askyesno(*args, **kwargs)
def delete_custom_keys(self):
keyset_name = self.custom_name.get()
delmsg = 'Are you sure you wish to delete the key set %r ?'
if not self.askyesno(
'Delete Key Set', delmsg % keyset_name, parent=self):
return
self.cd.deactivate_current_config()
changes.delete_section('keys', keyset_name)
item_list = idleConf.GetSectionList('user', 'keys')
item_list.sort()
if not item_list:
self.custom_keyset_on.state(('disabled',))
self.customlist.SetMenu(item_list, '- no custom keys -')
else:
self.customlist.SetMenu(item_list, item_list[0])
self.keyset_source.set(idleConf.defaultCfg['main']
.Get('Keys', 'default'))
self.builtin_name.set(idleConf.defaultCfg['main'].Get('Keys', 'name')
or idleConf.default_keys())
changes.save_all()
self.extpage.save_all_changed_extensions()
self.cd.activate_config_changes()
self.set_keys_type()
class WinPage(Frame):
def __init__(self, master):
super().__init__(master)
self.init_validators()
self.create_page_windows()
self.load_windows_cfg()
def init_validators(self):
digits_or_empty_re = re.compile(r'[0-9]*')
def is_digits_or_empty(s):
"Return 's is blank or contains only digits'"
return digits_or_empty_re.fullmatch(s) is not None
self.digits_only = (self.register(is_digits_or_empty), '%P',)
def create_page_windows(self):
self.startup_edit = tracers.add(
IntVar(self), ('main', 'General', 'editor-on-startup'))
self.win_width = tracers.add(
StringVar(self), ('main', 'EditorWindow', 'width'))
self.win_height = tracers.add(
StringVar(self), ('main', 'EditorWindow', 'height'))
self.indent_spaces = tracers.add(
StringVar(self), ('main', 'Indent', 'num-spaces'))
self.cursor_blink = tracers.add(
BooleanVar(self), ('main', 'EditorWindow', 'cursor-blink'))
self.autocomplete_wait = tracers.add(
StringVar(self), ('extensions', 'AutoComplete', 'popupwait'))
self.paren_style = tracers.add(
StringVar(self), ('extensions', 'ParenMatch', 'style'))
self.flash_delay = tracers.add(
StringVar(self), ('extensions', 'ParenMatch', 'flash-delay'))
self.paren_bell = tracers.add(
BooleanVar(self), ('extensions', 'ParenMatch', 'bell'))
self.format_width = tracers.add(
StringVar(self), ('extensions', 'FormatParagraph', 'max-width'))
frame_window = LabelFrame(self, borderwidth=2, relief=GROOVE,
text=' Window Preferences')
frame_run = Frame(frame_window, borderwidth=0)
startup_title = Label(frame_run, text='At Startup')
self.startup_editor_on = Radiobutton(
frame_run, variable=self.startup_edit, value=1,
text="Open Edit Window")
self.startup_shell_on = Radiobutton(
frame_run, variable=self.startup_edit, value=0,
text='Open Shell Window')
frame_win_size = Frame(frame_window, borderwidth=0)
win_size_title = Label(
frame_win_size, text='Initial Window Size (in characters)')
win_width_title = Label(frame_win_size, text='Width')
self.win_width_int = Entry(
frame_win_size, textvariable=self.win_width, width=3,
validatecommand=self.digits_only, validate='key',
)
win_height_title = Label(frame_win_size, text='Height')
self.win_height_int = Entry(
frame_win_size, textvariable=self.win_height, width=3,
validatecommand=self.digits_only, validate='key',
)
frame_cursor = Frame(frame_window, borderwidth=0)
indent_title = Label(frame_cursor,
text='Indent spaces (4 is standard)')
try:
self.indent_chooser = Spinbox(
frame_cursor, textvariable=self.indent_spaces,
from_=1, to=10, width=2,
validatecommand=self.digits_only, validate='key')
except TclError:
self.indent_chooser = Combobox(
frame_cursor, textvariable=self.indent_spaces,
state="readonly", values=list(range(1,11)), width=3)
cursor_blink_title = Label(frame_cursor, text='Cursor Blink')
self.cursor_blink_bool = Checkbutton(frame_cursor, text="Cursor blink",
variable=self.cursor_blink)
frame_autocomplete = Frame(frame_window, borderwidth=0,)
auto_wait_title = Label(frame_autocomplete,
text='Completions Popup Wait (milliseconds)')
self.auto_wait_int = Entry(
frame_autocomplete, textvariable=self.autocomplete_wait,
width=6, validatecommand=self.digits_only, validate='key')
frame_paren1 = Frame(frame_window, borderwidth=0)
paren_style_title = Label(frame_paren1, text='Paren Match Style')
self.paren_style_type = OptionMenu(
frame_paren1, self.paren_style, 'expression',
"opener","parens","expression")
frame_paren2 = Frame(frame_window, borderwidth=0)
paren_time_title = Label(
frame_paren2, text='Time Match Displayed (milliseconds)\n'
'(0 is until next input)')
self.paren_flash_time = Entry(
frame_paren2, textvariable=self.flash_delay, width=6,
validatecommand=self.digits_only, validate='key')
self.bell_on = Checkbutton(
frame_paren2, text="Bell on Mismatch", variable=self.paren_bell)
frame_format = Frame(frame_window, borderwidth=0)
format_width_title = Label(frame_format,
text='Format Paragraph Max Width')
self.format_width_int = Entry(
frame_format, textvariable=self.format_width, width=4,
validatecommand=self.digits_only, validate='key',
)
frame_window.pack(side=TOP, padx=5, pady=5, expand=TRUE, fill=BOTH)
frame_run.pack(side=TOP, padx=5, pady=0, fill=X)
startup_title.pack(side=LEFT, anchor=W, padx=5, pady=5)
self.startup_shell_on.pack(side=RIGHT, anchor=W, padx=5, pady=5)
self.startup_editor_on.pack(side=RIGHT, anchor=W, padx=5, pady=5)
frame_win_size.pack(side=TOP, padx=5, pady=0, fill=X)
win_size_title.pack(side=LEFT, anchor=W, padx=5, pady=5)
self.win_height_int.pack(side=RIGHT, anchor=E, padx=10, pady=5)
win_height_title.pack(side=RIGHT, anchor=E, pady=5)
self.win_width_int.pack(side=RIGHT, anchor=E, padx=10, pady=5)
win_width_title.pack(side=RIGHT, anchor=E, pady=5)
frame_cursor.pack(side=TOP, padx=5, pady=0, fill=X)
indent_title.pack(side=LEFT, anchor=W, padx=5)
self.indent_chooser.pack(side=LEFT, anchor=W, padx=10)
self.cursor_blink_bool.pack(side=RIGHT, anchor=E, padx=15, pady=5)
frame_autocomplete.pack(side=TOP, padx=5, pady=0, fill=X)
auto_wait_title.pack(side=LEFT, anchor=W, padx=5, pady=5)
self.auto_wait_int.pack(side=TOP, padx=10, pady=5)
frame_paren1.pack(side=TOP, padx=5, pady=0, fill=X)
paren_style_title.pack(side=LEFT, anchor=W, padx=5, pady=5)
self.paren_style_type.pack(side=TOP, padx=10, pady=5)
frame_paren2.pack(side=TOP, padx=5, pady=0, fill=X)
paren_time_title.pack(side=LEFT, anchor=W, padx=5)
self.bell_on.pack(side=RIGHT, anchor=E, padx=15, pady=5)
self.paren_flash_time.pack(side=TOP, anchor=W, padx=15, pady=5)
frame_format.pack(side=TOP, padx=5, pady=0, fill=X)
format_width_title.pack(side=LEFT, anchor=W, padx=5, pady=5)
self.format_width_int.pack(side=TOP, padx=10, pady=5)
def load_windows_cfg(self):
self.startup_edit.set(idleConf.GetOption(
'main', 'General', 'editor-on-startup', type='bool'))
self.win_width.set(idleConf.GetOption(
'main', 'EditorWindow', 'width', type='int'))
self.win_height.set(idleConf.GetOption(
'main', 'EditorWindow', 'height', type='int'))
self.indent_spaces.set(idleConf.GetOption(
'main', 'Indent', 'num-spaces', type='int'))
self.cursor_blink.set(idleConf.GetOption(
'main', 'EditorWindow', 'cursor-blink', type='bool'))
self.autocomplete_wait.set(idleConf.GetOption(
'extensions', 'AutoComplete', 'popupwait', type='int'))
self.paren_style.set(idleConf.GetOption(
'extensions', 'ParenMatch', 'style'))
self.flash_delay.set(idleConf.GetOption(
'extensions', 'ParenMatch', 'flash-delay', type='int'))
self.paren_bell.set(idleConf.GetOption(
'extensions', 'ParenMatch', 'bell'))
self.format_width.set(idleConf.GetOption(
'extensions', 'FormatParagraph', 'max-width', type='int'))
class ShedPage(Frame):
def __init__(self, master):
super().__init__(master)
self.init_validators()
self.create_page_shed()
self.load_shelled_cfg()
def init_validators(self):
digits_or_empty_re = re.compile(r'[0-9]*')
def is_digits_or_empty(s):
"Return 's is blank or contains only digits'"
return digits_or_empty_re.fullmatch(s) is not None
self.digits_only = (self.register(is_digits_or_empty), '%P',)
def create_page_shed(self):
self.auto_squeeze_min_lines = tracers.add(
StringVar(self), ('main', 'PyShell', 'auto-squeeze-min-lines'))
self.autosave = tracers.add(
IntVar(self), ('main', 'General', 'autosave'))
self.line_numbers_default = tracers.add(
BooleanVar(self),
('main', 'EditorWindow', 'line-numbers-default'))
self.context_lines = tracers.add(
StringVar(self), ('extensions', 'CodeContext', 'maxlines'))
frame_shell = LabelFrame(self, borderwidth=2, relief=GROOVE,
text=' Shell Preferences')
frame_editor = LabelFrame(self, borderwidth=2, relief=GROOVE,
text=' Editor Preferences')
frame_auto_squeeze_min_lines = Frame(frame_shell, borderwidth=0)
auto_squeeze_min_lines_title = Label(frame_auto_squeeze_min_lines,
text='Auto-Squeeze Min. Lines:')
self.auto_squeeze_min_lines_int = Entry(
frame_auto_squeeze_min_lines, width=4,
textvariable=self.auto_squeeze_min_lines,
validatecommand=self.digits_only, validate='key',
)
frame_save = Frame(frame_editor, borderwidth=0)
run_save_title = Label(frame_save, text='At Start of Run (F5) ')
self.save_ask_on = Radiobutton(
frame_save, variable=self.autosave, value=0,
text="Prompt to Save")
self.save_auto_on = Radiobutton(
frame_save, variable=self.autosave, value=1,
text='No Prompt')
frame_line_numbers_default = Frame(frame_editor, borderwidth=0)
line_numbers_default_title = Label(
frame_line_numbers_default, text='Show line numbers in new windows')
self.line_numbers_default_bool = Checkbutton(
frame_line_numbers_default,
variable=self.line_numbers_default,
width=1)
frame_context = Frame(frame_editor, borderwidth=0)
context_title = Label(frame_context, text='Max Context Lines :')
self.context_int = Entry(
frame_context, textvariable=self.context_lines, width=3,
validatecommand=self.digits_only, validate='key',
)
frame_shell.pack(side=TOP, padx=5, pady=5, fill=BOTH)
Label(self).pack() frame_editor.pack(side=TOP, padx=5, pady=5, fill=BOTH)
frame_auto_squeeze_min_lines.pack(side=TOP, padx=5, pady=0, fill=X)
auto_squeeze_min_lines_title.pack(side=LEFT, anchor=W, padx=5, pady=5)
self.auto_squeeze_min_lines_int.pack(side=TOP, padx=5, pady=5)
frame_save.pack(side=TOP, padx=5, pady=0, fill=X)
run_save_title.pack(side=LEFT, anchor=W, padx=5, pady=5)
self.save_auto_on.pack(side=RIGHT, anchor=W, padx=5, pady=5)
self.save_ask_on.pack(side=RIGHT, anchor=W, padx=5, pady=5)
frame_line_numbers_default.pack(side=TOP, padx=5, pady=0, fill=X)
line_numbers_default_title.pack(side=LEFT, anchor=W, padx=5, pady=5)
self.line_numbers_default_bool.pack(side=LEFT, padx=5, pady=5)
frame_context.pack(side=TOP, padx=5, pady=0, fill=X)
context_title.pack(side=LEFT, anchor=W, padx=5, pady=5)
self.context_int.pack(side=TOP, padx=5, pady=5)
def load_shelled_cfg(self):
self.auto_squeeze_min_lines.set(idleConf.GetOption(
'main', 'PyShell', 'auto-squeeze-min-lines', type='int'))
self.autosave.set(idleConf.GetOption(
'main', 'General', 'autosave', default=0, type='bool'))
self.line_numbers_default.set(idleConf.GetOption(
'main', 'EditorWindow', 'line-numbers-default', type='bool'))
self.context_lines.set(idleConf.GetOption(
'extensions', 'CodeContext', 'maxlines', type='int'))
class ExtPage(Frame):
def __init__(self, master):
super().__init__(master)
self.ext_defaultCfg = idleConf.defaultCfg['extensions']
self.ext_userCfg = idleConf.userCfg['extensions']
self.is_int = self.register(is_int)
self.load_extensions()
self.create_page_extensions()
def create_page_extensions(self):
self.extension_names = StringVar(self)
frame_ext = LabelFrame(self, borderwidth=2, relief=GROOVE,
text=' Feature Extensions ')
self.frame_help = HelpFrame(self, borderwidth=2, relief=GROOVE,
text=' Help Menu Extensions ')
frame_ext.rowconfigure(0, weight=1)
frame_ext.columnconfigure(2, weight=1)
self.extension_list = Listbox(frame_ext, listvariable=self.extension_names,
selectmode='browse')
self.extension_list.bind('<<ListboxSelect>>', self.extension_selected)
scroll = Scrollbar(frame_ext, command=self.extension_list.yview)
self.extension_list.yscrollcommand=scroll.set
self.details_frame = LabelFrame(frame_ext, 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_ext.configure(padding=10)
self.config_frame = {}
self.current_extension = None
self.outerframe = self self.tabbed_page_set = self.extension_list
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)
frame_ext.grid(row=0, column=0, sticky='nsew')
Label(self).grid(row=1, column=0) self.frame_help.grid(row=2, column=0, sticky='sew')
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))
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: value = def_obj 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):
"Handle selection of an extension from the list."
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):
f = VerticalScrolledFrame(self.details_frame, height=250, width=250)
self.config_frame[ext_name] = f
entry_area = f.interior
for row, opt in enumerate(self.extensions[ext_name]):
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, variable=var,
onvalue='True', offvalue='False', 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'), width=10
).grid(row=row, column=1, sticky=NSEW, padx=7)
else: Entry(entry_area, textvariable=var, width=15
).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 (value == default):
return self.ext_userCfg.RemoveOption(section, name)
return self.ext_userCfg.SetOption(section, name, value)
def save_all_changed_extensions(self):
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()
class HelpFrame(LabelFrame):
def __init__(self, master, **cfg):
super().__init__(master, **cfg)
self.create_frame_help()
self.load_helplist()
def create_frame_help(self):
frame_helplist = Frame(self)
self.helplist = Listbox(
frame_helplist, height=5, takefocus=True,
exportselection=FALSE)
scroll_helplist = Scrollbar(frame_helplist)
scroll_helplist['command'] = self.helplist.yview
self.helplist['yscrollcommand'] = scroll_helplist.set
self.helplist.bind('<ButtonRelease-1>', self.help_source_selected)
frame_buttons = Frame(self)
self.button_helplist_edit = Button(
frame_buttons, text='Edit', state='disabled',
width=8, command=self.helplist_item_edit)
self.button_helplist_add = Button(
frame_buttons, text='Add',
width=8, command=self.helplist_item_add)
self.button_helplist_remove = Button(
frame_buttons, text='Remove', state='disabled',
width=8, command=self.helplist_item_remove)
frame_helplist.pack(side=LEFT, padx=5, pady=5, expand=TRUE, fill=BOTH)
self.helplist.pack(side=LEFT, anchor=E, expand=TRUE, fill=BOTH)
scroll_helplist.pack(side=RIGHT, anchor=W, fill=Y)
frame_buttons.pack(side=RIGHT, padx=5, pady=5, fill=Y)
self.button_helplist_edit.pack(side=TOP, anchor=W, pady=5)
self.button_helplist_add.pack(side=TOP, anchor=W)
self.button_helplist_remove.pack(side=TOP, anchor=W, pady=5)
def help_source_selected(self, event):
"Handle event for selecting additional help."
self.set_add_delete_state()
def set_add_delete_state(self):
"Toggle the state for the help list buttons based on list entries."
if self.helplist.size() < 1: self.button_helplist_edit.state(('disabled',))
self.button_helplist_remove.state(('disabled',))
else: if self.helplist.curselection(): self.button_helplist_edit.state(('!disabled',))
self.button_helplist_remove.state(('!disabled',))
else: self.button_helplist_edit.state(('disabled',))
self.button_helplist_remove.state(('disabled',))
def helplist_item_add(self):
help_source = HelpSource(self, 'New Help Source').result
if help_source:
self.user_helplist.append(help_source)
self.helplist.insert(END, help_source[0])
self.update_help_changes()
def helplist_item_edit(self):
item_index = self.helplist.index(ANCHOR)
help_source = self.user_helplist[item_index]
new_help_source = HelpSource(
self, 'Edit Help Source',
menuitem=help_source[0],
filepath=help_source[1],
).result
if new_help_source and new_help_source != help_source:
self.user_helplist[item_index] = new_help_source
self.helplist.delete(item_index)
self.helplist.insert(item_index, new_help_source[0])
self.update_help_changes()
self.set_add_delete_state()
def helplist_item_remove(self):
item_index = self.helplist.index(ANCHOR)
del(self.user_helplist[item_index])
self.helplist.delete(item_index)
self.update_help_changes()
self.set_add_delete_state()
def update_help_changes(self):
"Clear and rebuild the HelpFiles section in changes"
changes['main']['HelpFiles'] = {}
for num in range(1, len(self.user_helplist) + 1):
changes.add_option(
'main', 'HelpFiles', str(num),
';'.join(self.user_helplist[num-1][:2]))
def load_helplist(self):
self.user_helplist = idleConf.GetAllExtraHelpSourcesList()
self.helplist.delete(0, 'end')
for help_item in self.user_helplist:
self.helplist.insert(END, help_item[0])
self.set_add_delete_state()
class VarTrace:
def __init__(self):
self.untraced = []
self.traced = []
def clear(self):
"Clear lists (for tests)."
self.untraced.clear()
self.traced.clear()
def add(self, var, callback):
if isinstance(callback, tuple):
callback = self.make_callback(var, callback)
self.untraced.append((var, callback))
return var
@staticmethod
def make_callback(var, config):
"Return default callback function to add values to changes instance."
def default_callback(*params):
"Add config values to changes instance."
changes.add_option(*config, var.get())
return default_callback
def attach(self):
"Attach callback to all vars that are not traced."
while self.untraced:
var, callback = self.untraced.pop()
var.trace_add('write', callback)
self.traced.append((var, callback))
def detach(self):
"Remove callback from traced vars."
while self.traced:
var, callback = self.traced.pop()
var.trace_remove('write', var.trace_info()[0][1])
self.untraced.append((var, callback))
tracers = VarTrace()
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. [Cancel] only cancels changes made since the last save.
'''
help_pages = {
'Fonts/Tabs':'''
Font sample: This shows what a selection of Basic Multilingual Plane
unicode characters look like for the current font selection. If the
selected font does not define a character, Tk attempts to find another
font that does. Substitute glyphs depend on what is available on a
particular system and will not necessarily have the same size as the
font selected. Line contains 20 characters up to Devanagari, 14 for
Tamil, and 10 for East Asia.
Hebrew and Arabic letters should display right to left, starting with
alef, \u05d0 and \u0627. Arabic digits display left to right. The
Devanagari and Tamil lines start with digits. The East Asian lines
are Chinese digits, Chinese Hanzi, Korean Hangul, and Japanese
Hiragana and Katakana.
You can edit the font sample. Changes remain until IDLE is closed.
''',
'Highlights': '''
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.
''',
'General': '''
General:
AutoComplete: Popupwait is milliseconds to wait after key char, without
cursor movement, before popping up completion box. Key char is '.' after
identifier or a '/' (or '\\' on Windows) within a string.
FormatParagraph: Max-width is max chars in lines after re-formatting.
Use with paragraphs in both strings and comment blocks.
ParenMatch: Style indicates what is highlighted when closer is entered:
'opener' - opener '({[' corresponding to closer; 'parens' - both chars;
'expression' (default) - also everything in between. Flash-delay is how
long to highlight if cursor is not moved (0 means forever).
CodeContext: Maxlines is the maximum number of code context lines to
display when Code Context is turned on for an editor window.
Shell Preferences: Auto-Squeeze Min. Lines is the minimum number of lines
of output to automatically "squeeze".
''',
'Extensions': '''
ZzDummy: This extension is provided as an example for how to create and
use an extension. Enable indicates whether the extension is active or
not; likewise enable_editor and enable_shell indicate which windows it
will be active on. For this extension, z-text is the text that will be
inserted at or removed from the beginning of the lines of selected text,
or the current line if no selection.
''',
}
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):
def __init__(self, parent, *args, **kw):
Frame.__init__(self, parent, *args, **kw)
vscrollbar = Scrollbar(self, orient=VERTICAL)
vscrollbar.pack(fill=Y, side=RIGHT, expand=FALSE)
canvas = Canvas(self, borderwidth=0, highlightthickness=0,
yscrollcommand=vscrollbar.set, width=240)
canvas.pack(side=LEFT, fill=BOTH, expand=TRUE)
vscrollbar.config(command=canvas.yview)
canvas.xview_moveto(0)
canvas.yview_moveto(0)
self.interior = interior = Frame(canvas)
interior_id = canvas.create_window(0, 0, window=interior, anchor=NW)
def _configure_interior(event):
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():
canvas.itemconfigure(interior_id, width=canvas.winfo_width())
canvas.bind('<Configure>', _configure_canvas)
return
if __name__ == '__main__':
from unittest import main
main('idlelib.idle_test.test_configdialog', verbosity=2, exit=False)
from idlelib.idle_test.htest import run
run(ConfigDialog)