jpayne@68: """IDLE Configuration Dialog: support user customization of IDLE by GUI jpayne@68: jpayne@68: Customize font faces, sizes, and colorization attributes. Set indentation jpayne@68: defaults. Customize keybindings. Colorization and keybindings can be jpayne@68: saved as user defined sets. Select startup options including shell/editor jpayne@68: and default window size. Define additional help sources. jpayne@68: jpayne@68: Note that tab width in IDLE is currently fixed at eight due to Tk issues. jpayne@68: Refer to comments in EditorWindow autoindent code for details. jpayne@68: jpayne@68: """ jpayne@68: import re jpayne@68: jpayne@68: from tkinter import (Toplevel, Listbox, Text, Scale, Canvas, jpayne@68: StringVar, BooleanVar, IntVar, TRUE, FALSE, jpayne@68: TOP, BOTTOM, RIGHT, LEFT, SOLID, GROOVE, jpayne@68: NONE, BOTH, X, Y, W, E, EW, NS, NSEW, NW, jpayne@68: HORIZONTAL, VERTICAL, ANCHOR, ACTIVE, END) jpayne@68: from tkinter.ttk import (Frame, LabelFrame, Button, Checkbutton, Entry, Label, jpayne@68: OptionMenu, Notebook, Radiobutton, Scrollbar, Style) jpayne@68: import tkinter.colorchooser as tkColorChooser jpayne@68: import tkinter.font as tkFont jpayne@68: from tkinter import messagebox jpayne@68: jpayne@68: from idlelib.config import idleConf, ConfigChanges jpayne@68: from idlelib.config_key import GetKeysDialog jpayne@68: from idlelib.dynoption import DynOptionMenu jpayne@68: from idlelib import macosx jpayne@68: from idlelib.query import SectionName, HelpSource jpayne@68: from idlelib.textview import view_text jpayne@68: from idlelib.autocomplete import AutoComplete jpayne@68: from idlelib.codecontext import CodeContext jpayne@68: from idlelib.parenmatch import ParenMatch jpayne@68: from idlelib.format import FormatParagraph jpayne@68: from idlelib.squeezer import Squeezer jpayne@68: from idlelib.textview import ScrollableTextFrame jpayne@68: jpayne@68: changes = ConfigChanges() jpayne@68: # Reload changed options in the following classes. jpayne@68: reloadables = (AutoComplete, CodeContext, ParenMatch, FormatParagraph, jpayne@68: Squeezer) jpayne@68: jpayne@68: jpayne@68: class ConfigDialog(Toplevel): jpayne@68: """Config dialog for IDLE. jpayne@68: """ jpayne@68: jpayne@68: def __init__(self, parent, title='', *, _htest=False, _utest=False): jpayne@68: """Show the tabbed dialog for user configuration. jpayne@68: jpayne@68: Args: jpayne@68: parent - parent of this dialog jpayne@68: title - string which is the title of this popup dialog jpayne@68: _htest - bool, change box location when running htest jpayne@68: _utest - bool, don't wait_window when running unittest jpayne@68: jpayne@68: Note: Focus set on font page fontlist. jpayne@68: jpayne@68: Methods: jpayne@68: create_widgets jpayne@68: cancel: Bound to DELETE_WINDOW protocol. jpayne@68: """ jpayne@68: Toplevel.__init__(self, parent) jpayne@68: self.parent = parent jpayne@68: if _htest: jpayne@68: parent.instance_dict = {} jpayne@68: if not _utest: jpayne@68: self.withdraw() jpayne@68: jpayne@68: self.configure(borderwidth=5) jpayne@68: self.title(title or 'IDLE Preferences') jpayne@68: x = parent.winfo_rootx() + 20 jpayne@68: y = parent.winfo_rooty() + (30 if not _htest else 150) jpayne@68: self.geometry(f'+{x}+{y}') jpayne@68: # Each theme element key is its display name. jpayne@68: # The first value of the tuple is the sample area tag name. jpayne@68: # The second value is the display name list sort index. jpayne@68: self.create_widgets() jpayne@68: self.resizable(height=FALSE, width=FALSE) jpayne@68: self.transient(parent) jpayne@68: self.protocol("WM_DELETE_WINDOW", self.cancel) jpayne@68: self.fontpage.fontlist.focus_set() jpayne@68: # XXX Decide whether to keep or delete these key bindings. jpayne@68: # Key bindings for this dialog. jpayne@68: # self.bind('', self.Cancel) #dismiss dialog, no save jpayne@68: # self.bind('', self.Apply) #apply changes, save jpayne@68: # self.bind('', self.Help) #context help jpayne@68: # Attach callbacks after loading config to avoid calling them. jpayne@68: tracers.attach() jpayne@68: jpayne@68: if not _utest: jpayne@68: self.grab_set() jpayne@68: self.wm_deiconify() jpayne@68: self.wait_window() jpayne@68: jpayne@68: def create_widgets(self): jpayne@68: """Create and place widgets for tabbed dialog. jpayne@68: jpayne@68: Widgets Bound to self: jpayne@68: note: Notebook jpayne@68: highpage: HighPage jpayne@68: fontpage: FontPage jpayne@68: keyspage: KeysPage jpayne@68: genpage: GenPage jpayne@68: extpage: self.create_page_extensions jpayne@68: jpayne@68: Methods: jpayne@68: create_action_buttons jpayne@68: load_configs: Load pages except for extensions. jpayne@68: activate_config_changes: Tell editors to reload. jpayne@68: """ jpayne@68: self.note = note = Notebook(self) jpayne@68: self.highpage = HighPage(note) jpayne@68: self.fontpage = FontPage(note, self.highpage) jpayne@68: self.keyspage = KeysPage(note) jpayne@68: self.genpage = GenPage(note) jpayne@68: self.extpage = self.create_page_extensions() jpayne@68: note.add(self.fontpage, text='Fonts/Tabs') jpayne@68: note.add(self.highpage, text='Highlights') jpayne@68: note.add(self.keyspage, text=' Keys ') jpayne@68: note.add(self.genpage, text=' General ') jpayne@68: note.add(self.extpage, text='Extensions') jpayne@68: note.enable_traversal() jpayne@68: note.pack(side=TOP, expand=TRUE, fill=BOTH) jpayne@68: self.create_action_buttons().pack(side=BOTTOM) jpayne@68: jpayne@68: def create_action_buttons(self): jpayne@68: """Return frame of action buttons for dialog. jpayne@68: jpayne@68: Methods: jpayne@68: ok jpayne@68: apply jpayne@68: cancel jpayne@68: help jpayne@68: jpayne@68: Widget Structure: jpayne@68: outer: Frame jpayne@68: buttons: Frame jpayne@68: (no assignment): Button (ok) jpayne@68: (no assignment): Button (apply) jpayne@68: (no assignment): Button (cancel) jpayne@68: (no assignment): Button (help) jpayne@68: (no assignment): Frame jpayne@68: """ jpayne@68: if macosx.isAquaTk(): jpayne@68: # Changing the default padding on OSX results in unreadable jpayne@68: # text in the buttons. jpayne@68: padding_args = {} jpayne@68: else: jpayne@68: padding_args = {'padding': (6, 3)} jpayne@68: outer = Frame(self, padding=2) jpayne@68: buttons = Frame(outer, padding=2) jpayne@68: for txt, cmd in ( jpayne@68: ('Ok', self.ok), jpayne@68: ('Apply', self.apply), jpayne@68: ('Cancel', self.cancel), jpayne@68: ('Help', self.help)): jpayne@68: Button(buttons, text=txt, command=cmd, takefocus=FALSE, jpayne@68: **padding_args).pack(side=LEFT, padx=5) jpayne@68: # Add space above buttons. jpayne@68: Frame(outer, height=2, borderwidth=0).pack(side=TOP) jpayne@68: buttons.pack(side=BOTTOM) jpayne@68: return outer jpayne@68: jpayne@68: def ok(self): jpayne@68: """Apply config changes, then dismiss dialog. jpayne@68: jpayne@68: Methods: jpayne@68: apply jpayne@68: destroy: inherited jpayne@68: """ jpayne@68: self.apply() jpayne@68: self.destroy() jpayne@68: jpayne@68: def apply(self): jpayne@68: """Apply config changes and leave dialog open. jpayne@68: jpayne@68: Methods: jpayne@68: deactivate_current_config jpayne@68: save_all_changed_extensions jpayne@68: activate_config_changes jpayne@68: """ jpayne@68: self.deactivate_current_config() jpayne@68: changes.save_all() jpayne@68: self.save_all_changed_extensions() jpayne@68: self.activate_config_changes() jpayne@68: jpayne@68: def cancel(self): jpayne@68: """Dismiss config dialog. jpayne@68: jpayne@68: Methods: jpayne@68: destroy: inherited jpayne@68: """ jpayne@68: self.destroy() jpayne@68: jpayne@68: def destroy(self): jpayne@68: global font_sample_text jpayne@68: font_sample_text = self.fontpage.font_sample.get('1.0', 'end') jpayne@68: self.grab_release() jpayne@68: super().destroy() jpayne@68: jpayne@68: def help(self): jpayne@68: """Create textview for config dialog help. jpayne@68: jpayne@68: Attributes accessed: jpayne@68: note jpayne@68: jpayne@68: Methods: jpayne@68: view_text: Method from textview module. jpayne@68: """ jpayne@68: page = self.note.tab(self.note.select(), option='text').strip() jpayne@68: view_text(self, title='Help for IDLE preferences', jpayne@68: text=help_common+help_pages.get(page, '')) jpayne@68: jpayne@68: def deactivate_current_config(self): jpayne@68: """Remove current key bindings. jpayne@68: Iterate over window instances defined in parent and remove jpayne@68: the keybindings. jpayne@68: """ jpayne@68: # Before a config is saved, some cleanup of current jpayne@68: # config must be done - remove the previous keybindings. jpayne@68: win_instances = self.parent.instance_dict.keys() jpayne@68: for instance in win_instances: jpayne@68: instance.RemoveKeybindings() jpayne@68: jpayne@68: def activate_config_changes(self): jpayne@68: """Apply configuration changes to current windows. jpayne@68: jpayne@68: Dynamically update the current parent window instances jpayne@68: with some of the configuration changes. jpayne@68: """ jpayne@68: win_instances = self.parent.instance_dict.keys() jpayne@68: for instance in win_instances: jpayne@68: instance.ResetColorizer() jpayne@68: instance.ResetFont() jpayne@68: instance.set_notabs_indentwidth() jpayne@68: instance.ApplyKeybindings() jpayne@68: instance.reset_help_menu_entries() jpayne@68: instance.update_cursor_blink() jpayne@68: for klass in reloadables: jpayne@68: klass.reload() jpayne@68: jpayne@68: def create_page_extensions(self): jpayne@68: """Part of the config dialog used for configuring IDLE extensions. jpayne@68: jpayne@68: This code is generic - it works for any and all IDLE extensions. jpayne@68: jpayne@68: IDLE extensions save their configuration options using idleConf. jpayne@68: This code reads the current configuration using idleConf, supplies a jpayne@68: GUI interface to change the configuration values, and saves the jpayne@68: changes using idleConf. jpayne@68: jpayne@68: Not all changes take effect immediately - some may require restarting IDLE. jpayne@68: This depends on each extension's implementation. jpayne@68: jpayne@68: All values are treated as text, and it is up to the user to supply jpayne@68: reasonable values. The only exception to this are the 'enable*' options, jpayne@68: which are boolean, and can be toggled with a True/False button. jpayne@68: jpayne@68: Methods: jpayne@68: load_extensions: jpayne@68: extension_selected: Handle selection from list. jpayne@68: create_extension_frame: Hold widgets for one extension. jpayne@68: set_extension_value: Set in userCfg['extensions']. jpayne@68: save_all_changed_extensions: Call extension page Save(). jpayne@68: """ jpayne@68: parent = self.parent jpayne@68: frame = Frame(self.note) jpayne@68: self.ext_defaultCfg = idleConf.defaultCfg['extensions'] jpayne@68: self.ext_userCfg = idleConf.userCfg['extensions'] jpayne@68: self.is_int = self.register(is_int) jpayne@68: self.load_extensions() jpayne@68: # Create widgets - a listbox shows all available extensions, with the jpayne@68: # controls for the extension selected in the listbox to the right. jpayne@68: self.extension_names = StringVar(self) jpayne@68: frame.rowconfigure(0, weight=1) jpayne@68: frame.columnconfigure(2, weight=1) jpayne@68: self.extension_list = Listbox(frame, listvariable=self.extension_names, jpayne@68: selectmode='browse') jpayne@68: self.extension_list.bind('<>', self.extension_selected) jpayne@68: scroll = Scrollbar(frame, command=self.extension_list.yview) jpayne@68: self.extension_list.yscrollcommand=scroll.set jpayne@68: self.details_frame = LabelFrame(frame, width=250, height=250) jpayne@68: self.extension_list.grid(column=0, row=0, sticky='nws') jpayne@68: scroll.grid(column=1, row=0, sticky='ns') jpayne@68: self.details_frame.grid(column=2, row=0, sticky='nsew', padx=[10, 0]) jpayne@68: frame.configure(padding=10) jpayne@68: self.config_frame = {} jpayne@68: self.current_extension = None jpayne@68: jpayne@68: self.outerframe = self # TEMPORARY jpayne@68: self.tabbed_page_set = self.extension_list # TEMPORARY jpayne@68: jpayne@68: # Create the frame holding controls for each extension. jpayne@68: ext_names = '' jpayne@68: for ext_name in sorted(self.extensions): jpayne@68: self.create_extension_frame(ext_name) jpayne@68: ext_names = ext_names + '{' + ext_name + '} ' jpayne@68: self.extension_names.set(ext_names) jpayne@68: self.extension_list.selection_set(0) jpayne@68: self.extension_selected(None) jpayne@68: jpayne@68: return frame jpayne@68: jpayne@68: def load_extensions(self): jpayne@68: "Fill self.extensions with data from the default and user configs." jpayne@68: self.extensions = {} jpayne@68: for ext_name in idleConf.GetExtensions(active_only=False): jpayne@68: # Former built-in extensions are already filtered out. jpayne@68: self.extensions[ext_name] = [] jpayne@68: jpayne@68: for ext_name in self.extensions: jpayne@68: opt_list = sorted(self.ext_defaultCfg.GetOptionList(ext_name)) jpayne@68: jpayne@68: # Bring 'enable' options to the beginning of the list. jpayne@68: enables = [opt_name for opt_name in opt_list jpayne@68: if opt_name.startswith('enable')] jpayne@68: for opt_name in enables: jpayne@68: opt_list.remove(opt_name) jpayne@68: opt_list = enables + opt_list jpayne@68: jpayne@68: for opt_name in opt_list: jpayne@68: def_str = self.ext_defaultCfg.Get( jpayne@68: ext_name, opt_name, raw=True) jpayne@68: try: jpayne@68: def_obj = {'True':True, 'False':False}[def_str] jpayne@68: opt_type = 'bool' jpayne@68: except KeyError: jpayne@68: try: jpayne@68: def_obj = int(def_str) jpayne@68: opt_type = 'int' jpayne@68: except ValueError: jpayne@68: def_obj = def_str jpayne@68: opt_type = None jpayne@68: try: jpayne@68: value = self.ext_userCfg.Get( jpayne@68: ext_name, opt_name, type=opt_type, raw=True, jpayne@68: default=def_obj) jpayne@68: except ValueError: # Need this until .Get fixed. jpayne@68: value = def_obj # Bad values overwritten by entry. jpayne@68: var = StringVar(self) jpayne@68: var.set(str(value)) jpayne@68: jpayne@68: self.extensions[ext_name].append({'name': opt_name, jpayne@68: 'type': opt_type, jpayne@68: 'default': def_str, jpayne@68: 'value': value, jpayne@68: 'var': var, jpayne@68: }) jpayne@68: jpayne@68: def extension_selected(self, event): jpayne@68: "Handle selection of an extension from the list." jpayne@68: newsel = self.extension_list.curselection() jpayne@68: if newsel: jpayne@68: newsel = self.extension_list.get(newsel) jpayne@68: if newsel is None or newsel != self.current_extension: jpayne@68: if self.current_extension: jpayne@68: self.details_frame.config(text='') jpayne@68: self.config_frame[self.current_extension].grid_forget() jpayne@68: self.current_extension = None jpayne@68: if newsel: jpayne@68: self.details_frame.config(text=newsel) jpayne@68: self.config_frame[newsel].grid(column=0, row=0, sticky='nsew') jpayne@68: self.current_extension = newsel jpayne@68: jpayne@68: def create_extension_frame(self, ext_name): jpayne@68: """Create a frame holding the widgets to configure one extension""" jpayne@68: f = VerticalScrolledFrame(self.details_frame, height=250, width=250) jpayne@68: self.config_frame[ext_name] = f jpayne@68: entry_area = f.interior jpayne@68: # Create an entry for each configuration option. jpayne@68: for row, opt in enumerate(self.extensions[ext_name]): jpayne@68: # Create a row with a label and entry/checkbutton. jpayne@68: label = Label(entry_area, text=opt['name']) jpayne@68: label.grid(row=row, column=0, sticky=NW) jpayne@68: var = opt['var'] jpayne@68: if opt['type'] == 'bool': jpayne@68: Checkbutton(entry_area, variable=var, jpayne@68: onvalue='True', offvalue='False', width=8 jpayne@68: ).grid(row=row, column=1, sticky=W, padx=7) jpayne@68: elif opt['type'] == 'int': jpayne@68: Entry(entry_area, textvariable=var, validate='key', jpayne@68: validatecommand=(self.is_int, '%P'), width=10 jpayne@68: ).grid(row=row, column=1, sticky=NSEW, padx=7) jpayne@68: jpayne@68: else: # type == 'str' jpayne@68: # Limit size to fit non-expanding space with larger font. jpayne@68: Entry(entry_area, textvariable=var, width=15 jpayne@68: ).grid(row=row, column=1, sticky=NSEW, padx=7) jpayne@68: return jpayne@68: jpayne@68: def set_extension_value(self, section, opt): jpayne@68: """Return True if the configuration was added or changed. jpayne@68: jpayne@68: If the value is the same as the default, then remove it jpayne@68: from user config file. jpayne@68: """ jpayne@68: name = opt['name'] jpayne@68: default = opt['default'] jpayne@68: value = opt['var'].get().strip() or default jpayne@68: opt['var'].set(value) jpayne@68: # if self.defaultCfg.has_section(section): jpayne@68: # Currently, always true; if not, indent to return. jpayne@68: if (value == default): jpayne@68: return self.ext_userCfg.RemoveOption(section, name) jpayne@68: # Set the option. jpayne@68: return self.ext_userCfg.SetOption(section, name, value) jpayne@68: jpayne@68: def save_all_changed_extensions(self): jpayne@68: """Save configuration changes to the user config file. jpayne@68: jpayne@68: Attributes accessed: jpayne@68: extensions jpayne@68: jpayne@68: Methods: jpayne@68: set_extension_value jpayne@68: """ jpayne@68: has_changes = False jpayne@68: for ext_name in self.extensions: jpayne@68: options = self.extensions[ext_name] jpayne@68: for opt in options: jpayne@68: if self.set_extension_value(ext_name, opt): jpayne@68: has_changes = True jpayne@68: if has_changes: jpayne@68: self.ext_userCfg.Save() jpayne@68: jpayne@68: jpayne@68: # class TabPage(Frame): # A template for Page classes. jpayne@68: # def __init__(self, master): jpayne@68: # super().__init__(master) jpayne@68: # self.create_page_tab() jpayne@68: # self.load_tab_cfg() jpayne@68: # def create_page_tab(self): jpayne@68: # # Define tk vars and register var and callback with tracers. jpayne@68: # # Create subframes and widgets. jpayne@68: # # Pack widgets. jpayne@68: # def load_tab_cfg(self): jpayne@68: # # Initialize widgets with data from idleConf. jpayne@68: # def var_changed_var_name(): jpayne@68: # # For each tk var that needs other than default callback. jpayne@68: # def other_methods(): jpayne@68: # # Define tab-specific behavior. jpayne@68: jpayne@68: font_sample_text = ( jpayne@68: '\n' jpayne@68: 'AaBbCcDdEeFfGgHhIiJj\n1234567890#:+=(){}[]\n' jpayne@68: '\u00a2\u00a3\u00a5\u00a7\u00a9\u00ab\u00ae\u00b6\u00bd\u011e' jpayne@68: '\u00c0\u00c1\u00c2\u00c3\u00c4\u00c5\u00c7\u00d0\u00d8\u00df\n' jpayne@68: '\n\n' jpayne@68: '\u0250\u0255\u0258\u025e\u025f\u0264\u026b\u026e\u0270\u0277' jpayne@68: '\u027b\u0281\u0283\u0286\u028e\u029e\u02a2\u02ab\u02ad\u02af\n' jpayne@68: '\u0391\u03b1\u0392\u03b2\u0393\u03b3\u0394\u03b4\u0395\u03b5' jpayne@68: '\u0396\u03b6\u0397\u03b7\u0398\u03b8\u0399\u03b9\u039a\u03ba\n' jpayne@68: '\u0411\u0431\u0414\u0434\u0416\u0436\u041f\u043f\u0424\u0444' jpayne@68: '\u0427\u0447\u042a\u044a\u042d\u044d\u0460\u0464\u046c\u04dc\n' jpayne@68: '\n\n' jpayne@68: '\u05d0\u05d1\u05d2\u05d3\u05d4\u05d5\u05d6\u05d7\u05d8\u05d9' jpayne@68: '\u05da\u05db\u05dc\u05dd\u05de\u05df\u05e0\u05e1\u05e2\u05e3\n' jpayne@68: '\u0627\u0628\u062c\u062f\u0647\u0648\u0632\u062d\u0637\u064a' jpayne@68: '\u0660\u0661\u0662\u0663\u0664\u0665\u0666\u0667\u0668\u0669\n' jpayne@68: '\n\n' jpayne@68: '\u0966\u0967\u0968\u0969\u096a\u096b\u096c\u096d\u096e\u096f' jpayne@68: '\u0905\u0906\u0907\u0908\u0909\u090a\u090f\u0910\u0913\u0914\n' jpayne@68: '\u0be6\u0be7\u0be8\u0be9\u0bea\u0beb\u0bec\u0bed\u0bee\u0bef' jpayne@68: '\u0b85\u0b87\u0b89\u0b8e\n' jpayne@68: '\n\n' jpayne@68: '\u3007\u4e00\u4e8c\u4e09\u56db\u4e94\u516d\u4e03\u516b\u4e5d\n' jpayne@68: '\u6c49\u5b57\u6f22\u5b57\u4eba\u6728\u706b\u571f\u91d1\u6c34\n' jpayne@68: '\uac00\ub0d0\ub354\ub824\ubaa8\ubd64\uc218\uc720\uc988\uce58\n' jpayne@68: '\u3042\u3044\u3046\u3048\u304a\u30a2\u30a4\u30a6\u30a8\u30aa\n' jpayne@68: ) jpayne@68: jpayne@68: jpayne@68: class FontPage(Frame): jpayne@68: jpayne@68: def __init__(self, master, highpage): jpayne@68: super().__init__(master) jpayne@68: self.highlight_sample = highpage.highlight_sample jpayne@68: self.create_page_font_tab() jpayne@68: self.load_font_cfg() jpayne@68: self.load_tab_cfg() jpayne@68: jpayne@68: def create_page_font_tab(self): jpayne@68: """Return frame of widgets for Font/Tabs tab. jpayne@68: jpayne@68: Fonts: Enable users to provisionally change font face, size, or jpayne@68: boldness and to see the consequence of proposed choices. Each jpayne@68: action set 3 options in changes structuree and changes the jpayne@68: corresponding aspect of the font sample on this page and jpayne@68: highlight sample on highlight page. jpayne@68: jpayne@68: Function load_font_cfg initializes font vars and widgets from jpayne@68: idleConf entries and tk. jpayne@68: jpayne@68: Fontlist: mouse button 1 click or up or down key invoke jpayne@68: on_fontlist_select(), which sets var font_name. jpayne@68: jpayne@68: Sizelist: clicking the menubutton opens the dropdown menu. A jpayne@68: mouse button 1 click or return key sets var font_size. jpayne@68: jpayne@68: Bold_toggle: clicking the box toggles var font_bold. jpayne@68: jpayne@68: Changing any of the font vars invokes var_changed_font, which jpayne@68: adds all 3 font options to changes and calls set_samples. jpayne@68: Set_samples applies a new font constructed from the font vars to jpayne@68: font_sample and to highlight_sample on the highlight page. jpayne@68: jpayne@68: Tabs: Enable users to change spaces entered for indent tabs. jpayne@68: Changing indent_scale value with the mouse sets Var space_num, jpayne@68: which invokes the default callback to add an entry to jpayne@68: changes. Load_tab_cfg initializes space_num to default. jpayne@68: jpayne@68: Widgets for FontPage(Frame): (*) widgets bound to self jpayne@68: frame_font: LabelFrame jpayne@68: frame_font_name: Frame jpayne@68: font_name_title: Label jpayne@68: (*)fontlist: ListBox - font_name jpayne@68: scroll_font: Scrollbar jpayne@68: frame_font_param: Frame jpayne@68: font_size_title: Label jpayne@68: (*)sizelist: DynOptionMenu - font_size jpayne@68: (*)bold_toggle: Checkbutton - font_bold jpayne@68: frame_sample: LabelFrame jpayne@68: (*)font_sample: Label jpayne@68: frame_indent: LabelFrame jpayne@68: indent_title: Label jpayne@68: (*)indent_scale: Scale - space_num jpayne@68: """ jpayne@68: self.font_name = tracers.add(StringVar(self), self.var_changed_font) jpayne@68: self.font_size = tracers.add(StringVar(self), self.var_changed_font) jpayne@68: self.font_bold = tracers.add(BooleanVar(self), self.var_changed_font) jpayne@68: self.space_num = tracers.add(IntVar(self), ('main', 'Indent', 'num-spaces')) jpayne@68: jpayne@68: # Define frames and widgets. jpayne@68: frame_font = LabelFrame( jpayne@68: self, borderwidth=2, relief=GROOVE, text=' Shell/Editor Font ') jpayne@68: frame_sample = LabelFrame( jpayne@68: self, borderwidth=2, relief=GROOVE, jpayne@68: text=' Font Sample (Editable) ') jpayne@68: frame_indent = LabelFrame( jpayne@68: self, borderwidth=2, relief=GROOVE, text=' Indentation Width ') jpayne@68: # frame_font. jpayne@68: frame_font_name = Frame(frame_font) jpayne@68: frame_font_param = Frame(frame_font) jpayne@68: font_name_title = Label( jpayne@68: frame_font_name, justify=LEFT, text='Font Face :') jpayne@68: self.fontlist = Listbox(frame_font_name, height=15, jpayne@68: takefocus=True, exportselection=FALSE) jpayne@68: self.fontlist.bind('', self.on_fontlist_select) jpayne@68: self.fontlist.bind('', self.on_fontlist_select) jpayne@68: self.fontlist.bind('', self.on_fontlist_select) jpayne@68: scroll_font = Scrollbar(frame_font_name) jpayne@68: scroll_font.config(command=self.fontlist.yview) jpayne@68: self.fontlist.config(yscrollcommand=scroll_font.set) jpayne@68: font_size_title = Label(frame_font_param, text='Size :') jpayne@68: self.sizelist = DynOptionMenu(frame_font_param, self.font_size, None) jpayne@68: self.bold_toggle = Checkbutton( jpayne@68: frame_font_param, variable=self.font_bold, jpayne@68: onvalue=1, offvalue=0, text='Bold') jpayne@68: # frame_sample. jpayne@68: font_sample_frame = ScrollableTextFrame(frame_sample) jpayne@68: self.font_sample = font_sample_frame.text jpayne@68: self.font_sample.config(wrap=NONE, width=1, height=1) jpayne@68: self.font_sample.insert(END, font_sample_text) jpayne@68: # frame_indent. jpayne@68: indent_title = Label( jpayne@68: frame_indent, justify=LEFT, jpayne@68: text='Python Standard: 4 Spaces!') jpayne@68: self.indent_scale = Scale( jpayne@68: frame_indent, variable=self.space_num, jpayne@68: orient='horizontal', tickinterval=2, from_=2, to=16) jpayne@68: jpayne@68: # Grid and pack widgets: jpayne@68: self.columnconfigure(1, weight=1) jpayne@68: self.rowconfigure(2, weight=1) jpayne@68: frame_font.grid(row=0, column=0, padx=5, pady=5) jpayne@68: frame_sample.grid(row=0, column=1, rowspan=3, padx=5, pady=5, jpayne@68: sticky='nsew') jpayne@68: frame_indent.grid(row=1, column=0, padx=5, pady=5, sticky='ew') jpayne@68: # frame_font. jpayne@68: frame_font_name.pack(side=TOP, padx=5, pady=5, fill=X) jpayne@68: frame_font_param.pack(side=TOP, padx=5, pady=5, fill=X) jpayne@68: font_name_title.pack(side=TOP, anchor=W) jpayne@68: self.fontlist.pack(side=LEFT, expand=TRUE, fill=X) jpayne@68: scroll_font.pack(side=LEFT, fill=Y) jpayne@68: font_size_title.pack(side=LEFT, anchor=W) jpayne@68: self.sizelist.pack(side=LEFT, anchor=W) jpayne@68: self.bold_toggle.pack(side=LEFT, anchor=W, padx=20) jpayne@68: # frame_sample. jpayne@68: font_sample_frame.pack(expand=TRUE, fill=BOTH) jpayne@68: # frame_indent. jpayne@68: indent_title.pack(side=TOP, anchor=W, padx=5) jpayne@68: self.indent_scale.pack(side=TOP, padx=5, fill=X) jpayne@68: jpayne@68: def load_font_cfg(self): jpayne@68: """Load current configuration settings for the font options. jpayne@68: jpayne@68: Retrieve current font with idleConf.GetFont and font families jpayne@68: from tk. Setup fontlist and set font_name. Setup sizelist, jpayne@68: which sets font_size. Set font_bold. Call set_samples. jpayne@68: """ jpayne@68: configured_font = idleConf.GetFont(self, 'main', 'EditorWindow') jpayne@68: font_name = configured_font[0].lower() jpayne@68: font_size = configured_font[1] jpayne@68: font_bold = configured_font[2]=='bold' jpayne@68: jpayne@68: # Set editor font selection list and font_name. jpayne@68: fonts = list(tkFont.families(self)) jpayne@68: fonts.sort() jpayne@68: for font in fonts: jpayne@68: self.fontlist.insert(END, font) jpayne@68: self.font_name.set(font_name) jpayne@68: lc_fonts = [s.lower() for s in fonts] jpayne@68: try: jpayne@68: current_font_index = lc_fonts.index(font_name) jpayne@68: self.fontlist.see(current_font_index) jpayne@68: self.fontlist.select_set(current_font_index) jpayne@68: self.fontlist.select_anchor(current_font_index) jpayne@68: self.fontlist.activate(current_font_index) jpayne@68: except ValueError: jpayne@68: pass jpayne@68: # Set font size dropdown. jpayne@68: self.sizelist.SetMenu(('7', '8', '9', '10', '11', '12', '13', '14', jpayne@68: '16', '18', '20', '22', '25', '29', '34', '40'), jpayne@68: font_size) jpayne@68: # Set font weight. jpayne@68: self.font_bold.set(font_bold) jpayne@68: self.set_samples() jpayne@68: jpayne@68: def var_changed_font(self, *params): jpayne@68: """Store changes to font attributes. jpayne@68: jpayne@68: When one font attribute changes, save them all, as they are jpayne@68: not independent from each other. In particular, when we are jpayne@68: overriding the default font, we need to write out everything. jpayne@68: """ jpayne@68: value = self.font_name.get() jpayne@68: changes.add_option('main', 'EditorWindow', 'font', value) jpayne@68: value = self.font_size.get() jpayne@68: changes.add_option('main', 'EditorWindow', 'font-size', value) jpayne@68: value = self.font_bold.get() jpayne@68: changes.add_option('main', 'EditorWindow', 'font-bold', value) jpayne@68: self.set_samples() jpayne@68: jpayne@68: def on_fontlist_select(self, event): jpayne@68: """Handle selecting a font from the list. jpayne@68: jpayne@68: Event can result from either mouse click or Up or Down key. jpayne@68: Set font_name and example displays to selection. jpayne@68: """ jpayne@68: font = self.fontlist.get( jpayne@68: ACTIVE if event.type.name == 'KeyRelease' else ANCHOR) jpayne@68: self.font_name.set(font.lower()) jpayne@68: jpayne@68: def set_samples(self, event=None): jpayne@68: """Update update both screen samples with the font settings. jpayne@68: jpayne@68: Called on font initialization and change events. jpayne@68: Accesses font_name, font_size, and font_bold Variables. jpayne@68: Updates font_sample and highlight page highlight_sample. jpayne@68: """ jpayne@68: font_name = self.font_name.get() jpayne@68: font_weight = tkFont.BOLD if self.font_bold.get() else tkFont.NORMAL jpayne@68: new_font = (font_name, self.font_size.get(), font_weight) jpayne@68: self.font_sample['font'] = new_font jpayne@68: self.highlight_sample['font'] = new_font jpayne@68: jpayne@68: def load_tab_cfg(self): jpayne@68: """Load current configuration settings for the tab options. jpayne@68: jpayne@68: Attributes updated: jpayne@68: space_num: Set to value from idleConf. jpayne@68: """ jpayne@68: # Set indent sizes. jpayne@68: space_num = idleConf.GetOption( jpayne@68: 'main', 'Indent', 'num-spaces', default=4, type='int') jpayne@68: self.space_num.set(space_num) jpayne@68: jpayne@68: def var_changed_space_num(self, *params): jpayne@68: "Store change to indentation size." jpayne@68: value = self.space_num.get() jpayne@68: changes.add_option('main', 'Indent', 'num-spaces', value) jpayne@68: jpayne@68: jpayne@68: class HighPage(Frame): jpayne@68: jpayne@68: def __init__(self, master): jpayne@68: super().__init__(master) jpayne@68: self.cd = master.master jpayne@68: self.style = Style(master) jpayne@68: self.create_page_highlight() jpayne@68: self.load_theme_cfg() jpayne@68: jpayne@68: def create_page_highlight(self): jpayne@68: """Return frame of widgets for Highlighting tab. jpayne@68: jpayne@68: Enable users to provisionally change foreground and background jpayne@68: colors applied to textual tags. Color mappings are stored in jpayne@68: complete listings called themes. Built-in themes in jpayne@68: idlelib/config-highlight.def are fixed as far as the dialog is jpayne@68: concerned. Any theme can be used as the base for a new custom jpayne@68: theme, stored in .idlerc/config-highlight.cfg. jpayne@68: jpayne@68: Function load_theme_cfg() initializes tk variables and theme jpayne@68: lists and calls paint_theme_sample() and set_highlight_target() jpayne@68: for the current theme. Radiobuttons builtin_theme_on and jpayne@68: custom_theme_on toggle var theme_source, which controls if the jpayne@68: current set of colors are from a builtin or custom theme. jpayne@68: DynOptionMenus builtinlist and customlist contain lists of the jpayne@68: builtin and custom themes, respectively, and the current item jpayne@68: from each list is stored in vars builtin_name and custom_name. jpayne@68: jpayne@68: Function paint_theme_sample() applies the colors from the theme jpayne@68: to the tags in text widget highlight_sample and then invokes jpayne@68: set_color_sample(). Function set_highlight_target() sets the state jpayne@68: of the radiobuttons fg_on and bg_on based on the tag and it also jpayne@68: invokes set_color_sample(). jpayne@68: jpayne@68: Function set_color_sample() sets the background color for the frame jpayne@68: holding the color selector. This provides a larger visual of the jpayne@68: color for the current tag and plane (foreground/background). jpayne@68: jpayne@68: Note: set_color_sample() is called from many places and is often jpayne@68: called more than once when a change is made. It is invoked when jpayne@68: foreground or background is selected (radiobuttons), from jpayne@68: paint_theme_sample() (theme is changed or load_cfg is called), and jpayne@68: from set_highlight_target() (target tag is changed or load_cfg called). jpayne@68: jpayne@68: Button delete_custom invokes delete_custom() to delete jpayne@68: a custom theme from idleConf.userCfg['highlight'] and changes. jpayne@68: Button save_custom invokes save_as_new_theme() which calls jpayne@68: get_new_theme_name() and create_new() to save a custom theme jpayne@68: and its colors to idleConf.userCfg['highlight']. jpayne@68: jpayne@68: Radiobuttons fg_on and bg_on toggle var fg_bg_toggle to control jpayne@68: if the current selected color for a tag is for the foreground or jpayne@68: background. jpayne@68: jpayne@68: DynOptionMenu targetlist contains a readable description of the jpayne@68: tags applied to Python source within IDLE. Selecting one of the jpayne@68: tags from this list populates highlight_target, which has a callback jpayne@68: function set_highlight_target(). jpayne@68: jpayne@68: Text widget highlight_sample displays a block of text (which is jpayne@68: mock Python code) in which is embedded the defined tags and reflects jpayne@68: the color attributes of the current theme and changes for those tags. jpayne@68: Mouse button 1 allows for selection of a tag and updates jpayne@68: highlight_target with that tag value. jpayne@68: jpayne@68: Note: The font in highlight_sample is set through the config in jpayne@68: the fonts tab. jpayne@68: jpayne@68: In other words, a tag can be selected either from targetlist or jpayne@68: by clicking on the sample text within highlight_sample. The jpayne@68: plane (foreground/background) is selected via the radiobutton. jpayne@68: Together, these two (tag and plane) control what color is jpayne@68: shown in set_color_sample() for the current theme. Button set_color jpayne@68: invokes get_color() which displays a ColorChooser to change the jpayne@68: color for the selected tag/plane. If a new color is picked, jpayne@68: it will be saved to changes and the highlight_sample and jpayne@68: frame background will be updated. jpayne@68: jpayne@68: Tk Variables: jpayne@68: color: Color of selected target. jpayne@68: builtin_name: Menu variable for built-in theme. jpayne@68: custom_name: Menu variable for custom theme. jpayne@68: fg_bg_toggle: Toggle for foreground/background color. jpayne@68: Note: this has no callback. jpayne@68: theme_source: Selector for built-in or custom theme. jpayne@68: highlight_target: Menu variable for the highlight tag target. jpayne@68: jpayne@68: Instance Data Attributes: jpayne@68: theme_elements: Dictionary of tags for text highlighting. jpayne@68: The key is the display name and the value is a tuple of jpayne@68: (tag name, display sort order). jpayne@68: jpayne@68: Methods [attachment]: jpayne@68: load_theme_cfg: Load current highlight colors. jpayne@68: get_color: Invoke colorchooser [button_set_color]. jpayne@68: set_color_sample_binding: Call set_color_sample [fg_bg_toggle]. jpayne@68: set_highlight_target: set fg_bg_toggle, set_color_sample(). jpayne@68: set_color_sample: Set frame background to target. jpayne@68: on_new_color_set: Set new color and add option. jpayne@68: paint_theme_sample: Recolor sample. jpayne@68: get_new_theme_name: Get from popup. jpayne@68: create_new: Combine theme with changes and save. jpayne@68: save_as_new_theme: Save [button_save_custom]. jpayne@68: set_theme_type: Command for [theme_source]. jpayne@68: delete_custom: Activate default [button_delete_custom]. jpayne@68: save_new: Save to userCfg['theme'] (is function). jpayne@68: jpayne@68: Widgets of highlights page frame: (*) widgets bound to self jpayne@68: frame_custom: LabelFrame jpayne@68: (*)highlight_sample: Text jpayne@68: (*)frame_color_set: Frame jpayne@68: (*)button_set_color: Button jpayne@68: (*)targetlist: DynOptionMenu - highlight_target jpayne@68: frame_fg_bg_toggle: Frame jpayne@68: (*)fg_on: Radiobutton - fg_bg_toggle jpayne@68: (*)bg_on: Radiobutton - fg_bg_toggle jpayne@68: (*)button_save_custom: Button jpayne@68: frame_theme: LabelFrame jpayne@68: theme_type_title: Label jpayne@68: (*)builtin_theme_on: Radiobutton - theme_source jpayne@68: (*)custom_theme_on: Radiobutton - theme_source jpayne@68: (*)builtinlist: DynOptionMenu - builtin_name jpayne@68: (*)customlist: DynOptionMenu - custom_name jpayne@68: (*)button_delete_custom: Button jpayne@68: (*)theme_message: Label jpayne@68: """ jpayne@68: self.theme_elements = { jpayne@68: 'Normal Code or Text': ('normal', '00'), jpayne@68: 'Code Context': ('context', '01'), jpayne@68: 'Python Keywords': ('keyword', '02'), jpayne@68: 'Python Definitions': ('definition', '03'), jpayne@68: 'Python Builtins': ('builtin', '04'), jpayne@68: 'Python Comments': ('comment', '05'), jpayne@68: 'Python Strings': ('string', '06'), jpayne@68: 'Selected Text': ('hilite', '07'), jpayne@68: 'Found Text': ('hit', '08'), jpayne@68: 'Cursor': ('cursor', '09'), jpayne@68: 'Editor Breakpoint': ('break', '10'), jpayne@68: 'Shell Prompt': ('console', '11'), jpayne@68: 'Error Text': ('error', '12'), jpayne@68: 'Shell User Output': ('stdout', '13'), jpayne@68: 'Shell User Exception': ('stderr', '14'), jpayne@68: 'Line Number': ('linenumber', '16'), jpayne@68: } jpayne@68: self.builtin_name = tracers.add( jpayne@68: StringVar(self), self.var_changed_builtin_name) jpayne@68: self.custom_name = tracers.add( jpayne@68: StringVar(self), self.var_changed_custom_name) jpayne@68: self.fg_bg_toggle = BooleanVar(self) jpayne@68: self.color = tracers.add( jpayne@68: StringVar(self), self.var_changed_color) jpayne@68: self.theme_source = tracers.add( jpayne@68: BooleanVar(self), self.var_changed_theme_source) jpayne@68: self.highlight_target = tracers.add( jpayne@68: StringVar(self), self.var_changed_highlight_target) jpayne@68: jpayne@68: # Create widgets: jpayne@68: # body frame and section frames. jpayne@68: frame_custom = LabelFrame(self, borderwidth=2, relief=GROOVE, jpayne@68: text=' Custom Highlighting ') jpayne@68: frame_theme = LabelFrame(self, borderwidth=2, relief=GROOVE, jpayne@68: text=' Highlighting Theme ') jpayne@68: # frame_custom. jpayne@68: sample_frame = ScrollableTextFrame( jpayne@68: frame_custom, relief=SOLID, borderwidth=1) jpayne@68: text = self.highlight_sample = sample_frame.text jpayne@68: text.configure( jpayne@68: font=('courier', 12, ''), cursor='hand2', width=1, height=1, jpayne@68: takefocus=FALSE, highlightthickness=0, wrap=NONE) jpayne@68: text.bind('', lambda e: 'break') jpayne@68: text.bind('', lambda e: 'break') jpayne@68: string_tags=( jpayne@68: ('# Click selects item.', 'comment'), ('\n', 'normal'), jpayne@68: ('code context section', 'context'), ('\n', 'normal'), jpayne@68: ('| cursor', 'cursor'), ('\n', 'normal'), jpayne@68: ('def', 'keyword'), (' ', 'normal'), jpayne@68: ('func', 'definition'), ('(param):\n ', 'normal'), jpayne@68: ('"Return None."', 'string'), ('\n var0 = ', 'normal'), jpayne@68: ("'string'", 'string'), ('\n var1 = ', 'normal'), jpayne@68: ("'selected'", 'hilite'), ('\n var2 = ', 'normal'), jpayne@68: ("'found'", 'hit'), ('\n var3 = ', 'normal'), jpayne@68: ('list', 'builtin'), ('(', 'normal'), jpayne@68: ('None', 'keyword'), (')\n', 'normal'), jpayne@68: (' breakpoint("line")', 'break'), ('\n\n', 'normal'), jpayne@68: ('>>>', 'console'), (' 3.14**2\n', 'normal'), jpayne@68: ('9.8596', 'stdout'), ('\n', 'normal'), jpayne@68: ('>>>', 'console'), (' pri ', 'normal'), jpayne@68: ('n', 'error'), ('t(\n', 'normal'), jpayne@68: ('SyntaxError', 'stderr'), ('\n', 'normal')) jpayne@68: for string, tag in string_tags: jpayne@68: text.insert(END, string, tag) jpayne@68: n_lines = len(text.get('1.0', END).splitlines()) jpayne@68: for lineno in range(1, n_lines): jpayne@68: text.insert(f'{lineno}.0', jpayne@68: f'{lineno:{len(str(n_lines))}d} ', jpayne@68: 'linenumber') jpayne@68: for element in self.theme_elements: jpayne@68: def tem(event, elem=element): jpayne@68: # event.widget.winfo_top_level().highlight_target.set(elem) jpayne@68: self.highlight_target.set(elem) jpayne@68: text.tag_bind( jpayne@68: self.theme_elements[element][0], '', tem) jpayne@68: text['state'] = 'disabled' jpayne@68: self.style.configure('frame_color_set.TFrame', borderwidth=1, jpayne@68: relief='solid') jpayne@68: self.frame_color_set = Frame(frame_custom, style='frame_color_set.TFrame') jpayne@68: frame_fg_bg_toggle = Frame(frame_custom) jpayne@68: self.button_set_color = Button( jpayne@68: self.frame_color_set, text='Choose Color for :', jpayne@68: command=self.get_color) jpayne@68: self.targetlist = DynOptionMenu( jpayne@68: self.frame_color_set, self.highlight_target, None, jpayne@68: highlightthickness=0) #, command=self.set_highlight_targetBinding jpayne@68: self.fg_on = Radiobutton( jpayne@68: frame_fg_bg_toggle, variable=self.fg_bg_toggle, value=1, jpayne@68: text='Foreground', command=self.set_color_sample_binding) jpayne@68: self.bg_on = Radiobutton( jpayne@68: frame_fg_bg_toggle, variable=self.fg_bg_toggle, value=0, jpayne@68: text='Background', command=self.set_color_sample_binding) jpayne@68: self.fg_bg_toggle.set(1) jpayne@68: self.button_save_custom = Button( jpayne@68: frame_custom, text='Save as New Custom Theme', jpayne@68: command=self.save_as_new_theme) jpayne@68: # frame_theme. jpayne@68: theme_type_title = Label(frame_theme, text='Select : ') jpayne@68: self.builtin_theme_on = Radiobutton( jpayne@68: frame_theme, variable=self.theme_source, value=1, jpayne@68: command=self.set_theme_type, text='a Built-in Theme') jpayne@68: self.custom_theme_on = Radiobutton( jpayne@68: frame_theme, variable=self.theme_source, value=0, jpayne@68: command=self.set_theme_type, text='a Custom Theme') jpayne@68: self.builtinlist = DynOptionMenu( jpayne@68: frame_theme, self.builtin_name, None, command=None) jpayne@68: self.customlist = DynOptionMenu( jpayne@68: frame_theme, self.custom_name, None, command=None) jpayne@68: self.button_delete_custom = Button( jpayne@68: frame_theme, text='Delete Custom Theme', jpayne@68: command=self.delete_custom) jpayne@68: self.theme_message = Label(frame_theme, borderwidth=2) jpayne@68: # Pack widgets: jpayne@68: # body. jpayne@68: frame_custom.pack(side=LEFT, padx=5, pady=5, expand=TRUE, fill=BOTH) jpayne@68: frame_theme.pack(side=TOP, padx=5, pady=5, fill=X) jpayne@68: # frame_custom. jpayne@68: self.frame_color_set.pack(side=TOP, padx=5, pady=5, fill=X) jpayne@68: frame_fg_bg_toggle.pack(side=TOP, padx=5, pady=0) jpayne@68: sample_frame.pack( jpayne@68: side=TOP, padx=5, pady=5, expand=TRUE, fill=BOTH) jpayne@68: self.button_set_color.pack(side=TOP, expand=TRUE, fill=X, padx=8, pady=4) jpayne@68: self.targetlist.pack(side=TOP, expand=TRUE, fill=X, padx=8, pady=3) jpayne@68: self.fg_on.pack(side=LEFT, anchor=E) jpayne@68: self.bg_on.pack(side=RIGHT, anchor=W) jpayne@68: self.button_save_custom.pack(side=BOTTOM, fill=X, padx=5, pady=5) jpayne@68: # frame_theme. jpayne@68: theme_type_title.pack(side=TOP, anchor=W, padx=5, pady=5) jpayne@68: self.builtin_theme_on.pack(side=TOP, anchor=W, padx=5) jpayne@68: self.custom_theme_on.pack(side=TOP, anchor=W, padx=5, pady=2) jpayne@68: self.builtinlist.pack(side=TOP, fill=X, padx=5, pady=5) jpayne@68: self.customlist.pack(side=TOP, fill=X, anchor=W, padx=5, pady=5) jpayne@68: self.button_delete_custom.pack(side=TOP, fill=X, padx=5, pady=5) jpayne@68: self.theme_message.pack(side=TOP, fill=X, pady=5) jpayne@68: jpayne@68: def load_theme_cfg(self): jpayne@68: """Load current configuration settings for the theme options. jpayne@68: jpayne@68: Based on the theme_source toggle, the theme is set as jpayne@68: either builtin or custom and the initial widget values jpayne@68: reflect the current settings from idleConf. jpayne@68: jpayne@68: Attributes updated: jpayne@68: theme_source: Set from idleConf. jpayne@68: builtinlist: List of default themes from idleConf. jpayne@68: customlist: List of custom themes from idleConf. jpayne@68: custom_theme_on: Disabled if there are no custom themes. jpayne@68: custom_theme: Message with additional information. jpayne@68: targetlist: Create menu from self.theme_elements. jpayne@68: jpayne@68: Methods: jpayne@68: set_theme_type jpayne@68: paint_theme_sample jpayne@68: set_highlight_target jpayne@68: """ jpayne@68: # Set current theme type radiobutton. jpayne@68: self.theme_source.set(idleConf.GetOption( jpayne@68: 'main', 'Theme', 'default', type='bool', default=1)) jpayne@68: # Set current theme. jpayne@68: current_option = idleConf.CurrentTheme() jpayne@68: # Load available theme option menus. jpayne@68: if self.theme_source.get(): # Default theme selected. jpayne@68: item_list = idleConf.GetSectionList('default', 'highlight') jpayne@68: item_list.sort() jpayne@68: self.builtinlist.SetMenu(item_list, current_option) jpayne@68: item_list = idleConf.GetSectionList('user', 'highlight') jpayne@68: item_list.sort() jpayne@68: if not item_list: jpayne@68: self.custom_theme_on.state(('disabled',)) jpayne@68: self.custom_name.set('- no custom themes -') jpayne@68: else: jpayne@68: self.customlist.SetMenu(item_list, item_list[0]) jpayne@68: else: # User theme selected. jpayne@68: item_list = idleConf.GetSectionList('user', 'highlight') jpayne@68: item_list.sort() jpayne@68: self.customlist.SetMenu(item_list, current_option) jpayne@68: item_list = idleConf.GetSectionList('default', 'highlight') jpayne@68: item_list.sort() jpayne@68: self.builtinlist.SetMenu(item_list, item_list[0]) jpayne@68: self.set_theme_type() jpayne@68: # Load theme element option menu. jpayne@68: theme_names = list(self.theme_elements.keys()) jpayne@68: theme_names.sort(key=lambda x: self.theme_elements[x][1]) jpayne@68: self.targetlist.SetMenu(theme_names, theme_names[0]) jpayne@68: self.paint_theme_sample() jpayne@68: self.set_highlight_target() jpayne@68: jpayne@68: def var_changed_builtin_name(self, *params): jpayne@68: """Process new builtin theme selection. jpayne@68: jpayne@68: Add the changed theme's name to the changed_items and recreate jpayne@68: the sample with the values from the selected theme. jpayne@68: """ jpayne@68: old_themes = ('IDLE Classic', 'IDLE New') jpayne@68: value = self.builtin_name.get() jpayne@68: if value not in old_themes: jpayne@68: if idleConf.GetOption('main', 'Theme', 'name') not in old_themes: jpayne@68: changes.add_option('main', 'Theme', 'name', old_themes[0]) jpayne@68: changes.add_option('main', 'Theme', 'name2', value) jpayne@68: self.theme_message['text'] = 'New theme, see Help' jpayne@68: else: jpayne@68: changes.add_option('main', 'Theme', 'name', value) jpayne@68: changes.add_option('main', 'Theme', 'name2', '') jpayne@68: self.theme_message['text'] = '' jpayne@68: self.paint_theme_sample() jpayne@68: jpayne@68: def var_changed_custom_name(self, *params): jpayne@68: """Process new custom theme selection. jpayne@68: jpayne@68: If a new custom theme is selected, add the name to the jpayne@68: changed_items and apply the theme to the sample. jpayne@68: """ jpayne@68: value = self.custom_name.get() jpayne@68: if value != '- no custom themes -': jpayne@68: changes.add_option('main', 'Theme', 'name', value) jpayne@68: self.paint_theme_sample() jpayne@68: jpayne@68: def var_changed_theme_source(self, *params): jpayne@68: """Process toggle between builtin and custom theme. jpayne@68: jpayne@68: Update the default toggle value and apply the newly jpayne@68: selected theme type. jpayne@68: """ jpayne@68: value = self.theme_source.get() jpayne@68: changes.add_option('main', 'Theme', 'default', value) jpayne@68: if value: jpayne@68: self.var_changed_builtin_name() jpayne@68: else: jpayne@68: self.var_changed_custom_name() jpayne@68: jpayne@68: def var_changed_color(self, *params): jpayne@68: "Process change to color choice." jpayne@68: self.on_new_color_set() jpayne@68: jpayne@68: def var_changed_highlight_target(self, *params): jpayne@68: "Process selection of new target tag for highlighting." jpayne@68: self.set_highlight_target() jpayne@68: jpayne@68: def set_theme_type(self): jpayne@68: """Set available screen options based on builtin or custom theme. jpayne@68: jpayne@68: Attributes accessed: jpayne@68: theme_source jpayne@68: jpayne@68: Attributes updated: jpayne@68: builtinlist jpayne@68: customlist jpayne@68: button_delete_custom jpayne@68: custom_theme_on jpayne@68: jpayne@68: Called from: jpayne@68: handler for builtin_theme_on and custom_theme_on jpayne@68: delete_custom jpayne@68: create_new jpayne@68: load_theme_cfg jpayne@68: """ jpayne@68: if self.theme_source.get(): jpayne@68: self.builtinlist['state'] = 'normal' jpayne@68: self.customlist['state'] = 'disabled' jpayne@68: self.button_delete_custom.state(('disabled',)) jpayne@68: else: jpayne@68: self.builtinlist['state'] = 'disabled' jpayne@68: self.custom_theme_on.state(('!disabled',)) jpayne@68: self.customlist['state'] = 'normal' jpayne@68: self.button_delete_custom.state(('!disabled',)) jpayne@68: jpayne@68: def get_color(self): jpayne@68: """Handle button to select a new color for the target tag. jpayne@68: jpayne@68: If a new color is selected while using a builtin theme, a jpayne@68: name must be supplied to create a custom theme. jpayne@68: jpayne@68: Attributes accessed: jpayne@68: highlight_target jpayne@68: frame_color_set jpayne@68: theme_source jpayne@68: jpayne@68: Attributes updated: jpayne@68: color jpayne@68: jpayne@68: Methods: jpayne@68: get_new_theme_name jpayne@68: create_new jpayne@68: """ jpayne@68: target = self.highlight_target.get() jpayne@68: prev_color = self.style.lookup(self.frame_color_set['style'], jpayne@68: 'background') jpayne@68: rgbTuplet, color_string = tkColorChooser.askcolor( jpayne@68: parent=self, title='Pick new color for : '+target, jpayne@68: initialcolor=prev_color) jpayne@68: if color_string and (color_string != prev_color): jpayne@68: # User didn't cancel and they chose a new color. jpayne@68: if self.theme_source.get(): # Current theme is a built-in. jpayne@68: message = ('Your changes will be saved as a new Custom Theme. ' jpayne@68: 'Enter a name for your new Custom Theme below.') jpayne@68: new_theme = self.get_new_theme_name(message) jpayne@68: if not new_theme: # User cancelled custom theme creation. jpayne@68: return jpayne@68: else: # Create new custom theme based on previously active theme. jpayne@68: self.create_new(new_theme) jpayne@68: self.color.set(color_string) jpayne@68: else: # Current theme is user defined. jpayne@68: self.color.set(color_string) jpayne@68: jpayne@68: def on_new_color_set(self): jpayne@68: "Display sample of new color selection on the dialog." jpayne@68: new_color = self.color.get() jpayne@68: self.style.configure('frame_color_set.TFrame', background=new_color) jpayne@68: plane = 'foreground' if self.fg_bg_toggle.get() else 'background' jpayne@68: sample_element = self.theme_elements[self.highlight_target.get()][0] jpayne@68: self.highlight_sample.tag_config(sample_element, **{plane: new_color}) jpayne@68: theme = self.custom_name.get() jpayne@68: theme_element = sample_element + '-' + plane jpayne@68: changes.add_option('highlight', theme, theme_element, new_color) jpayne@68: jpayne@68: def get_new_theme_name(self, message): jpayne@68: "Return name of new theme from query popup." jpayne@68: used_names = (idleConf.GetSectionList('user', 'highlight') + jpayne@68: idleConf.GetSectionList('default', 'highlight')) jpayne@68: new_theme = SectionName( jpayne@68: self, 'New Custom Theme', message, used_names).result jpayne@68: return new_theme jpayne@68: jpayne@68: def save_as_new_theme(self): jpayne@68: """Prompt for new theme name and create the theme. jpayne@68: jpayne@68: Methods: jpayne@68: get_new_theme_name jpayne@68: create_new jpayne@68: """ jpayne@68: new_theme_name = self.get_new_theme_name('New Theme Name:') jpayne@68: if new_theme_name: jpayne@68: self.create_new(new_theme_name) jpayne@68: jpayne@68: def create_new(self, new_theme_name): jpayne@68: """Create a new custom theme with the given name. jpayne@68: jpayne@68: Create the new theme based on the previously active theme jpayne@68: with the current changes applied. Once it is saved, then jpayne@68: activate the new theme. jpayne@68: jpayne@68: Attributes accessed: jpayne@68: builtin_name jpayne@68: custom_name jpayne@68: jpayne@68: Attributes updated: jpayne@68: customlist jpayne@68: theme_source jpayne@68: jpayne@68: Method: jpayne@68: save_new jpayne@68: set_theme_type jpayne@68: """ jpayne@68: if self.theme_source.get(): jpayne@68: theme_type = 'default' jpayne@68: theme_name = self.builtin_name.get() jpayne@68: else: jpayne@68: theme_type = 'user' jpayne@68: theme_name = self.custom_name.get() jpayne@68: new_theme = idleConf.GetThemeDict(theme_type, theme_name) jpayne@68: # Apply any of the old theme's unsaved changes to the new theme. jpayne@68: if theme_name in changes['highlight']: jpayne@68: theme_changes = changes['highlight'][theme_name] jpayne@68: for element in theme_changes: jpayne@68: new_theme[element] = theme_changes[element] jpayne@68: # Save the new theme. jpayne@68: self.save_new(new_theme_name, new_theme) jpayne@68: # Change GUI over to the new theme. jpayne@68: custom_theme_list = idleConf.GetSectionList('user', 'highlight') jpayne@68: custom_theme_list.sort() jpayne@68: self.customlist.SetMenu(custom_theme_list, new_theme_name) jpayne@68: self.theme_source.set(0) jpayne@68: self.set_theme_type() jpayne@68: jpayne@68: def set_highlight_target(self): jpayne@68: """Set fg/bg toggle and color based on highlight tag target. jpayne@68: jpayne@68: Instance variables accessed: jpayne@68: highlight_target jpayne@68: jpayne@68: Attributes updated: jpayne@68: fg_on jpayne@68: bg_on jpayne@68: fg_bg_toggle jpayne@68: jpayne@68: Methods: jpayne@68: set_color_sample jpayne@68: jpayne@68: Called from: jpayne@68: var_changed_highlight_target jpayne@68: load_theme_cfg jpayne@68: """ jpayne@68: if self.highlight_target.get() == 'Cursor': # bg not possible jpayne@68: self.fg_on.state(('disabled',)) jpayne@68: self.bg_on.state(('disabled',)) jpayne@68: self.fg_bg_toggle.set(1) jpayne@68: else: # Both fg and bg can be set. jpayne@68: self.fg_on.state(('!disabled',)) jpayne@68: self.bg_on.state(('!disabled',)) jpayne@68: self.fg_bg_toggle.set(1) jpayne@68: self.set_color_sample() jpayne@68: jpayne@68: def set_color_sample_binding(self, *args): jpayne@68: """Change color sample based on foreground/background toggle. jpayne@68: jpayne@68: Methods: jpayne@68: set_color_sample jpayne@68: """ jpayne@68: self.set_color_sample() jpayne@68: jpayne@68: def set_color_sample(self): jpayne@68: """Set the color of the frame background to reflect the selected target. jpayne@68: jpayne@68: Instance variables accessed: jpayne@68: theme_elements jpayne@68: highlight_target jpayne@68: fg_bg_toggle jpayne@68: highlight_sample jpayne@68: jpayne@68: Attributes updated: jpayne@68: frame_color_set jpayne@68: """ jpayne@68: # Set the color sample area. jpayne@68: tag = self.theme_elements[self.highlight_target.get()][0] jpayne@68: plane = 'foreground' if self.fg_bg_toggle.get() else 'background' jpayne@68: color = self.highlight_sample.tag_cget(tag, plane) jpayne@68: self.style.configure('frame_color_set.TFrame', background=color) jpayne@68: jpayne@68: def paint_theme_sample(self): jpayne@68: """Apply the theme colors to each element tag in the sample text. jpayne@68: jpayne@68: Instance attributes accessed: jpayne@68: theme_elements jpayne@68: theme_source jpayne@68: builtin_name jpayne@68: custom_name jpayne@68: jpayne@68: Attributes updated: jpayne@68: highlight_sample: Set the tag elements to the theme. jpayne@68: jpayne@68: Methods: jpayne@68: set_color_sample jpayne@68: jpayne@68: Called from: jpayne@68: var_changed_builtin_name jpayne@68: var_changed_custom_name jpayne@68: load_theme_cfg jpayne@68: """ jpayne@68: if self.theme_source.get(): # Default theme jpayne@68: theme = self.builtin_name.get() jpayne@68: else: # User theme jpayne@68: theme = self.custom_name.get() jpayne@68: for element_title in self.theme_elements: jpayne@68: element = self.theme_elements[element_title][0] jpayne@68: colors = idleConf.GetHighlight(theme, element) jpayne@68: if element == 'cursor': # Cursor sample needs special painting. jpayne@68: colors['background'] = idleConf.GetHighlight( jpayne@68: theme, 'normal')['background'] jpayne@68: # Handle any unsaved changes to this theme. jpayne@68: if theme in changes['highlight']: jpayne@68: theme_dict = changes['highlight'][theme] jpayne@68: if element + '-foreground' in theme_dict: jpayne@68: colors['foreground'] = theme_dict[element + '-foreground'] jpayne@68: if element + '-background' in theme_dict: jpayne@68: colors['background'] = theme_dict[element + '-background'] jpayne@68: self.highlight_sample.tag_config(element, **colors) jpayne@68: self.set_color_sample() jpayne@68: jpayne@68: def save_new(self, theme_name, theme): jpayne@68: """Save a newly created theme to idleConf. jpayne@68: jpayne@68: theme_name - string, the name of the new theme jpayne@68: theme - dictionary containing the new theme jpayne@68: """ jpayne@68: if not idleConf.userCfg['highlight'].has_section(theme_name): jpayne@68: idleConf.userCfg['highlight'].add_section(theme_name) jpayne@68: for element in theme: jpayne@68: value = theme[element] jpayne@68: idleConf.userCfg['highlight'].SetOption(theme_name, element, value) jpayne@68: jpayne@68: def askyesno(self, *args, **kwargs): jpayne@68: # Make testing easier. Could change implementation. jpayne@68: return messagebox.askyesno(*args, **kwargs) jpayne@68: jpayne@68: def delete_custom(self): jpayne@68: """Handle event to delete custom theme. jpayne@68: jpayne@68: The current theme is deactivated and the default theme is jpayne@68: activated. The custom theme is permanently removed from jpayne@68: the config file. jpayne@68: jpayne@68: Attributes accessed: jpayne@68: custom_name jpayne@68: jpayne@68: Attributes updated: jpayne@68: custom_theme_on jpayne@68: customlist jpayne@68: theme_source jpayne@68: builtin_name jpayne@68: jpayne@68: Methods: jpayne@68: deactivate_current_config jpayne@68: save_all_changed_extensions jpayne@68: activate_config_changes jpayne@68: set_theme_type jpayne@68: """ jpayne@68: theme_name = self.custom_name.get() jpayne@68: delmsg = 'Are you sure you wish to delete the theme %r ?' jpayne@68: if not self.askyesno( jpayne@68: 'Delete Theme', delmsg % theme_name, parent=self): jpayne@68: return jpayne@68: self.cd.deactivate_current_config() jpayne@68: # Remove theme from changes, config, and file. jpayne@68: changes.delete_section('highlight', theme_name) jpayne@68: # Reload user theme list. jpayne@68: item_list = idleConf.GetSectionList('user', 'highlight') jpayne@68: item_list.sort() jpayne@68: if not item_list: jpayne@68: self.custom_theme_on.state(('disabled',)) jpayne@68: self.customlist.SetMenu(item_list, '- no custom themes -') jpayne@68: else: jpayne@68: self.customlist.SetMenu(item_list, item_list[0]) jpayne@68: # Revert to default theme. jpayne@68: self.theme_source.set(idleConf.defaultCfg['main'].Get('Theme', 'default')) jpayne@68: self.builtin_name.set(idleConf.defaultCfg['main'].Get('Theme', 'name')) jpayne@68: # User can't back out of these changes, they must be applied now. jpayne@68: changes.save_all() jpayne@68: self.cd.save_all_changed_extensions() jpayne@68: self.cd.activate_config_changes() jpayne@68: self.set_theme_type() jpayne@68: jpayne@68: jpayne@68: class KeysPage(Frame): jpayne@68: jpayne@68: def __init__(self, master): jpayne@68: super().__init__(master) jpayne@68: self.cd = master.master jpayne@68: self.create_page_keys() jpayne@68: self.load_key_cfg() jpayne@68: jpayne@68: def create_page_keys(self): jpayne@68: """Return frame of widgets for Keys tab. jpayne@68: jpayne@68: Enable users to provisionally change both individual and sets of jpayne@68: keybindings (shortcut keys). Except for features implemented as jpayne@68: extensions, keybindings are stored in complete sets called jpayne@68: keysets. Built-in keysets in idlelib/config-keys.def are fixed jpayne@68: as far as the dialog is concerned. Any keyset can be used as the jpayne@68: base for a new custom keyset, stored in .idlerc/config-keys.cfg. jpayne@68: jpayne@68: Function load_key_cfg() initializes tk variables and keyset jpayne@68: lists and calls load_keys_list for the current keyset. jpayne@68: Radiobuttons builtin_keyset_on and custom_keyset_on toggle var jpayne@68: keyset_source, which controls if the current set of keybindings jpayne@68: are from a builtin or custom keyset. DynOptionMenus builtinlist jpayne@68: and customlist contain lists of the builtin and custom keysets, jpayne@68: respectively, and the current item from each list is stored in jpayne@68: vars builtin_name and custom_name. jpayne@68: jpayne@68: Button delete_custom_keys invokes delete_custom_keys() to delete jpayne@68: a custom keyset from idleConf.userCfg['keys'] and changes. Button jpayne@68: save_custom_keys invokes save_as_new_key_set() which calls jpayne@68: get_new_keys_name() and create_new_key_set() to save a custom keyset jpayne@68: and its keybindings to idleConf.userCfg['keys']. jpayne@68: jpayne@68: Listbox bindingslist contains all of the keybindings for the jpayne@68: selected keyset. The keybindings are loaded in load_keys_list() jpayne@68: and are pairs of (event, [keys]) where keys can be a list jpayne@68: of one or more key combinations to bind to the same event. jpayne@68: Mouse button 1 click invokes on_bindingslist_select(), which jpayne@68: allows button_new_keys to be clicked. jpayne@68: jpayne@68: So, an item is selected in listbindings, which activates jpayne@68: button_new_keys, and clicking button_new_keys calls function jpayne@68: get_new_keys(). Function get_new_keys() gets the key mappings from the jpayne@68: current keyset for the binding event item that was selected. The jpayne@68: function then displays another dialog, GetKeysDialog, with the jpayne@68: selected binding event and current keys and allows new key sequences jpayne@68: to be entered for that binding event. If the keys aren't jpayne@68: changed, nothing happens. If the keys are changed and the keyset jpayne@68: is a builtin, function get_new_keys_name() will be called jpayne@68: for input of a custom keyset name. If no name is given, then the jpayne@68: change to the keybinding will abort and no updates will be made. If jpayne@68: a custom name is entered in the prompt or if the current keyset was jpayne@68: already custom (and thus didn't require a prompt), then jpayne@68: idleConf.userCfg['keys'] is updated in function create_new_key_set() jpayne@68: with the change to the event binding. The item listing in bindingslist jpayne@68: is updated with the new keys. Var keybinding is also set which invokes jpayne@68: the callback function, var_changed_keybinding, to add the change to jpayne@68: the 'keys' or 'extensions' changes tracker based on the binding type. jpayne@68: jpayne@68: Tk Variables: jpayne@68: keybinding: Action/key bindings. jpayne@68: jpayne@68: Methods: jpayne@68: load_keys_list: Reload active set. jpayne@68: create_new_key_set: Combine active keyset and changes. jpayne@68: set_keys_type: Command for keyset_source. jpayne@68: save_new_key_set: Save to idleConf.userCfg['keys'] (is function). jpayne@68: deactivate_current_config: Remove keys bindings in editors. jpayne@68: jpayne@68: Widgets for KeysPage(frame): (*) widgets bound to self jpayne@68: frame_key_sets: LabelFrame jpayne@68: frames[0]: Frame jpayne@68: (*)builtin_keyset_on: Radiobutton - var keyset_source jpayne@68: (*)custom_keyset_on: Radiobutton - var keyset_source jpayne@68: (*)builtinlist: DynOptionMenu - var builtin_name, jpayne@68: func keybinding_selected jpayne@68: (*)customlist: DynOptionMenu - var custom_name, jpayne@68: func keybinding_selected jpayne@68: (*)keys_message: Label jpayne@68: frames[1]: Frame jpayne@68: (*)button_delete_custom_keys: Button - delete_custom_keys jpayne@68: (*)button_save_custom_keys: Button - save_as_new_key_set jpayne@68: frame_custom: LabelFrame jpayne@68: frame_target: Frame jpayne@68: target_title: Label jpayne@68: scroll_target_y: Scrollbar jpayne@68: scroll_target_x: Scrollbar jpayne@68: (*)bindingslist: ListBox - on_bindingslist_select jpayne@68: (*)button_new_keys: Button - get_new_keys & ..._name jpayne@68: """ jpayne@68: self.builtin_name = tracers.add( jpayne@68: StringVar(self), self.var_changed_builtin_name) jpayne@68: self.custom_name = tracers.add( jpayne@68: StringVar(self), self.var_changed_custom_name) jpayne@68: self.keyset_source = tracers.add( jpayne@68: BooleanVar(self), self.var_changed_keyset_source) jpayne@68: self.keybinding = tracers.add( jpayne@68: StringVar(self), self.var_changed_keybinding) jpayne@68: jpayne@68: # Create widgets: jpayne@68: # body and section frames. jpayne@68: frame_custom = LabelFrame( jpayne@68: self, borderwidth=2, relief=GROOVE, jpayne@68: text=' Custom Key Bindings ') jpayne@68: frame_key_sets = LabelFrame( jpayne@68: self, borderwidth=2, relief=GROOVE, text=' Key Set ') jpayne@68: # frame_custom. jpayne@68: frame_target = Frame(frame_custom) jpayne@68: target_title = Label(frame_target, text='Action - Key(s)') jpayne@68: scroll_target_y = Scrollbar(frame_target) jpayne@68: scroll_target_x = Scrollbar(frame_target, orient=HORIZONTAL) jpayne@68: self.bindingslist = Listbox( jpayne@68: frame_target, takefocus=FALSE, exportselection=FALSE) jpayne@68: self.bindingslist.bind('', jpayne@68: self.on_bindingslist_select) jpayne@68: scroll_target_y['command'] = self.bindingslist.yview jpayne@68: scroll_target_x['command'] = self.bindingslist.xview jpayne@68: self.bindingslist['yscrollcommand'] = scroll_target_y.set jpayne@68: self.bindingslist['xscrollcommand'] = scroll_target_x.set jpayne@68: self.button_new_keys = Button( jpayne@68: frame_custom, text='Get New Keys for Selection', jpayne@68: command=self.get_new_keys, state='disabled') jpayne@68: # frame_key_sets. jpayne@68: frames = [Frame(frame_key_sets, padding=2, borderwidth=0) jpayne@68: for i in range(2)] jpayne@68: self.builtin_keyset_on = Radiobutton( jpayne@68: frames[0], variable=self.keyset_source, value=1, jpayne@68: command=self.set_keys_type, text='Use a Built-in Key Set') jpayne@68: self.custom_keyset_on = Radiobutton( jpayne@68: frames[0], variable=self.keyset_source, value=0, jpayne@68: command=self.set_keys_type, text='Use a Custom Key Set') jpayne@68: self.builtinlist = DynOptionMenu( jpayne@68: frames[0], self.builtin_name, None, command=None) jpayne@68: self.customlist = DynOptionMenu( jpayne@68: frames[0], self.custom_name, None, command=None) jpayne@68: self.button_delete_custom_keys = Button( jpayne@68: frames[1], text='Delete Custom Key Set', jpayne@68: command=self.delete_custom_keys) jpayne@68: self.button_save_custom_keys = Button( jpayne@68: frames[1], text='Save as New Custom Key Set', jpayne@68: command=self.save_as_new_key_set) jpayne@68: self.keys_message = Label(frames[0], borderwidth=2) jpayne@68: jpayne@68: # Pack widgets: jpayne@68: # body. jpayne@68: frame_custom.pack(side=BOTTOM, padx=5, pady=5, expand=TRUE, fill=BOTH) jpayne@68: frame_key_sets.pack(side=BOTTOM, padx=5, pady=5, fill=BOTH) jpayne@68: # frame_custom. jpayne@68: self.button_new_keys.pack(side=BOTTOM, fill=X, padx=5, pady=5) jpayne@68: frame_target.pack(side=LEFT, padx=5, pady=5, expand=TRUE, fill=BOTH) jpayne@68: # frame_target. jpayne@68: frame_target.columnconfigure(0, weight=1) jpayne@68: frame_target.rowconfigure(1, weight=1) jpayne@68: target_title.grid(row=0, column=0, columnspan=2, sticky=W) jpayne@68: self.bindingslist.grid(row=1, column=0, sticky=NSEW) jpayne@68: scroll_target_y.grid(row=1, column=1, sticky=NS) jpayne@68: scroll_target_x.grid(row=2, column=0, sticky=EW) jpayne@68: # frame_key_sets. jpayne@68: self.builtin_keyset_on.grid(row=0, column=0, sticky=W+NS) jpayne@68: self.custom_keyset_on.grid(row=1, column=0, sticky=W+NS) jpayne@68: self.builtinlist.grid(row=0, column=1, sticky=NSEW) jpayne@68: self.customlist.grid(row=1, column=1, sticky=NSEW) jpayne@68: self.keys_message.grid(row=0, column=2, sticky=NSEW, padx=5, pady=5) jpayne@68: self.button_delete_custom_keys.pack(side=LEFT, fill=X, expand=True, padx=2) jpayne@68: self.button_save_custom_keys.pack(side=LEFT, fill=X, expand=True, padx=2) jpayne@68: frames[0].pack(side=TOP, fill=BOTH, expand=True) jpayne@68: frames[1].pack(side=TOP, fill=X, expand=True, pady=2) jpayne@68: jpayne@68: def load_key_cfg(self): jpayne@68: "Load current configuration settings for the keybinding options." jpayne@68: # Set current keys type radiobutton. jpayne@68: self.keyset_source.set(idleConf.GetOption( jpayne@68: 'main', 'Keys', 'default', type='bool', default=1)) jpayne@68: # Set current keys. jpayne@68: current_option = idleConf.CurrentKeys() jpayne@68: # Load available keyset option menus. jpayne@68: if self.keyset_source.get(): # Default theme selected. jpayne@68: item_list = idleConf.GetSectionList('default', 'keys') jpayne@68: item_list.sort() jpayne@68: self.builtinlist.SetMenu(item_list, current_option) jpayne@68: item_list = idleConf.GetSectionList('user', 'keys') jpayne@68: item_list.sort() jpayne@68: if not item_list: jpayne@68: self.custom_keyset_on.state(('disabled',)) jpayne@68: self.custom_name.set('- no custom keys -') jpayne@68: else: jpayne@68: self.customlist.SetMenu(item_list, item_list[0]) jpayne@68: else: # User key set selected. jpayne@68: item_list = idleConf.GetSectionList('user', 'keys') jpayne@68: item_list.sort() jpayne@68: self.customlist.SetMenu(item_list, current_option) jpayne@68: item_list = idleConf.GetSectionList('default', 'keys') jpayne@68: item_list.sort() jpayne@68: self.builtinlist.SetMenu(item_list, idleConf.default_keys()) jpayne@68: self.set_keys_type() jpayne@68: # Load keyset element list. jpayne@68: keyset_name = idleConf.CurrentKeys() jpayne@68: self.load_keys_list(keyset_name) jpayne@68: jpayne@68: def var_changed_builtin_name(self, *params): jpayne@68: "Process selection of builtin key set." jpayne@68: old_keys = ( jpayne@68: 'IDLE Classic Windows', jpayne@68: 'IDLE Classic Unix', jpayne@68: 'IDLE Classic Mac', jpayne@68: 'IDLE Classic OSX', jpayne@68: ) jpayne@68: value = self.builtin_name.get() jpayne@68: if value not in old_keys: jpayne@68: if idleConf.GetOption('main', 'Keys', 'name') not in old_keys: jpayne@68: changes.add_option('main', 'Keys', 'name', old_keys[0]) jpayne@68: changes.add_option('main', 'Keys', 'name2', value) jpayne@68: self.keys_message['text'] = 'New key set, see Help' jpayne@68: else: jpayne@68: changes.add_option('main', 'Keys', 'name', value) jpayne@68: changes.add_option('main', 'Keys', 'name2', '') jpayne@68: self.keys_message['text'] = '' jpayne@68: self.load_keys_list(value) jpayne@68: jpayne@68: def var_changed_custom_name(self, *params): jpayne@68: "Process selection of custom key set." jpayne@68: value = self.custom_name.get() jpayne@68: if value != '- no custom keys -': jpayne@68: changes.add_option('main', 'Keys', 'name', value) jpayne@68: self.load_keys_list(value) jpayne@68: jpayne@68: def var_changed_keyset_source(self, *params): jpayne@68: "Process toggle between builtin key set and custom key set." jpayne@68: value = self.keyset_source.get() jpayne@68: changes.add_option('main', 'Keys', 'default', value) jpayne@68: if value: jpayne@68: self.var_changed_builtin_name() jpayne@68: else: jpayne@68: self.var_changed_custom_name() jpayne@68: jpayne@68: def var_changed_keybinding(self, *params): jpayne@68: "Store change to a keybinding." jpayne@68: value = self.keybinding.get() jpayne@68: key_set = self.custom_name.get() jpayne@68: event = self.bindingslist.get(ANCHOR).split()[0] jpayne@68: if idleConf.IsCoreBinding(event): jpayne@68: changes.add_option('keys', key_set, event, value) jpayne@68: else: # Event is an extension binding. jpayne@68: ext_name = idleConf.GetExtnNameForEvent(event) jpayne@68: ext_keybind_section = ext_name + '_cfgBindings' jpayne@68: changes.add_option('extensions', ext_keybind_section, event, value) jpayne@68: jpayne@68: def set_keys_type(self): jpayne@68: "Set available screen options based on builtin or custom key set." jpayne@68: if self.keyset_source.get(): jpayne@68: self.builtinlist['state'] = 'normal' jpayne@68: self.customlist['state'] = 'disabled' jpayne@68: self.button_delete_custom_keys.state(('disabled',)) jpayne@68: else: jpayne@68: self.builtinlist['state'] = 'disabled' jpayne@68: self.custom_keyset_on.state(('!disabled',)) jpayne@68: self.customlist['state'] = 'normal' jpayne@68: self.button_delete_custom_keys.state(('!disabled',)) jpayne@68: jpayne@68: def get_new_keys(self): jpayne@68: """Handle event to change key binding for selected line. jpayne@68: jpayne@68: A selection of a key/binding in the list of current jpayne@68: bindings pops up a dialog to enter a new binding. If jpayne@68: the current key set is builtin and a binding has jpayne@68: changed, then a name for a custom key set needs to be jpayne@68: entered for the change to be applied. jpayne@68: """ jpayne@68: list_index = self.bindingslist.index(ANCHOR) jpayne@68: binding = self.bindingslist.get(list_index) jpayne@68: bind_name = binding.split()[0] jpayne@68: if self.keyset_source.get(): jpayne@68: current_key_set_name = self.builtin_name.get() jpayne@68: else: jpayne@68: current_key_set_name = self.custom_name.get() jpayne@68: current_bindings = idleConf.GetCurrentKeySet() jpayne@68: if current_key_set_name in changes['keys']: # unsaved changes jpayne@68: key_set_changes = changes['keys'][current_key_set_name] jpayne@68: for event in key_set_changes: jpayne@68: current_bindings[event] = key_set_changes[event].split() jpayne@68: current_key_sequences = list(current_bindings.values()) jpayne@68: new_keys = GetKeysDialog(self, 'Get New Keys', bind_name, jpayne@68: current_key_sequences).result jpayne@68: if new_keys: jpayne@68: if self.keyset_source.get(): # Current key set is a built-in. jpayne@68: message = ('Your changes will be saved as a new Custom Key Set.' jpayne@68: ' Enter a name for your new Custom Key Set below.') jpayne@68: new_keyset = self.get_new_keys_name(message) jpayne@68: if not new_keyset: # User cancelled custom key set creation. jpayne@68: self.bindingslist.select_set(list_index) jpayne@68: self.bindingslist.select_anchor(list_index) jpayne@68: return jpayne@68: else: # Create new custom key set based on previously active key set. jpayne@68: self.create_new_key_set(new_keyset) jpayne@68: self.bindingslist.delete(list_index) jpayne@68: self.bindingslist.insert(list_index, bind_name+' - '+new_keys) jpayne@68: self.bindingslist.select_set(list_index) jpayne@68: self.bindingslist.select_anchor(list_index) jpayne@68: self.keybinding.set(new_keys) jpayne@68: else: jpayne@68: self.bindingslist.select_set(list_index) jpayne@68: self.bindingslist.select_anchor(list_index) jpayne@68: jpayne@68: def get_new_keys_name(self, message): jpayne@68: "Return new key set name from query popup." jpayne@68: used_names = (idleConf.GetSectionList('user', 'keys') + jpayne@68: idleConf.GetSectionList('default', 'keys')) jpayne@68: new_keyset = SectionName( jpayne@68: self, 'New Custom Key Set', message, used_names).result jpayne@68: return new_keyset jpayne@68: jpayne@68: def save_as_new_key_set(self): jpayne@68: "Prompt for name of new key set and save changes using that name." jpayne@68: new_keys_name = self.get_new_keys_name('New Key Set Name:') jpayne@68: if new_keys_name: jpayne@68: self.create_new_key_set(new_keys_name) jpayne@68: jpayne@68: def on_bindingslist_select(self, event): jpayne@68: "Activate button to assign new keys to selected action." jpayne@68: self.button_new_keys.state(('!disabled',)) jpayne@68: jpayne@68: def create_new_key_set(self, new_key_set_name): jpayne@68: """Create a new custom key set with the given name. jpayne@68: jpayne@68: Copy the bindings/keys from the previously active keyset jpayne@68: to the new keyset and activate the new custom keyset. jpayne@68: """ jpayne@68: if self.keyset_source.get(): jpayne@68: prev_key_set_name = self.builtin_name.get() jpayne@68: else: jpayne@68: prev_key_set_name = self.custom_name.get() jpayne@68: prev_keys = idleConf.GetCoreKeys(prev_key_set_name) jpayne@68: new_keys = {} jpayne@68: for event in prev_keys: # Add key set to changed items. jpayne@68: event_name = event[2:-2] # Trim off the angle brackets. jpayne@68: binding = ' '.join(prev_keys[event]) jpayne@68: new_keys[event_name] = binding jpayne@68: # Handle any unsaved changes to prev key set. jpayne@68: if prev_key_set_name in changes['keys']: jpayne@68: key_set_changes = changes['keys'][prev_key_set_name] jpayne@68: for event in key_set_changes: jpayne@68: new_keys[event] = key_set_changes[event] jpayne@68: # Save the new key set. jpayne@68: self.save_new_key_set(new_key_set_name, new_keys) jpayne@68: # Change GUI over to the new key set. jpayne@68: custom_key_list = idleConf.GetSectionList('user', 'keys') jpayne@68: custom_key_list.sort() jpayne@68: self.customlist.SetMenu(custom_key_list, new_key_set_name) jpayne@68: self.keyset_source.set(0) jpayne@68: self.set_keys_type() jpayne@68: jpayne@68: def load_keys_list(self, keyset_name): jpayne@68: """Reload the list of action/key binding pairs for the active key set. jpayne@68: jpayne@68: An action/key binding can be selected to change the key binding. jpayne@68: """ jpayne@68: reselect = False jpayne@68: if self.bindingslist.curselection(): jpayne@68: reselect = True jpayne@68: list_index = self.bindingslist.index(ANCHOR) jpayne@68: keyset = idleConf.GetKeySet(keyset_name) jpayne@68: bind_names = list(keyset.keys()) jpayne@68: bind_names.sort() jpayne@68: self.bindingslist.delete(0, END) jpayne@68: for bind_name in bind_names: jpayne@68: key = ' '.join(keyset[bind_name]) jpayne@68: bind_name = bind_name[2:-2] # Trim off the angle brackets. jpayne@68: if keyset_name in changes['keys']: jpayne@68: # Handle any unsaved changes to this key set. jpayne@68: if bind_name in changes['keys'][keyset_name]: jpayne@68: key = changes['keys'][keyset_name][bind_name] jpayne@68: self.bindingslist.insert(END, bind_name+' - '+key) jpayne@68: if reselect: jpayne@68: self.bindingslist.see(list_index) jpayne@68: self.bindingslist.select_set(list_index) jpayne@68: self.bindingslist.select_anchor(list_index) jpayne@68: jpayne@68: @staticmethod jpayne@68: def save_new_key_set(keyset_name, keyset): jpayne@68: """Save a newly created core key set. jpayne@68: jpayne@68: Add keyset to idleConf.userCfg['keys'], not to disk. jpayne@68: If the keyset doesn't exist, it is created. The jpayne@68: binding/keys are taken from the keyset argument. jpayne@68: jpayne@68: keyset_name - string, the name of the new key set jpayne@68: keyset - dictionary containing the new keybindings jpayne@68: """ jpayne@68: if not idleConf.userCfg['keys'].has_section(keyset_name): jpayne@68: idleConf.userCfg['keys'].add_section(keyset_name) jpayne@68: for event in keyset: jpayne@68: value = keyset[event] jpayne@68: idleConf.userCfg['keys'].SetOption(keyset_name, event, value) jpayne@68: jpayne@68: def askyesno(self, *args, **kwargs): jpayne@68: # Make testing easier. Could change implementation. jpayne@68: return messagebox.askyesno(*args, **kwargs) jpayne@68: jpayne@68: def delete_custom_keys(self): jpayne@68: """Handle event to delete a custom key set. jpayne@68: jpayne@68: Applying the delete deactivates the current configuration and jpayne@68: reverts to the default. The custom key set is permanently jpayne@68: deleted from the config file. jpayne@68: """ jpayne@68: keyset_name = self.custom_name.get() jpayne@68: delmsg = 'Are you sure you wish to delete the key set %r ?' jpayne@68: if not self.askyesno( jpayne@68: 'Delete Key Set', delmsg % keyset_name, parent=self): jpayne@68: return jpayne@68: self.cd.deactivate_current_config() jpayne@68: # Remove key set from changes, config, and file. jpayne@68: changes.delete_section('keys', keyset_name) jpayne@68: # Reload user key set list. jpayne@68: item_list = idleConf.GetSectionList('user', 'keys') jpayne@68: item_list.sort() jpayne@68: if not item_list: jpayne@68: self.custom_keyset_on.state(('disabled',)) jpayne@68: self.customlist.SetMenu(item_list, '- no custom keys -') jpayne@68: else: jpayne@68: self.customlist.SetMenu(item_list, item_list[0]) jpayne@68: # Revert to default key set. jpayne@68: self.keyset_source.set(idleConf.defaultCfg['main'] jpayne@68: .Get('Keys', 'default')) jpayne@68: self.builtin_name.set(idleConf.defaultCfg['main'].Get('Keys', 'name') jpayne@68: or idleConf.default_keys()) jpayne@68: # User can't back out of these changes, they must be applied now. jpayne@68: changes.save_all() jpayne@68: self.cd.save_all_changed_extensions() jpayne@68: self.cd.activate_config_changes() jpayne@68: self.set_keys_type() jpayne@68: jpayne@68: jpayne@68: class GenPage(Frame): jpayne@68: jpayne@68: def __init__(self, master): jpayne@68: super().__init__(master) jpayne@68: jpayne@68: self.init_validators() jpayne@68: self.create_page_general() jpayne@68: self.load_general_cfg() jpayne@68: jpayne@68: def init_validators(self): jpayne@68: digits_or_empty_re = re.compile(r'[0-9]*') jpayne@68: def is_digits_or_empty(s): jpayne@68: "Return 's is blank or contains only digits'" jpayne@68: return digits_or_empty_re.fullmatch(s) is not None jpayne@68: self.digits_only = (self.register(is_digits_or_empty), '%P',) jpayne@68: jpayne@68: def create_page_general(self): jpayne@68: """Return frame of widgets for General tab. jpayne@68: jpayne@68: Enable users to provisionally change general options. Function jpayne@68: load_general_cfg initializes tk variables and helplist using jpayne@68: idleConf. Radiobuttons startup_shell_on and startup_editor_on jpayne@68: set var startup_edit. Radiobuttons save_ask_on and save_auto_on jpayne@68: set var autosave. Entry boxes win_width_int and win_height_int jpayne@68: set var win_width and win_height. Setting var_name invokes the jpayne@68: default callback that adds option to changes. jpayne@68: jpayne@68: Helplist: load_general_cfg loads list user_helplist with jpayne@68: name, position pairs and copies names to listbox helplist. jpayne@68: Clicking a name invokes help_source selected. Clicking jpayne@68: button_helplist_name invokes helplist_item_name, which also jpayne@68: changes user_helplist. These functions all call jpayne@68: set_add_delete_state. All but load call update_help_changes to jpayne@68: rewrite changes['main']['HelpFiles']. jpayne@68: jpayne@68: Widgets for GenPage(Frame): (*) widgets bound to self jpayne@68: frame_window: LabelFrame jpayne@68: frame_run: Frame jpayne@68: startup_title: Label jpayne@68: (*)startup_editor_on: Radiobutton - startup_edit jpayne@68: (*)startup_shell_on: Radiobutton - startup_edit jpayne@68: frame_win_size: Frame jpayne@68: win_size_title: Label jpayne@68: win_width_title: Label jpayne@68: (*)win_width_int: Entry - win_width jpayne@68: win_height_title: Label jpayne@68: (*)win_height_int: Entry - win_height jpayne@68: frame_cursor_blink: Frame jpayne@68: cursor_blink_title: Label jpayne@68: (*)cursor_blink_bool: Checkbutton - cursor_blink jpayne@68: frame_autocomplete: Frame jpayne@68: auto_wait_title: Label jpayne@68: (*)auto_wait_int: Entry - autocomplete_wait jpayne@68: frame_paren1: Frame jpayne@68: paren_style_title: Label jpayne@68: (*)paren_style_type: OptionMenu - paren_style jpayne@68: frame_paren2: Frame jpayne@68: paren_time_title: Label jpayne@68: (*)paren_flash_time: Entry - flash_delay jpayne@68: (*)bell_on: Checkbutton - paren_bell jpayne@68: frame_editor: LabelFrame jpayne@68: frame_save: Frame jpayne@68: run_save_title: Label jpayne@68: (*)save_ask_on: Radiobutton - autosave jpayne@68: (*)save_auto_on: Radiobutton - autosave jpayne@68: frame_format: Frame jpayne@68: format_width_title: Label jpayne@68: (*)format_width_int: Entry - format_width jpayne@68: frame_line_numbers_default: Frame jpayne@68: line_numbers_default_title: Label jpayne@68: (*)line_numbers_default_bool: Checkbutton - line_numbers_default jpayne@68: frame_context: Frame jpayne@68: context_title: Label jpayne@68: (*)context_int: Entry - context_lines jpayne@68: frame_shell: LabelFrame jpayne@68: frame_auto_squeeze_min_lines: Frame jpayne@68: auto_squeeze_min_lines_title: Label jpayne@68: (*)auto_squeeze_min_lines_int: Entry - auto_squeeze_min_lines jpayne@68: frame_help: LabelFrame jpayne@68: frame_helplist: Frame jpayne@68: frame_helplist_buttons: Frame jpayne@68: (*)button_helplist_edit jpayne@68: (*)button_helplist_add jpayne@68: (*)button_helplist_remove jpayne@68: (*)helplist: ListBox jpayne@68: scroll_helplist: Scrollbar jpayne@68: """ jpayne@68: # Integer values need StringVar because int('') raises. jpayne@68: self.startup_edit = tracers.add( jpayne@68: IntVar(self), ('main', 'General', 'editor-on-startup')) jpayne@68: self.win_width = tracers.add( jpayne@68: StringVar(self), ('main', 'EditorWindow', 'width')) jpayne@68: self.win_height = tracers.add( jpayne@68: StringVar(self), ('main', 'EditorWindow', 'height')) jpayne@68: self.cursor_blink = tracers.add( jpayne@68: BooleanVar(self), ('main', 'EditorWindow', 'cursor-blink')) jpayne@68: self.autocomplete_wait = tracers.add( jpayne@68: StringVar(self), ('extensions', 'AutoComplete', 'popupwait')) jpayne@68: self.paren_style = tracers.add( jpayne@68: StringVar(self), ('extensions', 'ParenMatch', 'style')) jpayne@68: self.flash_delay = tracers.add( jpayne@68: StringVar(self), ('extensions', 'ParenMatch', 'flash-delay')) jpayne@68: self.paren_bell = tracers.add( jpayne@68: BooleanVar(self), ('extensions', 'ParenMatch', 'bell')) jpayne@68: jpayne@68: self.auto_squeeze_min_lines = tracers.add( jpayne@68: StringVar(self), ('main', 'PyShell', 'auto-squeeze-min-lines')) jpayne@68: jpayne@68: self.autosave = tracers.add( jpayne@68: IntVar(self), ('main', 'General', 'autosave')) jpayne@68: self.format_width = tracers.add( jpayne@68: StringVar(self), ('extensions', 'FormatParagraph', 'max-width')) jpayne@68: self.line_numbers_default = tracers.add( jpayne@68: BooleanVar(self), jpayne@68: ('main', 'EditorWindow', 'line-numbers-default')) jpayne@68: self.context_lines = tracers.add( jpayne@68: StringVar(self), ('extensions', 'CodeContext', 'maxlines')) jpayne@68: jpayne@68: # Create widgets: jpayne@68: # Section frames. jpayne@68: frame_window = LabelFrame(self, borderwidth=2, relief=GROOVE, jpayne@68: text=' Window Preferences') jpayne@68: frame_editor = LabelFrame(self, borderwidth=2, relief=GROOVE, jpayne@68: text=' Editor Preferences') jpayne@68: frame_shell = LabelFrame(self, borderwidth=2, relief=GROOVE, jpayne@68: text=' Shell Preferences') jpayne@68: frame_help = LabelFrame(self, borderwidth=2, relief=GROOVE, jpayne@68: text=' Additional Help Sources ') jpayne@68: # Frame_window. jpayne@68: frame_run = Frame(frame_window, borderwidth=0) jpayne@68: startup_title = Label(frame_run, text='At Startup') jpayne@68: self.startup_editor_on = Radiobutton( jpayne@68: frame_run, variable=self.startup_edit, value=1, jpayne@68: text="Open Edit Window") jpayne@68: self.startup_shell_on = Radiobutton( jpayne@68: frame_run, variable=self.startup_edit, value=0, jpayne@68: text='Open Shell Window') jpayne@68: jpayne@68: frame_win_size = Frame(frame_window, borderwidth=0) jpayne@68: win_size_title = Label( jpayne@68: frame_win_size, text='Initial Window Size (in characters)') jpayne@68: win_width_title = Label(frame_win_size, text='Width') jpayne@68: self.win_width_int = Entry( jpayne@68: frame_win_size, textvariable=self.win_width, width=3, jpayne@68: validatecommand=self.digits_only, validate='key', jpayne@68: ) jpayne@68: win_height_title = Label(frame_win_size, text='Height') jpayne@68: self.win_height_int = Entry( jpayne@68: frame_win_size, textvariable=self.win_height, width=3, jpayne@68: validatecommand=self.digits_only, validate='key', jpayne@68: ) jpayne@68: jpayne@68: frame_cursor_blink = Frame(frame_window, borderwidth=0) jpayne@68: cursor_blink_title = Label(frame_cursor_blink, text='Cursor Blink') jpayne@68: self.cursor_blink_bool = Checkbutton(frame_cursor_blink, jpayne@68: variable=self.cursor_blink, width=1) jpayne@68: jpayne@68: frame_autocomplete = Frame(frame_window, borderwidth=0,) jpayne@68: auto_wait_title = Label(frame_autocomplete, jpayne@68: text='Completions Popup Wait (milliseconds)') jpayne@68: self.auto_wait_int = Entry(frame_autocomplete, width=6, jpayne@68: textvariable=self.autocomplete_wait, jpayne@68: validatecommand=self.digits_only, jpayne@68: validate='key', jpayne@68: ) jpayne@68: jpayne@68: frame_paren1 = Frame(frame_window, borderwidth=0) jpayne@68: paren_style_title = Label(frame_paren1, text='Paren Match Style') jpayne@68: self.paren_style_type = OptionMenu( jpayne@68: frame_paren1, self.paren_style, 'expression', jpayne@68: "opener","parens","expression") jpayne@68: frame_paren2 = Frame(frame_window, borderwidth=0) jpayne@68: paren_time_title = Label( jpayne@68: frame_paren2, text='Time Match Displayed (milliseconds)\n' jpayne@68: '(0 is until next input)') jpayne@68: self.paren_flash_time = Entry( jpayne@68: frame_paren2, textvariable=self.flash_delay, width=6) jpayne@68: self.bell_on = Checkbutton( jpayne@68: frame_paren2, text="Bell on Mismatch", variable=self.paren_bell) jpayne@68: jpayne@68: # Frame_editor. jpayne@68: frame_save = Frame(frame_editor, borderwidth=0) jpayne@68: run_save_title = Label(frame_save, text='At Start of Run (F5) ') jpayne@68: self.save_ask_on = Radiobutton( jpayne@68: frame_save, variable=self.autosave, value=0, jpayne@68: text="Prompt to Save") jpayne@68: self.save_auto_on = Radiobutton( jpayne@68: frame_save, variable=self.autosave, value=1, jpayne@68: text='No Prompt') jpayne@68: jpayne@68: frame_format = Frame(frame_editor, borderwidth=0) jpayne@68: format_width_title = Label(frame_format, jpayne@68: text='Format Paragraph Max Width') jpayne@68: self.format_width_int = Entry( jpayne@68: frame_format, textvariable=self.format_width, width=4, jpayne@68: validatecommand=self.digits_only, validate='key', jpayne@68: ) jpayne@68: jpayne@68: frame_line_numbers_default = Frame(frame_editor, borderwidth=0) jpayne@68: line_numbers_default_title = Label( jpayne@68: frame_line_numbers_default, text='Show line numbers in new windows') jpayne@68: self.line_numbers_default_bool = Checkbutton( jpayne@68: frame_line_numbers_default, jpayne@68: variable=self.line_numbers_default, jpayne@68: width=1) jpayne@68: jpayne@68: frame_context = Frame(frame_editor, borderwidth=0) jpayne@68: context_title = Label(frame_context, text='Max Context Lines :') jpayne@68: self.context_int = Entry( jpayne@68: frame_context, textvariable=self.context_lines, width=3, jpayne@68: validatecommand=self.digits_only, validate='key', jpayne@68: ) jpayne@68: jpayne@68: # Frame_shell. jpayne@68: frame_auto_squeeze_min_lines = Frame(frame_shell, borderwidth=0) jpayne@68: auto_squeeze_min_lines_title = Label(frame_auto_squeeze_min_lines, jpayne@68: text='Auto-Squeeze Min. Lines:') jpayne@68: self.auto_squeeze_min_lines_int = Entry( jpayne@68: frame_auto_squeeze_min_lines, width=4, jpayne@68: textvariable=self.auto_squeeze_min_lines, jpayne@68: validatecommand=self.digits_only, validate='key', jpayne@68: ) jpayne@68: jpayne@68: # frame_help. jpayne@68: frame_helplist = Frame(frame_help) jpayne@68: frame_helplist_buttons = Frame(frame_helplist) jpayne@68: self.helplist = Listbox( jpayne@68: frame_helplist, height=5, takefocus=True, jpayne@68: exportselection=FALSE) jpayne@68: scroll_helplist = Scrollbar(frame_helplist) jpayne@68: scroll_helplist['command'] = self.helplist.yview jpayne@68: self.helplist['yscrollcommand'] = scroll_helplist.set jpayne@68: self.helplist.bind('', self.help_source_selected) jpayne@68: self.button_helplist_edit = Button( jpayne@68: frame_helplist_buttons, text='Edit', state='disabled', jpayne@68: width=8, command=self.helplist_item_edit) jpayne@68: self.button_helplist_add = Button( jpayne@68: frame_helplist_buttons, text='Add', jpayne@68: width=8, command=self.helplist_item_add) jpayne@68: self.button_helplist_remove = Button( jpayne@68: frame_helplist_buttons, text='Remove', state='disabled', jpayne@68: width=8, command=self.helplist_item_remove) jpayne@68: jpayne@68: # Pack widgets: jpayne@68: # Body. jpayne@68: frame_window.pack(side=TOP, padx=5, pady=5, expand=TRUE, fill=BOTH) jpayne@68: frame_editor.pack(side=TOP, padx=5, pady=5, expand=TRUE, fill=BOTH) jpayne@68: frame_shell.pack(side=TOP, padx=5, pady=5, expand=TRUE, fill=BOTH) jpayne@68: frame_help.pack(side=TOP, padx=5, pady=5, expand=TRUE, fill=BOTH) jpayne@68: # frame_run. jpayne@68: frame_run.pack(side=TOP, padx=5, pady=0, fill=X) jpayne@68: startup_title.pack(side=LEFT, anchor=W, padx=5, pady=5) jpayne@68: self.startup_shell_on.pack(side=RIGHT, anchor=W, padx=5, pady=5) jpayne@68: self.startup_editor_on.pack(side=RIGHT, anchor=W, padx=5, pady=5) jpayne@68: # frame_win_size. jpayne@68: frame_win_size.pack(side=TOP, padx=5, pady=0, fill=X) jpayne@68: win_size_title.pack(side=LEFT, anchor=W, padx=5, pady=5) jpayne@68: self.win_height_int.pack(side=RIGHT, anchor=E, padx=10, pady=5) jpayne@68: win_height_title.pack(side=RIGHT, anchor=E, pady=5) jpayne@68: self.win_width_int.pack(side=RIGHT, anchor=E, padx=10, pady=5) jpayne@68: win_width_title.pack(side=RIGHT, anchor=E, pady=5) jpayne@68: # frame_cursor_blink. jpayne@68: frame_cursor_blink.pack(side=TOP, padx=5, pady=0, fill=X) jpayne@68: cursor_blink_title.pack(side=LEFT, anchor=W, padx=5, pady=5) jpayne@68: self.cursor_blink_bool.pack(side=LEFT, padx=5, pady=5) jpayne@68: # frame_autocomplete. jpayne@68: frame_autocomplete.pack(side=TOP, padx=5, pady=0, fill=X) jpayne@68: auto_wait_title.pack(side=LEFT, anchor=W, padx=5, pady=5) jpayne@68: self.auto_wait_int.pack(side=TOP, padx=10, pady=5) jpayne@68: # frame_paren. jpayne@68: frame_paren1.pack(side=TOP, padx=5, pady=0, fill=X) jpayne@68: paren_style_title.pack(side=LEFT, anchor=W, padx=5, pady=5) jpayne@68: self.paren_style_type.pack(side=TOP, padx=10, pady=5) jpayne@68: frame_paren2.pack(side=TOP, padx=5, pady=0, fill=X) jpayne@68: paren_time_title.pack(side=LEFT, anchor=W, padx=5) jpayne@68: self.bell_on.pack(side=RIGHT, anchor=E, padx=15, pady=5) jpayne@68: self.paren_flash_time.pack(side=TOP, anchor=W, padx=15, pady=5) jpayne@68: jpayne@68: # frame_save. jpayne@68: frame_save.pack(side=TOP, padx=5, pady=0, fill=X) jpayne@68: run_save_title.pack(side=LEFT, anchor=W, padx=5, pady=5) jpayne@68: self.save_auto_on.pack(side=RIGHT, anchor=W, padx=5, pady=5) jpayne@68: self.save_ask_on.pack(side=RIGHT, anchor=W, padx=5, pady=5) jpayne@68: # frame_format. jpayne@68: frame_format.pack(side=TOP, padx=5, pady=0, fill=X) jpayne@68: format_width_title.pack(side=LEFT, anchor=W, padx=5, pady=5) jpayne@68: self.format_width_int.pack(side=TOP, padx=10, pady=5) jpayne@68: # frame_line_numbers_default. jpayne@68: frame_line_numbers_default.pack(side=TOP, padx=5, pady=0, fill=X) jpayne@68: line_numbers_default_title.pack(side=LEFT, anchor=W, padx=5, pady=5) jpayne@68: self.line_numbers_default_bool.pack(side=LEFT, padx=5, pady=5) jpayne@68: # frame_context. jpayne@68: frame_context.pack(side=TOP, padx=5, pady=0, fill=X) jpayne@68: context_title.pack(side=LEFT, anchor=W, padx=5, pady=5) jpayne@68: self.context_int.pack(side=TOP, padx=5, pady=5) jpayne@68: jpayne@68: # frame_auto_squeeze_min_lines jpayne@68: frame_auto_squeeze_min_lines.pack(side=TOP, padx=5, pady=0, fill=X) jpayne@68: auto_squeeze_min_lines_title.pack(side=LEFT, anchor=W, padx=5, pady=5) jpayne@68: self.auto_squeeze_min_lines_int.pack(side=TOP, padx=5, pady=5) jpayne@68: jpayne@68: # frame_help. jpayne@68: frame_helplist_buttons.pack(side=RIGHT, padx=5, pady=5, fill=Y) jpayne@68: frame_helplist.pack(side=TOP, padx=5, pady=5, expand=TRUE, fill=BOTH) jpayne@68: scroll_helplist.pack(side=RIGHT, anchor=W, fill=Y) jpayne@68: self.helplist.pack(side=LEFT, anchor=E, expand=TRUE, fill=BOTH) jpayne@68: self.button_helplist_edit.pack(side=TOP, anchor=W, pady=5) jpayne@68: self.button_helplist_add.pack(side=TOP, anchor=W) jpayne@68: self.button_helplist_remove.pack(side=TOP, anchor=W, pady=5) jpayne@68: jpayne@68: def load_general_cfg(self): jpayne@68: "Load current configuration settings for the general options." jpayne@68: # Set variables for all windows. jpayne@68: self.startup_edit.set(idleConf.GetOption( jpayne@68: 'main', 'General', 'editor-on-startup', type='bool')) jpayne@68: self.win_width.set(idleConf.GetOption( jpayne@68: 'main', 'EditorWindow', 'width', type='int')) jpayne@68: self.win_height.set(idleConf.GetOption( jpayne@68: 'main', 'EditorWindow', 'height', type='int')) jpayne@68: self.cursor_blink.set(idleConf.GetOption( jpayne@68: 'main', 'EditorWindow', 'cursor-blink', type='bool')) jpayne@68: self.autocomplete_wait.set(idleConf.GetOption( jpayne@68: 'extensions', 'AutoComplete', 'popupwait', type='int')) jpayne@68: self.paren_style.set(idleConf.GetOption( jpayne@68: 'extensions', 'ParenMatch', 'style')) jpayne@68: self.flash_delay.set(idleConf.GetOption( jpayne@68: 'extensions', 'ParenMatch', 'flash-delay', type='int')) jpayne@68: self.paren_bell.set(idleConf.GetOption( jpayne@68: 'extensions', 'ParenMatch', 'bell')) jpayne@68: jpayne@68: # Set variables for editor windows. jpayne@68: self.autosave.set(idleConf.GetOption( jpayne@68: 'main', 'General', 'autosave', default=0, type='bool')) jpayne@68: self.format_width.set(idleConf.GetOption( jpayne@68: 'extensions', 'FormatParagraph', 'max-width', type='int')) jpayne@68: self.line_numbers_default.set(idleConf.GetOption( jpayne@68: 'main', 'EditorWindow', 'line-numbers-default', type='bool')) jpayne@68: self.context_lines.set(idleConf.GetOption( jpayne@68: 'extensions', 'CodeContext', 'maxlines', type='int')) jpayne@68: jpayne@68: # Set variables for shell windows. jpayne@68: self.auto_squeeze_min_lines.set(idleConf.GetOption( jpayne@68: 'main', 'PyShell', 'auto-squeeze-min-lines', type='int')) jpayne@68: jpayne@68: # Set additional help sources. jpayne@68: self.user_helplist = idleConf.GetAllExtraHelpSourcesList() jpayne@68: self.helplist.delete(0, 'end') jpayne@68: for help_item in self.user_helplist: jpayne@68: self.helplist.insert(END, help_item[0]) jpayne@68: self.set_add_delete_state() jpayne@68: jpayne@68: def help_source_selected(self, event): jpayne@68: "Handle event for selecting additional help." jpayne@68: self.set_add_delete_state() jpayne@68: jpayne@68: def set_add_delete_state(self): jpayne@68: "Toggle the state for the help list buttons based on list entries." jpayne@68: if self.helplist.size() < 1: # No entries in list. jpayne@68: self.button_helplist_edit.state(('disabled',)) jpayne@68: self.button_helplist_remove.state(('disabled',)) jpayne@68: else: # Some entries. jpayne@68: if self.helplist.curselection(): # There currently is a selection. jpayne@68: self.button_helplist_edit.state(('!disabled',)) jpayne@68: self.button_helplist_remove.state(('!disabled',)) jpayne@68: else: # There currently is not a selection. jpayne@68: self.button_helplist_edit.state(('disabled',)) jpayne@68: self.button_helplist_remove.state(('disabled',)) jpayne@68: jpayne@68: def helplist_item_add(self): jpayne@68: """Handle add button for the help list. jpayne@68: jpayne@68: Query for name and location of new help sources and add jpayne@68: them to the list. jpayne@68: """ jpayne@68: help_source = HelpSource(self, 'New Help Source').result jpayne@68: if help_source: jpayne@68: self.user_helplist.append(help_source) jpayne@68: self.helplist.insert(END, help_source[0]) jpayne@68: self.update_help_changes() jpayne@68: jpayne@68: def helplist_item_edit(self): jpayne@68: """Handle edit button for the help list. jpayne@68: jpayne@68: Query with existing help source information and update jpayne@68: config if the values are changed. jpayne@68: """ jpayne@68: item_index = self.helplist.index(ANCHOR) jpayne@68: help_source = self.user_helplist[item_index] jpayne@68: new_help_source = HelpSource( jpayne@68: self, 'Edit Help Source', jpayne@68: menuitem=help_source[0], jpayne@68: filepath=help_source[1], jpayne@68: ).result jpayne@68: if new_help_source and new_help_source != help_source: jpayne@68: self.user_helplist[item_index] = new_help_source jpayne@68: self.helplist.delete(item_index) jpayne@68: self.helplist.insert(item_index, new_help_source[0]) jpayne@68: self.update_help_changes() jpayne@68: self.set_add_delete_state() # Selected will be un-selected jpayne@68: jpayne@68: def helplist_item_remove(self): jpayne@68: """Handle remove button for the help list. jpayne@68: jpayne@68: Delete the help list item from config. jpayne@68: """ jpayne@68: item_index = self.helplist.index(ANCHOR) jpayne@68: del(self.user_helplist[item_index]) jpayne@68: self.helplist.delete(item_index) jpayne@68: self.update_help_changes() jpayne@68: self.set_add_delete_state() jpayne@68: jpayne@68: def update_help_changes(self): jpayne@68: "Clear and rebuild the HelpFiles section in changes" jpayne@68: changes['main']['HelpFiles'] = {} jpayne@68: for num in range(1, len(self.user_helplist) + 1): jpayne@68: changes.add_option( jpayne@68: 'main', 'HelpFiles', str(num), jpayne@68: ';'.join(self.user_helplist[num-1][:2])) jpayne@68: jpayne@68: jpayne@68: class VarTrace: jpayne@68: """Maintain Tk variables trace state.""" jpayne@68: jpayne@68: def __init__(self): jpayne@68: """Store Tk variables and callbacks. jpayne@68: jpayne@68: untraced: List of tuples (var, callback) jpayne@68: that do not have the callback attached jpayne@68: to the Tk var. jpayne@68: traced: List of tuples (var, callback) where jpayne@68: that callback has been attached to the var. jpayne@68: """ jpayne@68: self.untraced = [] jpayne@68: self.traced = [] jpayne@68: jpayne@68: def clear(self): jpayne@68: "Clear lists (for tests)." jpayne@68: # Call after all tests in a module to avoid memory leaks. jpayne@68: self.untraced.clear() jpayne@68: self.traced.clear() jpayne@68: jpayne@68: def add(self, var, callback): jpayne@68: """Add (var, callback) tuple to untraced list. jpayne@68: jpayne@68: Args: jpayne@68: var: Tk variable instance. jpayne@68: callback: Either function name to be used as a callback jpayne@68: or a tuple with IdleConf config-type, section, and jpayne@68: option names used in the default callback. jpayne@68: jpayne@68: Return: jpayne@68: Tk variable instance. jpayne@68: """ jpayne@68: if isinstance(callback, tuple): jpayne@68: callback = self.make_callback(var, callback) jpayne@68: self.untraced.append((var, callback)) jpayne@68: return var jpayne@68: jpayne@68: @staticmethod jpayne@68: def make_callback(var, config): jpayne@68: "Return default callback function to add values to changes instance." jpayne@68: def default_callback(*params): jpayne@68: "Add config values to changes instance." jpayne@68: changes.add_option(*config, var.get()) jpayne@68: return default_callback jpayne@68: jpayne@68: def attach(self): jpayne@68: "Attach callback to all vars that are not traced." jpayne@68: while self.untraced: jpayne@68: var, callback = self.untraced.pop() jpayne@68: var.trace_add('write', callback) jpayne@68: self.traced.append((var, callback)) jpayne@68: jpayne@68: def detach(self): jpayne@68: "Remove callback from traced vars." jpayne@68: while self.traced: jpayne@68: var, callback = self.traced.pop() jpayne@68: var.trace_remove('write', var.trace_info()[0][1]) jpayne@68: self.untraced.append((var, callback)) jpayne@68: jpayne@68: jpayne@68: tracers = VarTrace() jpayne@68: jpayne@68: help_common = '''\ jpayne@68: When you click either the Apply or Ok buttons, settings in this jpayne@68: dialog that are different from IDLE's default are saved in jpayne@68: a .idlerc directory in your home directory. Except as noted, jpayne@68: these changes apply to all versions of IDLE installed on this jpayne@68: machine. [Cancel] only cancels changes made since the last save. jpayne@68: ''' jpayne@68: help_pages = { jpayne@68: 'Fonts/Tabs':''' jpayne@68: Font sample: This shows what a selection of Basic Multilingual Plane jpayne@68: unicode characters look like for the current font selection. If the jpayne@68: selected font does not define a character, Tk attempts to find another jpayne@68: font that does. Substitute glyphs depend on what is available on a jpayne@68: particular system and will not necessarily have the same size as the jpayne@68: font selected. Line contains 20 characters up to Devanagari, 14 for jpayne@68: Tamil, and 10 for East Asia. jpayne@68: jpayne@68: Hebrew and Arabic letters should display right to left, starting with jpayne@68: alef, \u05d0 and \u0627. Arabic digits display left to right. The jpayne@68: Devanagari and Tamil lines start with digits. The East Asian lines jpayne@68: are Chinese digits, Chinese Hanzi, Korean Hangul, and Japanese jpayne@68: Hiragana and Katakana. jpayne@68: jpayne@68: You can edit the font sample. Changes remain until IDLE is closed. jpayne@68: ''', jpayne@68: 'Highlights': ''' jpayne@68: Highlighting: jpayne@68: The IDLE Dark color theme is new in October 2015. It can only jpayne@68: be used with older IDLE releases if it is saved as a custom jpayne@68: theme, with a different name. jpayne@68: ''', jpayne@68: 'Keys': ''' jpayne@68: Keys: jpayne@68: The IDLE Modern Unix key set is new in June 2016. It can only jpayne@68: be used with older IDLE releases if it is saved as a custom jpayne@68: key set, with a different name. jpayne@68: ''', jpayne@68: 'General': ''' jpayne@68: General: jpayne@68: jpayne@68: AutoComplete: Popupwait is milliseconds to wait after key char, without jpayne@68: cursor movement, before popping up completion box. Key char is '.' after jpayne@68: identifier or a '/' (or '\\' on Windows) within a string. jpayne@68: jpayne@68: FormatParagraph: Max-width is max chars in lines after re-formatting. jpayne@68: Use with paragraphs in both strings and comment blocks. jpayne@68: jpayne@68: ParenMatch: Style indicates what is highlighted when closer is entered: jpayne@68: 'opener' - opener '({[' corresponding to closer; 'parens' - both chars; jpayne@68: 'expression' (default) - also everything in between. Flash-delay is how jpayne@68: long to highlight if cursor is not moved (0 means forever). jpayne@68: jpayne@68: CodeContext: Maxlines is the maximum number of code context lines to jpayne@68: display when Code Context is turned on for an editor window. jpayne@68: jpayne@68: Shell Preferences: Auto-Squeeze Min. Lines is the minimum number of lines jpayne@68: of output to automatically "squeeze". jpayne@68: ''' jpayne@68: } jpayne@68: jpayne@68: jpayne@68: def is_int(s): jpayne@68: "Return 's is blank or represents an int'" jpayne@68: if not s: jpayne@68: return True jpayne@68: try: jpayne@68: int(s) jpayne@68: return True jpayne@68: except ValueError: jpayne@68: return False jpayne@68: jpayne@68: jpayne@68: class VerticalScrolledFrame(Frame): jpayne@68: """A pure Tkinter vertically scrollable frame. jpayne@68: jpayne@68: * Use the 'interior' attribute to place widgets inside the scrollable frame jpayne@68: * Construct and pack/place/grid normally jpayne@68: * This frame only allows vertical scrolling jpayne@68: """ jpayne@68: def __init__(self, parent, *args, **kw): jpayne@68: Frame.__init__(self, parent, *args, **kw) jpayne@68: jpayne@68: # Create a canvas object and a vertical scrollbar for scrolling it. jpayne@68: vscrollbar = Scrollbar(self, orient=VERTICAL) jpayne@68: vscrollbar.pack(fill=Y, side=RIGHT, expand=FALSE) jpayne@68: canvas = Canvas(self, borderwidth=0, highlightthickness=0, jpayne@68: yscrollcommand=vscrollbar.set, width=240) jpayne@68: canvas.pack(side=LEFT, fill=BOTH, expand=TRUE) jpayne@68: vscrollbar.config(command=canvas.yview) jpayne@68: jpayne@68: # Reset the view. jpayne@68: canvas.xview_moveto(0) jpayne@68: canvas.yview_moveto(0) jpayne@68: jpayne@68: # Create a frame inside the canvas which will be scrolled with it. jpayne@68: self.interior = interior = Frame(canvas) jpayne@68: interior_id = canvas.create_window(0, 0, window=interior, anchor=NW) jpayne@68: jpayne@68: # Track changes to the canvas and frame width and sync them, jpayne@68: # also updating the scrollbar. jpayne@68: def _configure_interior(event): jpayne@68: # Update the scrollbars to match the size of the inner frame. jpayne@68: size = (interior.winfo_reqwidth(), interior.winfo_reqheight()) jpayne@68: canvas.config(scrollregion="0 0 %s %s" % size) jpayne@68: interior.bind('', _configure_interior) jpayne@68: jpayne@68: def _configure_canvas(event): jpayne@68: if interior.winfo_reqwidth() != canvas.winfo_width(): jpayne@68: # Update the inner frame's width to fill the canvas. jpayne@68: canvas.itemconfigure(interior_id, width=canvas.winfo_width()) jpayne@68: canvas.bind('', _configure_canvas) jpayne@68: jpayne@68: return jpayne@68: jpayne@68: jpayne@68: if __name__ == '__main__': jpayne@68: from unittest import main jpayne@68: main('idlelib.idle_test.test_configdialog', verbosity=2, exit=False) jpayne@68: jpayne@68: from idlelib.idle_test.htest import run jpayne@68: run(ConfigDialog)