2415 lines
		
	
	
		
			103 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
			
		
		
	
	
			2415 lines
		
	
	
		
			103 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
| """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, Scale, Canvas,
 | |
|                      StringVar, BooleanVar, IntVar, TRUE, FALSE,
 | |
|                      TOP, BOTTOM, RIGHT, LEFT, SOLID, GROOVE,
 | |
|                      NONE, BOTH, X, Y, W, E, EW, NS, NSEW, NW,
 | |
|                      HORIZONTAL, VERTICAL, ANCHOR, ACTIVE, END, 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 GetKeysDialog
 | |
| from idlelib.dynoption import DynOptionMenu
 | |
| from idlelib import macosx
 | |
| from idlelib.query import SectionName, HelpSource
 | |
| from idlelib.textview import view_text
 | |
| from idlelib.autocomplete import AutoComplete
 | |
| from idlelib.codecontext import CodeContext
 | |
| from idlelib.parenmatch import ParenMatch
 | |
| from idlelib.format import FormatParagraph
 | |
| from idlelib.squeezer import Squeezer
 | |
| from idlelib.textview import ScrollableTextFrame
 | |
| 
 | |
| changes = ConfigChanges()
 | |
| # Reload changed options in the following classes.
 | |
| reloadables = (AutoComplete, CodeContext, ParenMatch, FormatParagraph,
 | |
|                Squeezer)
 | |
| 
 | |
| 
 | |
| class ConfigDialog(Toplevel):
 | |
|     """Config dialog for IDLE.
 | |
|     """
 | |
| 
 | |
|     def __init__(self, parent, title='', *, _htest=False, _utest=False):
 | |
|         """Show the tabbed dialog for user configuration.
 | |
| 
 | |
|         Args:
 | |
|             parent - parent of this dialog
 | |
|             title - string which is the title of this popup dialog
 | |
|             _htest - bool, change box location when running htest
 | |
|             _utest - bool, don't wait_window when running unittest
 | |
| 
 | |
|         Note: Focus set on font page fontlist.
 | |
| 
 | |
|         Methods:
 | |
|             create_widgets
 | |
|             cancel: Bound to DELETE_WINDOW protocol.
 | |
|         """
 | |
|         Toplevel.__init__(self, parent)
 | |
|         self.parent = parent
 | |
|         if _htest:
 | |
|             parent.instance_dict = {}
 | |
|         if not _utest:
 | |
|             self.withdraw()
 | |
| 
 | |
|         self.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/Tabs')
 | |
|         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 = GetKeysDialog(self, 'Get New Keys', bind_name,
 | |
|                 current_key_sequences).result
 | |
|         if new_keys:
 | |
|             if self.keyset_source.get():  # Current key set is a built-in.
 | |
|                 message = ('Your changes will be saved as a new Custom Key Set.'
 | |
|                            ' Enter a name for your new Custom Key Set below.')
 | |
|                 new_keyset = self.get_new_keys_name(message)
 | |
|                 if not new_keyset:  # User cancelled custom key set creation.
 | |
|                     self.bindingslist.select_set(list_index)
 | |
|                     self.bindingslist.select_anchor(list_index)
 | |
|                     return
 | |
|                 else:  # Create new custom key set based on previously active key set.
 | |
|                     self.create_new_key_set(new_keyset)
 | |
|             self.bindingslist.delete(list_index)
 | |
|             self.bindingslist.insert(list_index, bind_name+' - '+new_keys)
 | |
|             self.bindingslist.select_set(list_index)
 | |
|             self.bindingslist.select_anchor(list_index)
 | |
|             self.keybinding.set(new_keys)
 | |
|         else:
 | |
|             self.bindingslist.select_set(list_index)
 | |
|             self.bindingslist.select_anchor(list_index)
 | |
| 
 | |
|     def get_new_keys_name(self, message):
 | |
|         "Return new key set name from query popup."
 | |
|         used_names = (idleConf.GetSectionList('user', 'keys') +
 | |
|                 idleConf.GetSectionList('default', 'keys'))
 | |
|         new_keyset = SectionName(
 | |
|                 self, 'New Custom Key Set', message, used_names).result
 | |
|         return new_keyset
 | |
| 
 | |
|     def save_as_new_key_set(self):
 | |
|         "Prompt for name of new key set and save changes using that name."
 | |
|         new_keys_name = self.get_new_keys_name('New Key Set Name:')
 | |
|         if new_keys_name:
 | |
|             self.create_new_key_set(new_keys_name)
 | |
| 
 | |
|     def on_bindingslist_select(self, event):
 | |
|         "Activate button to assign new keys to selected action."
 | |
|         self.button_new_keys.state(('!disabled',))
 | |
| 
 | |
|     def create_new_key_set(self, new_key_set_name):
 | |
|         """Create a new custom key set with the given name.
 | |
