Mercurial > repos > rliterman > csp2
diff CSP2/CSP2_env/env-d9b9114564458d9d-741b3de822f2aaca6c6caa4325c4afce/lib/python3.8/idlelib/configdialog.py @ 68:5028fdace37b
planemo upload commit 2e9511a184a1ca667c7be0c6321a36dc4e3d116d
author | jpayne |
---|---|
date | Tue, 18 Mar 2025 16:23:26 -0400 |
parents | |
children |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/CSP2/CSP2_env/env-d9b9114564458d9d-741b3de822f2aaca6c6caa4325c4afce/lib/python3.8/idlelib/configdialog.py Tue Mar 18 16:23:26 2025 -0400 @@ -0,0 +1,2380 @@ +"""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. + +""" +import re + +from tkinter import (Toplevel, Listbox, Text, Scale, 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) +from tkinter.ttk import (Frame, LabelFrame, Button, Checkbutton, Entry, Label, + OptionMenu, Notebook, Radiobutton, Scrollbar, Style) +import tkinter.colorchooser as tkColorChooser +import tkinter.font as tkFont +from tkinter import messagebox + +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.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() +# Reload changed options in the following classes. +reloadables = (AutoComplete, CodeContext, ParenMatch, FormatParagraph, + Squeezer) + + +class ConfigDialog(Toplevel): + """Config dialog for IDLE. + """ + + def __init__(self, parent, title='', *, _htest=False, _utest=False): + """Show the tabbed dialog for user configuration. + + Args: + 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 + + Note: Focus set on font page fontlist. + + Methods: + create_widgets + cancel: Bound to DELETE_WINDOW protocol. + """ + Toplevel.__init__(self, parent) + self.parent = parent + if _htest: + parent.instance_dict = {} + if not _utest: + 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.create_widgets() + self.resizable(height=FALSE, width=FALSE) + self.transient(parent) + self.protocol("WM_DELETE_WINDOW", self.cancel) + self.fontpage.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 + # Attach callbacks after loading config to avoid calling them. + tracers.attach() + + if not _utest: + self.grab_set() + self.wm_deiconify() + self.wait_window() + + def create_widgets(self): + """Create and place widgets for tabbed dialog. + + Widgets Bound to self: + note: Notebook + highpage: HighPage + fontpage: FontPage + keyspage: KeysPage + genpage: GenPage + extpage: self.create_page_extensions + + Methods: + create_action_buttons + load_configs: Load pages except for extensions. + activate_config_changes: Tell editors to reload. + """ + self.note = note = Notebook(self) + self.highpage = HighPage(note) + self.fontpage = FontPage(note, self.highpage) + self.keyspage = KeysPage(note) + self.genpage = GenPage(note) + self.extpage = self.create_page_extensions() + note.add(self.fontpage, text='Fonts/Tabs') + note.add(self.highpage, text='Highlights') + note.add(self.keyspage, text=' Keys ') + note.add(self.genpage, text=' General ') + 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): + """Return frame of action buttons for dialog. + + Methods: + ok + apply + cancel + help + + Widget Structure: + outer: Frame + buttons: Frame + (no assignment): Button (ok) + (no assignment): Button (apply) + (no assignment): Button (cancel) + (no assignment): Button (help) + (no assignment): Frame + """ + if macosx.isAquaTk(): + # Changing the default padding on OSX results in unreadable + # text in the buttons. + padding_args = {} + else: + padding_args = {'padding': (6, 3)} + outer = Frame(self, padding=2) + buttons = Frame(outer, padding=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 ok(self): + """Apply config changes, then dismiss dialog. + + Methods: + apply + destroy: inherited + """ + self.apply() + self.destroy() + + def apply(self): + """Apply config changes and leave dialog open. + + Methods: + deactivate_current_config + save_all_changed_extensions + activate_config_changes + """ + self.deactivate_current_config() + changes.save_all() + self.save_all_changed_extensions() + self.activate_config_changes() + + def cancel(self): + """Dismiss config dialog. + + Methods: + destroy: inherited + """ + 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): + """Create textview for config dialog help. + + Attributes accessed: + note + + Methods: + view_text: Method from textview module. + """ + page = self.note.tab(self.note.select(), option='text').strip() + view_text(self, title='Help for IDLE preferences', + text=help_common+help_pages.get(page, '')) + + def deactivate_current_config(self): + """Remove current key bindings. + Iterate over window instances defined in parent and remove + the keybindings. + """ + # 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): + """Apply configuration changes to current windows. + + Dynamically update the current parent window instances + with some of the 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() + instance.update_cursor_blink() + for klass in reloadables: + klass.reload() + + 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. + + Methods: + load_extensions: + extension_selected: Handle selection from list. + create_extension_frame: Hold widgets for one extension. + set_extension_value: Set in userCfg['extensions']. + save_all_changed_extensions: Call extension page Save(). + """ + parent = self.parent + frame = Frame(self.note) + 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(padding=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) + + return frame + + 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): + # Former built-in extensions are already filtered out. + 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, 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: # type == 'str' + # Limit size to fit non-expanding space with larger font. + Entry(entry_area, textvariable=var, width=15 + ).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. + + Attributes accessed: + extensions + + Methods: + set_extension_value + """ + 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 TabPage(Frame): # A template for Page classes. +# def __init__(self, master): +# super().__init__(master) +# self.create_page_tab() +# self.load_tab_cfg() +# def create_page_tab(self): +# # Define tk vars and register var and callback with tracers. +# # Create subframes and widgets. +# # Pack widgets. +# def load_tab_cfg(self): +# # Initialize widgets with data from idleConf. +# def var_changed_var_name(): +# # For each tk var that needs other than default callback. +# def other_methods(): +# # Define tab-specific behavior. + +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_tab() + self.load_font_cfg() + self.load_tab_cfg() + + def create_page_font_tab(self): + """Return frame of widgets for Font/Tabs tab. + + Fonts: Enable users to provisionally change font face, size, or + boldness and to see the consequence of proposed choices. Each + action set 3 options in changes structuree and changes the + corresponding aspect of the font sample on this page and + highlight sample on highlight page. + + Function load_font_cfg initializes font vars and widgets from + idleConf entries and tk. + + Fontlist: mouse button 1 click or up or down key invoke + on_fontlist_select(), which sets var font_name. + + Sizelist: clicking the menubutton opens the dropdown menu. A + mouse button 1 click or return key sets var font_size. + + Bold_toggle: clicking the box toggles var font_bold. + + Changing any of the font vars invokes var_changed_font, which + adds all 3 font options to changes and calls set_samples. + Set_samples applies a new font constructed from the font vars to + font_sample and to highlight_sample on the highlight page. + + Tabs: Enable users to change spaces entered for indent tabs. + Changing indent_scale value with the mouse sets Var space_num, + which invokes the default callback to add an entry to + changes. Load_tab_cfg initializes space_num to default. + + Widgets for FontPage(Frame): (*) widgets bound to self + frame_font: LabelFrame + frame_font_name: Frame + font_name_title: Label + (*)fontlist: ListBox - font_name + scroll_font: Scrollbar + frame_font_param: Frame + font_size_title: Label + (*)sizelist: DynOptionMenu - font_size + (*)bold_toggle: Checkbutton - font_bold + frame_sample: LabelFrame + (*)font_sample: Label + frame_indent: LabelFrame + indent_title: Label + (*)indent_scale: Scale - space_num + """ + 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) + self.space_num = tracers.add(IntVar(self), ('main', 'Indent', 'num-spaces')) + + # Define frames and widgets. + 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_indent = LabelFrame( + self, 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=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') + # frame_sample. + 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) + # frame_indent. + indent_title = Label( + frame_indent, justify=LEFT, + text='Python Standard: 4 Spaces!') + self.indent_scale = Scale( + frame_indent, variable=self.space_num, + orient='horizontal', tickinterval=2, from_=2, to=16) + + # Grid and pack widgets: + 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_indent.grid(row=1, column=0, padx=5, pady=5, sticky='ew') + # 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.sizelist.pack(side=LEFT, anchor=W) + self.bold_toggle.pack(side=LEFT, anchor=W, padx=20) + # frame_sample. + font_sample_frame.pack(expand=TRUE, fill=BOTH) + # frame_indent. + indent_title.pack(side=TOP, anchor=W, padx=5) + self.indent_scale.pack(side=TOP, padx=5, fill=X) + + def load_font_cfg(self): + """Load current configuration settings for the font options. + + Retrieve current font with idleConf.GetFont and font families + from tk. Setup fontlist and set font_name. Setup sizelist, + which sets font_size. Set font_bold. Call set_samples. + """ + configured_font = idleConf.GetFont(self, 'main', 'EditorWindow') + font_name = configured_font[0].lower() + font_size = configured_font[1] + font_bold = configured_font[2]=='bold' + + # Set editor font selection list and font_name. + fonts = list(tkFont.families(self)) + fonts.sort() + 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 + # Set font size dropdown. + self.sizelist.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) + self.set_samples() + + 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) + self.set_samples() + + 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 displays to selection. + """ + font = self.fontlist.get( + ACTIVE if event.type.name == 'KeyRelease' else ANCHOR) + self.font_name.set(font.lower()) + + def set_samples(self, event=None): + """Update update both screen samples with the font settings. + + Called on font initialization and change events. + Accesses font_name, font_size, and font_bold Variables. + Updates font_sample and highlight page highlight_sample. + """ + 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 + + def load_tab_cfg(self): + """Load current configuration settings for the tab options. + + Attributes updated: + space_num: Set to value from idleConf. + """ + # Set indent sizes. + space_num = idleConf.GetOption( + 'main', 'Indent', 'num-spaces', default=4, type='int') + self.space_num.set(space_num) + + 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) + + +class HighPage(Frame): + + def __init__(self, master): + super().__init__(master) + self.cd = master.master + self.style = Style(master) + self.create_page_highlight() + self.load_theme_cfg() + + def create_page_highlight(self): + """Return frame of widgets for Highlighting tab. + + Enable users to provisionally change foreground and background + colors applied to textual tags. Color mappings are stored in + complete listings called themes. Built-in themes in + idlelib/config-highlight.def are fixed as far as the dialog is + concerned. Any theme can be used as the base for a new custom + theme, stored in .idlerc/config-highlight.cfg. + + Function load_theme_cfg() initializes tk variables and theme + lists and calls paint_theme_sample() and set_highlight_target() + for the current theme. Radiobuttons builtin_theme_on and + custom_theme_on toggle var theme_source, which controls if the + current set of colors are from a builtin or custom theme. + DynOptionMenus builtinlist and customlist contain lists of the + builtin and custom themes, respectively, and the current item + from each list is stored in vars builtin_name and custom_name. + + Function paint_theme_sample() applies the colors from the theme + to the tags in text widget highlight_sample and then invokes + set_color_sample(). Function set_highlight_target() sets the state + of the radiobuttons fg_on and bg_on based on the tag and it also + invokes set_color_sample(). + + Function set_color_sample() sets the background color for the frame + holding the color selector. This provides a larger visual of the + color for the current tag and plane (foreground/background). + + Note: set_color_sample() is called from many places and is often + called more than once when a change is made. It is invoked when + foreground or background is selected (radiobuttons), from + paint_theme_sample() (theme is changed or load_cfg is called), and + from set_highlight_target() (target tag is changed or load_cfg called). + + Button delete_custom invokes delete_custom() to delete + a custom theme from idleConf.userCfg['highlight'] and changes. + Button save_custom invokes save_as_new_theme() which calls + get_new_theme_name() and create_new() to save a custom theme + and its colors to idleConf.userCfg['highlight']. + + Radiobuttons fg_on and bg_on toggle var fg_bg_toggle to control + if the current selected color for a tag is for the foreground or + background. + + DynOptionMenu targetlist contains a readable description of the + tags applied to Python source within IDLE. Selecting one of the + tags from this list populates highlight_target, which has a callback + function set_highlight_target(). + + Text widget highlight_sample displays a block of text (which is + mock Python code) in which is embedded the defined tags and reflects + the color attributes of the current theme and changes for those tags. + Mouse button 1 allows for selection of a tag and updates + highlight_target with that tag value. + + Note: The font in highlight_sample is set through the config in + the fonts tab. + + In other words, a tag can be selected either from targetlist or + by clicking on the sample text within highlight_sample. The + plane (foreground/background) is selected via the radiobutton. + Together, these two (tag and plane) control what color is + shown in set_color_sample() for the current theme. Button set_color + invokes get_color() which displays a ColorChooser to change the + color for the selected tag/plane. If a new color is picked, + it will be saved to changes and the highlight_sample and + frame background will be updated. + + Tk Variables: + color: Color of selected target. + builtin_name: Menu variable for built-in theme. + custom_name: Menu variable for custom theme. + fg_bg_toggle: Toggle for foreground/background color. + Note: this has no callback. + theme_source: Selector for built-in or custom theme. + highlight_target: Menu variable for the highlight tag target. + + Instance Data Attributes: + theme_elements: Dictionary of tags for text highlighting. + The key is the display name and the value is a tuple of + (tag name, display sort order). + + Methods [attachment]: + load_theme_cfg: Load current highlight colors. + get_color: Invoke colorchooser [button_set_color]. + set_color_sample_binding: Call set_color_sample [fg_bg_toggle]. + set_highlight_target: set fg_bg_toggle, set_color_sample(). + set_color_sample: Set frame background to target. + on_new_color_set: Set new color and add option. + paint_theme_sample: Recolor sample. + get_new_theme_name: Get from popup. + create_new: Combine theme with changes and save. + save_as_new_theme: Save [button_save_custom]. + set_theme_type: Command for [theme_source]. + delete_custom: Activate default [button_delete_custom]. + save_new: Save to userCfg['theme'] (is function). + + Widgets of highlights page frame: (*) widgets bound to self + frame_custom: LabelFrame + (*)highlight_sample: Text + (*)frame_color_set: Frame + (*)button_set_color: Button + (*)targetlist: DynOptionMenu - highlight_target + frame_fg_bg_toggle: Frame + (*)fg_on: Radiobutton - fg_bg_toggle + (*)bg_on: Radiobutton - fg_bg_toggle + (*)button_save_custom: Button + frame_theme: LabelFrame + theme_type_title: Label + (*)builtin_theme_on: Radiobutton - theme_source + (*)custom_theme_on: Radiobutton - theme_source + (*)builtinlist: DynOptionMenu - builtin_name + (*)customlist: DynOptionMenu - custom_name + (*)button_delete_custom: Button + (*)theme_message: Label + """ + self.theme_elements = { + 'Normal Code or Text': ('normal', '00'), + 'Code Context': ('context', '01'), + 'Python Keywords': ('keyword', '02'), + 'Python Definitions': ('definition', '03'), + 'Python Builtins': ('builtin', '04'), + 'Python Comments': ('comment', '05'), + 'Python Strings': ('string', '06'), + 'Selected Text': ('hilite', '07'), + 'Found Text': ('hit', '08'), + 'Cursor': ('cursor', '09'), + 'Editor Breakpoint': ('break', '10'), + 'Shell Prompt': ('console', '11'), + 'Error Text': ('error', '12'), + 'Shell User Output': ('stdout', '13'), + 'Shell User Exception': ('stderr', '14'), + 'Line Number': ('linenumber', '16'), + } + 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) + + # Create widgets: + # body frame and section frames. + frame_custom = LabelFrame(self, borderwidth=2, relief=GROOVE, + text=' Custom Highlighting ') + frame_theme = LabelFrame(self, borderwidth=2, relief=GROOVE, + text=' Highlighting Theme ') + # frame_custom. + 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): + # event.widget.winfo_top_level().highlight_target.set(elem) + self.highlight_target.set(elem) + text.tag_bind( + self.theme_elements[element][0], '<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) #, command=self.set_highlight_targetBinding + 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) + # frame_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) + # Pack widgets: + # body. + frame_custom.pack(side=LEFT, padx=5, pady=5, expand=TRUE, fill=BOTH) + frame_theme.pack(side=TOP, padx=5, pady=5, fill=X) + # frame_custom. + 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) + # frame_theme. + 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): + """Load current configuration settings for the theme options. + + Based on the theme_source toggle, the theme is set as + either builtin or custom and the initial widget values + reflect the current settings from idleConf. + + Attributes updated: + theme_source: Set from idleConf. + builtinlist: List of default themes from idleConf. + customlist: List of custom themes from idleConf. + custom_theme_on: Disabled if there are no custom themes. + custom_theme: Message with additional information. + targetlist: Create menu from self.theme_elements. + + Methods: + set_theme_type + paint_theme_sample + set_highlight_target + """ + # Set current theme type radiobutton. + self.theme_source.set(idleConf.GetOption( + 'main', 'Theme', 'default', type='bool', default=1)) + # Set current theme. + current_option = idleConf.CurrentTheme() + # Load available theme option menus. + if self.theme_source.get(): # Default theme selected. + 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: # User theme selected. + 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() + # Load theme element option menu. + theme_names = list(self.theme_elements.keys()) + theme_names.sort(key=lambda x: self.theme_elements[x][1]) + self.targetlist.SetMenu(theme_names, theme_names[0]) + self.paint_theme_sample() + self.set_highlight_target() + + def var_changed_builtin_name(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_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): + """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_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): + """Process toggle between builtin and custom theme. + + Update the default toggle value and apply the newly + selected theme type. + """ + 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): + """Set available screen options based on builtin or custom theme. + + Attributes accessed: + theme_source + + Attributes updated: + builtinlist + customlist + button_delete_custom + custom_theme_on + + Called from: + handler for builtin_theme_on and custom_theme_on + delete_custom + create_new + load_theme_cfg + """ + 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): + """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. + + Attributes accessed: + highlight_target + frame_color_set + theme_source + + Attributes updated: + color + + Methods: + get_new_theme_name + create_new + """ + target = self.highlight_target.get() + prev_color = self.style.lookup(self.frame_color_set['style'], + 'background') + rgbTuplet, color_string = tkColorChooser.askcolor( + parent=self, title='Pick new color for : '+target, + initialcolor=prev_color) + if color_string and (color_string != prev_color): + # User didn't cancel and they chose a new color. + if self.theme_source.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(new_theme) + self.color.set(color_string) + else: # Current theme is user defined. + 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()][0] + 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): + """Prompt for new theme name and create the theme. + + Methods: + get_new_theme_name + create_new + """ + 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): + """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. + + Attributes accessed: + builtin_name + custom_name + + Attributes updated: + customlist + theme_source + + Method: + save_new + set_theme_type + """ + 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) + # 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(new_theme_name, new_theme) + # Change GUI over to the 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): + """Set fg/bg toggle and color based on highlight tag target. + + Instance variables accessed: + highlight_target + + Attributes updated: + fg_on + bg_on + fg_bg_toggle + + Methods: + set_color_sample + + Called from: + var_changed_highlight_target + load_theme_cfg + """ + if self.highlight_target.get() == 'Cursor': # bg not possible + self.fg_on.state(('disabled',)) + self.bg_on.state(('disabled',)) + self.fg_bg_toggle.set(1) + else: # Both fg and bg can be set. + 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): + """Change color sample based on foreground/background toggle. + + Methods: + set_color_sample + """ + self.set_color_sample() + + def set_color_sample(self): + """Set the color of the frame background to reflect the selected target. + + Instance variables accessed: + theme_elements + highlight_target + fg_bg_toggle + highlight_sample + + Attributes updated: + frame_color_set + """ + # Set the color sample area. + tag = self.theme_elements[self.highlight_target.get()][0] + 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): + """Apply the theme colors to each element tag in the sample text. + + Instance attributes accessed: + theme_elements + theme_source + builtin_name + custom_name + + Attributes updated: + highlight_sample: Set the tag elements to the theme. + + Methods: + set_color_sample + + Called from: + var_changed_builtin_name + var_changed_custom_name + load_theme_cfg + """ + if self.theme_source.get(): # Default theme + theme = self.builtin_name.get() + else: # User theme + theme = self.custom_name.get() + for element_title in self.theme_elements: + element = self.theme_elements[element_title][0] + colors = idleConf.GetHighlight(theme, element) + if element == 'cursor': # Cursor sample needs special painting. + colors['background'] = idleConf.GetHighlight( + theme, 'normal')['background'] + # Handle any unsaved changes to this theme. + 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): + """Save a newly created theme to idleConf. + + 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 askyesno(self, *args, **kwargs): + # Make testing easier. Could change implementation. + return messagebox.askyesno(*args, **kwargs) + + def delete_custom(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. + + Attributes accessed: + custom_name + + Attributes updated: + custom_theme_on + customlist + theme_source + builtin_name + + Methods: + deactivate_current_config + save_all_changed_extensions + activate_config_changes + set_theme_type + """ + 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() + # Remove theme from changes, config, and file. + changes.delete_section('highlight', theme_name) + # Reload user theme list. + 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]) + # Revert to default theme. + self.theme_source.set(idleConf.defaultCfg['main'].Get('Theme', 'default')) + self.builtin_name.set(idleConf.defaultCfg['main'].Get('Theme', 'name')) + # User can't back out of these changes, they must be applied now. + changes.save_all() + self.cd.save_all_changed_extensions() + self.cd.activate_config_changes() + self.set_theme_type() + + +class KeysPage(Frame): + + def __init__(self, master): + super().__init__(master) + self.cd = master.master + self.create_page_keys() + self.load_key_cfg() + + def create_page_keys(self): + """Return frame of widgets for Keys tab. + + Enable users to provisionally change both individual and sets of + keybindings (shortcut keys). Except for features implemented as + extensions, keybindings are stored in complete sets called + keysets. Built-in keysets in idlelib/config-keys.def are fixed + as far as the dialog is concerned. Any keyset can be used as the + base for a new custom keyset, stored in .idlerc/config-keys.cfg. + + Function load_key_cfg() initializes tk variables and keyset + lists and calls load_keys_list for the current keyset. + Radiobuttons builtin_keyset_on and custom_keyset_on toggle var + keyset_source, which controls if the current set of keybindings + are from a builtin or custom keyset. DynOptionMenus builtinlist + and customlist contain lists of the builtin and custom keysets, + respectively, and the current item from each list is stored in + vars builtin_name and custom_name. + + Button delete_custom_keys invokes delete_custom_keys() to delete + a custom keyset from idleConf.userCfg['keys'] and changes. Button + save_custom_keys invokes save_as_new_key_set() which calls + get_new_keys_name() and create_new_key_set() to save a custom keyset + and its keybindings to idleConf.userCfg['keys']. + + Listbox bindingslist contains all of the keybindings for the + selected keyset. The keybindings are loaded in load_keys_list() + and are pairs of (event, [keys]) where keys can be a list + of one or more key combinations to bind to the same event. + Mouse button 1 click invokes on_bindingslist_select(), which + allows button_new_keys to be clicked. + + So, an item is selected in listbindings, which activates + button_new_keys, and clicking button_new_keys calls function + get_new_keys(). Function get_new_keys() gets the key mappings from the + current keyset for the binding event item that was selected. The + function then displays another dialog, GetKeysDialog, with the + selected binding event and current keys and allows new key sequences + to be entered for that binding event. If the keys aren't + changed, nothing happens. If the keys are changed and the keyset + is a builtin, function get_new_keys_name() will be called + for input of a custom keyset name. If no name is given, then the + change to the keybinding will abort and no updates will be made. If + a custom name is entered in the prompt or if the current keyset was + already custom (and thus didn't require a prompt), then + idleConf.userCfg['keys'] is updated in function create_new_key_set() + with the change to the event binding. The item listing in bindingslist + is updated with the new keys. Var keybinding is also set which invokes + the callback function, var_changed_keybinding, to add the change to + the 'keys' or 'extensions' changes tracker based on the binding type. + + Tk Variables: + keybinding: Action/key bindings. + + Methods: + load_keys_list: Reload active set. + create_new_key_set: Combine active keyset and changes. + set_keys_type: Command for keyset_source. + save_new_key_set: Save to idleConf.userCfg['keys'] (is function). + deactivate_current_config: Remove keys bindings in editors. + + Widgets for KeysPage(frame): (*) widgets bound to self + frame_key_sets: LabelFrame + frames[0]: Frame + (*)builtin_keyset_on: Radiobutton - var keyset_source + (*)custom_keyset_on: Radiobutton - var keyset_source + (*)builtinlist: DynOptionMenu - var builtin_name, + func keybinding_selected + (*)customlist: DynOptionMenu - var custom_name, + func keybinding_selected + (*)keys_message: Label + frames[1]: Frame + (*)button_delete_custom_keys: Button - delete_custom_keys + (*)button_save_custom_keys: Button - save_as_new_key_set + frame_custom: LabelFrame + frame_target: Frame + target_title: Label + scroll_target_y: Scrollbar + scroll_target_x: Scrollbar + (*)bindingslist: ListBox - on_bindingslist_select + (*)button_new_keys: Button - get_new_keys & ..._name + """ + 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) + + # Create widgets: + # body and section frames. + 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_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.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') + # frame_key_sets. + 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) + + # Pack widgets: + # 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.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) + # frame_key_sets. + 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." + # Set current keys type radiobutton. + self.keyset_source.set(idleConf.GetOption( + 'main', 'Keys', 'default', type='bool', default=1)) + # Set current keys. + current_option = idleConf.CurrentKeys() + # Load available keyset option menus. + if self.keyset_source.get(): # Default theme selected. + 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: # User key set selected. + 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() + # Load keyset element list. + 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: # 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 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): + """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.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']: # 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.keyset_source.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.bindingslist.select_set(list_index) + self.bindingslist.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.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): + """Create a new custom key set with the given name. + + Copy the bindings/keys from the previously active keyset + to the new keyset and activate the new custom keyset. + """ + 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: # 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.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): + """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 = False + if self.bindingslist.curselection(): + reselect = True + list_index = self.bindingslist.index(ANCHOR) + keyset = idleConf.GetKeySet(keyset_name) + bind_names = list(keyset.keys()) + 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] # 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.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): + """Save a newly created core key set. + + Add keyset to idleConf.userCfg['keys'], not to disk. + If the keyset doesn't exist, it is created. The + binding/keys are taken from the keyset argument. + + keyset_name - string, the name of the new key set + keyset - dictionary containing the new keybindings + """ + 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 askyesno(self, *args, **kwargs): + # Make testing easier. Could change implementation. + return messagebox.askyesno(*args, **kwargs) + + 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_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() + # Remove key set from changes, config, and file. + changes.delete_section('keys', keyset_name) + # Reload user key set list. + 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]) + # Revert to default key set. + self.keyset_source.set(idleConf.defaultCfg['main'] + .Get('Keys', 'default')) + self.builtin_name.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.cd.save_all_changed_extensions() + self.cd.activate_config_changes() + self.set_keys_type() + + +class GenPage(Frame): + + def __init__(self, master): + super().__init__(master) + + self.init_validators() + self.create_page_general() + self.load_general_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_general(self): + """Return frame of widgets for General tab. + + Enable users to provisionally change general options. Function + load_general_cfg initializes tk variables and helplist using + idleConf. Radiobuttons startup_shell_on and startup_editor_on + set var startup_edit. Radiobuttons save_ask_on and save_auto_on + set var autosave. Entry boxes win_width_int and win_height_int + set var win_width and win_height. Setting var_name invokes the + default callback that adds option to changes. + + Helplist: load_general_cfg loads list user_helplist with + name, position pairs and copies names to listbox helplist. + Clicking a name invokes help_source selected. Clicking + button_helplist_name invokes helplist_item_name, which also + changes user_helplist. These functions all call + set_add_delete_state. All but load call update_help_changes to + rewrite changes['main']['HelpFiles']. + + Widgets for GenPage(Frame): (*) widgets bound to self + frame_window: LabelFrame + frame_run: Frame + startup_title: Label + (*)startup_editor_on: Radiobutton - startup_edit + (*)startup_shell_on: Radiobutton - startup_edit + frame_win_size: Frame + win_size_title: Label + win_width_title: Label + (*)win_width_int: Entry - win_width + win_height_title: Label + (*)win_height_int: Entry - win_height + frame_cursor_blink: Frame + cursor_blink_title: Label + (*)cursor_blink_bool: Checkbutton - cursor_blink + frame_autocomplete: Frame + auto_wait_title: Label + (*)auto_wait_int: Entry - autocomplete_wait + frame_paren1: Frame + paren_style_title: Label + (*)paren_style_type: OptionMenu - paren_style + frame_paren2: Frame + paren_time_title: Label + (*)paren_flash_time: Entry - flash_delay + (*)bell_on: Checkbutton - paren_bell + frame_editor: LabelFrame + frame_save: Frame + run_save_title: Label + (*)save_ask_on: Radiobutton - autosave + (*)save_auto_on: Radiobutton - autosave + frame_format: Frame + format_width_title: Label + (*)format_width_int: Entry - format_width + frame_line_numbers_default: Frame + line_numbers_default_title: Label + (*)line_numbers_default_bool: Checkbutton - line_numbers_default + frame_context: Frame + context_title: Label + (*)context_int: Entry - context_lines + frame_shell: LabelFrame + frame_auto_squeeze_min_lines: Frame + auto_squeeze_min_lines_title: Label + (*)auto_squeeze_min_lines_int: Entry - auto_squeeze_min_lines + frame_help: LabelFrame + frame_helplist: Frame + frame_helplist_buttons: Frame + (*)button_helplist_edit + (*)button_helplist_add + (*)button_helplist_remove + (*)helplist: ListBox + scroll_helplist: Scrollbar + """ + # Integer values need StringVar because int('') raises. + 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.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.auto_squeeze_min_lines = tracers.add( + StringVar(self), ('main', 'PyShell', 'auto-squeeze-min-lines')) + + self.autosave = tracers.add( + IntVar(self), ('main', 'General', 'autosave')) + self.format_width = tracers.add( + StringVar(self), ('extensions', 'FormatParagraph', 'max-width')) + self.line_numbers_default = tracers.add( + BooleanVar(self), + ('main', 'EditorWindow', 'line-numbers-default')) + self.context_lines = tracers.add( + StringVar(self), ('extensions', 'CodeContext', 'maxlines')) + + # Create widgets: + # Section frames. + frame_window = LabelFrame(self, borderwidth=2, relief=GROOVE, + text=' Window Preferences') + frame_editor = LabelFrame(self, borderwidth=2, relief=GROOVE, + text=' Editor Preferences') + frame_shell = LabelFrame(self, borderwidth=2, relief=GROOVE, + text=' Shell Preferences') + frame_help = LabelFrame(self, borderwidth=2, relief=GROOVE, + text=' Additional Help Sources ') + # Frame_window. + 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_blink = Frame(frame_window, borderwidth=0) + cursor_blink_title = Label(frame_cursor_blink, text='Cursor Blink') + self.cursor_blink_bool = Checkbutton(frame_cursor_blink, + variable=self.cursor_blink, width=1) + + 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, width=6, + textvariable=self.autocomplete_wait, + 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) + self.bell_on = Checkbutton( + frame_paren2, text="Bell on Mismatch", variable=self.paren_bell) + + # Frame_editor. + 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_format = Frame(frame_editor, 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_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. + 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_help. + frame_helplist = Frame(frame_help) + frame_helplist_buttons = Frame(frame_helplist) + 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) + 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) + + # Pack widgets: + # Body. + frame_window.pack(side=TOP, padx=5, pady=5, expand=TRUE, fill=BOTH) + frame_editor.pack(side=TOP, padx=5, pady=5, expand=TRUE, fill=BOTH) + frame_shell.pack(side=TOP, padx=5, pady=5, expand=TRUE, fill=BOTH) + frame_help.pack(side=TOP, padx=5, pady=5, expand=TRUE, fill=BOTH) + # frame_run. + 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. + 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_blink. + frame_cursor_blink.pack(side=TOP, padx=5, pady=0, fill=X) + cursor_blink_title.pack(side=LEFT, anchor=W, padx=5, pady=5) + self.cursor_blink_bool.pack(side=LEFT, padx=5, pady=5) + # frame_autocomplete. + 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_paren. + 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_save. + 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_format. + 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) + # frame_line_numbers_default. + 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. + 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) + + # frame_auto_squeeze_min_lines + 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_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.helplist.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) + + def load_general_cfg(self): + "Load current configuration settings for the general options." + # Set variables for all windows. + 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.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')) + + # Set variables for editor windows. + self.autosave.set(idleConf.GetOption( + 'main', 'General', 'autosave', default=0, type='bool')) + self.format_width.set(idleConf.GetOption( + 'extensions', 'FormatParagraph', 'max-width', type='int')) + 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')) + + # Set variables for shell windows. + self.auto_squeeze_min_lines.set(idleConf.GetOption( + 'main', 'PyShell', 'auto-squeeze-min-lines', type='int')) + + # Set additional help sources. + 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() + + 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: # No entries in list. + self.button_helplist_edit.state(('disabled',)) + self.button_helplist_remove.state(('disabled',)) + else: # Some entries. + if self.helplist.curselection(): # There currently is a selection. + self.button_helplist_edit.state(('!disabled',)) + self.button_helplist_remove.state(('!disabled',)) + else: # There currently is not a selection. + self.button_helplist_edit.state(('disabled',)) + self.button_helplist_remove.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) + self.helplist.insert(END, help_source[0]) + self.update_help_changes() + + 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.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() # Selected will be un-selected + + def helplist_item_remove(self): + """Handle remove button for the help list. + + Delete the help list item from config. + """ + 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])) + + +class VarTrace: + """Maintain Tk variables trace state.""" + + def __init__(self): + """Store Tk variables and callbacks. + + untraced: List of tuples (var, callback) + that do not have the callback attached + to the Tk var. + traced: List of tuples (var, callback) where + that callback has been attached to the var. + """ + self.untraced = [] + self.traced = [] + + def clear(self): + "Clear lists (for tests)." + # Call after all tests in a module to avoid memory leaks. + self.untraced.clear() + self.traced.clear() + + def add(self, var, callback): + """Add (var, callback) tuple to untraced list. + + Args: + var: Tk variable instance. + callback: Either function name to be used as a callback + or a tuple with IdleConf config-type, section, and + option names used in the default callback. + + Return: + Tk variable instance. + """ + 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". +''' +} + + +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, borderwidth=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__': + from unittest import main + main('idlelib.idle_test.test_configdialog', verbosity=2, exit=False) + + from idlelib.idle_test.htest import run + run(ConfigDialog)