Index: Bindings.py =================================================================== --- Bindings.py (revision 64043) +++ Bindings.py (working copy) @@ -70,6 +70,7 @@ ]), ('options', [ ('_Configure IDLE...', '<>'), + ('_Configure Extensions...', '<>'), None, ]), ('help', [ Index: configExtensionsDialog.py =================================================================== --- configExtensionsDialog.py (revision 0) +++ configExtensionsDialog.py (revision 0) @@ -0,0 +1,244 @@ +"A generic extension configuration dialog for IDLE." + +from Tkinter import * +from Tkconstants import GROOVE +from configHandler import idleConf +from tabbedpages import TabbedPageSet + +## XXX TODO: +## * Revert to default(s)? Per option or per extension? +## * List options in their original order (possible??) + +class VerticalScrolledFrame(Frame): + """A pure Tkinter scrollable frame that actually works! + + * Use the 'interior' attribute to place widgets inside the scrollable frame + * Construct and pack/place/grid normally + * This frame only allows vertical scrolling + + """ + def __init__(self, parent, *args, **kw): + Frame.__init__(self, parent, *args, **kw) + + # create a canvas object and a vertical scrollbar for scrolling it + vscrollbar = Scrollbar(self, orient=VERTICAL) + vscrollbar.pack(fill=Y, side=RIGHT, expand=FALSE) + canvas = Canvas(self, bd=0, highlightthickness=0, + yscrollcommand=vscrollbar.set) + 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) + if interior.winfo_reqwidth() != canvas.winfo_width(): + # update the canvas's width to fit the inner frame + canvas.config(width=interior.winfo_reqwidth()) + interior.bind('', _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_canvas) + + return + +class ConfigExtensionsDialog(Toplevel): + """A dialog for configuring IDLE extensions. + + This dialog is generic - it works for any and all IDLE extensions. + + IDLE extensions save their configuration options using idleConf. + ConfigExtensionsDialog reads the current configuration using idleConf, + supplies a GUI interface to change the configuration values, and saves the + changes using idleConf. + + Not all changes take effect immediately - some may require restarting IDLE. + This depends on each extension's implementation. + + All values are treated as text, and it is up to the user to supply + reasonable values. The only exception to this are the 'enable*' options, + which are boolean, and can be toggled with an True/False button. + + """ + def __init__(self, parent): + Toplevel.__init__(self, parent) + self.wm_withdraw() + + self.configure(borderwidth=5) + self.geometry("+%d+%d" % (parent.winfo_rootx()+20, + parent.winfo_rooty()+30)) + self.wm_title('IDLE Extensions Configuration') + + self.defaultCfg = idleConf.defaultCfg['extensions'] + self.userCfg = idleConf.userCfg['extensions'] + + self._load_extensions() + self._create_widgets() + + self.resizable(height=FALSE, width=FALSE) # don't allow resizing yet + self.transient(parent) + self.grab_set() + self.protocol("WM_DELETE_WINDOW", self._cancel_action) + self._tabbed_page_set.focus_set() + # wait for window to be generated + self.update() + # set current width as the minimum width + self.wm_minsize(self.winfo_width(), 1) + # now allow resizing + self.resizable(height=TRUE, width=TRUE) + + self.wm_deiconify() + self.wait_window() + + def _load_extensions(self): + "Fill _extensions with data from the default and user configurations." + self._extensions = {} + for ext_name in idleConf.GetExtensions(active_only=False): + self._extensions[ext_name] = [] + + for ext_name in self._extensions: + opt_list = self.defaultCfg.GetOptionList(ext_name) + opt_list.sort() + + # 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: + if opt_name in enables: + opt_type = 'bool' + else: + opt_type = None + default = self.defaultCfg.Get(ext_name, opt_name, + type=opt_type, raw=True) + value = self.userCfg.Get(ext_name, opt_name, + type=opt_type, raw=True, + default=default) + var = StringVar(self) + var.set(str(value)) + + self._extensions[ext_name].append( + {'name':opt_name, 'type':opt_type, + 'default':default, 'value':value, + 'var':var + }) + + def _create_widgets(self): + "Create the dialog's widgets." + self.rowconfigure(0, weight=1) + self.rowconfigure(1, weight=0) + self.columnconfigure(0, weight=1) + + # create the tabbed pages + self._tabbed_page_set = \ + TabbedPageSet(self, page_names=self._extensions.keys(), + n_rows=None, max_tabs_per_row=5, + page_class=TabbedPageSet.PageRemove) + self._tabbed_page_set.grid(row=0, column=0, sticky=NSEW) + for ext_name in self._extensions: + self._create_tab_page(ext_name) + + # create the action buttons - Ok, Apply, Cancel & Help + button_frame = Frame(self) + button_frame.grid(row=1, column=0, sticky=NSEW) + inner_button_frame = Frame(button_frame, pady=2) + inner_button_frame.pack(side=BOTTOM, expand=False) + def create_button(text, command, side): + btn = Button(inner_button_frame, text=text, command=command, + takefocus=FALSE, padx=6, pady=3) + btn.pack(side=side, padx=5) + create_button('Ok', self._ok_action, LEFT) + create_button('Apply', self._apply_action, LEFT) + create_button('Cancel', self._cancel_action, LEFT) +## create_button('Help', self._help_action, RIGHT) + + # vertical padding above the buttons + Frame(button_frame, height=2, borderwidth=0).pack(side=BOTTOM) + + def _create_tab_page(self, ext_name): + "Create the page for an extension." + + if TkVersion > 8.399: + page = LabelFrame(self._tabbed_page_set.pages[ext_name].frame, + border=2, padx=2, relief=GROOVE, + text=' %s '%ext_name) + else: + page = Frame(self._tabbed_page_set.pages[ext_name].frame, + border=2, padx=2, relief=GROOVE) + page.pack(fill=BOTH, expand=True, padx=2, pady=2) + + # create the scrollable frame which will contain the entries + scrolled_frame = VerticalScrolledFrame(page, pady=2, height=250) + scrolled_frame.pack(side=BOTTOM, fill=BOTH, expand=TRUE) + entry_area = scrolled_frame.interior + entry_area.columnconfigure(0,weight=0, pad=2) + entry_area.columnconfigure(1,weight=1) + + # create an entry for each configuration option + row = 0 + for opt in 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': + entry = Checkbutton(entry_area, textvariable=var, variable=var, + onvalue='True', offvalue='False', + indicatoron=FALSE, selectcolor='') + else: + entry = Entry(entry_area, textvariable=var) + entry.grid(row=row, column=1, sticky=NSEW) + row += 1 + return + + def _cancel_action(self): + self.destroy() + + def _ok_action(self): + self._apply_action() + self.destroy() + + def _apply_action(self): + self._save_all_changed_configs() + pass + + def _help_action(self): + pass + + def _set_user_value(self, section, item, value): + if self.defaultCfg.has_option(section, item): + if self.defaultCfg.Get(section, item, raw=True)==value: + # the setting equals a default setting, remove it from user cfg + return self.userCfg.RemoveOption(section, item) + # set the option + return self.userCfg.SetOption(section, item, value) + + def _save_all_changed_configs(self): + "Save configuration changes to the user config file." + has_changes = False + for ext_name in self._extensions: + options = self._extensions[ext_name] + for opt in options: + value = opt['var'].get() + name = opt['name'] + if self._set_user_value(ext_name, name, value): + has_changes = True + if has_changes: + self.userCfg.Save() Index: EditorWindow.py =================================================================== --- EditorWindow.py (revision 64043) +++ EditorWindow.py (working copy) @@ -16,7 +16,7 @@ import ReplaceDialog import PyParse from configHandler import idleConf -import aboutDialog, textView, configDialog +import aboutDialog, textView, configDialog, configExtensionsDialog import macosxSupport # The default tab setting for a Text widget, in average-width characters. @@ -127,6 +127,8 @@ text.bind("<>", self.python_docs) text.bind("<>", self.about_dialog) text.bind("<>", self.config_dialog) + text.bind("<>", + self.config_extensions_dialog) text.bind("<>", self.open_module) text.bind("<>", lambda event: "break") text.bind("<>", self.select_all) @@ -422,6 +424,9 @@ def config_dialog(self, event=None): configDialog.ConfigDialog(self.top,'Settings') + def config_extensions_dialog(self, event=None): + configExtensionsDialog.ConfigExtensionsDialog(self.top) + def help_dialog(self, event=None): fn=os.path.join(os.path.abspath(os.path.dirname(__file__)),'help.txt') textView.view_file(self.top,'Help',fn)