| 
 | |
|         Copy the bindings/keys from the previously active keyset
 | |
|         to the new keyset and activate the new custom keyset.
 | |
|         """
 | |
|         if self.keyset_source.get():
 | |
|             prev_key_set_name = self.builtin_name.get()
 | |
|         else:
 | |
|             prev_key_set_name = self.custom_name.get()
 | |
|         prev_keys = idleConf.GetCoreKeys(prev_key_set_name)
 | |
|         new_keys = {}
 | |
|         for event in prev_keys:  # Add key set to changed items.
 | |
|             event_name = event[2:-2]  # Trim off the angle brackets.
 | |
|             binding = ' '.join(prev_keys[event])
 | |
|             new_keys[event_name] = binding
 | |
|         # Handle any unsaved changes to prev key set.
 | |
|         if prev_key_set_name in changes['keys']:
 | |
|             key_set_changes = changes['keys'][prev_key_set_name]
 | |
|             for event in key_set_changes:
 | |
|                 new_keys[event] = key_set_changes[event]
 | |
|         # Save the new key set.
 | |
|         self.save_new_key_set(new_key_set_name, new_keys)
 | |
|         # Change GUI over to the new key set.
 | |
|         custom_key_list = idleConf.GetSectionList('user', 'keys')
 | |
|         custom_key_list.sort()
 | |
|         self.customlist.SetMenu(custom_key_list, new_key_set_name)
 | |
|         self.keyset_source.set(0)
 | |
|         self.set_keys_type()
 | |
| 
 | |
|     def load_keys_list(self, keyset_name):
 | |
|         """Reload the list of action/key binding pairs for the active key set.
 | |
| 
 | |
|         An action/key binding can be selected to change the key binding.
 | |
|         """
 | |
|         reselect = False
 | |
|         if self.bindingslist.curselection():
 | |
|             reselect = True
 | |
|             list_index = self.bindingslist.index(ANCHOR)
 | |
|         keyset = idleConf.GetKeySet(keyset_name)
 | |
|         bind_names = list(keyset.keys())
 | |
|         bind_names.sort()
 | |
|         self.bindingslist.delete(0, END)
 | |
|         for bind_name in bind_names:
 | |
|             key = ' '.join(keyset[bind_name])
 | |
|             bind_name = bind_name[2:-2]  # Trim off the angle brackets.
 | |
|             if keyset_name in changes['keys']:
 | |
|                 # Handle any unsaved changes to this key set.
 | |
|                 if bind_name in changes['keys'][keyset_name]:
 | |
|                     key = changes['keys'][keyset_name][bind_name]
 | |
|             self.bindingslist.insert(END, bind_name+' - '+key)
 | |
|         if reselect:
 | |
|             self.bindingslist.see(list_index)
 | |
|             self.bindingslist.select_set(list_index)
 | |
|             self.bindingslist.select_anchor(list_index)
 | |
| 
 | |
|     @staticmethod
 | |
|     def save_new_key_set(keyset_name, keyset):
 | |
|         """Save a newly created core key set.
 | |
| 
 | |
|         Add keyset to idleConf.userCfg['keys'], not to disk.
 | |
|         If the keyset doesn't exist, it is created.  The
 | |
|         binding/keys are taken from the keyset argument.
 | |
| 
 | |
|         keyset_name - string, the name of the new key set
 | |
|         keyset - dictionary containing the new keybindings
 | |
