diff options
author | Terry Jan Reedy <tjreedy@udel.edu> | 2014-10-23 00:15:18 (GMT) |
---|---|---|
committer | Terry Jan Reedy <tjreedy@udel.edu> | 2014-10-23 00:15:18 (GMT) |
commit | a9421fb3a3013a8aacc18959c28c1b0002f34025 (patch) | |
tree | 21947d5899838ca1deab5d84fcfb3a40abd226b8 /Lib/idlelib | |
parent | c803bd84b529f3a300f9ba808b60dd50e4e365bf (diff) | |
download | cpython-a9421fb3a3013a8aacc18959c28c1b0002f34025.zip cpython-a9421fb3a3013a8aacc18959c28c1b0002f34025.tar.gz cpython-a9421fb3a3013a8aacc18959c28c1b0002f34025.tar.bz2 |
Issue #3068: Add Idle extension configuration dialog to Options menu.
Original patch by Tal Einat.
Diffstat (limited to 'Lib/idlelib')
-rw-r--r-- | Lib/idlelib/Bindings.py | 3 | ||||
-rw-r--r-- | Lib/idlelib/EditorWindow.py | 4 | ||||
-rw-r--r-- | Lib/idlelib/configDialog.py | 273 | ||||
-rw-r--r-- | Lib/idlelib/configHandler.py | 3 | ||||
-rw-r--r-- | Lib/idlelib/idle_test/htest.py | 9 | ||||
-rw-r--r-- | Lib/idlelib/macosxSupport.py | 6 |
6 files changed, 278 insertions, 20 deletions
diff --git a/Lib/idlelib/Bindings.py b/Lib/idlelib/Bindings.py index c9bef21..226671c 100644 --- a/Lib/idlelib/Bindings.py +++ b/Lib/idlelib/Bindings.py @@ -77,7 +77,8 @@ menudefs = [ ('!_Auto-open Stack Viewer', '<<toggle-jit-stack-viewer>>'), ]), ('options', [ - ('_Configure IDLE...', '<<open-config-dialog>>'), + ('Configure _IDLE', '<<open-config-dialog>>'), + ('Configure _Extensions', '<<open-config-extensions-dialog>>'), None, ]), ('help', [ diff --git a/Lib/idlelib/EditorWindow.py b/Lib/idlelib/EditorWindow.py index bdf4a1a..7d2ea8f 100644 --- a/Lib/idlelib/EditorWindow.py +++ b/Lib/idlelib/EditorWindow.py @@ -189,6 +189,8 @@ class EditorWindow(object): text.bind("<<python-docs>>", self.python_docs) text.bind("<<about-idle>>", self.about_dialog) text.bind("<<open-config-dialog>>", self.config_dialog) + text.bind("<<open-config-extensions-dialog>>", + self.config_extensions_dialog) text.bind("<<open-module>>", self.open_module) text.bind("<<do-nothing>>", lambda event: "break") text.bind("<<select-all>>", self.select_all) @@ -543,6 +545,8 @@ class EditorWindow(object): def config_dialog(self, event=None): configDialog.ConfigDialog(self.top,'Settings') + def config_extensions_dialog(self, event=None): + configDialog.ConfigExtensionsDialog(self.top) def help_dialog(self, event=None): if self.root: diff --git a/Lib/idlelib/configDialog.py b/Lib/idlelib/configDialog.py index 938b0bc..57565ae 100644 --- a/Lib/idlelib/configDialog.py +++ b/Lib/idlelib/configDialog.py @@ -20,8 +20,8 @@ from idlelib.tabbedpages import TabbedPageSet from idlelib.keybindingDialog import GetKeysDialog from idlelib.configSectionNameDialog import GetCfgSectionNameDialog from idlelib.configHelpSourceEdit import GetHelpSourceDialog +from idlelib.tabbedpages import TabbedPageSet from idlelib import macosxSupport - class ConfigDialog(Toplevel): def __init__(self, parent, title='', _htest=False, _utest=False): @@ -85,8 +85,6 @@ class ConfigDialog(Toplevel): self.CreatePageKeys() self.CreatePageGeneral() self.create_action_buttons().pack(side=BOTTOM) - Frame(self, height=2, borderwidth=0).pack(side=BOTTOM) - def create_action_buttons(self): if macosxSupport.isAquaTk(): # Changing the default padding on OSX results in unreadable @@ -94,27 +92,30 @@ class ConfigDialog(Toplevel): paddingArgs = {} else: paddingArgs = {'padx':6, 'pady':3} - - frame = Frame(self, pady=2) + outer = Frame(self, pady=2) + buttons = Frame(outer, pady=2) self.buttonOk = Button( - frame, text='Ok', command=self.Ok, + buttons, text='Ok', command=self.Ok, takefocus=FALSE, **paddingArgs) self.buttonApply = Button( - frame, text='Apply', command=self.Apply, + buttons, text='Apply', command=self.Apply, takefocus=FALSE, **paddingArgs) self.buttonCancel = Button( - frame, text='Cancel', command=self.Cancel, + buttons, text='Cancel', command=self.Cancel, takefocus=FALSE, **paddingArgs) + self.buttonOk.pack(side=LEFT, padx=5) + self.buttonApply.pack(side=LEFT, padx=5) + self.buttonCancel.pack(side=LEFT, padx=5) # Comment out Help button creation and packing until implement self.Help ## self.buttonHelp = Button( -## frame, text='Help', command=self.Help, +## buttons, text='Help', command=self.Help, ## takefocus=FALSE, **paddingArgs) ## self.buttonHelp.pack(side=RIGHT, padx=5) - self.buttonOk.pack(side=LEFT, padx=5) - self.buttonApply.pack(side=LEFT, padx=5) - self.buttonCancel.pack(side=LEFT, padx=5) - return frame + # add space above buttons + Frame(outer, height=2, borderwidth=0).pack(side=TOP) + buttons.pack(side=BOTTOM) + return outer def CreatePageFontTab(self): parent = self.parent self.fontSize = StringVar(parent) @@ -1188,10 +1189,252 @@ class ConfigDialog(Toplevel): def Help(self): pass +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, 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>', _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 + +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 + +# TODO: +# * Revert to default(s)? Per option or per extension? +# * List options in their original order (possible??) +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, title=None, _htest=False): + Toplevel.__init__(self, parent) + self.wm_withdraw() + + self.configure(borderwidth=5) + self.geometry( + "+%d+%d" % (parent.winfo_rootx() + 20, + parent.winfo_rooty() + (30 if not _htest else 150))) + self.wm_title(title or 'IDLE Extensions Configuration') + + self.defaultCfg = idleConf.defaultCfg['extensions'] + self.userCfg = idleConf.userCfg['extensions'] + self.is_int = self.register(is_int) + self.load_extensions() + self.create_widgets() + + self.resizable(height=FALSE, width=FALSE) # don't allow resizing yet + self.transient(parent) + self.protocol("WM_DELETE_WINDOW", self.Cancel) + 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() + if not _htest: + self.grab_set() + self.wait_window() + + 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): + self.extensions[ext_name] = [] + + for ext_name in self.extensions: + opt_list = sorted(self.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.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.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 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) + + self.create_action_buttons().grid(row=1) + + create_action_buttons = ConfigDialog.create_action_buttons + + def create_tab_page(self, ext_name): + """Create the page for an extension.""" + + page = LabelFrame(self.tabbed_page_set.pages[ext_name].frame, + border=2, padx=2, relief=GROOVE, + text=' %s ' % ext_name) + page.pack(fill=BOTH, expand=True, padx=12, 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) + entry_area.columnconfigure(1, weight=1) + + # 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, textvariable=var, variable=var, + onvalue='True', offvalue='False', + indicatoron=FALSE, selectcolor='', 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') + ).grid(row=row, column=1, sticky=NSEW, padx=7) + + else: + Entry(entry_area, textvariable=var + ).grid(row=row, column=1, sticky=NSEW, padx=7) + return + + + Ok = ConfigDialog.Ok + + def Apply(self): + self.save_all_changed_configs() + pass + + Cancel = ConfigDialog.Cancel + + def Help(self): + pass + + def set_user_value(self, section, opt): + 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.userCfg.RemoveOption(section, name) + # set the option + return self.userCfg.SetOption(section, name, 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: + if self.set_user_value(ext_name, opt): + has_changes = True + if has_changes: + self.userCfg.Save() + + if __name__ == '__main__': import unittest unittest.main('idlelib.idle_test.test_configdialog', verbosity=2, exit=False) - from idlelib.idle_test.htest import run - run(ConfigDialog) + run(ConfigDialog, ConfigExtensionsDialog) diff --git a/Lib/idlelib/configHandler.py b/Lib/idlelib/configHandler.py index 7a282fb..b94b8f1 100644 --- a/Lib/idlelib/configHandler.py +++ b/Lib/idlelib/configHandler.py @@ -44,6 +44,9 @@ class IdleConfParser(ConfigParser): Get an option value for given section/option or return default. If type is specified, return as type. """ + # TODO Use default as fallback, at least if not None + # Should also print Warning(file, section, option). + # Currently may raise ValueError if not self.has_option(section, option): return default if type == 'bool': diff --git a/Lib/idlelib/idle_test/htest.py b/Lib/idlelib/idle_test/htest.py index 1fe5ad4..aa7f2e8 100644 --- a/Lib/idlelib/idle_test/htest.py +++ b/Lib/idlelib/idle_test/htest.py @@ -93,6 +93,15 @@ _class_browser_spec = { "Double clicking on items prints a traceback for an exception " "that is ignored." } +ConfigExtensionsDialog_spec = { + 'file': 'configDialog', + 'kwds': {'title': 'Test Extension Configuration', + '_htest': True,}, + 'msg': "IDLE extensions dialog.\n" + "\n[Ok] to close the dialog.[Apply] to apply the settings and " + "and [Cancel] to revert all changes.\nRe-run the test to ensure " + "changes made have persisted." + } _color_delegator_spec = { 'file': 'ColorDelegator', diff --git a/Lib/idlelib/macosxSupport.py b/Lib/idlelib/macosxSupport.py index b6488f8..65bd688 100644 --- a/Lib/idlelib/macosxSupport.py +++ b/Lib/idlelib/macosxSupport.py @@ -140,11 +140,9 @@ def overrideRootMenu(root, flist): # Remove the 'About' entry from the help menu, it is in the application # menu del Bindings.menudefs[-1][1][0:2] - - # Remove the 'Configure' entry from the options menu, it is in the + # Remove the 'Configure Idle' entry from the options menu, it is in the # application menu as 'Preferences' - del Bindings.menudefs[-2][1][0:2] - + del Bindings.menudefs[-2][1][0] menubar = Menu(root) root.configure(menu=menubar) menudict = {} |