| """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 (Toplevel, Frame, LabelFrame, Listbox, Label, Button, |
| Entry, Text, Scale, Radiobutton, Checkbutton, Canvas, |
| StringVar, BooleanVar, IntVar, TRUE, FALSE, |
| TOP, BOTTOM, RIGHT, LEFT, SOLID, GROOVE, NORMAL, DISABLED, |
| NONE, BOTH, X, Y, W, E, EW, NS, NSEW, NW, |
| HORIZONTAL, VERTICAL, ANCHOR, ACTIVE, END) |
| 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, ConfigChanges |
| 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 |
| |
| changes = ConfigChanges() |
| |
| |
| class ConfigDialog(Toplevel): |
| """Config dialog for IDLE. |
| """ |
| |
| def __init__(self, parent, title='', _htest=False, _utest=False): |
| """Show the tabbed dialog for user configuration. |
| |
| parent - parent of this dialog |
| title - string which is the title of this popup dialog |
| _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.withdraw() |
| |
| self.configure(borderwidth=5) |
| 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}') |
| # 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.theme_elements={ |
| '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.create_widgets() |
| self.resizable(height=FALSE, width=FALSE) |
| self.transient(parent) |
| self.grab_set() |
| self.protocol("WM_DELETE_WINDOW", self.cancel) |
| self.fontlist.focus_set() |
| # XXX Decide whether to keep or delete these key bindings. |
| # 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.load_configs() |
| self.attach_var_callbacks() # Avoid callbacks during load_configs. |
| |
| if not _utest: |
| self.wm_deiconify() |
| self.wait_window() |
| |
| def create_widgets(self): |
| "Create and place widgets for tabbed dialog." |
| self.tab_pages = TabbedPageSet(self, |
| page_names=['Fonts/Tabs', 'Highlighting', 'Keys', 'General', |
| 'Extensions']) |
| self.tab_pages.pack(side=TOP, expand=TRUE, fill=BOTH) |
| self.create_page_font_tab() |
| self.create_page_highlight() |
| self.create_page_keys() |
| self.create_page_general() |
| self.create_page_extensions() |
| self.create_action_buttons().pack(side=BOTTOM) |
| |
| def create_action_buttons(self): |
| "Return frame of action buttons for dialog." |
| if macosx.isAquaTk(): |
| # Changing the default padding on OSX results in unreadable |
| # text in the buttons. |
| padding_args = {} |
| else: |
| padding_args = {'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, |
| **padding_args).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 create_page_font_tab(self): |
| """Return frame of widgets for Font/Tabs tab. |
| |
| Configuration attributes: |
| font_size: Font size. |
| font_bold: Select font bold or not. |
| font_name: Font face. |
| space_num: Indentation width. |
| edit_font: Font widget with default font name, size, and weight. |
| """ |
| parent = self.parent |
| self.font_size = StringVar(parent) |
| self.font_bold = BooleanVar(parent) |
| self.font_name = StringVar(parent) |
| self.space_num = IntVar(parent) |
| self.edit_font = tkFont.Font(parent, ('courier', 10, 'normal')) |
| |
| # Create widgets. |
| # body and body section frames. |
| frame = self.tab_pages.pages['Fonts/Tabs'].frame |
| frame_font = LabelFrame( |
| frame, borderwidth=2, relief=GROOVE, text=' Base Editor Font ') |
| frame_indent = LabelFrame( |
| frame, borderwidth=2, relief=GROOVE, text=' Indentation Width ') |
| # frame_font |
| 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=5, takefocus=FALSE, exportselection=FALSE) |
| self.fontlist.bind('<<ListboxSelect>>', 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.opt_menu_font_size = DynOptionMenu( |
| frame_font_param, self.font_size, None, command=self.set_font_sample) |
| check_font_bold = Checkbutton( |
| frame_font_param, variable=self.font_bold, onvalue=1, |
| offvalue=0, text='Bold', command=self.set_font_sample) |
| frame_font_sample = Frame(frame_font, relief=SOLID, borderwidth=1) |
| self.font_sample = Label( |
| frame_font_sample, justify=LEFT, font=self.edit_font, |
| text='AaBbCcDdEe\nFfGgHhIiJjK\n1234567890\n#:+=(){}[]') |
| # frame_indent |
| frame_indent_size = Frame(frame_indent) |
| indent_size_title = Label( |
| frame_indent_size, justify=LEFT, |
| text='Python Standard: 4 Spaces!') |
| self.scale_indent_size = Scale( |
| frame_indent_size, variable=self.space_num, |
| orient='horizontal', tickinterval=2, from_=2, to=16) |
| |
| # Pack widgets. |
| # body |
| frame_font.pack(side=LEFT, padx=5, pady=5, expand=TRUE, fill=BOTH) |
| frame_indent.pack(side=LEFT, padx=5, pady=5, fill=Y) |
| # frame_font |
| 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.opt_menu_font_size.pack(side=LEFT, anchor=W) |
| check_font_bold.pack(side=LEFT, anchor=W, padx=20) |
| frame_font_sample.pack(side=TOP, padx=5, pady=5, expand=TRUE, fill=BOTH) |
| self.font_sample.pack(expand=TRUE, fill=BOTH) |
| # frame_indent |
| frame_indent_size.pack(side=TOP, fill=X) |
| indent_size_title.pack(side=TOP, anchor=W, padx=5) |
| self.scale_indent_size.pack(side=TOP, padx=5, fill=X) |
| |
| return frame |
| |
| def create_page_highlight(self): |
| """Return frame of widgets for Highlighting tab. |
| |
| Configuration attributes: |
| builtin_theme: Menu variable for built-in theme. |
| custom_theme: Menu variable for custom theme. |
| fg_bg_toggle: Toggle for foreground/background color. |
| colour: Color of selected target. |
| is_builtin_theme: Selector for built-in or custom theme. |
| highlight_target: Menu variable for the highlight tag target. |
| """ |
| parent = self.parent |
| self.builtin_theme = StringVar(parent) |
| self.custom_theme = StringVar(parent) |
| self.fg_bg_toggle = BooleanVar(parent) |
| self.colour = StringVar(parent) |
| self.is_builtin_theme = BooleanVar(parent) |
| self.highlight_target = StringVar(parent) |
| |
| ##widget creation |
| #body frame |
| frame = self.tab_pages.pages['Highlighting'].frame |
| #body section frames |
| frame_custom = LabelFrame(frame, borderwidth=2, relief=GROOVE, |
| text=' Custom Highlighting ') |
| frame_theme = LabelFrame(frame, borderwidth=2, relief=GROOVE, |
| text=' Highlighting Theme ') |
| #frame_custom |
| self.text_highlight_sample=Text( |
| frame_custom, relief=SOLID, borderwidth=1, |
| font=('courier', 12, ''), cursor='hand2', width=21, height=11, |
| takefocus=FALSE, highlightthickness=0, wrap=NONE) |
| text=self.text_highlight_sample |
| text.bind('<Double-Button-1>', lambda e: 'break') |
| text.bind('<B1-Motion>', lambda e: 'break') |
| text_and_tags=( |
| ('#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 texttag in text_and_tags: |
| text.insert(END, texttag[0], texttag[1]) |
| for element in self.theme_elements: |
| def tem(event, elem=element): |
| event.widget.winfo_toplevel().highlight_target.set(elem) |
| text.tag_bind( |
| self.theme_elements[element][0], '<ButtonPress-1>', tem) |
| text.config(state=DISABLED) |
| self.frame_colour_set = Frame(frame_custom, relief=SOLID, borderwidth=1) |
| frame_fg_bg_toggle = Frame(frame_custom) |
| button_set_colour = Button( |
| self.frame_colour_set, text='Choose Colour for :', |
| command=self.get_colour, highlightthickness=0) |
| self.opt_menu_highlight_target = DynOptionMenu( |
| self.frame_colour_set, self.highlight_target, None, |
| highlightthickness=0) #, command=self.set_highlight_targetBinding |
| self.radio_fg = Radiobutton( |
| frame_fg_bg_toggle, variable=self.fg_bg_toggle, value=1, |
| text='Foreground', command=self.set_colour_sample_binding) |
| self.radio_bg=Radiobutton( |
| frame_fg_bg_toggle, variable=self.fg_bg_toggle, value=0, |
| text='Background', command=self.set_colour_sample_binding) |
| self.fg_bg_toggle.set(1) |
| button_save_custom_theme = Button( |
| frame_custom, text='Save as New Custom Theme', |
| command=self.save_as_new_theme) |
| #frame_theme |
| theme_type_title = Label(frame_theme, text='Select : ') |
| self.radio_theme_builtin = Radiobutton( |
| frame_theme, variable=self.is_builtin_theme, value=1, |
| command=self.set_theme_type, text='a Built-in Theme') |
| self.radio_theme_custom = Radiobutton( |
| frame_theme, variable=self.is_builtin_theme, value=0, |
| command=self.set_theme_type, text='a Custom Theme') |
| self.opt_menu_theme_builtin = DynOptionMenu( |
| frame_theme, self.builtin_theme, None, command=None) |
| self.opt_menu_theme_custom=DynOptionMenu( |
| frame_theme, self.custom_theme, None, command=None) |
| self.button_delete_custom_theme=Button( |
| frame_theme, text='Delete Custom Theme', |
| command=self.delete_custom_theme) |
| self.new_custom_theme = Label(frame_theme, bd=2) |
| |
| ##widget packing |
| #body |
| frame_custom.pack(side=LEFT, padx=5, pady=5, expand=TRUE, fill=BOTH) |
| frame_theme.pack(side=LEFT, padx=5, pady=5, fill=Y) |
| #frame_custom |
| self.frame_colour_set.pack(side=TOP, padx=5, pady=5, expand=TRUE, fill=X) |
| frame_fg_bg_toggle.pack(side=TOP, padx=5, pady=0) |
| self.text_highlight_sample.pack( |
| side=TOP, padx=5, pady=5, expand=TRUE, fill=BOTH) |
| button_set_colour.pack(side=TOP, expand=TRUE, fill=X, padx=8, pady=4) |
| self.opt_menu_highlight_target.pack( |
| side=TOP, expand=TRUE, fill=X, padx=8, pady=3) |
| self.radio_fg.pack(side=LEFT, anchor=E) |
| self.radio_bg.pack(side=RIGHT, anchor=W) |
| button_save_custom_theme.pack(side=BOTTOM, fill=X, padx=5, pady=5) |
| #frame_theme |
| theme_type_title.pack(side=TOP, anchor=W, padx=5, pady=5) |
| self.radio_theme_builtin.pack(side=TOP, anchor=W, padx=5) |
| self.radio_theme_custom.pack(side=TOP, anchor=W, padx=5, pady=2) |
| self.opt_menu_theme_builtin.pack(side=TOP, fill=X, padx=5, pady=5) |
| self.opt_menu_theme_custom.pack(side=TOP, fill=X, anchor=W, padx=5, pady=5) |
| self.button_delete_custom_theme.pack(side=TOP, fill=X, padx=5, pady=5) |
| self.new_custom_theme.pack(side=TOP, fill=X, pady=5) |
| return frame |
| |
| def create_page_keys(self): |
| """Return frame of widgets for Keys tab. |
| |
| Configuration attributes: |
| builtin_keys: Menu variable for built-in keybindings. |
| custom_keys: Menu variable for custom keybindings. |
| are_keys_builtin: Selector for built-in or custom keybindings. |
| keybinding: Action/key bindings. |
| """ |
| parent = self.parent |
| self.builtin_keys = StringVar(parent) |
| self.custom_keys = StringVar(parent) |
| self.are_keys_builtin = BooleanVar(parent) |
| self.keybinding = StringVar(parent) |
| |
| ##widget creation |
| #body frame |
| frame = self.tab_pages.pages['Keys'].frame |
| #body section frames |
| frame_custom = LabelFrame( |
| frame, borderwidth=2, relief=GROOVE, |
| text=' Custom Key Bindings ') |
| frame_key_sets = LabelFrame( |
| frame, borderwidth=2, relief=GROOVE, text=' Key Set ') |
| #frame_custom |
| 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.list_bindings = Listbox( |
| frame_target, takefocus=FALSE, exportselection=FALSE) |
| self.list_bindings.bind('<ButtonRelease-1>', self.keybinding_selected) |
| scroll_target_y.config(command=self.list_bindings.yview) |
| scroll_target_x.config(command=self.list_bindings.xview) |
| self.list_bindings.config(yscrollcommand=scroll_target_y.set) |
| self.list_bindings.config(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) |
| #frame_key_sets |
| frames = [Frame(frame_key_sets, padx=2, pady=2, borderwidth=0) |
| for i in range(2)] |
| self.radio_keys_builtin = Radiobutton( |
| frames[0], variable=self.are_keys_builtin, value=1, |
| command=self.set_keys_type, text='Use a Built-in Key Set') |
| self.radio_keys_custom = Radiobutton( |
| frames[0], variable=self.are_keys_builtin, value=0, |
| command=self.set_keys_type, text='Use a Custom Key Set') |
| self.opt_menu_keys_builtin = DynOptionMenu( |
| frames[0], self.builtin_keys, None, command=None) |
| self.opt_menu_keys_custom = DynOptionMenu( |
| frames[0], self.custom_keys, None, command=None) |
| self.button_delete_custom_keys = Button( |
| frames[1], text='Delete Custom Key Set', |
| command=self.delete_custom_keys) |
| button_save_custom_keys = Button( |
| frames[1], text='Save as New Custom Key Set', |
| command=self.save_as_new_key_set) |
| self.new_custom_keys = Label(frames[0], bd=2) |
| |
| ##widget packing |
| #body |
| 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) |
| #frame_custom |
| 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 |
| frame_target.columnconfigure(0, weight=1) |
| frame_target.rowconfigure(1, weight=1) |
| target_title.grid(row=0, column=0, columnspan=2, sticky=W) |
| self.list_bindings.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) |
| #frame_key_sets |
| self.radio_keys_builtin.grid(row=0, column=0, sticky=W+NS) |
| self.radio_keys_custom.grid(row=1, column=0, sticky=W+NS) |
| self.opt_menu_keys_builtin.grid(row=0, column=1, sticky=NSEW) |
| self.opt_menu_keys_custom.grid(row=1, column=1, sticky=NSEW) |
| self.new_custom_keys.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) |
| 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) |
| return frame |
| |
| def create_page_general(self): |
| """Return frame of widgets for General tab. |
| |
| Configuration attributes: |
| win_width: Initial window width in characters. |
| win_height: Initial window height in characters. |
| startup_edit: Selector for opening in editor or shell mode. |
| autosave: Selector for save prompt popup when using Run. |
| """ |
| parent = self.parent |
| self.win_width = StringVar(parent) |
| self.win_height = StringVar(parent) |
| self.startup_edit = IntVar(parent) |
| self.autosave = IntVar(parent) |
| |
| #widget creation |
| #body |
| frame = self.tab_pages.pages['General'].frame |
| #body section frames |
| frame_run = LabelFrame(frame, borderwidth=2, relief=GROOVE, |
| text=' Startup Preferences ') |
| frame_save = LabelFrame(frame, borderwidth=2, relief=GROOVE, |
| text=' autosave Preferences ') |
| frame_win_size = Frame(frame, borderwidth=2, relief=GROOVE) |
| frame_help = LabelFrame(frame, borderwidth=2, relief=GROOVE, |
| text=' Additional Help Sources ') |
| #frame_run |
| startup_title = Label(frame_run, text='At Startup') |
| self.radio_startup_edit = Radiobutton( |
| frame_run, variable=self.startup_edit, value=1, |
| text="Open Edit Window") |
| self.radio_startup_shell = Radiobutton( |
| frame_run, variable=self.startup_edit, value=0, |
| text='Open Shell Window') |
| #frame_save |
| run_save_title = Label(frame_save, text='At Start of Run (F5) ') |
| self.radio_save_ask = Radiobutton( |
| frame_save, variable=self.autosave, value=0, |
| text="Prompt to Save") |
| self.radio_save_auto = Radiobutton( |
| frame_save, variable=self.autosave, value=1, |
| text='No Prompt') |
| #frame_win_size |
| win_size_title = Label( |
| frame_win_size, text='Initial Window Size (in characters)') |
| win_width_title = Label(frame_win_size, text='Width') |
| self.entry_win_width = Entry( |
| frame_win_size, textvariable=self.win_width, width=3) |
| win_height_title = Label(frame_win_size, text='Height') |
| self.entry_win_height = Entry( |
| frame_win_size, textvariable=self.win_height, width=3) |
| #frame_help |
| frame_helplist = Frame(frame_help) |
| frame_helplist_buttons = Frame(frame_helplist) |
| scroll_helplist = Scrollbar(frame_helplist) |
| self.list_help = Listbox( |
| frame_helplist, height=5, takefocus=FALSE, |
| exportselection=FALSE) |
| scroll_helplist.config(command=self.list_help.yview) |
| self.list_help.config(yscrollcommand=scroll_helplist.set) |
| self.list_help.bind('<ButtonRelease-1>', self.help_source_selected) |
| self.button_helplist_edit = Button( |
| frame_helplist_buttons, text='Edit', state=DISABLED, |
| width=8, command=self.helplist_item_edit) |
| self.button_helplist_add = Button( |
| frame_helplist_buttons, text='Add', |
| width=8, command=self.helplist_item_add) |
| self.button_helplist_remove = Button( |
| frame_helplist_buttons, text='Remove', state=DISABLED, |
| width=8, command=self.helplist_item_remove) |
| |
| #widget packing |
| #body |
| frame_run.pack(side=TOP, padx=5, pady=5, fill=X) |
| frame_save.pack(side=TOP, padx=5, pady=5, fill=X) |
| frame_win_size.pack(side=TOP, padx=5, pady=5, fill=X) |
| frame_help.pack(side=TOP, padx=5, pady=5, expand=TRUE, fill=BOTH) |
| #frame_run |
| startup_title.pack(side=LEFT, anchor=W, padx=5, pady=5) |
| self.radio_startup_shell.pack(side=RIGHT, anchor=W, padx=5, pady=5) |
| self.radio_startup_edit.pack(side=RIGHT, anchor=W, padx=5, pady=5) |
| #frame_save |
| run_save_title.pack(side=LEFT, anchor=W, padx=5, pady=5) |
| self.radio_save_auto.pack(side=RIGHT, anchor=W, padx=5, pady=5) |
| self.radio_save_ask.pack(side=RIGHT, anchor=W, padx=5, pady=5) |
| #frame_win_size |
| win_size_title.pack(side=LEFT, anchor=W, padx=5, pady=5) |
| self.entry_win_height.pack(side=RIGHT, anchor=E, padx=10, pady=5) |
| win_height_title.pack(side=RIGHT, anchor=E, pady=5) |
| self.entry_win_width.pack(side=RIGHT, anchor=E, padx=10, pady=5) |
| win_width_title.pack(side=RIGHT, anchor=E, pady=5) |
| #frame_help |
| frame_helplist_buttons.pack(side=RIGHT, padx=5, pady=5, fill=Y) |
| frame_helplist.pack(side=TOP, padx=5, pady=5, expand=TRUE, fill=BOTH) |
| scroll_helplist.pack(side=RIGHT, anchor=W, fill=Y) |
| self.list_help.pack(side=LEFT, anchor=E, expand=TRUE, fill=BOTH) |
| 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) |
| return frame |
| |
| def attach_var_callbacks(self): |
| "Attach callbacks to variables that can be changed." |
| self.font_size.trace_add('write', self.var_changed_font) |
| self.font_name.trace_add('write', self.var_changed_font) |
| self.font_bold.trace_add('write', self.var_changed_font) |
| self.space_num.trace_add('write', self.var_changed_space_num) |
| self.colour.trace_add('write', self.var_changed_colour) |
| self.builtin_theme.trace_add('write', self.var_changed_builtin_theme) |
| self.custom_theme.trace_add('write', self.var_changed_custom_theme) |
| self.is_builtin_theme.trace_add('write', self.var_changed_is_builtin_theme) |
| self.highlight_target.trace_add('write', self.var_changed_highlight_target) |
| self.keybinding.trace_add('write', self.var_changed_keybinding) |
| self.builtin_keys.trace_add('write', self.var_changed_builtin_keys) |
| self.custom_keys.trace_add('write', self.var_changed_custom_keys) |
| self.are_keys_builtin.trace_add('write', self.var_changed_are_keys_builtin) |
| self.win_width.trace_add('write', self.var_changed_win_width) |
| self.win_height.trace_add('write', self.var_changed_win_height) |
| self.startup_edit.trace_add('write', self.var_changed_startup_edit) |
| self.autosave.trace_add('write', self.var_changed_autosave) |
| |
| def remove_var_callbacks(self): |
| "Remove callbacks to prevent memory leaks." |
| for var in ( |
| self.font_size, self.font_name, self.font_bold, |
| self.space_num, self.colour, self.builtin_theme, |
| self.custom_theme, self.is_builtin_theme, self.highlight_target, |
| self.keybinding, self.builtin_keys, self.custom_keys, |
| self.are_keys_builtin, self.win_width, self.win_height, |
| self.startup_edit, self.autosave,): |
| var.trace_remove('write', var.trace_info()[0][1]) |
| |
| def var_changed_font(self, *params): |
| """Store changes to font attributes. |
| |
| 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.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) |
| |
| def var_changed_space_num(self, *params): |
| "Store change to indentation size." |
| value = self.space_num.get() |
| changes.add_option('main', 'Indent', 'num-spaces', value) |
| |
| def var_changed_colour(self, *params): |
| "Process change to color choice." |
| self.on_new_colour_set() |
| |
| def var_changed_builtin_theme(self, *params): |
| """Process new builtin theme selection. |
| |
| Add the changed theme's name to the changed_items and recreate |
| the sample with the values from the selected theme. |
| """ |
| old_themes = ('IDLE Classic', 'IDLE New') |
| value = self.builtin_theme.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.new_custom_theme.config(text='New theme, see Help', |
| fg='#500000') |
| else: |
| changes.add_option('main', 'Theme', 'name', value) |
| changes.add_option('main', 'Theme', 'name2', '') |
| self.new_custom_theme.config(text='', fg='black') |
| self.paint_theme_sample() |
| |
| def var_changed_custom_theme(self, *params): |
| """Process new custom theme selection. |
| |
| If a new custom theme is selected, add the name to the |
| changed_items and apply the theme to the sample. |
| """ |
| value = self.custom_theme.get() |
| if value != '- no custom themes -': |
| changes.add_option('main', 'Theme', 'name', value) |
| self.paint_theme_sample() |
| |
| def var_changed_is_builtin_theme(self, *params): |
| """Process toggle between builtin and custom theme. |
| |
| Update the default toggle value and apply the newly |
| selected theme type. |
| """ |
| value = self.is_builtin_theme.get() |
| changes.add_option('main', 'Theme', 'default', value) |
| if value: |
| self.var_changed_builtin_theme() |
| else: |
| self.var_changed_custom_theme() |
| |
| def var_changed_highlight_target(self, *params): |
| "Process selection of new target tag for highlighting." |
| self.set_highlight_target() |
| |
| def var_changed_keybinding(self, *params): |
| "Store change to a keybinding." |
| value = self.keybinding.get() |
| key_set = self.custom_keys.get() |
| event = self.list_bindings.get(ANCHOR).split()[0] |
| if idleConf.IsCoreBinding(event): |
| changes.add_option('keys', key_set, event, value) |
| else: # Event is an extension binding. |
| ext_name = idleConf.GetExtnNameForEvent(event) |
| ext_keybind_section = ext_name + '_cfgBindings' |
| changes.add_option('extensions', ext_keybind_section, event, value) |
| |
| def var_changed_builtin_keys(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_keys.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.new_custom_keys.config(text='New key set, see Help', |
| fg='#500000') |
| else: |
| changes.add_option('main', 'Keys', 'name', value) |
| changes.add_option('main', 'Keys', 'name2', '') |
| self.new_custom_keys.config(text='', fg='black') |
| self.load_keys_list(value) |
| |
| def var_changed_custom_keys(self, *params): |
| "Process selection of custom key set." |
| value = self.custom_keys.get() |
| if value != '- no custom keys -': |
| changes.add_option('main', 'Keys', 'name', value) |
| self.load_keys_list(value) |
| |
| def var_changed_are_keys_builtin(self, *params): |
| "Process toggle between builtin key set and custom key set." |
| value = self.are_keys_builtin.get() |
| changes.add_option('main', 'Keys', 'default', value) |
| if value: |
| self.var_changed_builtin_keys() |
| else: |
| self.var_changed_custom_keys() |
| |
| def var_changed_win_width(self, *params): |
| "Store change to window width." |
| value = self.win_width.get() |
| changes.add_option('main', 'EditorWindow', 'width', value) |
| |
| def var_changed_win_height(self, *params): |
| "Store change to window height." |
| value = self.win_height.get() |
| changes.add_option('main', 'EditorWindow', 'height', value) |
| |
| def var_changed_startup_edit(self, *params): |
| "Store change to toggle for starting IDLE in the editor or shell." |
| value = self.startup_edit.get() |
| changes.add_option('main', 'General', 'editor-on-startup', value) |
| |
| def var_changed_autosave(self, *params): |
| "Store change to autosave." |
| value = self.autosave.get() |
| changes.add_option('main', 'General', 'autosave', value) |
| |
| def set_theme_type(self): |
| "Set available screen options based on builtin or custom theme." |
| if self.is_builtin_theme.get(): |
| self.opt_menu_theme_builtin.config(state=NORMAL) |
| self.opt_menu_theme_custom.config(state=DISABLED) |
| self.button_delete_custom_theme.config(state=DISABLED) |
| else: |
| self.opt_menu_theme_builtin.config(state=DISABLED) |
| self.radio_theme_custom.config(state=NORMAL) |
| self.opt_menu_theme_custom.config(state=NORMAL) |
| self.button_delete_custom_theme.config(state=NORMAL) |
| |
| def set_keys_type(self): |
| "Set available screen options based on builtin or custom key set." |
| if self.are_keys_builtin.get(): |
| self.opt_menu_keys_builtin.config(state=NORMAL) |
| self.opt_menu_keys_custom.config(state=DISABLED) |
| self.button_delete_custom_keys.config(state=DISABLED) |
| else: |
| self.opt_menu_keys_builtin.config(state=DISABLED) |
| self.radio_keys_custom.config(state=NORMAL) |
| self.opt_menu_keys_custom.config(state=NORMAL) |
| self.button_delete_custom_keys.config(state=NORMAL) |
| |
| def get_new_keys(self): |
| """Handle event to change key binding for selected line. |
| |
| A selection of a key/binding in the list of current |
| bindings pops up a dialog to enter a new binding. If |
| the current key set is builtin and a binding has |
| changed, then a name for a custom key set needs to be |
| entered for the change to be applied. |
| """ |
| list_index = self.list_bindings.index(ANCHOR) |
| binding = self.list_bindings.get(list_index) |
| bind_name = binding.split()[0] |
| if self.are_keys_builtin.get(): |
| current_key_set_name = self.builtin_keys.get() |
| else: |
| current_key_set_name = self.custom_keys.get() |
| current_bindings = idleConf.GetCurrentKeySet() |
| if current_key_set_name in changes['keys']: # unsaved changes |
| 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 = GetKeysDialog(self, 'Get New Keys', bind_name, |
| current_key_sequences).result |
| if new_keys: |
| if self.are_keys_builtin.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.') |
| new_keyset = self.get_new_keys_name(message) |
| if not new_keyset: # User cancelled custom key set creation. |
| self.list_bindings.select_set(list_index) |
| self.list_bindings.select_anchor(list_index) |
| return |
| else: # Create new custom key set based on previously active key set. |
| self.create_new_key_set(new_keyset) |
| self.list_bindings.delete(list_index) |
| self.list_bindings.insert(list_index, bind_name+' - '+new_keys) |
| self.list_bindings.select_set(list_index) |
| self.list_bindings.select_anchor(list_index) |
| self.keybinding.set(new_keys) |
| else: |
| self.list_bindings.select_set(list_index) |
| self.list_bindings.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 keybinding_selected(self, event): |
| "Activate button to assign new keys to selected action." |
| self.button_new_keys.config(state=NORMAL) |
| |
| def create_new_key_set(self, new_key_set_name): |
| """Create a new custom key set with the given name. |
| |
| Create the new key set based on the previously active set |
| with the current changes applied. Once it is saved, then |
| activate the new key set. |
| """ |
| if self.are_keys_builtin.get(): |
| prev_key_set_name = self.builtin_keys.get() |
| else: |
| prev_key_set_name = self.custom_keys.get() |
| prev_keys = idleConf.GetCoreKeys(prev_key_set_name) |
| new_keys = {} |
| for event in prev_keys: # Add key set to changed items. |
| event_name = event[2:-2] # Trim off the angle brackets. |
| binding = ' '.join(prev_keys[event]) |
| new_keys[event_name] = binding |
| # Handle any unsaved changes to prev key set. |
| 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] |
| # Save the new key set. |
| self.save_new_key_set(new_key_set_name, new_keys) |
| # Change GUI over to the new key set. |
| custom_key_list = idleConf.GetSectionList('user', 'keys') |
| custom_key_list.sort() |
| self.opt_menu_keys_custom.SetMenu(custom_key_list, new_key_set_name) |
| self.are_keys_builtin.set(0) |
| self.set_keys_type() |
| |
| def load_keys_list(self, keyset_name): |
| """Reload the list of action/key binding pairs for the active key set. |
| |
| An action/key binding can be selected to change the key binding. |
| """ |
| reselect = 0 |
| if self.list_bindings.curselection(): |
| reselect = 1 |
| list_index = self.list_bindings.index(ANCHOR) |
| keyset = idleConf.GetKeySet(keyset_name) |
| bind_names = list(keyset.keys()) |
| bind_names.sort() |
| self.list_bindings.delete(0, END) |
| for bind_name in bind_names: |
| key = ' '.join(keyset[bind_name]) |
| bind_name = bind_name[2:-2] # Trim off the angle brackets. |
| if keyset_name in changes['keys']: |
| # Handle any unsaved changes to this key set. |
| if bind_name in changes['keys'][keyset_name]: |
| key = changes['keys'][keyset_name][bind_name] |
| self.list_bindings.insert(END, bind_name+' - '+key) |
| if reselect: |
| self.list_bindings.see(list_index) |
| self.list_bindings.select_set(list_index) |
| self.list_bindings.select_anchor(list_index) |
| |
| def delete_custom_keys(self): |
| """Handle event to delete a custom key set. |
| |
| Applying the delete deactivates the current configuration and |
| reverts to the default. The custom key set is permanently |
| deleted from the config file. |
| """ |
| keyset_name=self.custom_keys.get() |
| delmsg = 'Are you sure you wish to delete the key set %r ?' |
| if not tkMessageBox.askyesno( |
| 'Delete Key Set', delmsg % keyset_name, parent=self): |
| return |
| self.deactivate_current_config() |
| # Remove key set from changes, config, and file. |
| changes.remove(keyset_name) |
| # Reload user key set list. |
| item_list = idleConf.GetSectionList('user', 'keys') |
| item_list.sort() |
| if not item_list: |
| self.radio_keys_custom.config(state=DISABLED) |
| self.opt_menu_keys_custom.SetMenu(item_list, '- no custom keys -') |
| else: |
| self.opt_menu_keys_custom.SetMenu(item_list, item_list[0]) |
| # Revert to default key set. |
| self.are_keys_builtin.set(idleConf.defaultCfg['main'] |
| .Get('Keys', 'default')) |
| self.builtin_keys.set(idleConf.defaultCfg['main'].Get('Keys', 'name') |
| or idleConf.default_keys()) |
| # User can't back out of these changes, they must be applied now. |
| changes.save_all() |
| self.save_all_changed_extensions() |
| self.activate_config_changes() |
| self.set_keys_type() |
| |
| def delete_custom_theme(self): |
| """Handle event to delete custom theme. |
| |
| The current theme is deactivated and the default theme is |
| activated. The custom theme is permanently removed from |
| the config file. |
| """ |
| theme_name = self.custom_theme.get() |
| delmsg = 'Are you sure you wish to delete the theme %r ?' |
| if not tkMessageBox.askyesno( |
| 'Delete Theme', delmsg % theme_name, parent=self): |
| return |
| self.deactivate_current_config() |
| # Remove theme from changes, config, and file. |
| changes.delete_section('highlight') |
| # Reload user theme list. |
| item_list = idleConf.GetSectionList('user', 'highlight') |
| item_list.sort() |
| if not item_list: |
| self.radio_theme_custom.config(state=DISABLED) |
| self.opt_menu_theme_custom.SetMenu(item_list, '- no custom themes -') |
| else: |
| self.opt_menu_theme_custom.SetMenu(item_list, item_list[0]) |
| # Revert to default theme. |
| self.is_builtin_theme.set(idleConf.defaultCfg['main'].Get('Theme', 'default')) |
| self.builtin_theme.set(idleConf.defaultCfg['main'].Get('Theme', 'name')) |
| # User can't back out of these changes, they must be applied now. |
| changes.save_all() |
| self.save_all_changed_extensions() |
| self.activate_config_changes() |
| self.set_theme_type() |
| |
| def get_colour(self): |
| """Handle button to select a new color for the target tag. |
| |
| If a new color is selected while using a builtin theme, a |
| name must be supplied to create a custom theme. |
| """ |
| target = self.highlight_target.get() |
| prev_colour = self.frame_colour_set.cget('bg') |
| rgbTuplet, colour_string = tkColorChooser.askcolor( |
| parent=self, title='Pick new colour for : '+target, |
| initialcolor=prev_colour) |
| if colour_string and (colour_string != prev_colour): |
| # User didn't cancel and they chose a new colour. |
| if self.is_builtin_theme.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.') |
| new_theme = self.get_new_theme_name(message) |
| if not new_theme: # User cancelled custom theme creation. |
| return |
| else: # Create new custom theme based on previously active theme. |
| self.create_new_theme(new_theme) |
| self.colour.set(colour_string) |
| else: # Current theme is user defined. |
| self.colour.set(colour_string) |
| |
| def on_new_colour_set(self): |
| "Display sample of new color selection on the dialog." |
| new_colour=self.colour.get() |
| self.frame_colour_set.config(bg=new_colour) # Set sample. |
| plane ='foreground' if self.fg_bg_toggle.get() else 'background' |
| sample_element = self.theme_elements[self.highlight_target.get()][0] |
| self.text_highlight_sample.tag_config(sample_element, **{plane:new_colour}) |
| theme = self.custom_theme.get() |
| theme_element = sample_element + '-' + plane |
| changes.add_option('highlight', theme, theme_element, new_colour) |
| |
| 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): |
| "Prompt for new theme name and create the theme." |
| new_theme_name = self.get_new_theme_name('New Theme Name:') |
| if new_theme_name: |
| self.create_new_theme(new_theme_name) |
| |
| def create_new_theme(self, new_theme_name): |
| """Create a new custom theme with the given name. |
| |
| Create the new theme based on the previously active theme |
| with the current changes applied. Once it is saved, then |
| activate the new theme. |
| """ |
| if self.is_builtin_theme.get(): |
| theme_type = 'default' |
| theme_name = self.builtin_theme.get() |
| else: |
| theme_type = 'user' |
| theme_name = self.custom_theme.get() |
| new_theme = idleConf.GetThemeDict(theme_type, theme_name) |
| # Apply any of the old theme's unsaved changes to the new theme. |
| if theme_name in changes['highlight']: |
| theme_changes = changes['highlight'][theme_name] |
| for element in theme_changes: |
| new_theme[element] = theme_changes[element] |
| # Save the new theme. |
| self.save_new_theme(new_theme_name, new_theme) |
| # Change GUI over to the new theme. |
| custom_theme_list = idleConf.GetSectionList('user', 'highlight') |
| custom_theme_list.sort() |
| self.opt_menu_theme_custom.SetMenu(custom_theme_list, new_theme_name) |
| self.is_builtin_theme.set(0) |
| self.set_theme_type() |
| |
| def on_fontlist_select(self, event): |
| """Handle selecting a font from the list. |
| |
| Event can result from either mouse click or Up or Down key. |
| Set font_name and example display to selection. |
| """ |
| font = self.fontlist.get(ANCHOR if event.type == 3 else ACTIVE) |
| self.font_name.set(font.lower()) |
| self.set_font_sample() |
| |
| def set_font_sample(self, event=None): |
| "Update the screen samples with the font settings from the dialog." |
| 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.config(font=new_font) |
| self.text_highlight_sample.configure(font=new_font) |
| |
| def set_highlight_target(self): |
| "Set fg/bg toggle and color based on highlight tag target." |
| if self.highlight_target.get() == 'Cursor': # bg not possible |
| self.radio_fg.config(state=DISABLED) |
| self.radio_bg.config(state=DISABLED) |
| self.fg_bg_toggle.set(1) |
| else: # Both fg and bg can be set. |
| self.radio_fg.config(state=NORMAL) |
| self.radio_bg.config(state=NORMAL) |
| self.fg_bg_toggle.set(1) |
| self.set_colour_sample() |
| |
| def set_colour_sample_binding(self, *args): |
| "Change color sample based on foreground/background toggle." |
| self.set_colour_sample() |
| |
| def set_colour_sample(self): |
| "Set the color of the frame background to reflect the selected target." |
| # Set the colour sample area. |
| tag = self.theme_elements[self.highlight_target.get()][0] |
| plane = 'foreground' if self.fg_bg_toggle.get() else 'background' |
| colour = self.text_highlight_sample.tag_cget(tag, plane) |
| self.frame_colour_set.config(bg=colour) |
| |
| def paint_theme_sample(self): |
| "Apply the theme colors to each element tag in the sample text." |
| if self.is_builtin_theme.get(): # Default theme |
| theme = self.builtin_theme.get() |
| else: # User theme |
| theme = self.custom_theme.get() |
| for element_title in self.theme_elements: |
| element = self.theme_elements[element_title][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 changes['highlight']: |
| theme_dict = changes['highlight'][theme] |
| if element + '-foreground' in theme_dict: |
| colours['foreground'] = theme_dict[element + '-foreground'] |
| if element + '-background' in theme_dict: |
| colours['background'] = theme_dict[element + '-background'] |
| self.text_highlight_sample.tag_config(element, **colours) |
| self.set_colour_sample() |
| |
| def help_source_selected(self, event): |
| "Handle event for selecting additional help." |
| self.set_helplist_button_states() |
| |
| def set_helplist_button_states(self): |
| "Toggle the state for the help list buttons based on list entries." |
| if self.list_help.size() < 1: # No entries in list. |
| self.button_helplist_edit.config(state=DISABLED) |
| self.button_helplist_remove.config(state=DISABLED) |
| else: # Some entries. |
| if self.list_help.curselection(): # There currently is a selection. |
| self.button_helplist_edit.config(state=NORMAL) |
| self.button_helplist_remove.config(state=NORMAL) |
| else: # There currently is not a selection. |
| self.button_helplist_edit.config(state=DISABLED) |
| self.button_helplist_remove.config(state=DISABLED) |
| |
| def helplist_item_add(self): |
| """Handle add button for the help list. |
| |
| Query for name and location of new help sources and add |
| them to the list. |
| """ |
| help_source = HelpSource(self, 'New Help Source', |
| ).result |
| if help_source: |
| self.user_helplist.append((help_source[0], help_source[1])) |
| self.list_help.insert(END, help_source[0]) |
| self.update_user_help_changed_items() |
| self.set_helplist_button_states() |
| |
| def helplist_item_edit(self): |
| """Handle edit button for the help list. |
| |
| Query with existing help source information and update |
| config if the values are changed. |
| """ |
| item_index = self.list_help.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.list_help.delete(item_index) |
| self.list_help.insert(item_index, new_help_source[0]) |
| self.update_user_help_changed_items() |
| self.set_helplist_button_states() |
| |
| def helplist_item_remove(self): |
| """Handle remove button for the help list. |
| |
| Delete the help list item from config. |
| """ |
| item_index = self.list_help.index(ANCHOR) |
| del(self.user_helplist[item_index]) |
| self.list_help.delete(item_index) |
| self.update_user_help_changed_items() |
| self.set_helplist_button_states() |
| |
| def update_user_help_changed_items(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_font_cfg(self): |
| "Load current configuration settings for the font options." |
| # Set base editor font selection list. |
| fonts = list(tkFont.families(self)) |
| fonts.sort() |
| for font in fonts: |
| self.fontlist.insert(END, font) |
| configured_font = idleConf.GetFont(self, 'main', 'EditorWindow') |
| font_name = configured_font[0].lower() |
| font_size = configured_font[1] |
| font_bold = configured_font[2]=='bold' |
| 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 |
| # Set font size dropdown. |
| self.opt_menu_font_size.SetMenu(('7', '8', '9', '10', '11', '12', '13', |
| '14', '16', '18', '20', '22', |
| '25', '29', '34', '40'), font_size ) |
| # Set font weight. |
| self.font_bold.set(font_bold) |
| # Set font sample. |
| self.set_font_sample() |
| |
| def load_tab_cfg(self): |
| "Load current configuration settings for the tab options." |
| # Set indent sizes. |
| space_num = idleConf.GetOption( |
| 'main', 'Indent', 'num-spaces', default=4, type='int') |
| self.space_num.set(space_num) |
| |
| def load_theme_cfg(self): |
| "Load current configuration settings for the theme options." |
| # Set current theme type radiobutton. |
| self.is_builtin_theme.set(idleConf.GetOption( |
| 'main', 'Theme', 'default', type='bool', default=1)) |
| # Set current theme. |
| current_option = idleConf.CurrentTheme() |
| # Load available theme option menus. |
| if self.is_builtin_theme.get(): # Default theme selected. |
| item_list = idleConf.GetSectionList('default', 'highlight') |
| item_list.sort() |
| self.opt_menu_theme_builtin.SetMenu(item_list, current_option) |
| item_list = idleConf.GetSectionList('user', 'highlight') |
| item_list.sort() |
| if not item_list: |
| self.radio_theme_custom.config(state=DISABLED) |
| self.custom_theme.set('- no custom themes -') |
| else: |
| self.opt_menu_theme_custom.SetMenu(item_list, item_list[0]) |
| else: # User theme selected. |
| item_list = idleConf.GetSectionList('user', 'highlight') |
| item_list.sort() |
| self.opt_menu_theme_custom.SetMenu(item_list, current_option) |
| item_list = idleConf.GetSectionList('default', 'highlight') |
| item_list.sort() |
| self.opt_menu_theme_builtin.SetMenu(item_list, item_list[0]) |
| self.set_theme_type() |
| # Load theme element option menu. |
| theme_names = list(self.theme_elements.keys()) |
| theme_names.sort(key=lambda x: self.theme_elements[x][1]) |
| self.opt_menu_highlight_target.SetMenu(theme_names, theme_names[0]) |
| self.paint_theme_sample() |
| self.set_highlight_target() |
| |
| def load_key_cfg(self): |
| "Load current configuration settings for the keybinding options." |
| # Set current keys type radiobutton. |
| self.are_keys_builtin.set(idleConf.GetOption( |
| 'main', 'Keys', 'default', type='bool', default=1)) |
| # Set current keys. |
| current_option = idleConf.CurrentKeys() |
| # Load available keyset option menus. |
| if self.are_keys_builtin.get(): # Default theme selected. |
| item_list = idleConf.GetSectionList('default', 'keys') |
| item_list.sort() |
| self.opt_menu_keys_builtin.SetMenu(item_list, current_option) |
| item_list = idleConf.GetSectionList('user', 'keys') |
| item_list.sort() |
| if not item_list: |
| self.radio_keys_custom.config(state=DISABLED) |
| self.custom_keys.set('- no custom keys -') |
| else: |
| self.opt_menu_keys_custom.SetMenu(item_list, item_list[0]) |
| else: # User key set selected. |
| item_list = idleConf.GetSectionList('user', 'keys') |
| item_list.sort() |
| self.opt_menu_keys_custom.SetMenu(item_list, current_option) |
| item_list = idleConf.GetSectionList('default', 'keys') |
| item_list.sort() |
| self.opt_menu_keys_builtin.SetMenu(item_list, idleConf.default_keys()) |
| self.set_keys_type() |
| # Load keyset element list. |
| keyset_name = idleConf.CurrentKeys() |
| self.load_keys_list(keyset_name) |
| |
| def load_general_cfg(self): |
| "Load current configuration settings for the general options." |
| # Set startup state. |
| self.startup_edit.set(idleConf.GetOption( |
| 'main', 'General', 'editor-on-startup', default=1, type='bool')) |
| # Set autosave state. |
| self.autosave.set(idleConf.GetOption( |
| 'main', 'General', 'autosave', default=0, type='bool')) |
| # Set initial window size. |
| self.win_width.set(idleConf.GetOption( |
| 'main', 'EditorWindow', 'width', type='int')) |
| self.win_height.set(idleConf.GetOption( |
| 'main', 'EditorWindow', 'height', type='int')) |
| # Set additional help sources. |
| self.user_helplist = idleConf.GetAllExtraHelpSourcesList() |
| for help_item in self.user_helplist: |
| self.list_help.insert(END, help_item[0]) |
| self.set_helplist_button_states() |
| |
| def load_configs(self): |
| """Load configuration for each page. |
| |
| Load configuration from default and user config files and populate |
| the widgets on the config dialog pages. |
| """ |
| self.load_font_cfg() |
| self.load_tab_cfg() |
| self.load_theme_cfg() |
| self.load_key_cfg() |
| self.load_general_cfg() |
| # note: extension page handled separately |
| |
| def save_new_key_set(self, keyset_name, keyset): |
| """Save a newly created core key set. |
| |
| keyset_name - string, the name of the new key set |
| keyset - dictionary containing the new key set |
| """ |
| if not idleConf.userCfg['keys'].has_section(keyset_name): |
| idleConf.userCfg['keys'].add_section(keyset_name) |
| for event in keyset: |
| value = keyset[event] |
| idleConf.userCfg['keys'].SetOption(keyset_name, event, value) |
| |
| def save_new_theme(self, theme_name, theme): |
| """Save a newly created theme. |
| |
| theme_name - string, the name of the new theme |
| theme - dictionary containing the new theme |
| """ |
| if not idleConf.userCfg['highlight'].has_section(theme_name): |
| idleConf.userCfg['highlight'].add_section(theme_name) |
| for element in theme: |
| value = theme[element] |
| idleConf.userCfg['highlight'].SetOption(theme_name, element, value) |
| |
| def deactivate_current_config(self): |
| "Remove current key bindings." |
| # Before a config is saved, some cleanup of current |
| # config must be done - remove the previous keybindings. |
| win_instances = self.parent.instance_dict.keys() |
| for instance in win_instances: |
| instance.RemoveKeybindings() |
| |
| def activate_config_changes(self): |
| "Dynamically apply configuration changes" |
| win_instances = self.parent.instance_dict.keys() |
| for instance in win_instances: |
| instance.ResetColorizer() |
| instance.ResetFont() |
| instance.set_notabs_indentwidth() |
| instance.ApplyKeybindings() |
| instance.reset_help_menu_entries() |
| |
| def cancel(self): |
| "Dismiss config dialog." |
| self.destroy() |
| |
| def ok(self): |
| "Apply config changes, then dismiss dialog." |
| self.apply() |
| self.destroy() |
| |
| def apply(self): |
| "Apply config changes and leave dialog open." |
| self.deactivate_current_config() |
| changes.save_all() |
| self.save_all_changed_extensions() |
| self.activate_config_changes() |
| |
| def help(self): |
| "Create textview for config dialog help." |
| page = self.tab_pages._current_page |
| view_text(self, title='Help for IDLE preferences', |
| text=help_common+help_pages.get(page, '')) |
| |
| def create_page_extensions(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.tab_pages.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): |
| "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): |
| """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): |
| """Return True if the configuration was added or changed. |
| |
| If the value is the same as the default, then remove it |
| from user config file. |
| """ |
| 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. |
| ''', |
| 'Extensions': ''' |
| Extensions: |
| |
| Autocomplete: Popupwait is milleseconds 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). |
| ''' |
| } |
| |
| |
| 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) |