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