|         """
 | |
|         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 (Combobox < 8.5.9) > 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
 | |
| 
 | |
|     @staticmethod
 | |
|     def make_callback(var, config):
 | |
|         "Return default callback function to add values to changes instance."
 | |
|         def default_callback(*params):
 | |
|             "Add config values to changes instance."
 | |
|             changes.add_option(*config, var.get())
 | |
|         return default_callback
 | |
| 
 | |
|     def attach(self):
 | |
|         "Attach callback to all vars that are not traced."
 | |
|         while self.untraced:
 | |
|             var, callback = self.untraced.pop()
 | |
|             var.trace_add('write', callback)
 | |
|             self.traced.append((var, callback))
 | |
| 
 | |
|     def detach(self):
 | |
|         "Remove callback from traced vars."
 | |
|         while self.traced:
 | |
|             var, callback = self.traced.pop()
 | |
|             var.trace_remove('write', var.trace_info()[0][1])
 | |
|             self.untraced.append((var, callback))
 | |
| 
 | |
| 
 | |
| tracers = VarTrace()
 | |
| 
 | |
| help_common = '''\
 | |
| When you click either the Apply or Ok buttons, settings in this
 | |
| dialog that are different from IDLE's default are saved in
 | |
| a .idlerc directory in your home directory. Except as noted,
 | |
| these changes apply to all versions of IDLE installed on this
 | |
| machine. [Cancel] only cancels changes made since the last save.
 | |
| '''
 | |
| help_pages = {
 | |
|     'Fonts/Tabs':'''
 | |
| Font sample: This shows what a selection of Basic Multilingual Plane
 | |
| unicode characters look like for the current font selection.  If the
 | |
| selected font does not define a character, Tk attempts to find another
 | |
| font that does.  Substitute glyphs depend on what is available on a
 | |
| particular system and will not necessarily have the same size as the
 | |
| font selected.  Line contains 20 characters up to Devanagari, 14 for
 | |
| Tamil, and 10 for East Asia.
 | |
| 
 | |
| Hebrew and Arabic letters should display right to left, starting with
 | |
| alef, \u05d0 and \u0627.  Arabic digits display left to right.  The
 | |
| Devanagari and Tamil lines start with digits.  The East Asian lines
 | |
| are Chinese digits, Chinese Hanzi, Korean Hangul, and Japanese
 | |
| Hiragana and Katakana.
 | |
| 
 | |
| You can edit the font sample. Changes remain until IDLE is closed.
 | |
| ''',
 | |
|     'Highlights': '''
 | |
| Highlighting:
 | |
| The IDLE Dark color theme is new in October 2015.  It can only
 | |
| be used with older IDLE releases if it is saved as a custom
 | |
| theme, with a different name.
 | |
| ''',
 | |
|     'Keys': '''
 | |
| Keys:
 | |
| The IDLE Modern Unix key set is new in June 2016.  It can only
 | |
| be used with older IDLE releases if it is saved as a custom
 | |
| key set, with a different name.
 | |
| ''',
 | |
|      'General': '''
 | |
| General:
 | |
| 
 | |
| AutoComplete: Popupwait is milliseconds to wait after key char, without
 | |
| cursor movement, before popping up completion box.  Key char is '.' after
 | |
| identifier or a '/' (or '\\' on Windows) within a string.
 | |
| 
 | |
| FormatParagraph: Max-width is max chars in lines after re-formatting.
 | |
| Use with paragraphs in both strings and comment blocks.
 | |
| 
 | |
| ParenMatch: Style indicates what is highlighted when closer is entered:
 | |
| 'opener' - opener '({[' corresponding to closer; 'parens' - both chars;
 | |
| 'expression' (default) - also everything in between.  Flash-delay is how
 | |
| long to highlight if cursor is not moved (0 means forever).
 | |
| 
 | |
| CodeContext: Maxlines is the maximum number of code context lines to
 | |
| display when Code Context is turned on for an editor window.
 | |
| 
 | |
| Shell Preferences: Auto-Squeeze Min. Lines is the minimum number of lines
 | |
| of output to automatically "squeeze".
 | |
| ''',
 | |
|     'Extensions': '''
 | |
| ZzDummy: This extension is provided as an example for how to create and
 | |
| use an extension.  Enable indicates whether the extension is active or
 | |
| not; likewise enable_editor and enable_shell indicate which windows it
 | |
| will be active on.  For this extension, z-text is the text that will be
 | |
| inserted at or removed from the beginning of the lines of selected text,
 | |
| or the current line if no selection.
 | |
| ''',
 | |
| }
 | |
| 
 | |
| 
 | |
| def is_int(s):
 | |
|     "Return 's is blank or represents an int'"
 | |
|     if not s:
 | |
|         return True
 | |
|     try:
 | |
|         int(s)
 | |
|         return True
 | |
|     except ValueError:
 | |
|         return False
 | |
| 
 | |
| 
 | |
| class VerticalScrolledFrame(Frame):
 | |
|     """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)
 |